mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-03 03:03:24 -04:00
chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
@@ -30,10 +30,7 @@ const hasJsonFlag = (argv: string[]) =>
|
||||
const hasVersionFlag = (argv: string[]) =>
|
||||
argv.some((arg) => arg === "--version" || arg === "-V" || arg === "-v");
|
||||
|
||||
export function formatCliBannerLine(
|
||||
version: string,
|
||||
options: BannerOptions = {},
|
||||
): string {
|
||||
export function formatCliBannerLine(version: string, options: BannerOptions = {}): string {
|
||||
const commit = options.commit ?? resolveCommitHash({ env: options.env });
|
||||
const commitLabel = commit ?? "unknown";
|
||||
const tagline = pickTagline(options);
|
||||
|
||||
@@ -18,10 +18,7 @@ export function registerBrowserElementCommands(
|
||||
.option("--button <left|right|middle>", "Mouse button to use")
|
||||
.option("--modifiers <list>", "Comma-separated modifiers (Shift,Alt,Meta)")
|
||||
.action(async (ref: string | undefined, opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
const refValue = requireRef(ref);
|
||||
if (!refValue) return;
|
||||
const modifiers = opts.modifiers
|
||||
@@ -64,10 +61,7 @@ export function registerBrowserElementCommands(
|
||||
.option("--slowly", "Type slowly (human-like)", false)
|
||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||
.action(async (ref: string | undefined, text: string, opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
const refValue = requireRef(ref);
|
||||
if (!refValue) return;
|
||||
try {
|
||||
@@ -100,10 +94,7 @@ export function registerBrowserElementCommands(
|
||||
.argument("<key>", "Key to press (e.g. Enter)")
|
||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||
.action(async (key: string, opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
try {
|
||||
const result = await browserAct(
|
||||
baseUrl,
|
||||
@@ -127,10 +118,7 @@ export function registerBrowserElementCommands(
|
||||
.argument("<ref>", "Ref id from snapshot")
|
||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||
.action(async (ref: string, opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
try {
|
||||
const result = await browserAct(
|
||||
baseUrl,
|
||||
@@ -153,16 +141,11 @@ export function registerBrowserElementCommands(
|
||||
.description("Scroll an element into view by ref from snapshot")
|
||||
.argument("<ref>", "Ref id from snapshot")
|
||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||
.option(
|
||||
"--timeout-ms <ms>",
|
||||
"How long to wait for scroll (default: 20000)",
|
||||
(v: string) => Number(v),
|
||||
.option("--timeout-ms <ms>", "How long to wait for scroll (default: 20000)", (v: string) =>
|
||||
Number(v),
|
||||
)
|
||||
.action(async (ref: string | undefined, opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
const refValue = requireRef(ref);
|
||||
if (!refValue) return;
|
||||
try {
|
||||
@@ -172,9 +155,7 @@ export function registerBrowserElementCommands(
|
||||
kind: "scrollIntoView",
|
||||
ref: refValue,
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
timeoutMs: Number.isFinite(opts.timeoutMs)
|
||||
? opts.timeoutMs
|
||||
: undefined,
|
||||
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
|
||||
},
|
||||
{ profile },
|
||||
);
|
||||
@@ -196,10 +177,7 @@ export function registerBrowserElementCommands(
|
||||
.argument("<endRef>", "End ref id")
|
||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||
.action(async (startRef: string, endRef: string, opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
try {
|
||||
const result = await browserAct(
|
||||
baseUrl,
|
||||
@@ -229,10 +207,7 @@ export function registerBrowserElementCommands(
|
||||
.argument("<values...>", "Option values to select")
|
||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||
.action(async (ref: string, values: string[], opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
try {
|
||||
const result = await browserAct(
|
||||
baseUrl,
|
||||
|
||||
@@ -28,10 +28,7 @@ export function registerBrowserFilesAndDownloadsCommands(
|
||||
(v: string) => Number(v),
|
||||
)
|
||||
.action(async (paths: string[], opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
try {
|
||||
const result = await browserArmFileChooser(baseUrl, {
|
||||
paths,
|
||||
@@ -39,9 +36,7 @@ export function registerBrowserFilesAndDownloadsCommands(
|
||||
inputRef: opts.inputRef?.trim() || undefined,
|
||||
element: opts.element?.trim() || undefined,
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
timeoutMs: Number.isFinite(opts.timeoutMs)
|
||||
? opts.timeoutMs
|
||||
: undefined,
|
||||
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
|
||||
profile,
|
||||
});
|
||||
if (parent?.json) {
|
||||
@@ -66,17 +61,12 @@ export function registerBrowserFilesAndDownloadsCommands(
|
||||
(v: string) => Number(v),
|
||||
)
|
||||
.action(async (outPath: string | undefined, opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
try {
|
||||
const result = await browserWaitForDownload(baseUrl, {
|
||||
path: outPath?.trim() || undefined,
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
timeoutMs: Number.isFinite(opts.timeoutMs)
|
||||
? opts.timeoutMs
|
||||
: undefined,
|
||||
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
|
||||
profile,
|
||||
});
|
||||
if (parent?.json) {
|
||||
@@ -102,18 +92,13 @@ export function registerBrowserFilesAndDownloadsCommands(
|
||||
(v: string) => Number(v),
|
||||
)
|
||||
.action(async (ref: string, outPath: string, opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
try {
|
||||
const result = await browserDownload(baseUrl, {
|
||||
ref,
|
||||
path: outPath,
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
timeoutMs: Number.isFinite(opts.timeoutMs)
|
||||
? opts.timeoutMs
|
||||
: undefined,
|
||||
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
|
||||
profile,
|
||||
});
|
||||
if (parent?.json) {
|
||||
@@ -140,10 +125,7 @@ export function registerBrowserFilesAndDownloadsCommands(
|
||||
(v: string) => Number(v),
|
||||
)
|
||||
.action(async (opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
const accept = opts.accept ? true : opts.dismiss ? false : undefined;
|
||||
if (accept === undefined) {
|
||||
defaultRuntime.error(danger("Specify --accept or --dismiss"));
|
||||
@@ -155,9 +137,7 @@ export function registerBrowserFilesAndDownloadsCommands(
|
||||
accept,
|
||||
promptText: opts.prompt?.trim() || undefined,
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
timeoutMs: Number.isFinite(opts.timeoutMs)
|
||||
? opts.timeoutMs
|
||||
: undefined,
|
||||
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
|
||||
profile,
|
||||
});
|
||||
if (parent?.json) {
|
||||
|
||||
@@ -16,10 +16,7 @@ export function registerBrowserFormWaitEvalCommands(
|
||||
.option("--fields-file <path>", "Read JSON array from a file")
|
||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||
.action(async (opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
try {
|
||||
const fields = await readFields({
|
||||
fields: opts.fields,
|
||||
@@ -62,16 +59,11 @@ export function registerBrowserFormWaitEvalCommands(
|
||||
)
|
||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||
.action(async (selector: string | undefined, opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
try {
|
||||
const sel = selector?.trim() || undefined;
|
||||
const load =
|
||||
opts.load === "load" ||
|
||||
opts.load === "domcontentloaded" ||
|
||||
opts.load === "networkidle"
|
||||
opts.load === "load" || opts.load === "domcontentloaded" || opts.load === "networkidle"
|
||||
? (opts.load as "load" | "domcontentloaded" | "networkidle")
|
||||
: undefined;
|
||||
const result = await browserAct(
|
||||
@@ -86,9 +78,7 @@ export function registerBrowserFormWaitEvalCommands(
|
||||
loadState: load,
|
||||
fn: opts.fn?.trim() || undefined,
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
timeoutMs: Number.isFinite(opts.timeoutMs)
|
||||
? opts.timeoutMs
|
||||
: undefined,
|
||||
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
|
||||
},
|
||||
{ profile },
|
||||
);
|
||||
@@ -110,10 +100,7 @@ export function registerBrowserFormWaitEvalCommands(
|
||||
.option("--ref <id>", "Ref from snapshot")
|
||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||
.action(async (opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
if (!opts.fn) {
|
||||
defaultRuntime.error(danger("Missing --fn"));
|
||||
defaultRuntime.exit(1);
|
||||
|
||||
@@ -15,10 +15,7 @@ export function registerBrowserNavigationCommands(
|
||||
.argument("<url>", "URL to navigate to")
|
||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||
.action(async (url: string, opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
try {
|
||||
const result = await browserNavigate(baseUrl, {
|
||||
url,
|
||||
@@ -43,10 +40,7 @@ export function registerBrowserNavigationCommands(
|
||||
.argument("<height>", "Viewport height", (v: string) => Number(v))
|
||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||
.action(async (width: number, height: number, opts, cmd) => {
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(
|
||||
cmd,
|
||||
parentOpts,
|
||||
);
|
||||
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height)) {
|
||||
defaultRuntime.error(danger("width and height must be numbers"));
|
||||
defaultRuntime.exit(1);
|
||||
|
||||
@@ -40,9 +40,7 @@ export async function readFields(opts: {
|
||||
fields?: string;
|
||||
fieldsFile?: string;
|
||||
}): Promise<BrowserFormField[]> {
|
||||
const payload = opts.fieldsFile
|
||||
? await readFile(opts.fieldsFile)
|
||||
: (opts.fields ?? "");
|
||||
const payload = opts.fieldsFile ? await readFile(opts.fieldsFile) : (opts.fields ?? "");
|
||||
if (!payload.trim()) throw new Error("fields are required");
|
||||
const parsed = JSON.parse(payload) as unknown;
|
||||
if (!Array.isArray(parsed)) throw new Error("fields must be an array");
|
||||
@@ -66,8 +64,6 @@ export async function readFields(opts: {
|
||||
if (rec.value === undefined || rec.value === null) {
|
||||
return { ref, type };
|
||||
}
|
||||
throw new Error(
|
||||
`fields[${index}].value must be string, number, boolean, or null`,
|
||||
);
|
||||
throw new Error(`fields[${index}].value must be string, number, boolean, or null`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,10 +73,8 @@ export function registerBrowserActionObserveCommands(
|
||||
"How long to wait for the response (default: 20000)",
|
||||
(v: string) => Number(v),
|
||||
)
|
||||
.option(
|
||||
"--max-chars <n>",
|
||||
"Max body chars to return (default: 200000)",
|
||||
(v: string) => Number(v),
|
||||
.option("--max-chars <n>", "Max body chars to return (default: 200000)", (v: string) =>
|
||||
Number(v),
|
||||
)
|
||||
.action(async (url: string, opts, cmd) => {
|
||||
const parent = parentOpts(cmd);
|
||||
@@ -86,9 +84,7 @@ export function registerBrowserActionObserveCommands(
|
||||
const result = await browserResponseBody(baseUrl, {
|
||||
url,
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
timeoutMs: Number.isFinite(opts.timeoutMs)
|
||||
? opts.timeoutMs
|
||||
: undefined,
|
||||
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
|
||||
maxChars: Number.isFinite(opts.maxChars) ? opts.maxChars : undefined,
|
||||
profile,
|
||||
});
|
||||
|
||||
@@ -67,10 +67,7 @@ export function registerBrowserDebugCommands(
|
||||
}
|
||||
defaultRuntime.log(
|
||||
result.errors
|
||||
.map(
|
||||
(e) =>
|
||||
`${e.timestamp} ${e.name ? `${e.name}: ` : ""}${e.message}`,
|
||||
)
|
||||
.map((e) => `${e.timestamp} ${e.name ? `${e.name}: ` : ""}${e.message}`)
|
||||
.join("\n"),
|
||||
);
|
||||
} catch (err) {
|
||||
@@ -120,9 +117,7 @@ export function registerBrowserDebugCommands(
|
||||
}
|
||||
});
|
||||
|
||||
const trace = browser
|
||||
.command("trace")
|
||||
.description("Record a Playwright trace");
|
||||
const trace = browser.command("trace").description("Record a Playwright trace");
|
||||
|
||||
trace
|
||||
.command("start")
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { Command } from "commander";
|
||||
|
||||
import {
|
||||
browserSnapshot,
|
||||
resolveBrowserControlUrl,
|
||||
} from "../browser/client.js";
|
||||
import { browserSnapshot, resolveBrowserControlUrl } from "../browser/client.js";
|
||||
import { browserScreenshotAction } from "../browser/client-actions.js";
|
||||
import { danger } from "../globals.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
@@ -47,14 +44,10 @@ export function registerBrowserInspectCommands(
|
||||
|
||||
browser
|
||||
.command("snapshot")
|
||||
.description(
|
||||
"Capture a snapshot (default: ai; aria is the accessibility tree)",
|
||||
)
|
||||
.description("Capture a snapshot (default: ai; aria is the accessibility tree)")
|
||||
.option("--format <aria|ai>", "Snapshot format (default: ai)", "ai")
|
||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||
.option("--limit <n>", "Max nodes (default: 500/800)", (v: string) =>
|
||||
Number(v),
|
||||
)
|
||||
.option("--limit <n>", "Max nodes (default: 500/800)", (v: string) => Number(v))
|
||||
.option("--interactive", "Role snapshot: interactive elements only", false)
|
||||
.option("--compact", "Role snapshot: compact output", false)
|
||||
.option("--depth <n>", "Role snapshot: max depth", (v: string) => Number(v))
|
||||
@@ -88,9 +81,7 @@ export function registerBrowserInspectCommands(
|
||||
await fs.writeFile(opts.out, payload, "utf8");
|
||||
}
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(
|
||||
JSON.stringify({ ok: true, out: opts.out }, null, 2),
|
||||
);
|
||||
defaultRuntime.log(JSON.stringify({ ok: true, out: opts.out }, null, 2));
|
||||
} else {
|
||||
defaultRuntime.log(opts.out);
|
||||
}
|
||||
|
||||
@@ -71,9 +71,7 @@ export function registerBrowserManageCommands(
|
||||
return;
|
||||
}
|
||||
const name = status.profile ?? "clawd";
|
||||
defaultRuntime.log(
|
||||
info(`🦞 browser [${name}] running: ${status.running}`),
|
||||
);
|
||||
defaultRuntime.log(info(`🦞 browser [${name}] running: ${status.running}`));
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
@@ -95,9 +93,7 @@ export function registerBrowserManageCommands(
|
||||
return;
|
||||
}
|
||||
const name = status.profile ?? "clawd";
|
||||
defaultRuntime.log(
|
||||
info(`🦞 browser [${name}] running: ${status.running}`),
|
||||
);
|
||||
defaultRuntime.log(info(`🦞 browser [${name}] running: ${status.running}`));
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
@@ -149,8 +145,7 @@ export function registerBrowserManageCommands(
|
||||
defaultRuntime.log(
|
||||
tabs
|
||||
.map(
|
||||
(t, i) =>
|
||||
`${i + 1}. ${t.title || "(untitled)"}\n ${t.url}\n id: ${t.targetId}`,
|
||||
(t, i) => `${i + 1}. ${t.title || "(untitled)"}\n ${t.url}\n id: ${t.targetId}`,
|
||||
)
|
||||
.join("\n"),
|
||||
);
|
||||
@@ -184,8 +179,7 @@ export function registerBrowserManageCommands(
|
||||
defaultRuntime.log(
|
||||
tabs
|
||||
.map(
|
||||
(t, i) =>
|
||||
`${i + 1}. ${t.title || "(untitled)"}\n ${t.url}\n id: ${t.targetId}`,
|
||||
(t, i) => `${i + 1}. ${t.title || "(untitled)"}\n ${t.url}\n id: ${t.targetId}`,
|
||||
)
|
||||
.join("\n"),
|
||||
);
|
||||
@@ -257,9 +251,7 @@ export function registerBrowserManageCommands(
|
||||
const baseUrl = resolveBrowserControlUrl(parent?.url);
|
||||
const profile = parent?.browserProfile;
|
||||
const idx =
|
||||
typeof index === "number" && Number.isFinite(index)
|
||||
? Math.floor(index) - 1
|
||||
: undefined;
|
||||
typeof index === "number" && Number.isFinite(index) ? Math.floor(index) - 1 : undefined;
|
||||
if (typeof idx === "number" && idx < 0) {
|
||||
defaultRuntime.error(danger("index must be >= 1"));
|
||||
defaultRuntime.exit(1);
|
||||
@@ -372,9 +364,7 @@ export function registerBrowserManageCommands(
|
||||
const status = p.running ? "running" : "stopped";
|
||||
const tabs = p.running ? ` (${p.tabCount} tabs)` : "";
|
||||
const def = p.isDefault ? " [default]" : "";
|
||||
const loc = p.isRemote
|
||||
? `cdpUrl: ${p.cdpUrl}`
|
||||
: `port: ${p.cdpPort}`;
|
||||
const loc = p.isRemote ? `cdpUrl: ${p.cdpUrl}` : `port: ${p.cdpPort}`;
|
||||
const remote = p.isRemote ? " [remote]" : "";
|
||||
return `${p.name}: ${status}${tabs}${def}${remote}\n ${loc}, color: ${p.color}`;
|
||||
})
|
||||
@@ -389,40 +379,31 @@ export function registerBrowserManageCommands(
|
||||
browser
|
||||
.command("create-profile")
|
||||
.description("Create a new browser profile")
|
||||
.requiredOption(
|
||||
"--name <name>",
|
||||
"Profile name (lowercase, numbers, hyphens)",
|
||||
)
|
||||
.requiredOption("--name <name>", "Profile name (lowercase, numbers, hyphens)")
|
||||
.option("--color <hex>", "Profile color (hex format, e.g. #0066CC)")
|
||||
.option("--cdp-url <url>", "CDP URL for remote Chrome (http/https)")
|
||||
.action(
|
||||
async (opts: { name: string; color?: string; cdpUrl?: string }, cmd) => {
|
||||
const parent = parentOpts(cmd);
|
||||
const baseUrl = resolveBrowserControlUrl(parent?.url);
|
||||
try {
|
||||
const result = await browserCreateProfile(baseUrl, {
|
||||
name: opts.name,
|
||||
color: opts.color,
|
||||
cdpUrl: opts.cdpUrl,
|
||||
});
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
const loc = result.isRemote
|
||||
? ` cdpUrl: ${result.cdpUrl}`
|
||||
: ` port: ${result.cdpPort}`;
|
||||
defaultRuntime.log(
|
||||
info(
|
||||
`🦞 Created profile "${result.profile}"\n${loc}\n color: ${result.color}`,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
.action(async (opts: { name: string; color?: string; cdpUrl?: string }, cmd) => {
|
||||
const parent = parentOpts(cmd);
|
||||
const baseUrl = resolveBrowserControlUrl(parent?.url);
|
||||
try {
|
||||
const result = await browserCreateProfile(baseUrl, {
|
||||
name: opts.name,
|
||||
color: opts.color,
|
||||
cdpUrl: opts.cdpUrl,
|
||||
});
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
},
|
||||
);
|
||||
const loc = result.isRemote ? ` cdpUrl: ${result.cdpUrl}` : ` port: ${result.cdpPort}`;
|
||||
defaultRuntime.log(
|
||||
info(`🦞 Created profile "${result.profile}"\n${loc}\n color: ${result.color}`),
|
||||
);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
browser
|
||||
.command("delete-profile")
|
||||
|
||||
@@ -93,9 +93,7 @@ export function registerBrowserCookiesAndStorageCommands(
|
||||
}
|
||||
});
|
||||
|
||||
const storage = browser
|
||||
.command("storage")
|
||||
.description("Read/write localStorage/sessionStorage");
|
||||
const storage = browser.command("storage").description("Read/write localStorage/sessionStorage");
|
||||
|
||||
function registerStorageKind(kind: "local" | "session") {
|
||||
const cmd = storage.command(kind).description(`${kind}Storage commands`);
|
||||
|
||||
@@ -30,9 +30,7 @@ export function registerBrowserStateCommands(
|
||||
) {
|
||||
registerBrowserCookiesAndStorageCommands(browser, parentOpts);
|
||||
|
||||
const set = browser
|
||||
.command("set")
|
||||
.description("Browser environment settings");
|
||||
const set = browser.command("set").description("Browser environment settings");
|
||||
|
||||
set
|
||||
.command("viewport")
|
||||
@@ -118,9 +116,7 @@ export function registerBrowserStateCommands(
|
||||
throw new Error("headers json must be an object");
|
||||
}
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(
|
||||
parsed as Record<string, unknown>,
|
||||
)) {
|
||||
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
|
||||
if (typeof v === "string") headers[k] = v;
|
||||
}
|
||||
const result = await browserSetHeaders(baseUrl, {
|
||||
@@ -146,37 +142,28 @@ export function registerBrowserStateCommands(
|
||||
.argument("[username]", "Username")
|
||||
.argument("[password]", "Password")
|
||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||
.action(
|
||||
async (
|
||||
username: string | undefined,
|
||||
password: string | undefined,
|
||||
opts,
|
||||
cmd,
|
||||
) => {
|
||||
const parent = parentOpts(cmd);
|
||||
const baseUrl = resolveBrowserControlUrl(parent?.url);
|
||||
const profile = parent?.browserProfile;
|
||||
try {
|
||||
const result = await browserSetHttpCredentials(baseUrl, {
|
||||
username: username?.trim() || undefined,
|
||||
password,
|
||||
clear: Boolean(opts.clear),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
profile,
|
||||
});
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(
|
||||
opts.clear ? "credentials cleared" : "credentials set",
|
||||
);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
.action(async (username: string | undefined, password: string | undefined, opts, cmd) => {
|
||||
const parent = parentOpts(cmd);
|
||||
const baseUrl = resolveBrowserControlUrl(parent?.url);
|
||||
const profile = parent?.browserProfile;
|
||||
try {
|
||||
const result = await browserSetHttpCredentials(baseUrl, {
|
||||
username: username?.trim() || undefined,
|
||||
password,
|
||||
clear: Boolean(opts.clear),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
profile,
|
||||
});
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
},
|
||||
);
|
||||
defaultRuntime.log(opts.clear ? "credentials cleared" : "credentials set");
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
set
|
||||
.command("geo")
|
||||
@@ -187,41 +174,30 @@ export function registerBrowserStateCommands(
|
||||
.option("--accuracy <m>", "Accuracy in meters", (v: string) => Number(v))
|
||||
.option("--origin <origin>", "Origin to grant permissions for")
|
||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||
.action(
|
||||
async (
|
||||
latitude: number | undefined,
|
||||
longitude: number | undefined,
|
||||
opts,
|
||||
cmd,
|
||||
) => {
|
||||
const parent = parentOpts(cmd);
|
||||
const baseUrl = resolveBrowserControlUrl(parent?.url);
|
||||
const profile = parent?.browserProfile;
|
||||
try {
|
||||
const result = await browserSetGeolocation(baseUrl, {
|
||||
latitude: Number.isFinite(latitude) ? latitude : undefined,
|
||||
longitude: Number.isFinite(longitude) ? longitude : undefined,
|
||||
accuracy: Number.isFinite(opts.accuracy)
|
||||
? opts.accuracy
|
||||
: undefined,
|
||||
origin: opts.origin?.trim() || undefined,
|
||||
clear: Boolean(opts.clear),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
profile,
|
||||
});
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(
|
||||
opts.clear ? "geolocation cleared" : "geolocation set",
|
||||
);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
.action(async (latitude: number | undefined, longitude: number | undefined, opts, cmd) => {
|
||||
const parent = parentOpts(cmd);
|
||||
const baseUrl = resolveBrowserControlUrl(parent?.url);
|
||||
const profile = parent?.browserProfile;
|
||||
try {
|
||||
const result = await browserSetGeolocation(baseUrl, {
|
||||
latitude: Number.isFinite(latitude) ? latitude : undefined,
|
||||
longitude: Number.isFinite(longitude) ? longitude : undefined,
|
||||
accuracy: Number.isFinite(opts.accuracy) ? opts.accuracy : undefined,
|
||||
origin: opts.origin?.trim() || undefined,
|
||||
clear: Boolean(opts.clear),
|
||||
targetId: opts.targetId?.trim() || undefined,
|
||||
profile,
|
||||
});
|
||||
if (parent?.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
},
|
||||
);
|
||||
defaultRuntime.log(opts.clear ? "geolocation cleared" : "geolocation set");
|
||||
} catch (err) {
|
||||
defaultRuntime.error(danger(String(err)));
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
set
|
||||
.command("media")
|
||||
@@ -234,13 +210,7 @@ export function registerBrowserStateCommands(
|
||||
const profile = parent?.browserProfile;
|
||||
const v = value.trim().toLowerCase();
|
||||
const colorScheme =
|
||||
v === "dark"
|
||||
? "dark"
|
||||
: v === "light"
|
||||
? "light"
|
||||
: v === "none"
|
||||
? "none"
|
||||
: null;
|
||||
v === "dark" ? "dark" : v === "light" ? "light" : v === "none" ? "none" : null;
|
||||
if (!colorScheme) {
|
||||
defaultRuntime.error(danger("Expected dark|light|none"));
|
||||
defaultRuntime.exit(1);
|
||||
|
||||
@@ -17,14 +17,7 @@ describe("browser CLI --browser-profile flag", () => {
|
||||
capturedProfile = parent?.browserProfile;
|
||||
});
|
||||
|
||||
program.parse([
|
||||
"node",
|
||||
"test",
|
||||
"browser",
|
||||
"--browser-profile",
|
||||
"onasset",
|
||||
"status",
|
||||
]);
|
||||
program.parse(["node", "test", "browser", "--browser-profile", "onasset", "status"]);
|
||||
|
||||
expect(capturedProfile).toBe("onasset");
|
||||
});
|
||||
|
||||
@@ -7,10 +7,7 @@ import { theme } from "../terminal/theme.js";
|
||||
import { registerBrowserActionInputCommands } from "./browser-cli-actions-input.js";
|
||||
import { registerBrowserActionObserveCommands } from "./browser-cli-actions-observe.js";
|
||||
import { registerBrowserDebugCommands } from "./browser-cli-debug.js";
|
||||
import {
|
||||
browserActionExamples,
|
||||
browserCoreExamples,
|
||||
} from "./browser-cli-examples.js";
|
||||
import { browserActionExamples, browserCoreExamples } from "./browser-cli-examples.js";
|
||||
import { registerBrowserInspectCommands } from "./browser-cli-inspect.js";
|
||||
import { registerBrowserManageCommands } from "./browser-cli-manage.js";
|
||||
import type { BrowserParentOpts } from "./browser-cli-shared.js";
|
||||
@@ -20,14 +17,8 @@ export function registerBrowserCli(program: Command) {
|
||||
const browser = program
|
||||
.command("browser")
|
||||
.description("Manage clawd's dedicated browser (Chrome/Chromium)")
|
||||
.option(
|
||||
"--url <url>",
|
||||
"Override browser control URL (default from ~/.clawdbot/clawdbot.json)",
|
||||
)
|
||||
.option(
|
||||
"--browser-profile <name>",
|
||||
"Browser profile name (default from config)",
|
||||
)
|
||||
.option("--url <url>", "Override browser control URL (default from ~/.clawdbot/clawdbot.json)")
|
||||
.option("--browser-profile <name>", "Browser profile name (default from config)")
|
||||
.option("--json", "Output machine-readable JSON", false)
|
||||
.addHelpText(
|
||||
"after",
|
||||
@@ -39,14 +30,11 @@ export function registerBrowserCli(program: Command) {
|
||||
)
|
||||
.action(() => {
|
||||
browser.outputHelp();
|
||||
defaultRuntime.error(
|
||||
danger('Missing subcommand. Try: "clawdbot browser status"'),
|
||||
);
|
||||
defaultRuntime.error(danger('Missing subcommand. Try: "clawdbot browser status"'));
|
||||
defaultRuntime.exit(1);
|
||||
});
|
||||
|
||||
const parentOpts = (cmd: Command) =>
|
||||
cmd.parent?.opts?.() as BrowserParentOpts;
|
||||
const parentOpts = (cmd: Command) => cmd.parent?.opts?.() as BrowserParentOpts;
|
||||
|
||||
registerBrowserManageCommands(browser, parentOpts);
|
||||
registerBrowserInspectCommands(browser, parentOpts);
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import {
|
||||
getChannelPlugin,
|
||||
normalizeChannelId,
|
||||
} from "../channels/plugins/index.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js";
|
||||
import { DEFAULT_CHAT_CHANNEL } from "../channels/registry.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { setVerbose } from "../globals.js";
|
||||
@@ -30,8 +27,7 @@ export async function runChannelLogin(
|
||||
// Auth-only flow: do not mutate channel config here.
|
||||
setVerbose(Boolean(opts.verbose));
|
||||
const cfg = loadConfig();
|
||||
const accountId =
|
||||
opts.account?.trim() || resolveChannelDefaultAccountId({ plugin, cfg });
|
||||
const accountId = opts.account?.trim() || resolveChannelDefaultAccountId({ plugin, cfg });
|
||||
await plugin.auth.login({
|
||||
cfg,
|
||||
accountId,
|
||||
@@ -56,8 +52,7 @@ export async function runChannelLogout(
|
||||
}
|
||||
// Auth-only flow: resolve account + clear session state only.
|
||||
const cfg = loadConfig();
|
||||
const accountId =
|
||||
opts.account?.trim() || resolveChannelDefaultAccountId({ plugin, cfg });
|
||||
const accountId = opts.account?.trim() || resolveChannelDefaultAccountId({ plugin, cfg });
|
||||
const account = plugin.config.resolveAccount(cfg, accountId);
|
||||
await plugin.gateway.logoutAccount({
|
||||
cfg,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { Command } from "commander";
|
||||
|
||||
export function hasExplicitOptions(
|
||||
command: Command,
|
||||
names: readonly string[],
|
||||
): boolean {
|
||||
export function hasExplicitOptions(command: Command, names: readonly string[]): boolean {
|
||||
if (typeof command.getOptionValueSource !== "function") {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
import { Command } from "commander";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const callGatewayFromCli = vi.fn(
|
||||
async (method: string, _opts: unknown, params?: unknown) => {
|
||||
if (method === "cron.status") return { enabled: true };
|
||||
return { ok: true, params };
|
||||
},
|
||||
);
|
||||
const callGatewayFromCli = vi.fn(async (method: string, _opts: unknown, params?: unknown) => {
|
||||
if (method === "cron.status") return { enabled: true };
|
||||
return { ok: true, params };
|
||||
});
|
||||
|
||||
vi.mock("./gateway-rpc.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./gateway-rpc.js")>(
|
||||
"./gateway-rpc.js",
|
||||
);
|
||||
const actual = await vi.importActual<typeof import("./gateway-rpc.js")>("./gateway-rpc.js");
|
||||
return {
|
||||
...actual,
|
||||
callGatewayFromCli: (
|
||||
method: string,
|
||||
opts: unknown,
|
||||
params?: unknown,
|
||||
extra?: unknown,
|
||||
) => callGatewayFromCli(method, opts, params, extra),
|
||||
callGatewayFromCli: (method: string, opts: unknown, params?: unknown, extra?: unknown) =>
|
||||
callGatewayFromCli(method, opts, params, extra),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -63,9 +54,7 @@ describe("cron cli", () => {
|
||||
{ from: "user" },
|
||||
);
|
||||
|
||||
const addCall = callGatewayFromCli.mock.calls.find(
|
||||
(call) => call[0] === "cron.add",
|
||||
);
|
||||
const addCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.add");
|
||||
const params = addCall?.[2] as {
|
||||
payload?: { model?: string; thinking?: string };
|
||||
};
|
||||
@@ -100,9 +89,7 @@ describe("cron cli", () => {
|
||||
{ from: "user" },
|
||||
);
|
||||
|
||||
const addCall = callGatewayFromCli.mock.calls.find(
|
||||
(call) => call[0] === "cron.add",
|
||||
);
|
||||
const addCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.add");
|
||||
const params = addCall?.[2] as { agentId?: string };
|
||||
expect(params?.agentId).toBe("ops");
|
||||
});
|
||||
@@ -116,23 +103,11 @@ describe("cron cli", () => {
|
||||
registerCronCli(program);
|
||||
|
||||
await program.parseAsync(
|
||||
[
|
||||
"cron",
|
||||
"edit",
|
||||
"job-1",
|
||||
"--message",
|
||||
"hello",
|
||||
"--model",
|
||||
" ",
|
||||
"--thinking",
|
||||
" ",
|
||||
],
|
||||
["cron", "edit", "job-1", "--message", "hello", "--model", " ", "--thinking", " "],
|
||||
{ from: "user" },
|
||||
);
|
||||
|
||||
const updateCall = callGatewayFromCli.mock.calls.find(
|
||||
(call) => call[0] === "cron.update",
|
||||
);
|
||||
const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update");
|
||||
const patch = updateCall?.[2] as {
|
||||
patch?: { payload?: { model?: string; thinking?: string } };
|
||||
};
|
||||
@@ -164,9 +139,7 @@ describe("cron cli", () => {
|
||||
{ from: "user" },
|
||||
);
|
||||
|
||||
const updateCall = callGatewayFromCli.mock.calls.find(
|
||||
(call) => call[0] === "cron.update",
|
||||
);
|
||||
const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update");
|
||||
const patch = updateCall?.[2] as {
|
||||
patch?: { payload?: { model?: string; thinking?: string } };
|
||||
};
|
||||
@@ -183,14 +156,11 @@ describe("cron cli", () => {
|
||||
program.exitOverride();
|
||||
registerCronCli(program);
|
||||
|
||||
await program.parseAsync(
|
||||
["cron", "edit", "job-1", "--agent", " Ops ", "--message", "hello"],
|
||||
{ from: "user" },
|
||||
);
|
||||
await program.parseAsync(["cron", "edit", "job-1", "--agent", " Ops ", "--message", "hello"], {
|
||||
from: "user",
|
||||
});
|
||||
|
||||
const updateCall = callGatewayFromCli.mock.calls.find(
|
||||
(call) => call[0] === "cron.update",
|
||||
);
|
||||
const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update");
|
||||
const patch = updateCall?.[2] as { patch?: { agentId?: unknown } };
|
||||
expect(patch?.patch?.agentId).toBe("Ops");
|
||||
|
||||
@@ -198,9 +168,7 @@ describe("cron cli", () => {
|
||||
await program.parseAsync(["cron", "edit", "job-2", "--clear-agent"], {
|
||||
from: "user",
|
||||
});
|
||||
const clearCall = callGatewayFromCli.mock.calls.find(
|
||||
(call) => call[0] === "cron.update",
|
||||
);
|
||||
const clearCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update");
|
||||
const clearPatch = clearCall?.[2] as { patch?: { agentId?: unknown } };
|
||||
expect(clearPatch?.patch?.agentId).toBeNull();
|
||||
});
|
||||
@@ -213,14 +181,11 @@ describe("cron cli", () => {
|
||||
program.exitOverride();
|
||||
registerCronCli(program);
|
||||
|
||||
await program.parseAsync(
|
||||
["cron", "edit", "job-1", "--model", "opus", "--thinking", "low"],
|
||||
{ from: "user" },
|
||||
);
|
||||
await program.parseAsync(["cron", "edit", "job-1", "--model", "opus", "--thinking", "low"], {
|
||||
from: "user",
|
||||
});
|
||||
|
||||
const updateCall = callGatewayFromCli.mock.calls.find(
|
||||
(call) => call[0] === "cron.update",
|
||||
);
|
||||
const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update");
|
||||
const patch = updateCall?.[2] as { patch?: { payload?: unknown } };
|
||||
|
||||
expect(patch?.patch?.payload).toBeUndefined();
|
||||
|
||||
@@ -67,53 +67,27 @@ export function registerCronAddCommand(cron: Command) {
|
||||
.requiredOption("--name <name>", "Job name")
|
||||
.option("--description <text>", "Optional description")
|
||||
.option("--disabled", "Create job disabled", false)
|
||||
.option(
|
||||
"--delete-after-run",
|
||||
"Delete one-shot job after it succeeds",
|
||||
false,
|
||||
)
|
||||
.option("--delete-after-run", "Delete one-shot job after it succeeds", false)
|
||||
.option("--agent <id>", "Agent id for this job")
|
||||
.option("--session <target>", "Session target (main|isolated)", "main")
|
||||
.option(
|
||||
"--wake <mode>",
|
||||
"Wake mode (now|next-heartbeat)",
|
||||
"next-heartbeat",
|
||||
)
|
||||
.option("--wake <mode>", "Wake mode (now|next-heartbeat)", "next-heartbeat")
|
||||
.option("--at <when>", "Run once at time (ISO) or +duration (e.g. 20m)")
|
||||
.option("--every <duration>", "Run every duration (e.g. 10m, 1h)")
|
||||
.option("--cron <expr>", "Cron expression (5-field)")
|
||||
.option("--tz <iana>", "Timezone for cron expressions (IANA)", "")
|
||||
.option("--system-event <text>", "System event payload (main session)")
|
||||
.option("--message <text>", "Agent message payload")
|
||||
.option(
|
||||
"--thinking <level>",
|
||||
"Thinking level for agent jobs (off|minimal|low|medium|high)",
|
||||
)
|
||||
.option(
|
||||
"--model <model>",
|
||||
"Model override for agent jobs (provider/model or alias)",
|
||||
)
|
||||
.option("--thinking <level>", "Thinking level for agent jobs (off|minimal|low|medium|high)")
|
||||
.option("--model <model>", "Model override for agent jobs (provider/model or alias)")
|
||||
.option("--timeout-seconds <n>", "Timeout seconds for agent jobs")
|
||||
.option("--deliver", "Deliver agent output", false)
|
||||
.option(
|
||||
"--channel <channel>",
|
||||
`Delivery channel (${CRON_CHANNEL_OPTIONS})`,
|
||||
"last",
|
||||
)
|
||||
.option("--channel <channel>", `Delivery channel (${CRON_CHANNEL_OPTIONS})`, "last")
|
||||
.option(
|
||||
"--to <dest>",
|
||||
"Delivery destination (E.164, Telegram chatId, or Discord channel/user)",
|
||||
)
|
||||
.option(
|
||||
"--best-effort-deliver",
|
||||
"Do not fail the job if delivery fails",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--post-prefix <prefix>",
|
||||
"Prefix for summary system event",
|
||||
"Cron",
|
||||
)
|
||||
.option("--best-effort-deliver", "Do not fail the job if delivery fails", false)
|
||||
.option("--post-prefix <prefix>", "Prefix for summary system event", "Cron")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts: GatewayRpcOpts & Record<string, unknown>) => {
|
||||
try {
|
||||
@@ -121,49 +95,34 @@ export function registerCronAddCommand(cron: Command) {
|
||||
const at = typeof opts.at === "string" ? opts.at : "";
|
||||
const every = typeof opts.every === "string" ? opts.every : "";
|
||||
const cronExpr = typeof opts.cron === "string" ? opts.cron : "";
|
||||
const chosen = [
|
||||
Boolean(at),
|
||||
Boolean(every),
|
||||
Boolean(cronExpr),
|
||||
].filter(Boolean).length;
|
||||
const chosen = [Boolean(at), Boolean(every), Boolean(cronExpr)].filter(Boolean).length;
|
||||
if (chosen !== 1) {
|
||||
throw new Error(
|
||||
"Choose exactly one schedule: --at, --every, or --cron",
|
||||
);
|
||||
throw new Error("Choose exactly one schedule: --at, --every, or --cron");
|
||||
}
|
||||
if (at) {
|
||||
const atMs = parseAtMs(at);
|
||||
if (!atMs)
|
||||
throw new Error(
|
||||
"Invalid --at; use ISO time or duration like 20m",
|
||||
);
|
||||
if (!atMs) throw new Error("Invalid --at; use ISO time or duration like 20m");
|
||||
return { kind: "at" as const, atMs };
|
||||
}
|
||||
if (every) {
|
||||
const everyMs = parseDurationMs(every);
|
||||
if (!everyMs)
|
||||
throw new Error("Invalid --every; use e.g. 10m, 1h, 1d");
|
||||
if (!everyMs) throw new Error("Invalid --every; use e.g. 10m, 1h, 1d");
|
||||
return { kind: "every" as const, everyMs };
|
||||
}
|
||||
return {
|
||||
kind: "cron" as const,
|
||||
expr: cronExpr,
|
||||
tz:
|
||||
typeof opts.tz === "string" && opts.tz.trim()
|
||||
? opts.tz.trim()
|
||||
: undefined,
|
||||
tz: typeof opts.tz === "string" && opts.tz.trim() ? opts.tz.trim() : undefined,
|
||||
};
|
||||
})();
|
||||
|
||||
const sessionTargetRaw =
|
||||
typeof opts.session === "string" ? opts.session : "main";
|
||||
const sessionTargetRaw = typeof opts.session === "string" ? opts.session : "main";
|
||||
const sessionTarget = sessionTargetRaw.trim() || "main";
|
||||
if (sessionTarget !== "main" && sessionTarget !== "isolated") {
|
||||
throw new Error("--session must be main or isolated");
|
||||
}
|
||||
|
||||
const wakeModeRaw =
|
||||
typeof opts.wake === "string" ? opts.wake : "next-heartbeat";
|
||||
const wakeModeRaw = typeof opts.wake === "string" ? opts.wake : "next-heartbeat";
|
||||
const wakeMode = wakeModeRaw.trim() || "next-heartbeat";
|
||||
if (wakeMode !== "now" && wakeMode !== "next-heartbeat") {
|
||||
throw new Error("--wake must be now or next-heartbeat");
|
||||
@@ -175,46 +134,28 @@ export function registerCronAddCommand(cron: Command) {
|
||||
: undefined;
|
||||
|
||||
const payload = (() => {
|
||||
const systemEvent =
|
||||
typeof opts.systemEvent === "string"
|
||||
? opts.systemEvent.trim()
|
||||
: "";
|
||||
const message =
|
||||
typeof opts.message === "string" ? opts.message.trim() : "";
|
||||
const chosen = [Boolean(systemEvent), Boolean(message)].filter(
|
||||
Boolean,
|
||||
).length;
|
||||
const systemEvent = typeof opts.systemEvent === "string" ? opts.systemEvent.trim() : "";
|
||||
const message = typeof opts.message === "string" ? opts.message.trim() : "";
|
||||
const chosen = [Boolean(systemEvent), Boolean(message)].filter(Boolean).length;
|
||||
if (chosen !== 1) {
|
||||
throw new Error(
|
||||
"Choose exactly one payload: --system-event or --message",
|
||||
);
|
||||
throw new Error("Choose exactly one payload: --system-event or --message");
|
||||
}
|
||||
if (systemEvent)
|
||||
return { kind: "systemEvent" as const, text: systemEvent };
|
||||
const timeoutSeconds = parsePositiveIntOrUndefined(
|
||||
opts.timeoutSeconds,
|
||||
);
|
||||
if (systemEvent) return { kind: "systemEvent" as const, text: systemEvent };
|
||||
const timeoutSeconds = parsePositiveIntOrUndefined(opts.timeoutSeconds);
|
||||
return {
|
||||
kind: "agentTurn" as const,
|
||||
message,
|
||||
model:
|
||||
typeof opts.model === "string" && opts.model.trim()
|
||||
? opts.model.trim()
|
||||
: undefined,
|
||||
typeof opts.model === "string" && opts.model.trim() ? opts.model.trim() : undefined,
|
||||
thinking:
|
||||
typeof opts.thinking === "string" && opts.thinking.trim()
|
||||
? opts.thinking.trim()
|
||||
: undefined,
|
||||
timeoutSeconds:
|
||||
timeoutSeconds && Number.isFinite(timeoutSeconds)
|
||||
? timeoutSeconds
|
||||
: undefined,
|
||||
timeoutSeconds && Number.isFinite(timeoutSeconds) ? timeoutSeconds : undefined,
|
||||
deliver: Boolean(opts.deliver),
|
||||
channel: typeof opts.channel === "string" ? opts.channel : "last",
|
||||
to:
|
||||
typeof opts.to === "string" && opts.to.trim()
|
||||
? opts.to.trim()
|
||||
: undefined,
|
||||
to: typeof opts.to === "string" && opts.to.trim() ? opts.to.trim() : undefined,
|
||||
bestEffortDeliver: Boolean(opts.bestEffortDeliver),
|
||||
};
|
||||
})();
|
||||
@@ -230,8 +171,7 @@ export function registerCronAddCommand(cron: Command) {
|
||||
sessionTarget === "isolated"
|
||||
? {
|
||||
postToMainPrefix:
|
||||
typeof opts.postPrefix === "string" &&
|
||||
opts.postPrefix.trim()
|
||||
typeof opts.postPrefix === "string" && opts.postPrefix.trim()
|
||||
? opts.postPrefix.trim()
|
||||
: "Cron",
|
||||
}
|
||||
|
||||
@@ -20,11 +20,7 @@ export function registerCronEditCommand(cron: Command) {
|
||||
.option("--description <text>", "Set description")
|
||||
.option("--enable", "Enable job", false)
|
||||
.option("--disable", "Disable job", false)
|
||||
.option(
|
||||
"--delete-after-run",
|
||||
"Delete one-shot job after it succeeds",
|
||||
false,
|
||||
)
|
||||
.option("--delete-after-run", "Delete one-shot job after it succeeds", false)
|
||||
.option("--keep-after-run", "Keep one-shot job after it succeeds", false)
|
||||
.option("--session <target>", "Session target (main|isolated)")
|
||||
.option("--agent <id>", "Set agent id")
|
||||
@@ -40,19 +36,12 @@ export function registerCronEditCommand(cron: Command) {
|
||||
.option("--model <model>", "Model override for agent jobs")
|
||||
.option("--timeout-seconds <n>", "Timeout seconds for agent jobs")
|
||||
.option("--deliver", "Deliver agent output", false)
|
||||
.option(
|
||||
"--channel <channel>",
|
||||
`Delivery channel (${CRON_CHANNEL_OPTIONS})`,
|
||||
)
|
||||
.option("--channel <channel>", `Delivery channel (${CRON_CHANNEL_OPTIONS})`)
|
||||
.option(
|
||||
"--to <dest>",
|
||||
"Delivery destination (E.164, Telegram chatId, or Discord channel/user)",
|
||||
)
|
||||
.option(
|
||||
"--best-effort-deliver",
|
||||
"Do not fail job if delivery fails",
|
||||
false,
|
||||
)
|
||||
.option("--best-effort-deliver", "Do not fail job if delivery fails", false)
|
||||
.option("--post-prefix <prefix>", "Prefix for summary system event")
|
||||
.action(async (id, opts) => {
|
||||
try {
|
||||
@@ -72,21 +61,17 @@ export function registerCronEditCommand(cron: Command) {
|
||||
|
||||
const patch: Record<string, unknown> = {};
|
||||
if (typeof opts.name === "string") patch.name = opts.name;
|
||||
if (typeof opts.description === "string")
|
||||
patch.description = opts.description;
|
||||
if (typeof opts.description === "string") patch.description = opts.description;
|
||||
if (opts.enable && opts.disable)
|
||||
throw new Error("Choose --enable or --disable, not both");
|
||||
if (opts.enable) patch.enabled = true;
|
||||
if (opts.disable) patch.enabled = false;
|
||||
if (opts.deleteAfterRun && opts.keepAfterRun) {
|
||||
throw new Error(
|
||||
"Choose --delete-after-run or --keep-after-run, not both",
|
||||
);
|
||||
throw new Error("Choose --delete-after-run or --keep-after-run, not both");
|
||||
}
|
||||
if (opts.deleteAfterRun) patch.deleteAfterRun = true;
|
||||
if (opts.keepAfterRun) patch.deleteAfterRun = false;
|
||||
if (typeof opts.session === "string")
|
||||
patch.sessionTarget = opts.session;
|
||||
if (typeof opts.session === "string") patch.sessionTarget = opts.session;
|
||||
if (typeof opts.wake === "string") patch.wakeMode = opts.wake;
|
||||
if (opts.agent && opts.clearAgent) {
|
||||
throw new Error("Use --agent or --clear-agent, not both");
|
||||
@@ -98,11 +83,8 @@ export function registerCronEditCommand(cron: Command) {
|
||||
patch.agentId = null;
|
||||
}
|
||||
|
||||
const scheduleChosen = [opts.at, opts.every, opts.cron].filter(
|
||||
Boolean,
|
||||
).length;
|
||||
if (scheduleChosen > 1)
|
||||
throw new Error("Choose at most one schedule change");
|
||||
const scheduleChosen = [opts.at, opts.every, opts.cron].filter(Boolean).length;
|
||||
if (scheduleChosen > 1) throw new Error("Choose at most one schedule change");
|
||||
if (opts.at) {
|
||||
const atMs = parseAtMs(String(opts.at));
|
||||
if (!atMs) throw new Error("Invalid --at");
|
||||
@@ -115,18 +97,12 @@ export function registerCronEditCommand(cron: Command) {
|
||||
patch.schedule = {
|
||||
kind: "cron",
|
||||
expr: String(opts.cron),
|
||||
tz:
|
||||
typeof opts.tz === "string" && opts.tz.trim()
|
||||
? opts.tz.trim()
|
||||
: undefined,
|
||||
tz: typeof opts.tz === "string" && opts.tz.trim() ? opts.tz.trim() : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const payloadChosen = [opts.systemEvent, opts.message].filter(
|
||||
Boolean,
|
||||
).length;
|
||||
if (payloadChosen > 1)
|
||||
throw new Error("Choose at most one payload change");
|
||||
const payloadChosen = [opts.systemEvent, opts.message].filter(Boolean).length;
|
||||
if (payloadChosen > 1) throw new Error("Choose at most one payload change");
|
||||
if (opts.systemEvent) {
|
||||
patch.payload = {
|
||||
kind: "systemEvent",
|
||||
@@ -134,9 +110,7 @@ export function registerCronEditCommand(cron: Command) {
|
||||
};
|
||||
} else if (opts.message) {
|
||||
const model =
|
||||
typeof opts.model === "string" && opts.model.trim()
|
||||
? opts.model.trim()
|
||||
: undefined;
|
||||
typeof opts.model === "string" && opts.model.trim() ? opts.model.trim() : undefined;
|
||||
const thinking =
|
||||
typeof opts.thinking === "string" && opts.thinking.trim()
|
||||
? opts.thinking.trim()
|
||||
@@ -150,12 +124,9 @@ export function registerCronEditCommand(cron: Command) {
|
||||
model,
|
||||
thinking,
|
||||
timeoutSeconds:
|
||||
timeoutSeconds && Number.isFinite(timeoutSeconds)
|
||||
? timeoutSeconds
|
||||
: undefined,
|
||||
timeoutSeconds && Number.isFinite(timeoutSeconds) ? timeoutSeconds : undefined,
|
||||
deliver: Boolean(opts.deliver),
|
||||
channel:
|
||||
typeof opts.channel === "string" ? opts.channel : undefined,
|
||||
channel: typeof opts.channel === "string" ? opts.channel : undefined,
|
||||
to: typeof opts.to === "string" ? opts.to : undefined,
|
||||
bestEffortDeliver: Boolean(opts.bestEffortDeliver),
|
||||
};
|
||||
@@ -163,9 +134,7 @@ export function registerCronEditCommand(cron: Command) {
|
||||
|
||||
if (typeof opts.postPrefix === "string") {
|
||||
patch.isolation = {
|
||||
postToMainPrefix: opts.postPrefix.trim()
|
||||
? opts.postPrefix
|
||||
: "Cron",
|
||||
postToMainPrefix: opts.postPrefix.trim() ? opts.postPrefix : "Cron",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -73,8 +73,7 @@ export function registerCronSimpleCommands(cron: Command) {
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
const limitRaw = Number.parseInt(String(opts.limit ?? "50"), 10);
|
||||
const limit =
|
||||
Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : 50;
|
||||
const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : 50;
|
||||
const id = String(opts.id);
|
||||
const res = await callGatewayFromCli("cron.runs", opts, {
|
||||
id,
|
||||
|
||||
@@ -19,10 +19,7 @@ export function registerCronCli(program: Command) {
|
||||
.addHelpText(
|
||||
"after",
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink(
|
||||
"/cron-jobs",
|
||||
"docs.clawd.bot/cron-jobs",
|
||||
)}\n`,
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink("/cron-jobs", "docs.clawd.bot/cron-jobs")}\n`,
|
||||
);
|
||||
|
||||
registerCronStatusCommand(cron);
|
||||
|
||||
@@ -8,15 +8,9 @@ export function registerWakeCommand(program: Command) {
|
||||
addGatewayClientOptions(
|
||||
program
|
||||
.command("wake")
|
||||
.description(
|
||||
"Enqueue a system event and optionally trigger an immediate heartbeat",
|
||||
)
|
||||
.description("Enqueue a system event and optionally trigger an immediate heartbeat")
|
||||
.requiredOption("--text <text>", "System event text")
|
||||
.option(
|
||||
"--mode <mode>",
|
||||
"Wake mode (now|next-heartbeat)",
|
||||
"next-heartbeat",
|
||||
)
|
||||
.option("--mode <mode>", "Wake mode (now|next-heartbeat)", "next-heartbeat")
|
||||
.option("--json", "Output JSON", false),
|
||||
).action(async (opts: GatewayRpcOpts & { text?: string; mode?: string }) => {
|
||||
try {
|
||||
|
||||
@@ -108,11 +108,8 @@ const formatRelative = (ms: number | null | undefined, nowMs: number) => {
|
||||
|
||||
const formatSchedule = (schedule: CronSchedule) => {
|
||||
if (schedule.kind === "at") return `at ${formatIsoMinute(schedule.atMs)}`;
|
||||
if (schedule.kind === "every")
|
||||
return `every ${formatDuration(schedule.everyMs)}`;
|
||||
return schedule.tz
|
||||
? `cron ${schedule.expr} @ ${schedule.tz}`
|
||||
: `cron ${schedule.expr}`;
|
||||
if (schedule.kind === "every") return `every ${formatDuration(schedule.everyMs)}`;
|
||||
return schedule.tz ? `cron ${schedule.expr} @ ${schedule.tz}` : `cron ${schedule.expr}`;
|
||||
};
|
||||
|
||||
const formatStatus = (job: CronJob) => {
|
||||
@@ -153,26 +150,17 @@ export function printCronList(jobs: CronJob[], runtime = defaultRuntime) {
|
||||
job.enabled ? formatRelative(job.state.nextRunAtMs, now) : "-",
|
||||
CRON_NEXT_PAD,
|
||||
);
|
||||
const lastLabel = pad(
|
||||
formatRelative(job.state.lastRunAtMs, now),
|
||||
CRON_LAST_PAD,
|
||||
);
|
||||
const lastLabel = pad(formatRelative(job.state.lastRunAtMs, now), CRON_LAST_PAD);
|
||||
const statusRaw = formatStatus(job);
|
||||
const statusLabel = pad(statusRaw, CRON_STATUS_PAD);
|
||||
const targetLabel = pad(job.sessionTarget, CRON_TARGET_PAD);
|
||||
const agentLabel = pad(
|
||||
truncate(job.agentId ?? "default", CRON_AGENT_PAD),
|
||||
CRON_AGENT_PAD,
|
||||
);
|
||||
const agentLabel = pad(truncate(job.agentId ?? "default", CRON_AGENT_PAD), CRON_AGENT_PAD);
|
||||
|
||||
const coloredStatus = (() => {
|
||||
if (statusRaw === "ok") return colorize(rich, theme.success, statusLabel);
|
||||
if (statusRaw === "error")
|
||||
return colorize(rich, theme.error, statusLabel);
|
||||
if (statusRaw === "running")
|
||||
return colorize(rich, theme.warn, statusLabel);
|
||||
if (statusRaw === "skipped")
|
||||
return colorize(rich, theme.muted, statusLabel);
|
||||
if (statusRaw === "error") return colorize(rich, theme.error, statusLabel);
|
||||
if (statusRaw === "running") return colorize(rich, theme.warn, statusLabel);
|
||||
if (statusRaw === "skipped") return colorize(rich, theme.muted, statusLabel);
|
||||
return colorize(rich, theme.muted, statusLabel);
|
||||
})();
|
||||
|
||||
|
||||
@@ -35,8 +35,7 @@ vi.mock("../gateway/call.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../daemon/program-args.js", () => ({
|
||||
resolveGatewayProgramArguments: (opts: unknown) =>
|
||||
resolveGatewayProgramArguments(opts),
|
||||
resolveGatewayProgramArguments: (opts: unknown) => resolveGatewayProgramArguments(opts),
|
||||
}));
|
||||
|
||||
vi.mock("../daemon/service.js", () => ({
|
||||
@@ -59,8 +58,7 @@ vi.mock("../daemon/legacy.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../daemon/inspect.js", () => ({
|
||||
findExtraGatewayServices: (env: unknown, opts?: unknown) =>
|
||||
findExtraGatewayServices(env, opts),
|
||||
findExtraGatewayServices: (env: unknown, opts?: unknown) => findExtraGatewayServices(env, opts),
|
||||
renderGatewayServiceCleanupHints: () => [],
|
||||
}));
|
||||
|
||||
@@ -78,8 +76,7 @@ vi.mock("./deps.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./progress.js", () => ({
|
||||
withProgress: async (_opts: unknown, fn: () => Promise<unknown>) =>
|
||||
await fn(),
|
||||
withProgress: async (_opts: unknown, fn: () => Promise<unknown>) => await fn(),
|
||||
}));
|
||||
|
||||
describe("daemon-cli coverage", () => {
|
||||
@@ -129,9 +126,7 @@ describe("daemon-cli coverage", () => {
|
||||
await program.parseAsync(["daemon", "status"], { from: "user" });
|
||||
|
||||
expect(callGateway).toHaveBeenCalledTimes(1);
|
||||
expect(callGateway).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ method: "status" }),
|
||||
);
|
||||
expect(callGateway).toHaveBeenCalledWith(expect.objectContaining({ method: "status" }));
|
||||
expect(findExtraGatewayServices).toHaveBeenCalled();
|
||||
expect(inspectPortUsage).toHaveBeenCalled();
|
||||
}, 20_000);
|
||||
|
||||
@@ -38,9 +38,7 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
const runtimeRaw = opts.runtime
|
||||
? String(opts.runtime)
|
||||
: DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
||||
const runtimeRaw = opts.runtime ? String(opts.runtime) : DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
||||
if (!isGatewayDaemonRuntime(runtimeRaw)) {
|
||||
defaultRuntime.error('Invalid --runtime (use "node" or "bun")');
|
||||
defaultRuntime.exit(1);
|
||||
@@ -66,19 +64,17 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
}
|
||||
|
||||
const devMode =
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
||||
process.argv[1]?.endsWith(".ts");
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) && process.argv[1]?.endsWith(".ts");
|
||||
const nodePath = await resolvePreferredNodePath({
|
||||
env: process.env,
|
||||
runtime: runtimeRaw,
|
||||
});
|
||||
const { programArguments, workingDirectory } =
|
||||
await resolveGatewayProgramArguments({
|
||||
port,
|
||||
dev: devMode,
|
||||
runtime: runtimeRaw,
|
||||
nodePath,
|
||||
});
|
||||
const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({
|
||||
port,
|
||||
dev: devMode,
|
||||
runtime: runtimeRaw,
|
||||
nodePath,
|
||||
});
|
||||
if (runtimeRaw === "node") {
|
||||
const systemNode = await resolveSystemNodeInfo({ env: process.env });
|
||||
const warning = renderSystemNodeWarning(systemNode, programArguments[0]);
|
||||
@@ -87,14 +83,9 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
const environment = buildServiceEnvironment({
|
||||
env: process.env,
|
||||
port,
|
||||
token:
|
||||
opts.token ||
|
||||
cfg.gateway?.auth?.token ||
|
||||
process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||
token: opts.token || cfg.gateway?.auth?.token || process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||
launchdLabel:
|
||||
process.platform === "darwin"
|
||||
? resolveGatewayLaunchAgentLabel(profile)
|
||||
: undefined,
|
||||
process.platform === "darwin" ? resolveGatewayLaunchAgentLabel(profile) : undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../../utils/message-channel.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
|
||||
import { withProgress } from "../progress.js";
|
||||
|
||||
export async function probeGatewayStatus(opts: {
|
||||
|
||||
@@ -17,20 +17,13 @@ export function registerDaemonCli(program: Command) {
|
||||
.description("Manage the Gateway daemon service (launchd/systemd/schtasks)")
|
||||
.addHelpText(
|
||||
"after",
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink(
|
||||
"/gateway",
|
||||
"docs.clawd.bot/gateway",
|
||||
)}\n`,
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/gateway", "docs.clawd.bot/gateway")}\n`,
|
||||
);
|
||||
|
||||
daemon
|
||||
.command("status")
|
||||
.description("Show daemon install status + probe the Gateway")
|
||||
.option(
|
||||
"--url <url>",
|
||||
"Gateway WebSocket URL (defaults to config/remote/local)",
|
||||
)
|
||||
.option("--url <url>", "Gateway WebSocket URL (defaults to config/remote/local)")
|
||||
.option("--token <token>", "Gateway token (if required)")
|
||||
.option("--password <password>", "Gateway password (password auth)")
|
||||
.option("--timeout <ms>", "Timeout in ms", "10000")
|
||||
|
||||
@@ -20,9 +20,7 @@ export function parsePort(raw: unknown): number | null {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function parsePortFromArgs(
|
||||
programArguments: string[] | undefined,
|
||||
): number | null {
|
||||
export function parsePortFromArgs(programArguments: string[] | undefined): number | null {
|
||||
if (!programArguments?.length) return null;
|
||||
for (let i = 0; i < programArguments.length; i += 1) {
|
||||
const arg = programArguments[i];
|
||||
@@ -51,9 +49,7 @@ export function pickProbeHostForBind(
|
||||
return "127.0.0.1";
|
||||
}
|
||||
|
||||
export function safeDaemonEnv(
|
||||
env: Record<string, string> | undefined,
|
||||
): string[] {
|
||||
export function safeDaemonEnv(env: Record<string, string> | undefined): string[] {
|
||||
if (!env) return [];
|
||||
const allow = [
|
||||
"CLAWDBOT_PROFILE",
|
||||
@@ -138,9 +134,7 @@ export function renderRuntimeHints(
|
||||
hints.push(`Launchd stderr (if installed): ${logs.stderrPath}`);
|
||||
} else if (process.platform === "linux") {
|
||||
const unit = resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
hints.push(
|
||||
`Logs: journalctl --user -u ${unit}.service -n 200 --no-pager`,
|
||||
);
|
||||
hints.push(`Logs: journalctl --user -u ${unit}.service -n 200 --no-pager`);
|
||||
} else if (process.platform === "win32") {
|
||||
const task = resolveGatewayWindowsTaskName(env.CLAWDBOT_PROFILE);
|
||||
hints.push(`Logs: schtasks /Query /TN "${task}" /V /FO LIST`);
|
||||
@@ -149,18 +143,13 @@ export function renderRuntimeHints(
|
||||
return hints;
|
||||
}
|
||||
|
||||
export function renderGatewayServiceStartHints(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
export function renderGatewayServiceStartHints(env: NodeJS.ProcessEnv = process.env): string[] {
|
||||
const base = ["clawdbot daemon install", "clawdbot gateway"];
|
||||
const profile = env.CLAWDBOT_PROFILE;
|
||||
switch (process.platform) {
|
||||
case "darwin": {
|
||||
const label = resolveGatewayLaunchAgentLabel(profile);
|
||||
return [
|
||||
...base,
|
||||
`launchctl bootstrap gui/$UID ~/Library/LaunchAgents/${label}.plist`,
|
||||
];
|
||||
return [...base, `launchctl bootstrap gui/$UID ~/Library/LaunchAgents/${label}.plist`];
|
||||
}
|
||||
case "linux": {
|
||||
const unit = resolveGatewaySystemdServiceName(profile);
|
||||
|
||||
@@ -4,10 +4,7 @@ import {
|
||||
resolveGatewayPort,
|
||||
resolveStateDir,
|
||||
} from "../../config/config.js";
|
||||
import type {
|
||||
BridgeBindMode,
|
||||
GatewayControlUiConfig,
|
||||
} from "../../config/types.js";
|
||||
import type { BridgeBindMode, GatewayControlUiConfig } from "../../config/types.js";
|
||||
import { readLastGatewayErrorLine } from "../../daemon/diagnostics.js";
|
||||
import type { FindExtraGatewayServicesOptions } from "../../daemon/inspect.js";
|
||||
import { findExtraGatewayServices } from "../../daemon/inspect.js";
|
||||
@@ -24,11 +21,7 @@ import {
|
||||
} from "../../infra/ports.js";
|
||||
import { pickPrimaryTailnetIPv4 } from "../../infra/tailnet.js";
|
||||
import { probeGatewayStatus } from "./probe.js";
|
||||
import {
|
||||
normalizeListenerAddress,
|
||||
parsePortFromArgs,
|
||||
pickProbeHostForBind,
|
||||
} from "./shared.js";
|
||||
import { normalizeListenerAddress, parsePortFromArgs, pickProbeHostForBind } from "./shared.js";
|
||||
import type { GatewayRpcOpts } from "./types.js";
|
||||
|
||||
type ConfigSummary = {
|
||||
@@ -104,10 +97,7 @@ export type DaemonStatus = {
|
||||
extraServices: Array<{ label: string; detail: string; scope: string }>;
|
||||
};
|
||||
|
||||
function shouldReportPortUsage(
|
||||
status: PortUsageStatus | undefined,
|
||||
rpcOk?: boolean,
|
||||
) {
|
||||
function shouldReportPortUsage(status: PortUsageStatus | undefined, rpcOk?: boolean) {
|
||||
if (status !== "busy") return false;
|
||||
if (rpcOk === true) return false;
|
||||
return true;
|
||||
@@ -142,10 +132,7 @@ export async function gatherDaemonStatus(
|
||||
...(serviceEnv ?? undefined),
|
||||
} satisfies Record<string, string | undefined>;
|
||||
|
||||
const cliConfigPath = resolveConfigPath(
|
||||
process.env,
|
||||
resolveStateDir(process.env),
|
||||
);
|
||||
const cliConfigPath = resolveConfigPath(process.env, resolveStateDir(process.env));
|
||||
const daemonConfigPath = resolveConfigPath(
|
||||
mergedDaemonEnv as NodeJS.ProcessEnv,
|
||||
resolveStateDir(mergedDaemonEnv as NodeJS.ProcessEnv),
|
||||
@@ -175,16 +162,13 @@ export async function gatherDaemonStatus(
|
||||
path: daemonSnapshot?.path ?? daemonConfigPath,
|
||||
exists: daemonSnapshot?.exists ?? false,
|
||||
valid: daemonSnapshot?.valid ?? true,
|
||||
...(daemonSnapshot?.issues?.length
|
||||
? { issues: daemonSnapshot.issues }
|
||||
: {}),
|
||||
...(daemonSnapshot?.issues?.length ? { issues: daemonSnapshot.issues } : {}),
|
||||
controlUi: daemonCfg.gateway?.controlUi,
|
||||
};
|
||||
const configMismatch = cliConfigSummary.path !== daemonConfigSummary.path;
|
||||
|
||||
const portFromArgs = parsePortFromArgs(command?.programArguments);
|
||||
const daemonPort =
|
||||
portFromArgs ?? resolveGatewayPort(daemonCfg, mergedDaemonEnv);
|
||||
const daemonPort = portFromArgs ?? resolveGatewayPort(daemonCfg, mergedDaemonEnv);
|
||||
const portSource: GatewayStatusSummary["portSource"] = portFromArgs
|
||||
? "service args"
|
||||
: "env/config";
|
||||
@@ -199,9 +183,7 @@ export async function gatherDaemonStatus(
|
||||
const tailnetIPv4 = pickPrimaryTailnetIPv4();
|
||||
const probeHost = pickProbeHostForBind(bindMode, tailnetIPv4, customBindHost);
|
||||
const probeUrlOverride =
|
||||
typeof opts.rpc.url === "string" && opts.rpc.url.trim().length > 0
|
||||
? opts.rpc.url.trim()
|
||||
: null;
|
||||
typeof opts.rpc.url === "string" && opts.rpc.url.trim().length > 0 ? opts.rpc.url.trim() : null;
|
||||
const probeUrl = probeUrlOverride ?? `ws://${probeHost}:${daemonPort}`;
|
||||
const probeNote =
|
||||
!probeUrlOverride && bindMode === "lan"
|
||||
@@ -241,8 +223,7 @@ export async function gatherDaemonStatus(
|
||||
).catch(() => []);
|
||||
|
||||
const timeoutMsRaw = Number.parseInt(String(opts.rpc.timeout ?? "10000"), 10);
|
||||
const timeoutMs =
|
||||
Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 10_000;
|
||||
const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 10_000;
|
||||
|
||||
const rpc = opts.probe
|
||||
? await probeGatewayStatus({
|
||||
@@ -262,15 +243,8 @@ export async function gatherDaemonStatus(
|
||||
: undefined;
|
||||
|
||||
let lastError: string | undefined;
|
||||
if (
|
||||
loaded &&
|
||||
runtime?.status === "running" &&
|
||||
portStatus &&
|
||||
portStatus.status !== "busy"
|
||||
) {
|
||||
lastError =
|
||||
(await readLastGatewayErrorLine(mergedDaemonEnv as NodeJS.ProcessEnv)) ??
|
||||
undefined;
|
||||
if (loaded && runtime?.status === "running" && portStatus && portStatus.status !== "busy") {
|
||||
lastError = (await readLastGatewayErrorLine(mergedDaemonEnv as NodeJS.ProcessEnv)) ?? undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -306,12 +280,8 @@ export async function gatherDaemonStatus(
|
||||
};
|
||||
}
|
||||
|
||||
export function renderPortDiagnosticsForCli(
|
||||
status: DaemonStatus,
|
||||
rpcOk?: boolean,
|
||||
): string[] {
|
||||
if (!status.port || !shouldReportPortUsage(status.port.status, rpcOk))
|
||||
return [];
|
||||
export function renderPortDiagnosticsForCli(status: DaemonStatus, rpcOk?: boolean): string[] {
|
||||
if (!status.port || !shouldReportPortUsage(status.port.status, rpcOk)) return [];
|
||||
return formatPortDiagnostics({
|
||||
port: status.port.port,
|
||||
status: status.port.status,
|
||||
|
||||
@@ -8,21 +8,14 @@ import { resolveGatewayLogPaths } from "../../daemon/launchd.js";
|
||||
import { getResolvedLoggerSettings } from "../../logging.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { colorize, isRich, theme } from "../../terminal/theme.js";
|
||||
import {
|
||||
formatRuntimeStatus,
|
||||
renderRuntimeHints,
|
||||
safeDaemonEnv,
|
||||
} from "./shared.js";
|
||||
import { formatRuntimeStatus, renderRuntimeHints, safeDaemonEnv } from "./shared.js";
|
||||
import {
|
||||
type DaemonStatus,
|
||||
renderPortDiagnosticsForCli,
|
||||
resolvePortListeningAddresses,
|
||||
} from "./status.gather.js";
|
||||
|
||||
export function printDaemonStatus(
|
||||
status: DaemonStatus,
|
||||
opts: { json: boolean },
|
||||
) {
|
||||
export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(JSON.stringify(status, null, 2));
|
||||
return;
|
||||
@@ -41,9 +34,7 @@ export function printDaemonStatus(
|
||||
const serviceStatus = service.loaded
|
||||
? okText(service.loadedText)
|
||||
: warnText(service.notLoadedText);
|
||||
defaultRuntime.log(
|
||||
`${label("Service:")} ${accent(service.label)} (${serviceStatus})`,
|
||||
);
|
||||
defaultRuntime.log(`${label("Service:")} ${accent(service.label)} (${serviceStatus})`);
|
||||
try {
|
||||
const logFile = getResolvedLoggerSettings().file;
|
||||
defaultRuntime.log(`${label("File logs:")} ${infoText(logFile)}`);
|
||||
@@ -56,14 +47,10 @@ export function printDaemonStatus(
|
||||
);
|
||||
}
|
||||
if (service.command?.sourcePath) {
|
||||
defaultRuntime.log(
|
||||
`${label("Service file:")} ${infoText(service.command.sourcePath)}`,
|
||||
);
|
||||
defaultRuntime.log(`${label("Service file:")} ${infoText(service.command.sourcePath)}`);
|
||||
}
|
||||
if (service.command?.workingDirectory) {
|
||||
defaultRuntime.log(
|
||||
`${label("Working dir:")} ${infoText(service.command.workingDirectory)}`,
|
||||
);
|
||||
defaultRuntime.log(`${label("Working dir:")} ${infoText(service.command.workingDirectory)}`);
|
||||
}
|
||||
const daemonEnvLines = safeDaemonEnv(service.command?.environment);
|
||||
if (daemonEnvLines.length > 0) {
|
||||
@@ -72,19 +59,13 @@ export function printDaemonStatus(
|
||||
spacer();
|
||||
|
||||
if (service.configAudit?.issues.length) {
|
||||
defaultRuntime.error(
|
||||
warnText("Service config looks out of date or non-standard."),
|
||||
);
|
||||
defaultRuntime.error(warnText("Service config looks out of date or non-standard."));
|
||||
for (const issue of service.configAudit.issues) {
|
||||
const detail = issue.detail ? ` (${issue.detail})` : "";
|
||||
defaultRuntime.error(
|
||||
`${warnText("Service config issue:")} ${issue.message}${detail}`,
|
||||
);
|
||||
defaultRuntime.error(`${warnText("Service config issue:")} ${issue.message}${detail}`);
|
||||
}
|
||||
defaultRuntime.error(
|
||||
warnText(
|
||||
'Recommendation: run "clawdbot doctor" (or "clawdbot doctor --repair").',
|
||||
),
|
||||
warnText('Recommendation: run "clawdbot doctor" (or "clawdbot doctor --repair").'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,9 +110,7 @@ export function printDaemonStatus(
|
||||
defaultRuntime.log(
|
||||
`${label("Gateway:")} bind=${infoText(status.gateway.bindMode)} (${infoText(bindHost)}), port=${infoText(String(status.gateway.port))} (${infoText(status.gateway.portSource)})`,
|
||||
);
|
||||
defaultRuntime.log(
|
||||
`${label("Probe target:")} ${infoText(status.gateway.probeUrl)}`,
|
||||
);
|
||||
defaultRuntime.log(`${label("Probe target:")} ${infoText(status.gateway.probeUrl)}`);
|
||||
const controlUiEnabled = status.config?.daemon?.controlUi?.enabled ?? true;
|
||||
if (!controlUiEnabled) {
|
||||
defaultRuntime.log(`${label("Dashboard:")} ${warnText("disabled")}`);
|
||||
@@ -145,9 +124,7 @@ export function printDaemonStatus(
|
||||
defaultRuntime.log(`${label("Dashboard:")} ${infoText(links.httpUrl)}`);
|
||||
}
|
||||
if (status.gateway.probeNote) {
|
||||
defaultRuntime.log(
|
||||
`${label("Probe note:")} ${infoText(status.gateway.probeNote)}`,
|
||||
);
|
||||
defaultRuntime.log(`${label("Probe note:")} ${infoText(status.gateway.probeNote)}`);
|
||||
}
|
||||
spacer();
|
||||
}
|
||||
@@ -163,21 +140,12 @@ export function printDaemonStatus(
|
||||
: runtimeStatus === "unknown"
|
||||
? theme.muted
|
||||
: theme.warn;
|
||||
defaultRuntime.log(
|
||||
`${label("Runtime:")} ${colorize(rich, runtimeColor, runtimeLine)}`,
|
||||
);
|
||||
defaultRuntime.log(`${label("Runtime:")} ${colorize(rich, runtimeColor, runtimeLine)}`);
|
||||
}
|
||||
|
||||
if (
|
||||
rpc &&
|
||||
!rpc.ok &&
|
||||
service.loaded &&
|
||||
service.runtime?.status === "running"
|
||||
) {
|
||||
if (rpc && !rpc.ok && service.loaded && service.runtime?.status === "running") {
|
||||
defaultRuntime.log(
|
||||
warnText(
|
||||
"Warm-up: launch agents can take a few seconds. Try again shortly.",
|
||||
),
|
||||
warnText("Warm-up: launch agents can take a few seconds. Try again shortly."),
|
||||
);
|
||||
}
|
||||
if (rpc) {
|
||||
@@ -203,9 +171,7 @@ export function printDaemonStatus(
|
||||
}
|
||||
} else if (service.loaded && service.runtime?.status === "stopped") {
|
||||
defaultRuntime.error(
|
||||
errorText(
|
||||
"Service is loaded but not running (likely exited immediately).",
|
||||
),
|
||||
errorText("Service is loaded but not running (likely exited immediately)."),
|
||||
);
|
||||
for (const hint of renderRuntimeHints(
|
||||
service.runtime,
|
||||
@@ -217,8 +183,7 @@ export function printDaemonStatus(
|
||||
}
|
||||
|
||||
if (service.runtime?.cachedLabel) {
|
||||
const env = (service.command?.environment ??
|
||||
process.env) as NodeJS.ProcessEnv;
|
||||
const env = (service.command?.environment ?? process.env) as NodeJS.ProcessEnv;
|
||||
const labelValue = resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
defaultRuntime.error(
|
||||
errorText(
|
||||
@@ -236,9 +201,7 @@ export function printDaemonStatus(
|
||||
if (status.port) {
|
||||
const addrs = resolvePortListeningAddresses(status);
|
||||
if (addrs.length > 0) {
|
||||
defaultRuntime.log(
|
||||
`${label("Listening:")} ${infoText(addrs.join(", "))}`,
|
||||
);
|
||||
defaultRuntime.log(`${label("Listening:")} ${infoText(addrs.join(", "))}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,23 +218,16 @@ export function printDaemonStatus(
|
||||
status.port.status !== "busy"
|
||||
) {
|
||||
defaultRuntime.error(
|
||||
errorText(
|
||||
`Gateway port ${status.port.port} is not listening (service appears running).`,
|
||||
),
|
||||
errorText(`Gateway port ${status.port.port} is not listening (service appears running).`),
|
||||
);
|
||||
if (status.lastError) {
|
||||
defaultRuntime.error(
|
||||
`${errorText("Last gateway error:")} ${status.lastError}`,
|
||||
);
|
||||
defaultRuntime.error(`${errorText("Last gateway error:")} ${status.lastError}`);
|
||||
}
|
||||
if (process.platform === "linux") {
|
||||
const env = (service.command?.environment ??
|
||||
process.env) as NodeJS.ProcessEnv;
|
||||
const env = (service.command?.environment ?? process.env) as NodeJS.ProcessEnv;
|
||||
const unit = resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
defaultRuntime.error(
|
||||
errorText(
|
||||
`Logs: journalctl --user -u ${unit}.service -n 200 --no-pager`,
|
||||
),
|
||||
errorText(`Logs: journalctl --user -u ${unit}.service -n 200 --no-pager`),
|
||||
);
|
||||
} else if (process.platform === "darwin") {
|
||||
const logs = resolveGatewayLogPaths(
|
||||
@@ -293,13 +249,9 @@ export function printDaemonStatus(
|
||||
}
|
||||
|
||||
if (extraServices.length > 0) {
|
||||
defaultRuntime.error(
|
||||
errorText("Other gateway-like services detected (best effort):"),
|
||||
);
|
||||
defaultRuntime.error(errorText("Other gateway-like services detected (best effort):"));
|
||||
for (const svc of extraServices) {
|
||||
defaultRuntime.error(
|
||||
`- ${errorText(svc.label)} (${svc.scope}, ${svc.detail})`,
|
||||
);
|
||||
defaultRuntime.error(`- ${errorText(svc.label)} (${svc.scope}, ${svc.detail})`);
|
||||
}
|
||||
for (const hint of renderGatewayServiceCleanupHints()) {
|
||||
defaultRuntime.error(`${errorText("Cleanup hint:")} ${hint}`);
|
||||
@@ -322,7 +274,5 @@ export function printDaemonStatus(
|
||||
}
|
||||
|
||||
defaultRuntime.log(`${label("Troubles:")} run clawdbot status`);
|
||||
defaultRuntime.log(
|
||||
`${label("Troubleshooting:")} https://docs.clawd.bot/troubleshooting`,
|
||||
);
|
||||
defaultRuntime.log(`${label("Troubleshooting:")} https://docs.clawd.bot/troubleshooting`);
|
||||
}
|
||||
|
||||
@@ -14,9 +14,7 @@ export async function runDaemonStatus(opts: DaemonStatusOptions) {
|
||||
printDaemonStatus(status, { json: Boolean(opts.json) });
|
||||
} catch (err) {
|
||||
const rich = isRich();
|
||||
defaultRuntime.error(
|
||||
colorize(rich, theme.error, `Daemon status failed: ${String(err)}`),
|
||||
);
|
||||
defaultRuntime.error(colorize(rich, theme.error, `Daemon status failed: ${String(err)}`));
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,7 @@ export function createDefaultDeps(): CliDeps {
|
||||
}
|
||||
|
||||
// Provider docking: extend this mapping when adding new outbound send deps.
|
||||
export function createOutboundSendDeps(
|
||||
deps: CliDeps,
|
||||
cfg: ClawdbotConfig,
|
||||
): OutboundSendDeps {
|
||||
export function createOutboundSendDeps(deps: CliDeps, cfg: ClawdbotConfig): OutboundSendDeps {
|
||||
return {
|
||||
sendWhatsApp: deps.sendMessageWhatsApp,
|
||||
sendTelegram: deps.sendMessageTelegram,
|
||||
|
||||
@@ -5,14 +5,8 @@ import path from "node:path";
|
||||
import type { Command } from "commander";
|
||||
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import {
|
||||
pickPrimaryTailnetIPv4,
|
||||
pickPrimaryTailnetIPv6,
|
||||
} from "../infra/tailnet.js";
|
||||
import {
|
||||
getWideAreaZonePath,
|
||||
WIDE_AREA_DISCOVERY_DOMAIN,
|
||||
} from "../infra/widearea-dns.js";
|
||||
import { pickPrimaryTailnetIPv4, pickPrimaryTailnetIPv6 } from "../infra/tailnet.js";
|
||||
import { getWideAreaZonePath, WIDE_AREA_DISCOVERY_DOMAIN } from "../infra/widearea-dns.js";
|
||||
|
||||
type RunOpts = { allowFailure?: boolean; inherit?: boolean };
|
||||
|
||||
@@ -50,9 +44,7 @@ function writeFileSudoIfNeeded(filePath: string, content: string): void {
|
||||
});
|
||||
if (res.error) throw res.error;
|
||||
if (res.status !== 0) {
|
||||
throw new Error(
|
||||
`sudo tee ${filePath} failed: exit ${res.status ?? "unknown"}`,
|
||||
);
|
||||
throw new Error(`sudo tee ${filePath} failed: exit ${res.status ?? "unknown"}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +94,7 @@ export function registerDnsCli(program: Command) {
|
||||
|
||||
dns
|
||||
.command("setup")
|
||||
.description(
|
||||
"Set up CoreDNS to serve clawdbot.internal for unicast DNS-SD (Wide-Area Bonjour)",
|
||||
)
|
||||
.description("Set up CoreDNS to serve clawdbot.internal for unicast DNS-SD (Wide-Area Bonjour)")
|
||||
.option(
|
||||
"--apply",
|
||||
"Install/update CoreDNS config and (re)start the service (requires sudo)",
|
||||
@@ -135,9 +125,7 @@ export function registerDnsCli(program: Command) {
|
||||
);
|
||||
console.log("");
|
||||
console.log("Tailscale admin (DNS → Nameservers):");
|
||||
console.log(
|
||||
`- Add nameserver: ${tailnetIPv4 ?? "<this machine's tailnet IPv4>"}`,
|
||||
);
|
||||
console.log(`- Add nameserver: ${tailnetIPv4 ?? "<this machine's tailnet IPv4>"}`);
|
||||
console.log(`- Restrict to domain (Split DNS): clawdbot.internal`);
|
||||
|
||||
if (!opts.apply) {
|
||||
@@ -150,9 +138,7 @@ export function registerDnsCli(program: Command) {
|
||||
throw new Error("dns setup is currently supported on macOS only");
|
||||
}
|
||||
if (!tailnetIPv4 && !tailnetIPv6) {
|
||||
throw new Error(
|
||||
"no tailnet IP detected; ensure Tailscale is running on this machine",
|
||||
);
|
||||
throw new Error("no tailnet IP detected; ensure Tailscale is running on this machine");
|
||||
}
|
||||
|
||||
const prefix = detectBrewPrefix();
|
||||
@@ -176,9 +162,7 @@ export function registerDnsCli(program: Command) {
|
||||
ensureImportLine(corefilePath, importGlob);
|
||||
}
|
||||
|
||||
const bindArgs = [tailnetIPv4, tailnetIPv6].filter((v): v is string =>
|
||||
Boolean(v?.trim()),
|
||||
);
|
||||
const bindArgs = [tailnetIPv4, tailnetIPv6].filter((v): v is string => Boolean(v?.trim()));
|
||||
|
||||
const server = [
|
||||
`${WIDE_AREA_DISCOVERY_DOMAIN.replace(/\.$/, "")}:53 {`,
|
||||
|
||||
@@ -12,11 +12,7 @@ export function registerDocsCli(program: Command) {
|
||||
.argument("[query...]", "Search query")
|
||||
.addHelpText(
|
||||
"after",
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink(
|
||||
"/hubs",
|
||||
"docs.clawd.bot/hubs",
|
||||
)}\n`,
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/hubs", "docs.clawd.bot/hubs")}\n`,
|
||||
)
|
||||
.action(async (queryParts: string[]) => {
|
||||
try {
|
||||
|
||||
@@ -53,8 +53,7 @@ vi.mock("../gateway/call.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/server.js", () => ({
|
||||
startGatewayServer: (port: number, opts?: unknown) =>
|
||||
startGatewayServer(port, opts),
|
||||
startGatewayServer: (port: number, opts?: unknown) => startGatewayServer(port, opts),
|
||||
}));
|
||||
|
||||
vi.mock("../globals.js", () => ({
|
||||
@@ -111,10 +110,9 @@ describe("gateway-cli coverage", () => {
|
||||
program.exitOverride();
|
||||
registerGatewayCli(program);
|
||||
|
||||
await program.parseAsync(
|
||||
["gateway", "call", "health", "--params", '{"x":1}', "--json"],
|
||||
{ from: "user" },
|
||||
);
|
||||
await program.parseAsync(["gateway", "call", "health", "--params", '{"x":1}', "--json"], {
|
||||
from: "user",
|
||||
});
|
||||
|
||||
expect(callGateway).toHaveBeenCalledTimes(1);
|
||||
expect(runtimeLogs.join("\n")).toContain('"ok": true');
|
||||
@@ -235,10 +233,7 @@ describe("gateway-cli coverage", () => {
|
||||
registerGatewayCli(program);
|
||||
|
||||
await expect(
|
||||
program.parseAsync(
|
||||
["gateway", "call", "status", "--params", "not-json"],
|
||||
{ from: "user" },
|
||||
),
|
||||
program.parseAsync(["gateway", "call", "status", "--params", "not-json"], { from: "user" }),
|
||||
).rejects.toThrow("__exit__:1");
|
||||
|
||||
expect(callGateway).not.toHaveBeenCalled();
|
||||
@@ -283,18 +278,15 @@ describe("gateway-cli coverage", () => {
|
||||
const beforeSigterm = new Set(process.listeners("SIGTERM"));
|
||||
const beforeSigint = new Set(process.listeners("SIGINT"));
|
||||
await expect(
|
||||
programStartFail.parseAsync(
|
||||
["gateway", "--port", "18789", "--allow-unconfigured"],
|
||||
{ from: "user" },
|
||||
),
|
||||
programStartFail.parseAsync(["gateway", "--port", "18789", "--allow-unconfigured"], {
|
||||
from: "user",
|
||||
}),
|
||||
).rejects.toThrow("__exit__:1");
|
||||
for (const listener of process.listeners("SIGTERM")) {
|
||||
if (!beforeSigterm.has(listener))
|
||||
process.removeListener("SIGTERM", listener);
|
||||
if (!beforeSigterm.has(listener)) process.removeListener("SIGTERM", listener);
|
||||
}
|
||||
for (const listener of process.listeners("SIGINT")) {
|
||||
if (!beforeSigint.has(listener))
|
||||
process.removeListener("SIGINT", listener);
|
||||
if (!beforeSigint.has(listener)) process.removeListener("SIGINT", listener);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { Command } from "commander";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../../utils/message-channel.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
|
||||
import { withProgress } from "../progress.js";
|
||||
|
||||
export type GatewayRpcOpts = {
|
||||
@@ -17,21 +14,14 @@ export type GatewayRpcOpts = {
|
||||
|
||||
export const gatewayCallOpts = (cmd: Command) =>
|
||||
cmd
|
||||
.option(
|
||||
"--url <url>",
|
||||
"Gateway WebSocket URL (defaults to gateway.remote.url when configured)",
|
||||
)
|
||||
.option("--url <url>", "Gateway WebSocket URL (defaults to gateway.remote.url when configured)")
|
||||
.option("--token <token>", "Gateway token (if required)")
|
||||
.option("--password <password>", "Gateway password (password auth)")
|
||||
.option("--timeout <ms>", "Timeout in ms", "10000")
|
||||
.option("--expect-final", "Wait for final response (agent)", false)
|
||||
.option("--json", "Output JSON", false);
|
||||
|
||||
export const callGatewayCli = async (
|
||||
method: string,
|
||||
opts: GatewayRpcOpts,
|
||||
params?: unknown,
|
||||
) =>
|
||||
export const callGatewayCli = async (method: string, opts: GatewayRpcOpts, params?: unknown) =>
|
||||
withProgress(
|
||||
{
|
||||
label: `Gateway ${method}`,
|
||||
|
||||
@@ -18,15 +18,9 @@ const DEV_TEMPLATE_DIR = path.resolve(
|
||||
"../../../docs/reference/templates",
|
||||
);
|
||||
|
||||
async function loadDevTemplate(
|
||||
name: string,
|
||||
fallback: string,
|
||||
): Promise<string> {
|
||||
async function loadDevTemplate(name: string, fallback: string): Promise<string> {
|
||||
try {
|
||||
const raw = await fs.promises.readFile(
|
||||
path.join(DEV_TEMPLATE_DIR, name),
|
||||
"utf-8",
|
||||
);
|
||||
const raw = await fs.promises.readFile(path.join(DEV_TEMPLATE_DIR, name), "utf-8");
|
||||
if (!raw.startsWith("---")) return raw;
|
||||
const endIndex = raw.indexOf("\n---", 3);
|
||||
if (endIndex === -1) return raw;
|
||||
@@ -36,9 +30,7 @@ async function loadDevTemplate(
|
||||
}
|
||||
}
|
||||
|
||||
const resolveDevWorkspaceDir = (
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string => {
|
||||
const resolveDevWorkspaceDir = (env: NodeJS.ProcessEnv = process.env): string => {
|
||||
const baseDir = resolveDefaultAgentWorkspaceDir(env, os.homedir);
|
||||
const profile = env.CLAWDBOT_PROFILE?.trim().toLowerCase();
|
||||
if (profile === "dev") return baseDir;
|
||||
|
||||
@@ -6,10 +6,7 @@ export type GatewayDiscoverOpts = {
|
||||
json?: boolean;
|
||||
};
|
||||
|
||||
export function parseDiscoverTimeoutMs(
|
||||
raw: unknown,
|
||||
fallbackMs: number,
|
||||
): number {
|
||||
export function parseDiscoverTimeoutMs(raw: unknown, fallbackMs: number): number {
|
||||
if (raw === undefined || raw === null) return fallbackMs;
|
||||
const value =
|
||||
typeof raw === "string"
|
||||
@@ -38,9 +35,7 @@ export function pickGatewayPort(beacon: GatewayBonjourBeacon): number {
|
||||
return port > 0 ? port : 18789;
|
||||
}
|
||||
|
||||
export function dedupeBeacons(
|
||||
beacons: GatewayBonjourBeacon[],
|
||||
): GatewayBonjourBeacon[] {
|
||||
export function dedupeBeacons(beacons: GatewayBonjourBeacon[]): GatewayBonjourBeacon[] {
|
||||
const out: GatewayBonjourBeacon[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const b of beacons) {
|
||||
@@ -61,15 +56,8 @@ export function dedupeBeacons(
|
||||
return out;
|
||||
}
|
||||
|
||||
export function renderBeaconLines(
|
||||
beacon: GatewayBonjourBeacon,
|
||||
rich: boolean,
|
||||
): string[] {
|
||||
const nameRaw = (
|
||||
beacon.displayName ||
|
||||
beacon.instanceName ||
|
||||
"Gateway"
|
||||
).trim();
|
||||
export function renderBeaconLines(beacon: GatewayBonjourBeacon, rich: boolean): string[] {
|
||||
const nameRaw = (beacon.displayName || beacon.instanceName || "Gateway").trim();
|
||||
const domainRaw = (beacon.domain || "local.").trim();
|
||||
|
||||
const title = colorize(rich, theme.accentBright, nameRaw);
|
||||
@@ -82,9 +70,7 @@ export function renderBeaconLines(
|
||||
const lines = [`- ${title} ${domain}`];
|
||||
|
||||
if (beacon.tailnetDns) {
|
||||
lines.push(
|
||||
` ${colorize(rich, theme.info, "tailnet")}: ${beacon.tailnetDns}`,
|
||||
);
|
||||
lines.push(` ${colorize(rich, theme.info, "tailnet")}: ${beacon.tailnetDns}`);
|
||||
}
|
||||
if (beacon.lanHost) {
|
||||
lines.push(` ${colorize(rich, theme.info, "lan")}: ${beacon.lanHost}`);
|
||||
@@ -94,15 +80,11 @@ export function renderBeaconLines(
|
||||
}
|
||||
|
||||
if (wsUrl) {
|
||||
lines.push(
|
||||
` ${colorize(rich, theme.muted, "ws")}: ${colorize(rich, theme.command, wsUrl)}`,
|
||||
);
|
||||
lines.push(` ${colorize(rich, theme.muted, "ws")}: ${colorize(rich, theme.command, wsUrl)}`);
|
||||
}
|
||||
if (typeof beacon.sshPort === "number" && beacon.sshPort > 0 && host) {
|
||||
const ssh = `ssh -N -L 18789:127.0.0.1:18789 <user>@${host} -p ${beacon.sshPort}`;
|
||||
lines.push(
|
||||
` ${colorize(rich, theme.muted, "ssh")}: ${colorize(rich, theme.command, ssh)}`,
|
||||
);
|
||||
lines.push(` ${colorize(rich, theme.muted, "ssh")}: ${colorize(rich, theme.command, ssh)}`);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { Command } from "commander";
|
||||
import { gatewayStatusCommand } from "../../commands/gateway-status.js";
|
||||
import {
|
||||
formatHealthChannelLines,
|
||||
type HealthSummary,
|
||||
} from "../../commands/health.js";
|
||||
import { formatHealthChannelLines, type HealthSummary } from "../../commands/health.js";
|
||||
import { discoverGatewayBeacons } from "../../infra/bonjour-discovery.js";
|
||||
import { WIDE_AREA_DISCOVERY_DOMAIN } from "../../infra/widearea-dns.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
@@ -28,11 +25,7 @@ export function registerGatewayCli(program: Command) {
|
||||
.description("Run the WebSocket Gateway")
|
||||
.addHelpText(
|
||||
"after",
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink(
|
||||
"/gateway",
|
||||
"docs.clawd.bot/gateway",
|
||||
)}\n`,
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/gateway", "docs.clawd.bot/gateway")}\n`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -48,10 +41,7 @@ export function registerGatewayCli(program: Command) {
|
||||
gateway
|
||||
.command("call")
|
||||
.description("Call a Gateway method")
|
||||
.argument(
|
||||
"<method>",
|
||||
"Method name (health/status/system-presence/cron.*)",
|
||||
)
|
||||
.argument("<method>", "Method name (health/status/system-presence/cron.*)")
|
||||
.option("--params <json>", "JSON object string for params", "{}")
|
||||
.action(async (method, opts) => {
|
||||
try {
|
||||
@@ -86,11 +76,8 @@ export function registerGatewayCli(program: Command) {
|
||||
}
|
||||
const rich = isRich();
|
||||
const obj =
|
||||
result && typeof result === "object"
|
||||
? (result as Record<string, unknown>)
|
||||
: {};
|
||||
const durationMs =
|
||||
typeof obj.durationMs === "number" ? obj.durationMs : null;
|
||||
result && typeof result === "object" ? (result as Record<string, unknown>) : {};
|
||||
const durationMs = typeof obj.durationMs === "number" ? obj.durationMs : null;
|
||||
defaultRuntime.log(colorize(rich, theme.heading, "Gateway Health"));
|
||||
defaultRuntime.log(
|
||||
`${colorize(rich, theme.success, "OK")}${durationMs != null ? ` (${durationMs}ms)` : ""}`,
|
||||
@@ -109,23 +96,11 @@ export function registerGatewayCli(program: Command) {
|
||||
|
||||
gateway
|
||||
.command("status")
|
||||
.description(
|
||||
"Show gateway reachability + discovery + health + status summary (local + remote)",
|
||||
)
|
||||
.option(
|
||||
"--url <url>",
|
||||
"Explicit Gateway WebSocket URL (still probes localhost)",
|
||||
)
|
||||
.option(
|
||||
"--ssh <target>",
|
||||
"SSH target for remote gateway tunnel (user@host or user@host:port)",
|
||||
)
|
||||
.description("Show gateway reachability + discovery + health + status summary (local + remote)")
|
||||
.option("--url <url>", "Explicit Gateway WebSocket URL (still probes localhost)")
|
||||
.option("--ssh <target>", "SSH target for remote gateway tunnel (user@host or user@host:port)")
|
||||
.option("--ssh-identity <path>", "SSH identity file path")
|
||||
.option(
|
||||
"--ssh-auto",
|
||||
"Try to derive an SSH target from Bonjour discovery",
|
||||
false,
|
||||
)
|
||||
.option("--ssh-auto", "Try to derive an SSH target from Bonjour discovery", false)
|
||||
.option("--token <token>", "Gateway token (applies to all probes)")
|
||||
.option("--password <password>", "Gateway password (applies to all probes)")
|
||||
.option("--timeout <ms>", "Overall probe budget in ms", "3000")
|
||||
|
||||
@@ -27,9 +27,7 @@ export async function runGatewayLoop(params: {
|
||||
}
|
||||
shuttingDown = true;
|
||||
const isRestart = action === "restart";
|
||||
gatewayLog.info(
|
||||
`received ${signal}; ${isRestart ? "restarting" : "shutting down"}`,
|
||||
);
|
||||
gatewayLog.info(`received ${signal}; ${isRestart ? "restarting" : "shutting down"}`);
|
||||
|
||||
const forceExitTimer = setTimeout(() => {
|
||||
gatewayLog.error("shutdown timed out; exiting without full cleanup");
|
||||
|
||||
@@ -15,10 +15,7 @@ import { setGatewayWsLogStyle } from "../../gateway/ws-logging.js";
|
||||
import { setVerbose } from "../../globals.js";
|
||||
import { GatewayLockError } from "../../infra/gateway-lock.js";
|
||||
import { formatPortDiagnostics, inspectPortUsage } from "../../infra/ports.js";
|
||||
import {
|
||||
createSubsystemLogger,
|
||||
setConsoleSubsystemFilter,
|
||||
} from "../../logging.js";
|
||||
import { createSubsystemLogger, setConsoleSubsystemFilter } from "../../logging.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { forceFreePortAndWait } from "../ports.js";
|
||||
import { ensureDevGatewayConfig } from "./dev.js";
|
||||
@@ -57,12 +54,8 @@ type GatewayRunParams = {
|
||||
|
||||
const gatewayLog = createSubsystemLogger("gateway");
|
||||
|
||||
async function runGatewayCommand(
|
||||
opts: GatewayRunOpts,
|
||||
params: GatewayRunParams = {},
|
||||
) {
|
||||
const isDevProfile =
|
||||
process.env.CLAWDBOT_PROFILE?.trim().toLowerCase() === "dev";
|
||||
async function runGatewayCommand(opts: GatewayRunOpts, params: GatewayRunParams = {}) {
|
||||
const isDevProfile = process.env.CLAWDBOT_PROFILE?.trim().toLowerCase() === "dev";
|
||||
const devMode = Boolean(opts.dev) || isDevProfile;
|
||||
if (opts.reset && !devMode) {
|
||||
defaultRuntime.error("Use --reset with --dev.");
|
||||
@@ -81,9 +74,7 @@ async function runGatewayCommand(
|
||||
setConsoleSubsystemFilter(["agent/claude-cli"]);
|
||||
process.env.CLAWDBOT_CLAUDE_CLI_LOG_OUTPUT = "1";
|
||||
}
|
||||
const wsLogRaw = (opts.compact ? "compact" : opts.wsLog) as
|
||||
| string
|
||||
| undefined;
|
||||
const wsLogRaw = (opts.compact ? "compact" : opts.wsLog) as string | undefined;
|
||||
const wsLogStyle: GatewayWsLogStyle =
|
||||
wsLogRaw === "compact" ? "compact" : wsLogRaw === "full" ? "full" : "auto";
|
||||
if (
|
||||
@@ -122,12 +113,11 @@ async function runGatewayCommand(
|
||||
}
|
||||
if (opts.force) {
|
||||
try {
|
||||
const { killed, waitedMs, escalatedToSigkill } =
|
||||
await forceFreePortAndWait(port, {
|
||||
timeoutMs: 2000,
|
||||
intervalMs: 100,
|
||||
sigtermTimeoutMs: 700,
|
||||
});
|
||||
const { killed, waitedMs, escalatedToSigkill } = await forceFreePortAndWait(port, {
|
||||
timeoutMs: 2000,
|
||||
intervalMs: 100,
|
||||
sigtermTimeoutMs: 700,
|
||||
});
|
||||
if (killed.length === 0) {
|
||||
gatewayLog.info(`force: no listeners on port ${port}`);
|
||||
} else {
|
||||
@@ -137,14 +127,10 @@ async function runGatewayCommand(
|
||||
);
|
||||
}
|
||||
if (escalatedToSigkill) {
|
||||
gatewayLog.info(
|
||||
`force: escalated to SIGKILL while freeing port ${port}`,
|
||||
);
|
||||
gatewayLog.info(`force: escalated to SIGKILL while freeing port ${port}`);
|
||||
}
|
||||
if (waitedMs > 0) {
|
||||
gatewayLog.info(
|
||||
`force: waited ${waitedMs}ms for port ${port} to free`,
|
||||
);
|
||||
gatewayLog.info(`force: waited ${waitedMs}ms for port ${port} to free`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -167,15 +153,11 @@ async function runGatewayCommand(
|
||||
}
|
||||
const tailscaleRaw = toOptionString(opts.tailscale);
|
||||
const tailscaleMode =
|
||||
tailscaleRaw === "off" ||
|
||||
tailscaleRaw === "serve" ||
|
||||
tailscaleRaw === "funnel"
|
||||
tailscaleRaw === "off" || tailscaleRaw === "serve" || tailscaleRaw === "funnel"
|
||||
? tailscaleRaw
|
||||
: null;
|
||||
if (tailscaleRaw && !tailscaleMode) {
|
||||
defaultRuntime.error(
|
||||
'Invalid --tailscale (use "off", "serve", or "funnel")',
|
||||
);
|
||||
defaultRuntime.error('Invalid --tailscale (use "off", "serve", or "funnel")');
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
@@ -199,16 +181,11 @@ async function runGatewayCommand(
|
||||
}
|
||||
const bindRaw = toOptionString(opts.bind) ?? cfg.gateway?.bind ?? "loopback";
|
||||
const bind =
|
||||
bindRaw === "loopback" ||
|
||||
bindRaw === "lan" ||
|
||||
bindRaw === "auto" ||
|
||||
bindRaw === "custom"
|
||||
bindRaw === "loopback" || bindRaw === "lan" || bindRaw === "auto" || bindRaw === "custom"
|
||||
? bindRaw
|
||||
: null;
|
||||
if (!bind) {
|
||||
defaultRuntime.error(
|
||||
'Invalid --bind (use "loopback", "lan", "auto", or "custom")',
|
||||
);
|
||||
defaultRuntime.error('Invalid --bind (use "loopback", "lan", "auto", or "custom")');
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
@@ -231,9 +208,7 @@ async function runGatewayCommand(
|
||||
const passwordValue = resolvedAuth.password;
|
||||
const authHints: string[] = [];
|
||||
if (miskeys.hasGatewayToken) {
|
||||
authHints.push(
|
||||
'Found "gateway.token" in config. Use "gateway.auth.token" instead.',
|
||||
);
|
||||
authHints.push('Found "gateway.token" in config. Use "gateway.auth.token" instead.');
|
||||
}
|
||||
if (miskeys.hasRemoteToken) {
|
||||
authHints.push(
|
||||
@@ -306,9 +281,7 @@ async function runGatewayCommand(
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof GatewayLockError ||
|
||||
(err &&
|
||||
typeof err === "object" &&
|
||||
(err as { name?: string }).name === "GatewayLockError")
|
||||
(err && typeof err === "object" && (err as { name?: string }).name === "GatewayLockError")
|
||||
) {
|
||||
const errMessage = describeUnknownError(err);
|
||||
defaultRuntime.error(
|
||||
@@ -333,10 +306,7 @@ async function runGatewayCommand(
|
||||
}
|
||||
}
|
||||
|
||||
export function addGatewayRunCommand(
|
||||
cmd: Command,
|
||||
params: GatewayRunParams = {},
|
||||
): Command {
|
||||
export function addGatewayRunCommand(cmd: Command, params: GatewayRunParams = {}): Command {
|
||||
return cmd
|
||||
.option("--port <port>", "Port for the gateway WebSocket")
|
||||
.option(
|
||||
@@ -349,10 +319,7 @@ export function addGatewayRunCommand(
|
||||
)
|
||||
.option("--auth <mode>", 'Gateway auth mode ("token"|"password")')
|
||||
.option("--password <password>", "Password for auth mode=password")
|
||||
.option(
|
||||
"--tailscale <mode>",
|
||||
'Tailscale exposure mode ("off"|"serve"|"funnel")',
|
||||
)
|
||||
.option("--tailscale <mode>", 'Tailscale exposure mode ("off"|"serve"|"funnel")')
|
||||
.option(
|
||||
"--tailscale-reset-on-exit",
|
||||
"Reset Tailscale serve/funnel configuration on shutdown",
|
||||
@@ -363,32 +330,20 @@ export function addGatewayRunCommand(
|
||||
"Allow gateway start without gateway.mode=local in config",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--dev",
|
||||
"Create a dev config + workspace if missing (no BOOTSTRAP.md)",
|
||||
false,
|
||||
)
|
||||
.option("--dev", "Create a dev config + workspace if missing (no BOOTSTRAP.md)", false)
|
||||
.option(
|
||||
"--reset",
|
||||
"Reset dev config + credentials + sessions + workspace (requires --dev)",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--force",
|
||||
"Kill any existing listener on the target port before starting",
|
||||
false,
|
||||
)
|
||||
.option("--force", "Kill any existing listener on the target port before starting", false)
|
||||
.option("--verbose", "Verbose logging to stdout/stderr", false)
|
||||
.option(
|
||||
"--claude-cli-logs",
|
||||
"Only show claude-cli logs in the console (includes stdout/stderr)",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--ws-log <style>",
|
||||
'WebSocket log style ("auto"|"full"|"compact")',
|
||||
"auto",
|
||||
)
|
||||
.option("--ws-log <style>", 'WebSocket log style ("auto"|"full"|"compact")', "auto")
|
||||
.option("--compact", 'Alias for "--ws-log compact"', false)
|
||||
.option("--raw-stream", "Log raw model stream events to jsonl", false)
|
||||
.option("--raw-stream-path <path>", "Raw stream jsonl path")
|
||||
|
||||
@@ -22,8 +22,7 @@ export function parsePort(raw: unknown): number | null {
|
||||
|
||||
export const toOptionString = (value: unknown): string | undefined => {
|
||||
if (typeof value === "string") return value;
|
||||
if (typeof value === "number" || typeof value === "bigint")
|
||||
return value.toString();
|
||||
if (typeof value === "number" || typeof value === "bigint") return value.toString();
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -59,15 +58,11 @@ export function extractGatewayMiskeys(parsed: unknown): {
|
||||
const hasGatewayToken = "token" in (gateway as Record<string, unknown>);
|
||||
const remote = (gateway as Record<string, unknown>).remote;
|
||||
const hasRemoteToken =
|
||||
remote && typeof remote === "object"
|
||||
? "token" in (remote as Record<string, unknown>)
|
||||
: false;
|
||||
remote && typeof remote === "object" ? "token" in (remote as Record<string, unknown>) : false;
|
||||
return { hasGatewayToken, hasRemoteToken };
|
||||
}
|
||||
|
||||
export function renderGatewayServiceStopHints(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
export function renderGatewayServiceStopHints(env: NodeJS.ProcessEnv = process.env): string[] {
|
||||
const profile = env.CLAWDBOT_PROFILE;
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { Command } from "commander";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-channel.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||
import { withProgress } from "./progress.js";
|
||||
|
||||
export type GatewayRpcOpts = {
|
||||
@@ -16,10 +13,7 @@ export type GatewayRpcOpts = {
|
||||
|
||||
export function addGatewayClientOptions(cmd: Command) {
|
||||
return cmd
|
||||
.option(
|
||||
"--url <url>",
|
||||
"Gateway WebSocket URL (defaults to gateway.remote.url when configured)",
|
||||
)
|
||||
.option("--url <url>", "Gateway WebSocket URL (defaults to gateway.remote.url when configured)")
|
||||
.option("--token <token>", "Gateway token (if required)")
|
||||
.option("--timeout <ms>", "Timeout in ms", "10000")
|
||||
.option("--expect-final", "Wait for final response (agent)", false);
|
||||
|
||||
@@ -77,9 +77,7 @@ describe("gateway SIGTERM", () => {
|
||||
|
||||
it("exits 0 on SIGTERM", { timeout: 180_000 }, async () => {
|
||||
const port = await getFreePort();
|
||||
const stateDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "clawdbot-gateway-test-"),
|
||||
);
|
||||
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-gateway-test-"));
|
||||
const configPath = path.join(stateDir, "clawdbot.json");
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
@@ -134,14 +132,9 @@ describe("gateway SIGTERM", () => {
|
||||
const result = await new Promise<{
|
||||
code: number | null;
|
||||
signal: NodeJS.Signals | null;
|
||||
}>((resolve) =>
|
||||
proc.once("exit", (code, signal) => resolve({ code, signal })),
|
||||
);
|
||||
}>((resolve) => proc.once("exit", (code, signal) => resolve({ code, signal })));
|
||||
|
||||
if (
|
||||
result.code !== 0 &&
|
||||
!(result.code === null && result.signal === "SIGTERM")
|
||||
) {
|
||||
if (result.code !== 0 && !(result.code === null && result.signal === "SIGTERM")) {
|
||||
const stdout = out.join("");
|
||||
const stderr = err.join("");
|
||||
throw new Error(
|
||||
|
||||
@@ -20,13 +20,9 @@ import {
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
|
||||
export function registerHooksCli(program: Command) {
|
||||
const hooks = program
|
||||
.command("hooks")
|
||||
.description("Webhook helpers and hook-based integrations");
|
||||
const hooks = program.command("hooks").description("Webhook helpers and hook-based integrations");
|
||||
|
||||
const gmail = hooks
|
||||
.command("gmail")
|
||||
.description("Gmail Pub/Sub hooks (via gogcli)");
|
||||
const gmail = hooks.command("gmail").description("Gmail Pub/Sub hooks (via gogcli)");
|
||||
|
||||
gmail
|
||||
.command("setup")
|
||||
@@ -34,42 +30,22 @@ export function registerHooksCli(program: Command) {
|
||||
.requiredOption("--account <email>", "Gmail account to watch")
|
||||
.option("--project <id>", "GCP project id (OAuth client owner)")
|
||||
.option("--topic <name>", "Pub/Sub topic name", DEFAULT_GMAIL_TOPIC)
|
||||
.option(
|
||||
"--subscription <name>",
|
||||
"Pub/Sub subscription name",
|
||||
DEFAULT_GMAIL_SUBSCRIPTION,
|
||||
)
|
||||
.option("--subscription <name>", "Pub/Sub subscription name", DEFAULT_GMAIL_SUBSCRIPTION)
|
||||
.option("--label <label>", "Gmail label to watch", DEFAULT_GMAIL_LABEL)
|
||||
.option("--hook-url <url>", "Clawdbot hook URL")
|
||||
.option("--hook-token <token>", "Clawdbot hook token")
|
||||
.option("--push-token <token>", "Push token for gog watch serve")
|
||||
.option(
|
||||
"--bind <host>",
|
||||
"gog watch serve bind host",
|
||||
DEFAULT_GMAIL_SERVE_BIND,
|
||||
)
|
||||
.option(
|
||||
"--port <port>",
|
||||
"gog watch serve port",
|
||||
String(DEFAULT_GMAIL_SERVE_PORT),
|
||||
)
|
||||
.option("--bind <host>", "gog watch serve bind host", DEFAULT_GMAIL_SERVE_BIND)
|
||||
.option("--port <port>", "gog watch serve port", String(DEFAULT_GMAIL_SERVE_PORT))
|
||||
.option("--path <path>", "gog watch serve path", DEFAULT_GMAIL_SERVE_PATH)
|
||||
.option("--include-body", "Include email body snippets", true)
|
||||
.option(
|
||||
"--max-bytes <n>",
|
||||
"Max bytes for body snippets",
|
||||
String(DEFAULT_GMAIL_MAX_BYTES),
|
||||
)
|
||||
.option("--max-bytes <n>", "Max bytes for body snippets", String(DEFAULT_GMAIL_MAX_BYTES))
|
||||
.option(
|
||||
"--renew-minutes <n>",
|
||||
"Renew watch every N minutes",
|
||||
String(DEFAULT_GMAIL_RENEW_MINUTES),
|
||||
)
|
||||
.option(
|
||||
"--tailscale <mode>",
|
||||
"Expose push endpoint via tailscale (funnel|serve|off)",
|
||||
"funnel",
|
||||
)
|
||||
.option("--tailscale <mode>", "Expose push endpoint via tailscale (funnel|serve|off)", "funnel")
|
||||
.option("--tailscale-path <path>", "Path for tailscale serve/funnel")
|
||||
.option(
|
||||
"--tailscale-target <target>",
|
||||
@@ -103,10 +79,7 @@ export function registerHooksCli(program: Command) {
|
||||
.option("--include-body", "Include email body snippets")
|
||||
.option("--max-bytes <n>", "Max bytes for body snippets")
|
||||
.option("--renew-minutes <n>", "Renew watch every N minutes")
|
||||
.option(
|
||||
"--tailscale <mode>",
|
||||
"Expose push endpoint via tailscale (funnel|serve|off)",
|
||||
)
|
||||
.option("--tailscale <mode>", "Expose push endpoint via tailscale (funnel|serve|off)")
|
||||
.option("--tailscale-path <path>", "Path for tailscale serve/funnel")
|
||||
.option(
|
||||
"--tailscale-target <target>",
|
||||
@@ -123,9 +96,7 @@ export function registerHooksCli(program: Command) {
|
||||
});
|
||||
}
|
||||
|
||||
function parseGmailSetupOptions(
|
||||
raw: Record<string, unknown>,
|
||||
): GmailSetupOptions {
|
||||
function parseGmailSetupOptions(raw: Record<string, unknown>): GmailSetupOptions {
|
||||
const accountRaw = raw.account;
|
||||
const account = typeof accountRaw === "string" ? accountRaw.trim() : "";
|
||||
if (!account) throw new Error("--account is required");
|
||||
|
||||
@@ -53,10 +53,7 @@ async function fetchLogs(
|
||||
return payload as LogsTailPayload;
|
||||
}
|
||||
|
||||
function formatLogTimestamp(
|
||||
value?: string,
|
||||
mode: "pretty" | "plain" = "plain",
|
||||
) {
|
||||
function formatLogTimestamp(value?: string, mode: "pretty" | "plain" = "plain") {
|
||||
if (!value) return "";
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.getTime())) return value;
|
||||
@@ -74,10 +71,7 @@ function formatLogLine(
|
||||
const parsed = parseLogLine(raw);
|
||||
if (!parsed) return raw;
|
||||
const label = parsed.subsystem ?? parsed.module ?? "";
|
||||
const time = formatLogTimestamp(
|
||||
parsed.time,
|
||||
opts.pretty ? "pretty" : "plain",
|
||||
);
|
||||
const time = formatLogTimestamp(parsed.time, opts.pretty ? "pretty" : "plain");
|
||||
const level = parsed.level ?? "";
|
||||
const levelLabel = level.padEnd(5).trim();
|
||||
const message = parsed.message || parsed.raw;
|
||||
@@ -158,11 +152,7 @@ export function registerLogsCli(program: Command) {
|
||||
.option("--no-color", "Disable ANSI colors")
|
||||
.addHelpText(
|
||||
"after",
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink(
|
||||
"/logging",
|
||||
"docs.clawd.bot/logging",
|
||||
)}\n`,
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/logging", "docs.clawd.bot/logging")}\n`,
|
||||
);
|
||||
|
||||
addGatewayClientOptions(logs);
|
||||
@@ -216,9 +206,7 @@ export function registerLogsCli(program: Command) {
|
||||
}
|
||||
} else {
|
||||
if (first && payload.file) {
|
||||
const prefix = pretty
|
||||
? colorize(rich, theme.muted, "Log file:")
|
||||
: "Log file:";
|
||||
const prefix = pretty ? colorize(rich, theme.muted, "Log file:") : "Log file:";
|
||||
defaultRuntime.log(`${prefix} ${payload.file}`);
|
||||
}
|
||||
for (const line of lines) {
|
||||
|
||||
@@ -41,9 +41,7 @@ export function registerMemoryCli(program: Command) {
|
||||
const lines = [
|
||||
`${chalk.bold.cyan("Memory Search")} (${agentId})`,
|
||||
`Provider: ${status.provider} (requested: ${status.requestedProvider})`,
|
||||
status.fallback
|
||||
? chalk.yellow(`Fallback: ${status.fallback.from}`)
|
||||
: null,
|
||||
status.fallback ? chalk.yellow(`Fallback: ${status.fallback.from}`) : null,
|
||||
`Files: ${status.files}`,
|
||||
`Chunks: ${status.chunks}`,
|
||||
`Dirty: ${status.dirty ? "yes" : "no"}`,
|
||||
|
||||
@@ -14,39 +14,30 @@ vi.mock("../commands/models.js", async () => {
|
||||
});
|
||||
|
||||
describe("models cli", () => {
|
||||
it(
|
||||
"registers github-copilot login command",
|
||||
{ timeout: 15_000 },
|
||||
async () => {
|
||||
const { Command } = await import("commander");
|
||||
const { registerModelsCli } = await import("./models-cli.js");
|
||||
it("registers github-copilot login command", { timeout: 15_000 }, async () => {
|
||||
const { Command } = await import("commander");
|
||||
const { registerModelsCli } = await import("./models-cli.js");
|
||||
|
||||
const program = new Command();
|
||||
registerModelsCli(program);
|
||||
const program = new Command();
|
||||
registerModelsCli(program);
|
||||
|
||||
const models = program.commands.find((cmd) => cmd.name() === "models");
|
||||
expect(models).toBeTruthy();
|
||||
const models = program.commands.find((cmd) => cmd.name() === "models");
|
||||
expect(models).toBeTruthy();
|
||||
|
||||
const auth = models?.commands.find((cmd) => cmd.name() === "auth");
|
||||
expect(auth).toBeTruthy();
|
||||
const auth = models?.commands.find((cmd) => cmd.name() === "auth");
|
||||
expect(auth).toBeTruthy();
|
||||
|
||||
const login = auth?.commands.find(
|
||||
(cmd) => cmd.name() === "login-github-copilot",
|
||||
);
|
||||
expect(login).toBeTruthy();
|
||||
const login = auth?.commands.find((cmd) => cmd.name() === "login-github-copilot");
|
||||
expect(login).toBeTruthy();
|
||||
|
||||
await program.parseAsync(
|
||||
["models", "auth", "login-github-copilot", "--yes"],
|
||||
{
|
||||
from: "user",
|
||||
},
|
||||
);
|
||||
await program.parseAsync(["models", "auth", "login-github-copilot", "--yes"], {
|
||||
from: "user",
|
||||
});
|
||||
|
||||
expect(githubCopilotLoginCommand).toHaveBeenCalledTimes(1);
|
||||
expect(githubCopilotLoginCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ yes: true }),
|
||||
expect.any(Object),
|
||||
);
|
||||
},
|
||||
);
|
||||
expect(githubCopilotLoginCommand).toHaveBeenCalledTimes(1);
|
||||
expect(githubCopilotLoginCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ yes: true }),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,23 +33,11 @@ export function registerModelsCli(program: Command) {
|
||||
const models = program
|
||||
.command("models")
|
||||
.description("Model discovery, scanning, and configuration")
|
||||
.option(
|
||||
"--status-json",
|
||||
"Output JSON (alias for `models status --json`)",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--status-plain",
|
||||
"Plain output (alias for `models status --plain`)",
|
||||
false,
|
||||
)
|
||||
.option("--status-json", "Output JSON (alias for `models status --json`)", false)
|
||||
.option("--status-plain", "Plain output (alias for `models status --plain`)", false)
|
||||
.addHelpText(
|
||||
"after",
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink(
|
||||
"/models",
|
||||
"docs.clawd.bot/models",
|
||||
)}\n`,
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/models", "docs.clawd.bot/models")}\n`,
|
||||
);
|
||||
|
||||
models
|
||||
@@ -157,9 +145,7 @@ export function registerModelsCli(program: Command) {
|
||||
}
|
||||
});
|
||||
|
||||
const fallbacks = models
|
||||
.command("fallbacks")
|
||||
.description("Manage model fallback list");
|
||||
const fallbacks = models.command("fallbacks").description("Manage model fallback list");
|
||||
|
||||
fallbacks
|
||||
.command("list")
|
||||
@@ -281,16 +267,8 @@ export function registerModelsCli(program: Command) {
|
||||
.option("--no-probe", "Skip live probes; list free candidates only")
|
||||
.option("--yes", "Accept defaults without prompting", false)
|
||||
.option("--no-input", "Disable prompts (use defaults)")
|
||||
.option(
|
||||
"--set-default",
|
||||
"Set agents.defaults.model to the first selection",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--set-image",
|
||||
"Set agents.defaults.imageModel to the first image selection",
|
||||
false,
|
||||
)
|
||||
.option("--set-default", "Set agents.defaults.model to the first selection", false)
|
||||
.option("--set-image", "Set agents.defaults.imageModel to the first image selection", false)
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
@@ -377,13 +355,8 @@ export function registerModelsCli(program: Command) {
|
||||
|
||||
auth
|
||||
.command("login-github-copilot")
|
||||
.description(
|
||||
"Login to GitHub Copilot via GitHub device flow (TTY required)",
|
||||
)
|
||||
.option(
|
||||
"--profile-id <id>",
|
||||
"Auth profile id (default: github-copilot:github)",
|
||||
)
|
||||
.description("Login to GitHub Copilot via GitHub device flow (TTY required)")
|
||||
.option("--profile-id <id>", "Auth profile id (default: github-copilot:github)")
|
||||
.option("--yes", "Overwrite existing profile without prompting", false)
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
@@ -400,9 +373,7 @@ export function registerModelsCli(program: Command) {
|
||||
}
|
||||
});
|
||||
|
||||
const order = auth
|
||||
.command("order")
|
||||
.description("Manage per-agent auth profile order overrides");
|
||||
const order = auth.command("order").description("Manage per-agent auth profile order overrides");
|
||||
|
||||
order
|
||||
.command("get")
|
||||
@@ -428,9 +399,7 @@ export function registerModelsCli(program: Command) {
|
||||
|
||||
order
|
||||
.command("set")
|
||||
.description(
|
||||
"Set per-agent auth order override (locks rotation to this list)",
|
||||
)
|
||||
.description("Set per-agent auth order override (locks rotation to this list)")
|
||||
.requiredOption("--provider <name>", "Provider id (e.g. anthropic)")
|
||||
.option("--agent <id>", "Agent id (default: configured default agent)")
|
||||
.argument("<profileIds...>", "Auth profile ids (e.g. anthropic:claude-cli)")
|
||||
@@ -452,9 +421,7 @@ export function registerModelsCli(program: Command) {
|
||||
|
||||
order
|
||||
.command("clear")
|
||||
.description(
|
||||
"Clear per-agent auth order override (fall back to config/round-robin)",
|
||||
)
|
||||
.description("Clear per-agent auth order override (fall back to config/round-robin)")
|
||||
.requiredOption("--provider <name>", "Provider id (e.g. anthropic)")
|
||||
.option("--agent <id>", "Agent id (default: configured default agent)")
|
||||
.action(async (opts) => {
|
||||
|
||||
@@ -20,9 +20,7 @@ export type CameraClipPayload = {
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
|
||||
}
|
||||
|
||||
function asString(value: unknown): string | undefined {
|
||||
@@ -30,9 +28,7 @@ function asString(value: unknown): string | undefined {
|
||||
}
|
||||
|
||||
function asNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value)
|
||||
? value
|
||||
: undefined;
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function asBoolean(value: unknown): boolean | undefined {
|
||||
@@ -57,12 +53,7 @@ export function parseCameraClipPayload(value: unknown): CameraClipPayload {
|
||||
const base64 = asString(obj.base64);
|
||||
const durationMs = asNumber(obj.durationMs);
|
||||
const hasAudio = asBoolean(obj.hasAudio);
|
||||
if (
|
||||
!format ||
|
||||
!base64 ||
|
||||
durationMs === undefined ||
|
||||
hasAudio === undefined
|
||||
) {
|
||||
if (!format || !base64 || durationMs === undefined || hasAudio === undefined) {
|
||||
throw new Error("invalid camera.clip payload");
|
||||
}
|
||||
return { format, base64, durationMs, hasAudio };
|
||||
@@ -79,10 +70,7 @@ export function cameraTempPath(opts: {
|
||||
const id = opts.id ?? randomUUID();
|
||||
const facingPart = opts.facing ? `-${opts.facing}` : "";
|
||||
const ext = opts.ext.startsWith(".") ? opts.ext : `.${opts.ext}`;
|
||||
return path.join(
|
||||
tmpDir,
|
||||
`clawdbot-camera-${opts.kind}${facingPart}-${id}${ext}`,
|
||||
);
|
||||
return path.join(tmpDir, `clawdbot-camera-${opts.kind}${facingPart}-${id}${ext}`);
|
||||
}
|
||||
|
||||
export async function writeBase64ToFile(filePath: string, base64: string) {
|
||||
|
||||
@@ -4,9 +4,7 @@ import { parseCanvasSnapshotPayload } from "./nodes-canvas.js";
|
||||
|
||||
describe("nodes canvas helpers", () => {
|
||||
it("parses canvas.snapshot payload", () => {
|
||||
expect(
|
||||
parseCanvasSnapshotPayload({ format: "png", base64: "aGk=" }),
|
||||
).toEqual({
|
||||
expect(parseCanvasSnapshotPayload({ format: "png", base64: "aGk=" })).toEqual({
|
||||
format: "png",
|
||||
base64: "aGk=",
|
||||
});
|
||||
|
||||
@@ -8,18 +8,14 @@ export type CanvasSnapshotPayload = {
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
|
||||
}
|
||||
|
||||
function asString(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
export function parseCanvasSnapshotPayload(
|
||||
value: unknown,
|
||||
): CanvasSnapshotPayload {
|
||||
export function parseCanvasSnapshotPayload(value: unknown): CanvasSnapshotPayload {
|
||||
const obj = asRecord(value);
|
||||
const format = asString(obj.format);
|
||||
const base64 = asString(obj.base64);
|
||||
@@ -29,11 +25,7 @@ export function parseCanvasSnapshotPayload(
|
||||
return { format, base64 };
|
||||
}
|
||||
|
||||
export function canvasSnapshotTempPath(opts: {
|
||||
ext: string;
|
||||
tmpDir?: string;
|
||||
id?: string;
|
||||
}) {
|
||||
export function canvasSnapshotTempPath(opts: { ext: string; tmpDir?: string; id?: string }) {
|
||||
const tmpDir = opts.tmpDir ?? os.tmpdir();
|
||||
const id = opts.id ?? randomUUID();
|
||||
const ext = opts.ext.startsWith(".") ? opts.ext : `.${opts.ext}`;
|
||||
|
||||
@@ -101,9 +101,7 @@ describe("nodes-cli coverage", () => {
|
||||
{ from: "user" },
|
||||
);
|
||||
|
||||
const invoke = callGateway.mock.calls.find(
|
||||
(call) => call[0]?.method === "node.invoke",
|
||||
)?.[0];
|
||||
const invoke = callGateway.mock.calls.find((call) => call[0]?.method === "node.invoke")?.[0];
|
||||
|
||||
expect(invoke).toBeTruthy();
|
||||
expect(invoke?.params?.idempotencyKey).toBe("rk_test");
|
||||
@@ -144,9 +142,7 @@ describe("nodes-cli coverage", () => {
|
||||
{ from: "user" },
|
||||
);
|
||||
|
||||
const invoke = callGateway.mock.calls.find(
|
||||
(call) => call[0]?.method === "node.invoke",
|
||||
)?.[0];
|
||||
const invoke = callGateway.mock.calls.find((call) => call[0]?.method === "node.invoke")?.[0];
|
||||
|
||||
expect(invoke).toBeTruthy();
|
||||
expect(invoke?.params?.command).toBe("system.notify");
|
||||
@@ -188,9 +184,7 @@ describe("nodes-cli coverage", () => {
|
||||
{ from: "user" },
|
||||
);
|
||||
|
||||
const invoke = callGateway.mock.calls.find(
|
||||
(call) => call[0]?.method === "node.invoke",
|
||||
)?.[0];
|
||||
const invoke = callGateway.mock.calls.find((call) => call[0]?.method === "node.invoke")?.[0];
|
||||
|
||||
expect(invoke).toBeTruthy();
|
||||
expect(invoke?.params?.command).toBe("location.get");
|
||||
|
||||
@@ -61,9 +61,7 @@ export function validateA2UIJsonl(jsonl: string) {
|
||||
const actionKeys = A2UI_ACTION_KEYS.filter((key) => key in record);
|
||||
if (actionKeys.length !== 1) {
|
||||
errors.push(
|
||||
`line ${idx + 1}: expected exactly one action key (${A2UI_ACTION_KEYS.join(
|
||||
", ",
|
||||
)})`,
|
||||
`line ${idx + 1}: expected exactly one action key (${A2UI_ACTION_KEYS.join(", ")})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import type {
|
||||
NodeListNode,
|
||||
PairedNode,
|
||||
PairingList,
|
||||
PendingRequest,
|
||||
} from "./types.js";
|
||||
import type { NodeListNode, PairedNode, PairingList, PendingRequest } from "./types.js";
|
||||
|
||||
export function formatAge(msAgo: number) {
|
||||
const s = Math.max(0, Math.floor(msAgo / 1000));
|
||||
@@ -17,22 +12,14 @@ export function formatAge(msAgo: number) {
|
||||
}
|
||||
|
||||
export function parsePairingList(value: unknown): PairingList {
|
||||
const obj =
|
||||
typeof value === "object" && value !== null
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
const pending = Array.isArray(obj.pending)
|
||||
? (obj.pending as PendingRequest[])
|
||||
: [];
|
||||
const obj = typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
|
||||
const pending = Array.isArray(obj.pending) ? (obj.pending as PendingRequest[]) : [];
|
||||
const paired = Array.isArray(obj.paired) ? (obj.paired as PairedNode[]) : [];
|
||||
return { pending, paired };
|
||||
}
|
||||
|
||||
export function parseNodeList(value: unknown): NodeListNode[] {
|
||||
const obj =
|
||||
typeof value === "object" && value !== null
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
const obj = typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
|
||||
return Array.isArray(obj.nodes) ? (obj.nodes as NodeListNode[]) : [];
|
||||
}
|
||||
|
||||
@@ -43,8 +30,6 @@ export function formatPermissions(raw: unknown) {
|
||||
.filter(([key]) => key.length > 0)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]));
|
||||
if (entries.length === 0) return null;
|
||||
const parts = entries.map(
|
||||
([key, granted]) => `${key}=${granted ? "yes" : "no"}`,
|
||||
);
|
||||
const parts = entries.map(([key, granted]) => `${key}=${granted ? "yes" : "no"}`);
|
||||
return `[${parts.join(", ")}]`;
|
||||
}
|
||||
|
||||
@@ -21,9 +21,7 @@ const parseFacing = (value: string): CameraFacing => {
|
||||
};
|
||||
|
||||
export function registerNodesCameraCommands(nodes: Command) {
|
||||
const camera = nodes
|
||||
.command("camera")
|
||||
.description("Capture camera media from a paired node");
|
||||
const camera = nodes.command("camera").description("Capture camera media from a paired node");
|
||||
|
||||
nodesCallOpts(
|
||||
camera
|
||||
@@ -40,10 +38,7 @@ export function registerNodesCameraCommands(nodes: Command) {
|
||||
idempotencyKey: randomIdempotencyKey(),
|
||||
})) as unknown;
|
||||
|
||||
const res =
|
||||
typeof raw === "object" && raw !== null
|
||||
? (raw as { payload?: unknown })
|
||||
: {};
|
||||
const res = typeof raw === "object" && raw !== null ? (raw as { payload?: unknown }) : {};
|
||||
const payload =
|
||||
typeof res.payload === "object" && res.payload !== null
|
||||
? (res.payload as { devices?: unknown })
|
||||
@@ -62,12 +57,8 @@ export function registerNodesCameraCommands(nodes: Command) {
|
||||
|
||||
for (const device of devices) {
|
||||
const id = typeof device.id === "string" ? device.id : "";
|
||||
const name =
|
||||
typeof device.name === "string" ? device.name : "Unknown Camera";
|
||||
const position =
|
||||
typeof device.position === "string"
|
||||
? device.position
|
||||
: "unspecified";
|
||||
const name = typeof device.name === "string" ? device.name : "Unknown Camera";
|
||||
const position = typeof device.position === "string" ? device.position : "unspecified";
|
||||
defaultRuntime.log(`${name} (${position})${id ? ` — ${id}` : ""}`);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -87,15 +78,8 @@ export function registerNodesCameraCommands(nodes: Command) {
|
||||
.option("--device-id <id>", "Camera device id (from nodes camera list)")
|
||||
.option("--max-width <px>", "Max width in px (optional)")
|
||||
.option("--quality <0-1>", "JPEG quality (default 0.9)")
|
||||
.option(
|
||||
"--delay-ms <ms>",
|
||||
"Delay before capture in ms (macOS default 2000)",
|
||||
)
|
||||
.option(
|
||||
"--invoke-timeout <ms>",
|
||||
"Node invoke timeout in ms (default 20000)",
|
||||
"20000",
|
||||
)
|
||||
.option("--delay-ms <ms>", "Delay before capture in ms (macOS default 2000)")
|
||||
.option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 20000)", "20000")
|
||||
.action(async (opts: NodesRpcOpts) => {
|
||||
try {
|
||||
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
|
||||
@@ -113,18 +97,10 @@ export function registerNodesCameraCommands(nodes: Command) {
|
||||
);
|
||||
})();
|
||||
|
||||
const maxWidth = opts.maxWidth
|
||||
? Number.parseInt(String(opts.maxWidth), 10)
|
||||
: undefined;
|
||||
const quality = opts.quality
|
||||
? Number.parseFloat(String(opts.quality))
|
||||
: undefined;
|
||||
const delayMs = opts.delayMs
|
||||
? Number.parseInt(String(opts.delayMs), 10)
|
||||
: undefined;
|
||||
const deviceId = opts.deviceId
|
||||
? String(opts.deviceId).trim()
|
||||
: undefined;
|
||||
const maxWidth = opts.maxWidth ? Number.parseInt(String(opts.maxWidth), 10) : undefined;
|
||||
const quality = opts.quality ? Number.parseFloat(String(opts.quality)) : undefined;
|
||||
const delayMs = opts.delayMs ? Number.parseInt(String(opts.delayMs), 10) : undefined;
|
||||
const deviceId = opts.deviceId ? String(opts.deviceId).trim() : undefined;
|
||||
const timeoutMs = opts.invokeTimeout
|
||||
? Number.parseInt(String(opts.invokeTimeout), 10)
|
||||
: undefined;
|
||||
@@ -154,15 +130,9 @@ export function registerNodesCameraCommands(nodes: Command) {
|
||||
invokeParams.timeoutMs = timeoutMs;
|
||||
}
|
||||
|
||||
const raw = (await callGatewayCli(
|
||||
"node.invoke",
|
||||
opts,
|
||||
invokeParams,
|
||||
)) as unknown;
|
||||
const raw = (await callGatewayCli("node.invoke", opts, invokeParams)) as unknown;
|
||||
const res =
|
||||
typeof raw === "object" && raw !== null
|
||||
? (raw as { payload?: unknown })
|
||||
: {};
|
||||
typeof raw === "object" && raw !== null ? (raw as { payload?: unknown }) : {};
|
||||
const payload = parseCameraSnapPayload(res.payload);
|
||||
const filePath = cameraTempPath({
|
||||
kind: "snap",
|
||||
@@ -194,9 +164,7 @@ export function registerNodesCameraCommands(nodes: Command) {
|
||||
nodesCallOpts(
|
||||
camera
|
||||
.command("clip")
|
||||
.description(
|
||||
"Capture a short video clip from a node camera (prints MEDIA:<path>)",
|
||||
)
|
||||
.description("Capture a short video clip from a node camera (prints MEDIA:<path>)")
|
||||
.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP")
|
||||
.option("--facing <front|back>", "Camera facing", "front")
|
||||
.option("--device-id <id>", "Camera device id (from nodes camera list)")
|
||||
@@ -206,11 +174,7 @@ export function registerNodesCameraCommands(nodes: Command) {
|
||||
"3000",
|
||||
)
|
||||
.option("--no-audio", "Disable audio capture")
|
||||
.option(
|
||||
"--invoke-timeout <ms>",
|
||||
"Node invoke timeout in ms (default 90000)",
|
||||
"90000",
|
||||
)
|
||||
.option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 90000)", "90000")
|
||||
.action(async (opts: NodesRpcOpts & { audio?: boolean }) => {
|
||||
try {
|
||||
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
|
||||
@@ -220,9 +184,7 @@ export function registerNodesCameraCommands(nodes: Command) {
|
||||
const timeoutMs = opts.invokeTimeout
|
||||
? Number.parseInt(String(opts.invokeTimeout), 10)
|
||||
: undefined;
|
||||
const deviceId = opts.deviceId
|
||||
? String(opts.deviceId).trim()
|
||||
: undefined;
|
||||
const deviceId = opts.deviceId ? String(opts.deviceId).trim() : undefined;
|
||||
|
||||
const invokeParams: Record<string, unknown> = {
|
||||
nodeId,
|
||||
@@ -240,15 +202,8 @@ export function registerNodesCameraCommands(nodes: Command) {
|
||||
invokeParams.timeoutMs = timeoutMs;
|
||||
}
|
||||
|
||||
const raw = (await callGatewayCli(
|
||||
"node.invoke",
|
||||
opts,
|
||||
invokeParams,
|
||||
)) as unknown;
|
||||
const res =
|
||||
typeof raw === "object" && raw !== null
|
||||
? (raw as { payload?: unknown })
|
||||
: {};
|
||||
const raw = (await callGatewayCli("node.invoke", opts, invokeParams)) as unknown;
|
||||
const res = typeof raw === "object" && raw !== null ? (raw as { payload?: unknown }) : {};
|
||||
const payload = parseCameraClipPayload(res.payload);
|
||||
const filePath = cameraTempPath({
|
||||
kind: "clip",
|
||||
|
||||
@@ -3,20 +3,13 @@ import type { Command } from "commander";
|
||||
import { randomIdempotencyKey } from "../../gateway/call.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { writeBase64ToFile } from "../nodes-camera.js";
|
||||
import {
|
||||
canvasSnapshotTempPath,
|
||||
parseCanvasSnapshotPayload,
|
||||
} from "../nodes-canvas.js";
|
||||
import { canvasSnapshotTempPath, parseCanvasSnapshotPayload } from "../nodes-canvas.js";
|
||||
import { parseTimeoutMs } from "../nodes-run.js";
|
||||
import { buildA2UITextJsonl, validateA2UIJsonl } from "./a2ui-jsonl.js";
|
||||
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
|
||||
import type { NodesRpcOpts } from "./types.js";
|
||||
|
||||
async function invokeCanvas(
|
||||
opts: NodesRpcOpts,
|
||||
command: string,
|
||||
params?: Record<string, unknown>,
|
||||
) {
|
||||
async function invokeCanvas(opts: NodesRpcOpts, command: string, params?: Record<string, unknown>) {
|
||||
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
|
||||
const invokeParams: Record<string, unknown> = {
|
||||
nodeId,
|
||||
@@ -44,11 +37,7 @@ export function registerNodesCanvasCommands(nodes: Command) {
|
||||
.option("--format <png|jpg|jpeg>", "Image format", "jpg")
|
||||
.option("--max-width <px>", "Max width in px (optional)")
|
||||
.option("--quality <0-1>", "JPEG quality (optional)")
|
||||
.option(
|
||||
"--invoke-timeout <ms>",
|
||||
"Node invoke timeout in ms (default 20000)",
|
||||
"20000",
|
||||
)
|
||||
.option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 20000)", "20000")
|
||||
.action(async (opts: NodesRpcOpts) => {
|
||||
try {
|
||||
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
|
||||
@@ -56,23 +45,13 @@ export function registerNodesCanvasCommands(nodes: Command) {
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const formatForParams =
|
||||
formatOpt === "jpg"
|
||||
? "jpeg"
|
||||
: formatOpt === "jpeg"
|
||||
? "jpeg"
|
||||
: "png";
|
||||
formatOpt === "jpg" ? "jpeg" : formatOpt === "jpeg" ? "jpeg" : "png";
|
||||
if (formatForParams !== "png" && formatForParams !== "jpeg") {
|
||||
throw new Error(
|
||||
`invalid format: ${String(opts.format)} (expected png|jpg|jpeg)`,
|
||||
);
|
||||
throw new Error(`invalid format: ${String(opts.format)} (expected png|jpg|jpeg)`);
|
||||
}
|
||||
|
||||
const maxWidth = opts.maxWidth
|
||||
? Number.parseInt(String(opts.maxWidth), 10)
|
||||
: undefined;
|
||||
const quality = opts.quality
|
||||
? Number.parseFloat(String(opts.quality))
|
||||
: undefined;
|
||||
const maxWidth = opts.maxWidth ? Number.parseInt(String(opts.maxWidth), 10) : undefined;
|
||||
const quality = opts.quality ? Number.parseFloat(String(opts.quality)) : undefined;
|
||||
const timeoutMs = opts.invokeTimeout
|
||||
? Number.parseInt(String(opts.invokeTimeout), 10)
|
||||
: undefined;
|
||||
@@ -91,15 +70,8 @@ export function registerNodesCanvasCommands(nodes: Command) {
|
||||
invokeParams.timeoutMs = timeoutMs;
|
||||
}
|
||||
|
||||
const raw = (await callGatewayCli(
|
||||
"node.invoke",
|
||||
opts,
|
||||
invokeParams,
|
||||
)) as unknown;
|
||||
const res =
|
||||
typeof raw === "object" && raw !== null
|
||||
? (raw as { payload?: unknown })
|
||||
: {};
|
||||
const raw = (await callGatewayCli("node.invoke", opts, invokeParams)) as unknown;
|
||||
const res = typeof raw === "object" && raw !== null ? (raw as { payload?: unknown }) : {};
|
||||
const payload = parseCanvasSnapshotPayload(res.payload);
|
||||
const filePath = canvasSnapshotTempPath({
|
||||
ext: payload.format === "jpeg" ? "jpg" : payload.format,
|
||||
@@ -108,11 +80,7 @@ export function registerNodesCanvasCommands(nodes: Command) {
|
||||
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(
|
||||
JSON.stringify(
|
||||
{ file: { path: filePath, format: payload.format } },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
JSON.stringify({ file: { path: filePath, format: payload.format } }, null, 2),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -230,9 +198,7 @@ export function registerNodesCanvasCommands(nodes: Command) {
|
||||
}),
|
||||
);
|
||||
|
||||
const a2ui = canvas
|
||||
.command("a2ui")
|
||||
.description("Render A2UI content on the canvas");
|
||||
const a2ui = canvas.command("a2ui").description("Render A2UI content on the canvas");
|
||||
|
||||
nodesCallOpts(
|
||||
a2ui
|
||||
@@ -283,9 +249,7 @@ export function registerNodesCanvasCommands(nodes: Command) {
|
||||
await invokeCanvas(opts, "canvas.a2ui.reset", undefined);
|
||||
if (!opts.json) defaultRuntime.log("canvas a2ui reset ok");
|
||||
} catch (err) {
|
||||
defaultRuntime.error(
|
||||
`nodes canvas a2ui reset failed: ${String(err)}`,
|
||||
);
|
||||
defaultRuntime.error(`nodes canvas a2ui reset failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -2,12 +2,7 @@ import type { Command } from "commander";
|
||||
import { randomIdempotencyKey } from "../../gateway/call.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { parseEnvPairs, parseTimeoutMs } from "../nodes-run.js";
|
||||
import {
|
||||
callGatewayCli,
|
||||
nodesCallOpts,
|
||||
resolveNodeId,
|
||||
unauthorizedHintForMessage,
|
||||
} from "./rpc.js";
|
||||
import { callGatewayCli, nodesCallOpts, resolveNodeId, unauthorizedHintForMessage } from "./rpc.js";
|
||||
import type { NodesRpcOpts } from "./types.js";
|
||||
|
||||
export function registerNodesInvokeCommands(nodes: Command) {
|
||||
@@ -18,11 +13,7 @@ export function registerNodesInvokeCommands(nodes: Command) {
|
||||
.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP")
|
||||
.requiredOption("--command <command>", "Command (e.g. canvas.eval)")
|
||||
.option("--params <json>", "JSON object string for params", "{}")
|
||||
.option(
|
||||
"--invoke-timeout <ms>",
|
||||
"Node invoke timeout in ms (default 15000)",
|
||||
"15000",
|
||||
)
|
||||
.option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 15000)", "15000")
|
||||
.option("--idempotency-key <key>", "Idempotency key (optional)")
|
||||
.action(async (opts: NodesRpcOpts) => {
|
||||
try {
|
||||
@@ -42,19 +33,13 @@ export function registerNodesInvokeCommands(nodes: Command) {
|
||||
nodeId,
|
||||
command,
|
||||
params,
|
||||
idempotencyKey: String(
|
||||
opts.idempotencyKey ?? randomIdempotencyKey(),
|
||||
),
|
||||
idempotencyKey: String(opts.idempotencyKey ?? randomIdempotencyKey()),
|
||||
};
|
||||
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs)) {
|
||||
invokeParams.timeoutMs = timeoutMs;
|
||||
}
|
||||
|
||||
const result = await callGatewayCli(
|
||||
"node.invoke",
|
||||
opts,
|
||||
invokeParams,
|
||||
);
|
||||
const result = await callGatewayCli("node.invoke", opts, invokeParams);
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`nodes invoke failed: ${String(err)}`);
|
||||
@@ -77,11 +62,7 @@ export function registerNodesInvokeCommands(nodes: Command) {
|
||||
)
|
||||
.option("--command-timeout <ms>", "Command timeout (ms)")
|
||||
.option("--needs-screen-recording", "Require screen recording permission")
|
||||
.option(
|
||||
"--invoke-timeout <ms>",
|
||||
"Node invoke timeout in ms (default 30000)",
|
||||
"30000",
|
||||
)
|
||||
.option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 30000)", "30000")
|
||||
.argument("<command...>", "Command and args")
|
||||
.action(async (command: string[], opts: NodesRpcOpts) => {
|
||||
try {
|
||||
@@ -103,19 +84,13 @@ export function registerNodesInvokeCommands(nodes: Command) {
|
||||
timeoutMs,
|
||||
needsScreenRecording: opts.needsScreenRecording === true,
|
||||
},
|
||||
idempotencyKey: String(
|
||||
opts.idempotencyKey ?? randomIdempotencyKey(),
|
||||
),
|
||||
idempotencyKey: String(opts.idempotencyKey ?? randomIdempotencyKey()),
|
||||
};
|
||||
if (invokeTimeout !== undefined) {
|
||||
invokeParams.timeoutMs = invokeTimeout;
|
||||
}
|
||||
|
||||
const result = (await callGatewayCli(
|
||||
"node.invoke",
|
||||
opts,
|
||||
invokeParams,
|
||||
)) as unknown;
|
||||
const result = (await callGatewayCli("node.invoke", opts, invokeParams)) as unknown;
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
@@ -126,12 +101,9 @@ export function registerNodesInvokeCommands(nodes: Command) {
|
||||
? (result as { payload?: Record<string, unknown> }).payload
|
||||
: undefined;
|
||||
|
||||
const stdout =
|
||||
typeof payload?.stdout === "string" ? payload.stdout : "";
|
||||
const stderr =
|
||||
typeof payload?.stderr === "string" ? payload.stderr : "";
|
||||
const exitCode =
|
||||
typeof payload?.exitCode === "number" ? payload.exitCode : null;
|
||||
const stdout = typeof payload?.stdout === "string" ? payload.stdout : "";
|
||||
const stderr = typeof payload?.stderr === "string" ? payload.stderr : "";
|
||||
const exitCode = typeof payload?.exitCode === "number" ? payload.exitCode : null;
|
||||
const timedOut = payload?.timedOut === true;
|
||||
const success = payload?.success === true;
|
||||
|
||||
|
||||
@@ -5,9 +5,7 @@ import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
|
||||
import type { NodesRpcOpts } from "./types.js";
|
||||
|
||||
export function registerNodesLocationCommands(nodes: Command) {
|
||||
const location = nodes
|
||||
.command("location")
|
||||
.description("Fetch location from a paired node");
|
||||
const location = nodes.command("location").description("Fetch location from a paired node");
|
||||
|
||||
nodesCallOpts(
|
||||
location
|
||||
@@ -20,21 +18,13 @@ export function registerNodesLocationCommands(nodes: Command) {
|
||||
"Desired accuracy (default: balanced/precise depending on node setting)",
|
||||
)
|
||||
.option("--location-timeout <ms>", "Location fix timeout (ms)", "10000")
|
||||
.option(
|
||||
"--invoke-timeout <ms>",
|
||||
"Node invoke timeout in ms (default 20000)",
|
||||
"20000",
|
||||
)
|
||||
.option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 20000)", "20000")
|
||||
.action(async (opts: NodesRpcOpts) => {
|
||||
try {
|
||||
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
|
||||
const maxAgeMs = opts.maxAge
|
||||
? Number.parseInt(String(opts.maxAge), 10)
|
||||
: undefined;
|
||||
const maxAgeMs = opts.maxAge ? Number.parseInt(String(opts.maxAge), 10) : undefined;
|
||||
const desiredAccuracyRaw =
|
||||
typeof opts.accuracy === "string"
|
||||
? opts.accuracy.trim().toLowerCase()
|
||||
: undefined;
|
||||
typeof opts.accuracy === "string" ? opts.accuracy.trim().toLowerCase() : undefined;
|
||||
const desiredAccuracy =
|
||||
desiredAccuracyRaw === "coarse" ||
|
||||
desiredAccuracyRaw === "balanced" ||
|
||||
@@ -58,22 +48,12 @@ export function registerNodesLocationCommands(nodes: Command) {
|
||||
},
|
||||
idempotencyKey: randomIdempotencyKey(),
|
||||
};
|
||||
if (
|
||||
typeof invokeTimeoutMs === "number" &&
|
||||
Number.isFinite(invokeTimeoutMs)
|
||||
) {
|
||||
if (typeof invokeTimeoutMs === "number" && Number.isFinite(invokeTimeoutMs)) {
|
||||
invokeParams.timeoutMs = invokeTimeoutMs;
|
||||
}
|
||||
|
||||
const raw = (await callGatewayCli(
|
||||
"node.invoke",
|
||||
opts,
|
||||
invokeParams,
|
||||
)) as unknown;
|
||||
const res =
|
||||
typeof raw === "object" && raw !== null
|
||||
? (raw as { payload?: unknown })
|
||||
: {};
|
||||
const raw = (await callGatewayCli("node.invoke", opts, invokeParams)) as unknown;
|
||||
const res = typeof raw === "object" && raw !== null ? (raw as { payload?: unknown }) : {};
|
||||
const payload =
|
||||
res.payload && typeof res.payload === "object"
|
||||
? (res.payload as Record<string, unknown>)
|
||||
@@ -88,8 +68,7 @@ export function registerNodesLocationCommands(nodes: Command) {
|
||||
const lon = payload.lon;
|
||||
const acc = payload.accuracyMeters;
|
||||
if (typeof lat === "number" && typeof lon === "number") {
|
||||
const accText =
|
||||
typeof acc === "number" ? ` ±${acc.toFixed(1)}m` : "";
|
||||
const accText = typeof acc === "number" ? ` ±${acc.toFixed(1)}m` : "";
|
||||
defaultRuntime.log(`${lat},${lon}${accText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -13,16 +13,9 @@ export function registerNodesNotifyCommand(nodes: Command) {
|
||||
.option("--title <text>", "Notification title")
|
||||
.option("--body <text>", "Notification body")
|
||||
.option("--sound <name>", "Notification sound")
|
||||
.option(
|
||||
"--priority <passive|active|timeSensitive>",
|
||||
"Notification priority",
|
||||
)
|
||||
.option("--priority <passive|active|timeSensitive>", "Notification priority")
|
||||
.option("--delivery <system|overlay|auto>", "Delivery mode", "system")
|
||||
.option(
|
||||
"--invoke-timeout <ms>",
|
||||
"Node invoke timeout in ms (default 15000)",
|
||||
"15000",
|
||||
)
|
||||
.option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 15000)", "15000")
|
||||
.action(async (opts: NodesRpcOpts) => {
|
||||
try {
|
||||
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
|
||||
@@ -44,22 +37,13 @@ export function registerNodesNotifyCommand(nodes: Command) {
|
||||
priority: opts.priority,
|
||||
delivery: opts.delivery,
|
||||
},
|
||||
idempotencyKey: String(
|
||||
opts.idempotencyKey ?? randomIdempotencyKey(),
|
||||
),
|
||||
idempotencyKey: String(opts.idempotencyKey ?? randomIdempotencyKey()),
|
||||
};
|
||||
if (
|
||||
typeof invokeTimeout === "number" &&
|
||||
Number.isFinite(invokeTimeout)
|
||||
) {
|
||||
if (typeof invokeTimeout === "number" && Number.isFinite(invokeTimeout)) {
|
||||
invokeParams.timeoutMs = invokeTimeout;
|
||||
}
|
||||
|
||||
const result = await callGatewayCli(
|
||||
"node.invoke",
|
||||
opts,
|
||||
invokeParams,
|
||||
);
|
||||
const result = await callGatewayCli("node.invoke", opts, invokeParams);
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
|
||||
@@ -11,11 +11,7 @@ export function registerNodesPairingCommands(nodes: Command) {
|
||||
.description("List pending pairing requests")
|
||||
.action(async (opts: NodesRpcOpts) => {
|
||||
try {
|
||||
const result = (await callGatewayCli(
|
||||
"node.pair.list",
|
||||
opts,
|
||||
{},
|
||||
)) as unknown;
|
||||
const result = (await callGatewayCli("node.pair.list", opts, {})) as unknown;
|
||||
const { pending } = parsePairingList(result);
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(JSON.stringify(pending, null, 2));
|
||||
@@ -29,10 +25,7 @@ export function registerNodesPairingCommands(nodes: Command) {
|
||||
const name = r.displayName || r.nodeId;
|
||||
const repair = r.isRepair ? " (repair)" : "";
|
||||
const ip = r.remoteIp ? ` · ${r.remoteIp}` : "";
|
||||
const age =
|
||||
typeof r.ts === "number"
|
||||
? ` · ${formatAge(Date.now() - r.ts)} ago`
|
||||
: "";
|
||||
const age = typeof r.ts === "number" ? ` · ${formatAge(Date.now() - r.ts)} ago` : "";
|
||||
defaultRuntime.log(`- ${r.requestId}: ${name}${repair}${ip}${age}`);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -18,20 +18,14 @@ export function registerNodesScreenCommands(nodes: Command) {
|
||||
nodesCallOpts(
|
||||
screen
|
||||
.command("record")
|
||||
.description(
|
||||
"Capture a short screen recording from a node (prints MEDIA:<path>)",
|
||||
)
|
||||
.description("Capture a short screen recording from a node (prints MEDIA:<path>)")
|
||||
.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP")
|
||||
.option("--screen <index>", "Screen index (0 = primary)", "0")
|
||||
.option("--duration <ms|10s>", "Clip duration (ms or 10s)", "10000")
|
||||
.option("--fps <fps>", "Frames per second", "10")
|
||||
.option("--no-audio", "Disable microphone audio capture")
|
||||
.option("--out <path>", "Output path")
|
||||
.option(
|
||||
"--invoke-timeout <ms>",
|
||||
"Node invoke timeout in ms (default 120000)",
|
||||
"120000",
|
||||
)
|
||||
.option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 120000)", "120000")
|
||||
.action(async (opts: NodesRpcOpts & { out?: string }) => {
|
||||
try {
|
||||
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
|
||||
@@ -47,9 +41,7 @@ export function registerNodesScreenCommands(nodes: Command) {
|
||||
command: "screen.record",
|
||||
params: {
|
||||
durationMs: Number.isFinite(durationMs) ? durationMs : undefined,
|
||||
screenIndex: Number.isFinite(screenIndex)
|
||||
? screenIndex
|
||||
: undefined,
|
||||
screenIndex: Number.isFinite(screenIndex) ? screenIndex : undefined,
|
||||
fps: Number.isFinite(fps) ? fps : undefined,
|
||||
format: "mp4",
|
||||
includeAudio: opts.audio !== false,
|
||||
@@ -60,22 +52,11 @@ export function registerNodesScreenCommands(nodes: Command) {
|
||||
invokeParams.timeoutMs = timeoutMs;
|
||||
}
|
||||
|
||||
const raw = (await callGatewayCli(
|
||||
"node.invoke",
|
||||
opts,
|
||||
invokeParams,
|
||||
)) as unknown;
|
||||
const res =
|
||||
typeof raw === "object" && raw !== null
|
||||
? (raw as { payload?: unknown })
|
||||
: {};
|
||||
const raw = (await callGatewayCli("node.invoke", opts, invokeParams)) as unknown;
|
||||
const res = typeof raw === "object" && raw !== null ? (raw as { payload?: unknown }) : {};
|
||||
const parsed = parseScreenRecordPayload(res.payload);
|
||||
const filePath =
|
||||
opts.out ?? screenRecordTempPath({ ext: parsed.format || "mp4" });
|
||||
const written = await writeScreenRecordToFile(
|
||||
filePath,
|
||||
parsed.base64,
|
||||
);
|
||||
const filePath = opts.out ?? screenRecordTempPath({ ext: parsed.format || "mp4" });
|
||||
const written = await writeScreenRecordToFile(filePath, parsed.base64);
|
||||
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import type { Command } from "commander";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import {
|
||||
formatAge,
|
||||
formatPermissions,
|
||||
parseNodeList,
|
||||
parsePairingList,
|
||||
} from "./format.js";
|
||||
import { formatAge, formatPermissions, parseNodeList, parsePairingList } from "./format.js";
|
||||
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
|
||||
import type { NodesRpcOpts } from "./types.js";
|
||||
|
||||
@@ -16,20 +11,14 @@ export function registerNodesStatusCommands(nodes: Command) {
|
||||
.description("List known nodes with connection status and capabilities")
|
||||
.action(async (opts: NodesRpcOpts) => {
|
||||
try {
|
||||
const result = (await callGatewayCli(
|
||||
"node.list",
|
||||
opts,
|
||||
{},
|
||||
)) as unknown;
|
||||
const result = (await callGatewayCli("node.list", opts, {})) as unknown;
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
const nodes = parseNodeList(result);
|
||||
const pairedCount = nodes.filter((n) => Boolean(n.paired)).length;
|
||||
const connectedCount = nodes.filter((n) =>
|
||||
Boolean(n.connected),
|
||||
).length;
|
||||
const connectedCount = nodes.filter((n) => Boolean(n.connected)).length;
|
||||
defaultRuntime.log(
|
||||
`Known: ${nodes.length} · Paired: ${pairedCount} · Connected: ${connectedCount}`,
|
||||
);
|
||||
@@ -78,22 +67,15 @@ export function registerNodesStatusCommands(nodes: Command) {
|
||||
typeof result === "object" && result !== null
|
||||
? (result as Record<string, unknown>)
|
||||
: {};
|
||||
const displayName =
|
||||
typeof obj.displayName === "string" ? obj.displayName : nodeId;
|
||||
const displayName = typeof obj.displayName === "string" ? obj.displayName : nodeId;
|
||||
const connected = Boolean(obj.connected);
|
||||
const caps = Array.isArray(obj.caps)
|
||||
? obj.caps.map(String).filter(Boolean).sort()
|
||||
: null;
|
||||
const caps = Array.isArray(obj.caps) ? obj.caps.map(String).filter(Boolean).sort() : null;
|
||||
const commands = Array.isArray(obj.commands)
|
||||
? obj.commands.map(String).filter(Boolean).sort()
|
||||
: [];
|
||||
const perms = formatPermissions(obj.permissions);
|
||||
const family =
|
||||
typeof obj.deviceFamily === "string" ? obj.deviceFamily : null;
|
||||
const model =
|
||||
typeof obj.modelIdentifier === "string"
|
||||
? obj.modelIdentifier
|
||||
: null;
|
||||
const family = typeof obj.deviceFamily === "string" ? obj.deviceFamily : null;
|
||||
const model = typeof obj.modelIdentifier === "string" ? obj.modelIdentifier : null;
|
||||
const ip = typeof obj.remoteIp === "string" ? obj.remoteIp : null;
|
||||
|
||||
const parts: string[] = ["Node:", displayName, nodeId];
|
||||
@@ -123,32 +105,21 @@ export function registerNodesStatusCommands(nodes: Command) {
|
||||
.description("List pending and paired nodes")
|
||||
.action(async (opts: NodesRpcOpts) => {
|
||||
try {
|
||||
const result = (await callGatewayCli(
|
||||
"node.pair.list",
|
||||
opts,
|
||||
{},
|
||||
)) as unknown;
|
||||
const result = (await callGatewayCli("node.pair.list", opts, {})) as unknown;
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
const { pending, paired } = parsePairingList(result);
|
||||
defaultRuntime.log(
|
||||
`Pending: ${pending.length} · Paired: ${paired.length}`,
|
||||
);
|
||||
defaultRuntime.log(`Pending: ${pending.length} · Paired: ${paired.length}`);
|
||||
if (pending.length > 0) {
|
||||
defaultRuntime.log("\nPending:");
|
||||
for (const r of pending) {
|
||||
const name = r.displayName || r.nodeId;
|
||||
const repair = r.isRepair ? " (repair)" : "";
|
||||
const ip = r.remoteIp ? ` · ${r.remoteIp}` : "";
|
||||
const age =
|
||||
typeof r.ts === "number"
|
||||
? ` · ${formatAge(Date.now() - r.ts)} ago`
|
||||
: "";
|
||||
defaultRuntime.log(
|
||||
`- ${r.requestId}: ${name}${repair}${ip}${age}`,
|
||||
);
|
||||
const age = typeof r.ts === "number" ? ` · ${formatAge(Date.now() - r.ts)} ago` : "";
|
||||
defaultRuntime.log(`- ${r.requestId}: ${name}${repair}${ip}${age}`);
|
||||
}
|
||||
}
|
||||
if (paired.length > 0) {
|
||||
|
||||
@@ -16,11 +16,7 @@ export function registerNodesCli(program: Command) {
|
||||
.description("Manage gateway-owned node pairing")
|
||||
.addHelpText(
|
||||
"after",
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink(
|
||||
"/nodes",
|
||||
"docs.clawd.bot/nodes",
|
||||
)}\n`,
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/nodes", "docs.clawd.bot/nodes")}\n`,
|
||||
);
|
||||
|
||||
registerNodesStatusCommands(nodes);
|
||||
|
||||
@@ -1,35 +1,18 @@
|
||||
import type { Command } from "commander";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../../utils/message-channel.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
|
||||
import { withProgress } from "../progress.js";
|
||||
import { parseNodeList, parsePairingList } from "./format.js";
|
||||
import type { NodeListNode, NodesRpcOpts } from "./types.js";
|
||||
|
||||
export const nodesCallOpts = (
|
||||
cmd: Command,
|
||||
defaults?: { timeoutMs?: number },
|
||||
) =>
|
||||
export const nodesCallOpts = (cmd: Command, defaults?: { timeoutMs?: number }) =>
|
||||
cmd
|
||||
.option(
|
||||
"--url <url>",
|
||||
"Gateway WebSocket URL (defaults to gateway.remote.url when configured)",
|
||||
)
|
||||
.option("--url <url>", "Gateway WebSocket URL (defaults to gateway.remote.url when configured)")
|
||||
.option("--token <token>", "Gateway token (if required)")
|
||||
.option(
|
||||
"--timeout <ms>",
|
||||
"Timeout in ms",
|
||||
String(defaults?.timeoutMs ?? 10_000),
|
||||
)
|
||||
.option("--timeout <ms>", "Timeout in ms", String(defaults?.timeoutMs ?? 10_000))
|
||||
.option("--json", "Output JSON", false);
|
||||
|
||||
export const callGatewayCli = async (
|
||||
method: string,
|
||||
opts: NodesRpcOpts,
|
||||
params?: unknown,
|
||||
) =>
|
||||
export const callGatewayCli = async (method: string, opts: NodesRpcOpts, params?: unknown) =>
|
||||
withProgress(
|
||||
{
|
||||
label: `Nodes ${method}`,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { parseTimeoutMs } from "./parse-timeout.js";
|
||||
|
||||
export function parseEnvPairs(
|
||||
pairs: unknown,
|
||||
): Record<string, string> | undefined {
|
||||
export function parseEnvPairs(pairs: unknown): Record<string, string> | undefined {
|
||||
if (!Array.isArray(pairs) || pairs.length === 0) return undefined;
|
||||
const env: Record<string, string> = {};
|
||||
for (const pair of pairs) {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
parseScreenRecordPayload,
|
||||
screenRecordTempPath,
|
||||
} from "./nodes-screen.js";
|
||||
import { parseScreenRecordPayload, screenRecordTempPath } from "./nodes-screen.js";
|
||||
|
||||
describe("nodes screen helpers", () => {
|
||||
it("parses screen.record payload", () => {
|
||||
|
||||
@@ -14,9 +14,7 @@ export type ScreenRecordPayload = {
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
|
||||
}
|
||||
|
||||
function asString(value: unknown): string | undefined {
|
||||
@@ -35,26 +33,18 @@ export function parseScreenRecordPayload(value: unknown): ScreenRecordPayload {
|
||||
base64,
|
||||
durationMs: typeof obj.durationMs === "number" ? obj.durationMs : undefined,
|
||||
fps: typeof obj.fps === "number" ? obj.fps : undefined,
|
||||
screenIndex:
|
||||
typeof obj.screenIndex === "number" ? obj.screenIndex : undefined,
|
||||
screenIndex: typeof obj.screenIndex === "number" ? obj.screenIndex : undefined,
|
||||
hasAudio: typeof obj.hasAudio === "boolean" ? obj.hasAudio : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function screenRecordTempPath(opts: {
|
||||
ext: string;
|
||||
tmpDir?: string;
|
||||
id?: string;
|
||||
}) {
|
||||
export function screenRecordTempPath(opts: { ext: string; tmpDir?: string; id?: string }) {
|
||||
const tmpDir = opts.tmpDir ?? os.tmpdir();
|
||||
const id = opts.id ?? randomUUID();
|
||||
const ext = opts.ext.startsWith(".") ? opts.ext : `.${opts.ext}`;
|
||||
return path.join(tmpDir, `clawdbot-screen-record-${id}${ext}`);
|
||||
}
|
||||
|
||||
export async function writeScreenRecordToFile(
|
||||
filePath: string,
|
||||
base64: string,
|
||||
) {
|
||||
export async function writeScreenRecordToFile(filePath: string, base64: string) {
|
||||
return writeBase64ToFile(filePath, base64);
|
||||
}
|
||||
|
||||
@@ -50,9 +50,7 @@ describe("pairing cli", () => {
|
||||
await program.parseAsync(["pairing", "list", "--channel", "telegram"], {
|
||||
from: "user",
|
||||
});
|
||||
expect(log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("telegramUserId=123"),
|
||||
);
|
||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("telegramUserId=123"));
|
||||
});
|
||||
|
||||
it("accepts channel as positional for list", async () => {
|
||||
@@ -86,9 +84,7 @@ describe("pairing cli", () => {
|
||||
await program.parseAsync(["pairing", "list", "--channel", "discord"], {
|
||||
from: "user",
|
||||
});
|
||||
expect(log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("discordUserId=999"),
|
||||
);
|
||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("discordUserId=999"));
|
||||
});
|
||||
|
||||
it("accepts channel as positional for approve (npm-run compatible)", async () => {
|
||||
|
||||
@@ -54,9 +54,7 @@ export function registerPairingCli(program: Command) {
|
||||
for (const r of requests) {
|
||||
const meta = r.meta ? JSON.stringify(r.meta) : "";
|
||||
const idLabel = resolvePairingIdLabel(channel);
|
||||
console.log(
|
||||
`${r.code} ${idLabel}=${r.id}${meta ? ` meta=${meta}` : ""} ${r.createdAt}`,
|
||||
);
|
||||
console.log(`${r.code} ${idLabel}=${r.id}${meta ? ` meta=${meta}` : ""} ${r.createdAt}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -86,9 +84,7 @@ export function registerPairingCli(program: Command) {
|
||||
code: String(resolvedCode),
|
||||
});
|
||||
if (!approved) {
|
||||
throw new Error(
|
||||
`No pending pairing request found for code: ${String(resolvedCode)}`,
|
||||
);
|
||||
throw new Error(`No pending pairing request found for code: ${String(resolvedCode)}`);
|
||||
}
|
||||
|
||||
console.log(`Approved ${channel} sender ${approved.id}.`);
|
||||
|
||||
@@ -2,10 +2,7 @@ export type DurationMsParseOptions = {
|
||||
defaultUnit?: "ms" | "s" | "m" | "h" | "d";
|
||||
};
|
||||
|
||||
export function parseDurationMs(
|
||||
raw: string,
|
||||
opts?: DurationMsParseOptions,
|
||||
): number {
|
||||
export function parseDurationMs(raw: string, opts?: DurationMsParseOptions): number {
|
||||
const trimmed = String(raw ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
@@ -19,12 +16,7 @@ export function parseDurationMs(
|
||||
throw new Error(`invalid duration: ${raw}`);
|
||||
}
|
||||
|
||||
const unit = (m[2] ?? opts?.defaultUnit ?? "ms") as
|
||||
| "ms"
|
||||
| "s"
|
||||
| "m"
|
||||
| "h"
|
||||
| "d";
|
||||
const unit = (m[2] ?? opts?.defaultUnit ?? "ms") as "ms" | "s" | "m" | "h" | "d";
|
||||
const multiplier =
|
||||
unit === "ms"
|
||||
? 1
|
||||
|
||||
@@ -4,10 +4,7 @@ import chalk from "chalk";
|
||||
import type { Command } from "commander";
|
||||
|
||||
import { loadConfig, writeConfigFile } from "../config/config.js";
|
||||
import {
|
||||
installPluginFromArchive,
|
||||
installPluginFromNpmSpec,
|
||||
} from "../plugins/install.js";
|
||||
import { installPluginFromArchive, installPluginFromNpmSpec } from "../plugins/install.js";
|
||||
import type { PluginRecord } from "../plugins/registry.js";
|
||||
import { buildPluginStatusReport } from "../plugins/status.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
@@ -33,8 +30,7 @@ function formatPluginLine(plugin: PluginRecord, verbose = false): string {
|
||||
? chalk.yellow("disabled")
|
||||
: chalk.red("error");
|
||||
const name = plugin.name ? chalk.white(plugin.name) : chalk.white(plugin.id);
|
||||
const idSuffix =
|
||||
plugin.name !== plugin.id ? chalk.gray(` (${plugin.id})`) : "";
|
||||
const idSuffix = plugin.name !== plugin.id ? chalk.gray(` (${plugin.id})`) : "";
|
||||
const desc = plugin.description
|
||||
? chalk.gray(
|
||||
plugin.description.length > 60
|
||||
@@ -58,9 +54,7 @@ function formatPluginLine(plugin: PluginRecord, verbose = false): string {
|
||||
}
|
||||
|
||||
export function registerPluginsCli(program: Command) {
|
||||
const plugins = program
|
||||
.command("plugins")
|
||||
.description("Manage Clawdbot plugins/extensions");
|
||||
const plugins = program.command("plugins").description("Manage Clawdbot plugins/extensions");
|
||||
|
||||
plugins
|
||||
.command("list")
|
||||
@@ -160,20 +154,14 @@ export function registerPluginsCli(program: Command) {
|
||||
entries: {
|
||||
...cfg.plugins?.entries,
|
||||
[id]: {
|
||||
...(
|
||||
cfg.plugins?.entries as
|
||||
| Record<string, { enabled?: boolean }>
|
||||
| undefined
|
||||
)?.[id],
|
||||
...(cfg.plugins?.entries as Record<string, { enabled?: boolean }> | undefined)?.[id],
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await writeConfigFile(next);
|
||||
defaultRuntime.log(
|
||||
`Enabled plugin "${id}". Restart the gateway to apply.`,
|
||||
);
|
||||
defaultRuntime.log(`Enabled plugin "${id}". Restart the gateway to apply.`);
|
||||
});
|
||||
|
||||
plugins
|
||||
@@ -189,20 +177,14 @@ export function registerPluginsCli(program: Command) {
|
||||
entries: {
|
||||
...cfg.plugins?.entries,
|
||||
[id]: {
|
||||
...(
|
||||
cfg.plugins?.entries as
|
||||
| Record<string, { enabled?: boolean }>
|
||||
| undefined
|
||||
)?.[id],
|
||||
...(cfg.plugins?.entries as Record<string, { enabled?: boolean }> | undefined)?.[id],
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await writeConfigFile(next);
|
||||
defaultRuntime.log(
|
||||
`Disabled plugin "${id}". Restart the gateway to apply.`,
|
||||
);
|
||||
defaultRuntime.log(`Disabled plugin "${id}". Restart the gateway to apply.`);
|
||||
});
|
||||
|
||||
plugins
|
||||
@@ -235,9 +217,7 @@ export function registerPluginsCli(program: Command) {
|
||||
entries: {
|
||||
...cfg.plugins?.entries,
|
||||
[result.pluginId]: {
|
||||
...(cfg.plugins?.entries?.[result.pluginId] as
|
||||
| object
|
||||
| undefined),
|
||||
...(cfg.plugins?.entries?.[result.pluginId] as object | undefined),
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
@@ -301,9 +281,7 @@ export function registerPluginsCli(program: Command) {
|
||||
entries: {
|
||||
...cfg.plugins?.entries,
|
||||
[result.pluginId]: {
|
||||
...(cfg.plugins?.entries?.[result.pluginId] as
|
||||
| object
|
||||
| undefined),
|
||||
...(cfg.plugins?.entries?.[result.pluginId] as object | undefined),
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
@@ -331,9 +309,7 @@ export function registerPluginsCli(program: Command) {
|
||||
if (errors.length > 0) {
|
||||
lines.push(chalk.bold.red("Plugin errors:"));
|
||||
for (const entry of errors) {
|
||||
lines.push(
|
||||
`- ${entry.id}: ${entry.error ?? "failed to load"} (${entry.source})`,
|
||||
);
|
||||
lines.push(`- ${entry.id}: ${entry.error ?? "failed to load"} (${entry.source})`);
|
||||
}
|
||||
}
|
||||
if (diags.length > 0) {
|
||||
|
||||
@@ -30,11 +30,9 @@ export function parseLsofOutput(output: string): PortProcess[] {
|
||||
|
||||
export function listPortListeners(port: number): PortProcess[] {
|
||||
try {
|
||||
const out = execFileSync(
|
||||
"lsof",
|
||||
["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-FpFc"],
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
const out = execFileSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-FpFc"], {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
return parseLsofOutput(out);
|
||||
} catch (err: unknown) {
|
||||
const status = (err as { status?: number }).status;
|
||||
@@ -86,10 +84,7 @@ export async function forceFreePortAndWait(
|
||||
): Promise<ForceFreePortResult> {
|
||||
const timeoutMs = Math.max(opts.timeoutMs ?? 1500, 0);
|
||||
const intervalMs = Math.max(opts.intervalMs ?? 100, 1);
|
||||
const sigtermTimeoutMs = Math.min(
|
||||
Math.max(opts.sigtermTimeoutMs ?? 600, 0),
|
||||
timeoutMs,
|
||||
);
|
||||
const sigtermTimeoutMs = Math.min(Math.max(opts.sigtermTimeoutMs ?? 600, 0), timeoutMs);
|
||||
|
||||
const killed = forceFreePort(port);
|
||||
if (killed.length === 0) {
|
||||
@@ -97,8 +92,7 @@ export async function forceFreePortAndWait(
|
||||
}
|
||||
|
||||
let waitedMs = 0;
|
||||
const triesSigterm =
|
||||
intervalMs > 0 ? Math.ceil(sigtermTimeoutMs / intervalMs) : 0;
|
||||
const triesSigterm = intervalMs > 0 ? Math.ceil(sigtermTimeoutMs / intervalMs) : 0;
|
||||
for (let i = 0; i < triesSigterm; i++) {
|
||||
if (listPortListeners(port).length === 0) {
|
||||
return { killed, waitedMs, escalatedToSigkill: false };
|
||||
@@ -115,8 +109,7 @@ export async function forceFreePortAndWait(
|
||||
killPids(remaining, "SIGKILL");
|
||||
|
||||
const remainingBudget = Math.max(timeoutMs - waitedMs, 0);
|
||||
const triesSigkill =
|
||||
intervalMs > 0 ? Math.ceil(remainingBudget / intervalMs) : 0;
|
||||
const triesSigkill = intervalMs > 0 ? Math.ceil(remainingBudget / intervalMs) : 0;
|
||||
for (let i = 0; i < triesSigkill; i++) {
|
||||
if (listPortListeners(port).length === 0) {
|
||||
return { killed, waitedMs, escalatedToSigkill: true };
|
||||
|
||||
@@ -13,13 +13,7 @@ describe("parseCliProfileArgs", () => {
|
||||
]);
|
||||
if (!res.ok) throw new Error(res.error);
|
||||
expect(res.profile).toBeNull();
|
||||
expect(res.argv).toEqual([
|
||||
"node",
|
||||
"clawdbot",
|
||||
"gateway",
|
||||
"--dev",
|
||||
"--allow-unconfigured",
|
||||
]);
|
||||
expect(res.argv).toEqual(["node", "clawdbot", "gateway", "--dev", "--allow-unconfigured"]);
|
||||
});
|
||||
|
||||
it("still accepts global --dev before subcommand", () => {
|
||||
@@ -30,13 +24,7 @@ describe("parseCliProfileArgs", () => {
|
||||
});
|
||||
|
||||
it("parses --profile value and strips it", () => {
|
||||
const res = parseCliProfileArgs([
|
||||
"node",
|
||||
"clawdbot",
|
||||
"--profile",
|
||||
"work",
|
||||
"status",
|
||||
]);
|
||||
const res = parseCliProfileArgs(["node", "clawdbot", "--profile", "work", "status"]);
|
||||
if (!res.ok) throw new Error(res.error);
|
||||
expect(res.profile).toBe("work");
|
||||
expect(res.argv).toEqual(["node", "clawdbot", "status"]);
|
||||
@@ -48,26 +36,12 @@ describe("parseCliProfileArgs", () => {
|
||||
});
|
||||
|
||||
it("rejects combining --dev with --profile (dev first)", () => {
|
||||
const res = parseCliProfileArgs([
|
||||
"node",
|
||||
"clawdbot",
|
||||
"--dev",
|
||||
"--profile",
|
||||
"work",
|
||||
"status",
|
||||
]);
|
||||
const res = parseCliProfileArgs(["node", "clawdbot", "--dev", "--profile", "work", "status"]);
|
||||
expect(res.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects combining --dev with --profile (profile first)", () => {
|
||||
const res = parseCliProfileArgs([
|
||||
"node",
|
||||
"clawdbot",
|
||||
"--profile",
|
||||
"work",
|
||||
"--dev",
|
||||
"status",
|
||||
]);
|
||||
const res = parseCliProfileArgs(["node", "clawdbot", "--profile", "work", "--dev", "status"]);
|
||||
expect(res.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -83,9 +57,7 @@ describe("applyCliProfileEnv", () => {
|
||||
const expectedStateDir = path.join("/home/peter", ".clawdbot-dev");
|
||||
expect(env.CLAWDBOT_PROFILE).toBe("dev");
|
||||
expect(env.CLAWDBOT_STATE_DIR).toBe(expectedStateDir);
|
||||
expect(env.CLAWDBOT_CONFIG_PATH).toBe(
|
||||
path.join(expectedStateDir, "clawdbot.json"),
|
||||
);
|
||||
expect(env.CLAWDBOT_CONFIG_PATH).toBe(path.join(expectedStateDir, "clawdbot.json"));
|
||||
expect(env.CLAWDBOT_GATEWAY_PORT).toBe("19001");
|
||||
});
|
||||
|
||||
@@ -101,8 +73,6 @@ describe("applyCliProfileEnv", () => {
|
||||
});
|
||||
expect(env.CLAWDBOT_STATE_DIR).toBe("/custom");
|
||||
expect(env.CLAWDBOT_GATEWAY_PORT).toBe("19099");
|
||||
expect(env.CLAWDBOT_CONFIG_PATH).toBe(
|
||||
path.join("/custom", "clawdbot.json"),
|
||||
);
|
||||
expect(env.CLAWDBOT_CONFIG_PATH).toBe(path.join("/custom", "clawdbot.json"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,10 +84,7 @@ export function parseCliProfileArgs(argv: string[]): CliProfileParseResult {
|
||||
return { ok: true, profile, argv: out };
|
||||
}
|
||||
|
||||
function resolveProfileStateDir(
|
||||
profile: string,
|
||||
homedir: () => string,
|
||||
): string {
|
||||
function resolveProfileStateDir(profile: string, homedir: () => string): string {
|
||||
const suffix = profile.toLowerCase() === "default" ? "" : `-${profile}`;
|
||||
return path.join(homedir(), `.clawdbot${suffix}`);
|
||||
}
|
||||
@@ -105,8 +102,7 @@ export function applyCliProfileEnv(params: {
|
||||
// Convenience only: fill defaults, never override explicit env values.
|
||||
env.CLAWDBOT_PROFILE = profile;
|
||||
|
||||
const stateDir =
|
||||
env.CLAWDBOT_STATE_DIR?.trim() || resolveProfileStateDir(profile, homedir);
|
||||
const stateDir = env.CLAWDBOT_STATE_DIR?.trim() || resolveProfileStateDir(profile, homedir);
|
||||
if (!env.CLAWDBOT_STATE_DIR?.trim()) env.CLAWDBOT_STATE_DIR = stateDir;
|
||||
|
||||
if (!env.CLAWDBOT_CONFIG_PATH?.trim()) {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("node:child_process", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("node:child_process")>(
|
||||
"node:child_process",
|
||||
);
|
||||
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
|
||||
return {
|
||||
...actual,
|
||||
execFileSync: vi.fn(),
|
||||
|
||||
@@ -58,9 +58,7 @@ describe("cli program (nodes basics)", () => {
|
||||
const program = buildProgram();
|
||||
runtime.log.mockClear();
|
||||
await program.parseAsync(["nodes", "list"], { from: "user" });
|
||||
expect(callGateway).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ method: "node.pair.list" }),
|
||||
);
|
||||
expect(callGateway).toHaveBeenCalledWith(expect.objectContaining({ method: "node.pair.list" }));
|
||||
expect(runtime.log).toHaveBeenCalledWith("Pending: 0 · Paired: 0");
|
||||
});
|
||||
|
||||
@@ -88,9 +86,7 @@ describe("cli program (nodes basics)", () => {
|
||||
expect.objectContaining({ method: "node.list", params: {} }),
|
||||
);
|
||||
|
||||
const output = runtime.log.mock.calls
|
||||
.map((c) => String(c[0] ?? ""))
|
||||
.join("\n");
|
||||
const output = runtime.log.mock.calls.map((c) => String(c[0] ?? "")).join("\n");
|
||||
expect(output).toContain("Known: 1 · Paired: 1 · Connected: 1");
|
||||
expect(output).toContain("iOS Node");
|
||||
expect(output).toContain("device: iPad");
|
||||
@@ -119,9 +115,7 @@ describe("cli program (nodes basics)", () => {
|
||||
runtime.log.mockClear();
|
||||
await program.parseAsync(["nodes", "status"], { from: "user" });
|
||||
|
||||
const output = runtime.log.mock.calls
|
||||
.map((c) => String(c[0] ?? ""))
|
||||
.join("\n");
|
||||
const output = runtime.log.mock.calls.map((c) => String(c[0] ?? "")).join("\n");
|
||||
expect(output).toContain("Known: 1 · Paired: 0 · Connected: 1");
|
||||
expect(output).toContain("Peter's Tab S10 Ultra");
|
||||
expect(output).toContain("device: Android");
|
||||
@@ -171,9 +165,7 @@ describe("cli program (nodes basics)", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const out = runtime.log.mock.calls
|
||||
.map((c) => String(c[0] ?? ""))
|
||||
.join("\n");
|
||||
const out = runtime.log.mock.calls.map((c) => String(c[0] ?? "")).join("\n");
|
||||
expect(out).toContain("Commands:");
|
||||
expect(out).toContain("canvas.eval");
|
||||
});
|
||||
|
||||
@@ -82,10 +82,7 @@ describe("cli program (nodes media)", () => {
|
||||
|
||||
const program = buildProgram();
|
||||
runtime.log.mockClear();
|
||||
await program.parseAsync(
|
||||
["nodes", "camera", "snap", "--node", "ios-node"],
|
||||
{ from: "user" },
|
||||
);
|
||||
await program.parseAsync(["nodes", "camera", "snap", "--node", "ios-node"], { from: "user" });
|
||||
|
||||
expect(callGateway).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
@@ -436,14 +433,11 @@ describe("cli program (nodes media)", () => {
|
||||
runtime.error.mockClear();
|
||||
|
||||
await expect(
|
||||
program.parseAsync(
|
||||
["nodes", "camera", "snap", "--node", "ios-node", "--facing", "nope"],
|
||||
{ from: "user" },
|
||||
),
|
||||
program.parseAsync(["nodes", "camera", "snap", "--node", "ios-node", "--facing", "nope"], {
|
||||
from: "user",
|
||||
}),
|
||||
).rejects.toThrow(/exit/i);
|
||||
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/invalid facing/i),
|
||||
);
|
||||
expect(runtime.error).toHaveBeenCalledWith(expect.stringMatching(/invalid facing/i));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,12 +55,9 @@ describe("cli program (smoke)", () => {
|
||||
|
||||
it("runs message with required options", async () => {
|
||||
const program = buildProgram();
|
||||
await program.parseAsync(
|
||||
["message", "send", "--to", "+1", "--message", "hi"],
|
||||
{
|
||||
from: "user",
|
||||
},
|
||||
);
|
||||
await program.parseAsync(["message", "send", "--to", "+1", "--message", "hi"], {
|
||||
from: "user",
|
||||
});
|
||||
expect(messageCommand).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -73,9 +70,7 @@ describe("cli program (smoke)", () => {
|
||||
it("runs tui without overriding timeout", async () => {
|
||||
const program = buildProgram();
|
||||
await program.parseAsync(["tui"], { from: "user" });
|
||||
expect(runTui).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ timeoutMs: undefined }),
|
||||
);
|
||||
expect(runTui).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: undefined }));
|
||||
});
|
||||
|
||||
it("runs tui with explicit timeout override", async () => {
|
||||
@@ -83,20 +78,14 @@ describe("cli program (smoke)", () => {
|
||||
await program.parseAsync(["tui", "--timeout-ms", "45000"], {
|
||||
from: "user",
|
||||
});
|
||||
expect(runTui).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ timeoutMs: 45000 }),
|
||||
);
|
||||
expect(runTui).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 45000 }));
|
||||
});
|
||||
|
||||
it("warns and ignores invalid tui timeout override", async () => {
|
||||
const program = buildProgram();
|
||||
await program.parseAsync(["tui", "--timeout-ms", "nope"], { from: "user" });
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
'warning: invalid --timeout-ms "nope"; ignoring',
|
||||
);
|
||||
expect(runTui).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ timeoutMs: undefined }),
|
||||
);
|
||||
expect(runtime.error).toHaveBeenCalledWith('warning: invalid --timeout-ms "nope"; ignoring');
|
||||
expect(runTui).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: undefined }));
|
||||
});
|
||||
|
||||
it("runs config alias as configure", async () => {
|
||||
@@ -252,9 +241,6 @@ describe("cli program (smoke)", () => {
|
||||
await program.parseAsync(["channels", "logout", "--account", "work"], {
|
||||
from: "user",
|
||||
});
|
||||
expect(runChannelLogout).toHaveBeenCalledWith(
|
||||
{ channel: undefined, account: "work" },
|
||||
runtime,
|
||||
);
|
||||
expect(runChannelLogout).toHaveBeenCalledWith({ channel: undefined, account: "work" }, runtime);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,14 +14,8 @@ const EXAMPLES = [
|
||||
"Send via your web session and print JSON result.",
|
||||
],
|
||||
["clawdbot gateway --port 18789", "Run the WebSocket Gateway locally."],
|
||||
[
|
||||
"clawdbot --dev gateway",
|
||||
"Run a dev Gateway (isolated state/config) on ws://127.0.0.1:19001.",
|
||||
],
|
||||
[
|
||||
"clawdbot gateway --force",
|
||||
"Kill anything bound to the default gateway port, then start it.",
|
||||
],
|
||||
["clawdbot --dev gateway", "Run a dev Gateway (isolated state/config) on ws://127.0.0.1:19001."],
|
||||
["clawdbot gateway --force", "Kill anything bound to the default gateway port, then start it."],
|
||||
["clawdbot gateway ...", "Gateway control via WebSocket."],
|
||||
[
|
||||
'clawdbot agent --to +15555550123 --message "Run summary" --deliver',
|
||||
@@ -88,8 +82,6 @@ export function configureProgramHelp(program: Command, ctx: ProgramContext) {
|
||||
|
||||
program.addHelpText("afterAll", () => {
|
||||
const docs = formatDocsLink("/cli", "docs.clawd.bot/cli");
|
||||
return `\n${theme.heading("Examples:")}\n${fmtExamples}\n\n${theme.muted(
|
||||
"Docs:",
|
||||
)} ${docs}\n`;
|
||||
return `\n${theme.heading("Examples:")}\n${fmtExamples}\n\n${theme.muted("Docs:")} ${docs}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
export function collectOption(
|
||||
value: string,
|
||||
previous: string[] = [],
|
||||
): string[] {
|
||||
export function collectOption(value: string, previous: string[] = []): string[] {
|
||||
return [...previous, value];
|
||||
}
|
||||
|
||||
export function parsePositiveIntOrUndefined(
|
||||
value: unknown,
|
||||
): number | undefined {
|
||||
export function parsePositiveIntOrUndefined(value: unknown): number | undefined {
|
||||
if (value === undefined || value === null || value === "") return undefined;
|
||||
if (typeof value === "number") {
|
||||
if (!Number.isFinite(value)) return undefined;
|
||||
|
||||
@@ -8,10 +8,7 @@ export type MessageCliHelpers = {
|
||||
withMessageBase: (command: Command) => Command;
|
||||
withMessageTarget: (command: Command) => Command;
|
||||
withRequiredMessageTarget: (command: Command) => Command;
|
||||
runMessageAction: (
|
||||
action: string,
|
||||
opts: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
runMessageAction: (action: string, opts: Record<string, unknown>) => Promise<void>;
|
||||
};
|
||||
|
||||
export function createMessageCliHelpers(
|
||||
@@ -37,10 +34,7 @@ export function createMessageCliHelpers(
|
||||
"Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack channel/user, or iMessage handle/chat_id",
|
||||
);
|
||||
|
||||
const runMessageAction = async (
|
||||
action: string,
|
||||
opts: Record<string, unknown>,
|
||||
) => {
|
||||
const runMessageAction = async (action: string, opts: Record<string, unknown>) => {
|
||||
setVerbose(Boolean(opts.verbose));
|
||||
const deps = createDefaultDeps();
|
||||
try {
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import type { Command } from "commander";
|
||||
import type { MessageCliHelpers } from "./helpers.js";
|
||||
|
||||
export function registerMessageDiscordAdminCommands(
|
||||
message: Command,
|
||||
helpers: MessageCliHelpers,
|
||||
) {
|
||||
export function registerMessageDiscordAdminCommands(message: Command, helpers: MessageCliHelpers) {
|
||||
const role = message.command("role").description("Role actions");
|
||||
helpers
|
||||
.withMessageBase(
|
||||
role
|
||||
.command("info")
|
||||
.description("List roles")
|
||||
.requiredOption("--guild-id <id>", "Guild id"),
|
||||
role.command("info").description("List roles").requiredOption("--guild-id <id>", "Guild id"),
|
||||
)
|
||||
.action(async (opts) => {
|
||||
await helpers.runMessageAction("role-info", opts);
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { Command } from "commander";
|
||||
import { collectOption } from "../helpers.js";
|
||||
import type { MessageCliHelpers } from "./helpers.js";
|
||||
|
||||
export function registerMessageEmojiCommands(
|
||||
message: Command,
|
||||
helpers: MessageCliHelpers,
|
||||
) {
|
||||
export function registerMessageEmojiCommands(message: Command, helpers: MessageCliHelpers) {
|
||||
const emoji = message.command("emoji").description("Emoji actions");
|
||||
|
||||
helpers
|
||||
@@ -24,28 +21,18 @@ export function registerMessageEmojiCommands(
|
||||
)
|
||||
.requiredOption("--emoji-name <name>", "Emoji name")
|
||||
.requiredOption("--media <path-or-url>", "Emoji media (path or URL)")
|
||||
.option(
|
||||
"--role-ids <id>",
|
||||
"Role id (repeat)",
|
||||
collectOption,
|
||||
[] as string[],
|
||||
)
|
||||
.option("--role-ids <id>", "Role id (repeat)", collectOption, [] as string[])
|
||||
.action(async (opts) => {
|
||||
await helpers.runMessageAction("emoji-upload", opts);
|
||||
});
|
||||
}
|
||||
|
||||
export function registerMessageStickerCommands(
|
||||
message: Command,
|
||||
helpers: MessageCliHelpers,
|
||||
) {
|
||||
export function registerMessageStickerCommands(message: Command, helpers: MessageCliHelpers) {
|
||||
const sticker = message.command("sticker").description("Sticker actions");
|
||||
|
||||
helpers
|
||||
.withMessageBase(
|
||||
helpers.withRequiredMessageTarget(
|
||||
sticker.command("send").description("Send stickers"),
|
||||
),
|
||||
helpers.withRequiredMessageTarget(sticker.command("send").description("Send stickers")),
|
||||
)
|
||||
.requiredOption("--sticker-id <id>", "Sticker id (repeat)", collectOption)
|
||||
.option("-m, --message <text>", "Optional message body")
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { Command } from "commander";
|
||||
import { collectOption } from "../helpers.js";
|
||||
import type { MessageCliHelpers } from "./helpers.js";
|
||||
|
||||
export function registerMessagePermissionsCommand(
|
||||
message: Command,
|
||||
helpers: MessageCliHelpers,
|
||||
) {
|
||||
export function registerMessagePermissionsCommand(message: Command, helpers: MessageCliHelpers) {
|
||||
helpers
|
||||
.withMessageBase(
|
||||
helpers.withMessageTarget(
|
||||
@@ -18,30 +15,15 @@ export function registerMessagePermissionsCommand(
|
||||
});
|
||||
}
|
||||
|
||||
export function registerMessageSearchCommand(
|
||||
message: Command,
|
||||
helpers: MessageCliHelpers,
|
||||
) {
|
||||
export function registerMessageSearchCommand(message: Command, helpers: MessageCliHelpers) {
|
||||
helpers
|
||||
.withMessageBase(
|
||||
message.command("search").description("Search Discord messages"),
|
||||
)
|
||||
.withMessageBase(message.command("search").description("Search Discord messages"))
|
||||
.requiredOption("--guild-id <id>", "Guild id")
|
||||
.requiredOption("--query <text>", "Search query")
|
||||
.option("--channel-id <id>", "Channel id")
|
||||
.option(
|
||||
"--channel-ids <id>",
|
||||
"Channel id (repeat)",
|
||||
collectOption,
|
||||
[] as string[],
|
||||
)
|
||||
.option("--channel-ids <id>", "Channel id (repeat)", collectOption, [] as string[])
|
||||
.option("--author-id <id>", "Author id")
|
||||
.option(
|
||||
"--author-ids <id>",
|
||||
"Author id (repeat)",
|
||||
collectOption,
|
||||
[] as string[],
|
||||
)
|
||||
.option("--author-ids <id>", "Author id (repeat)", collectOption, [] as string[])
|
||||
.option("--limit <n>", "Result limit")
|
||||
.action(async (opts) => {
|
||||
await helpers.runMessageAction("search", opts);
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
import type { Command } from "commander";
|
||||
import type { MessageCliHelpers } from "./helpers.js";
|
||||
|
||||
export function registerMessagePinCommands(
|
||||
message: Command,
|
||||
helpers: MessageCliHelpers,
|
||||
) {
|
||||
export function registerMessagePinCommands(message: Command, helpers: MessageCliHelpers) {
|
||||
const withPinsTarget = (command: Command) =>
|
||||
command.option(
|
||||
"--channel-id <id>",
|
||||
"Channel id (defaults to --to; required for WhatsApp)",
|
||||
);
|
||||
command.option("--channel-id <id>", "Channel id (defaults to --to; required for WhatsApp)");
|
||||
|
||||
const pins = [
|
||||
helpers
|
||||
.withMessageBase(
|
||||
withPinsTarget(
|
||||
helpers.withMessageTarget(
|
||||
message.command("pin").description("Pin a message"),
|
||||
),
|
||||
helpers.withMessageTarget(message.command("pin").description("Pin a message")),
|
||||
),
|
||||
)
|
||||
.requiredOption("--message-id <id>", "Message id")
|
||||
@@ -27,9 +19,7 @@ export function registerMessagePinCommands(
|
||||
helpers
|
||||
.withMessageBase(
|
||||
withPinsTarget(
|
||||
helpers.withMessageTarget(
|
||||
message.command("unpin").description("Unpin a message"),
|
||||
),
|
||||
helpers.withMessageTarget(message.command("unpin").description("Unpin a message")),
|
||||
),
|
||||
)
|
||||
.requiredOption("--message-id <id>", "Message id")
|
||||
@@ -38,9 +28,7 @@ export function registerMessagePinCommands(
|
||||
}),
|
||||
helpers
|
||||
.withMessageBase(
|
||||
helpers.withMessageTarget(
|
||||
message.command("pins").description("List pinned messages"),
|
||||
),
|
||||
helpers.withMessageTarget(message.command("pins").description("List pinned messages")),
|
||||
)
|
||||
.option("--channel-id <id>", "Channel id (defaults to --to)")
|
||||
.option("--limit <n>", "Result limit")
|
||||
|
||||
@@ -2,15 +2,10 @@ import type { Command } from "commander";
|
||||
import { collectOption } from "../helpers.js";
|
||||
import type { MessageCliHelpers } from "./helpers.js";
|
||||
|
||||
export function registerMessagePollCommand(
|
||||
message: Command,
|
||||
helpers: MessageCliHelpers,
|
||||
) {
|
||||
export function registerMessagePollCommand(message: Command, helpers: MessageCliHelpers) {
|
||||
helpers
|
||||
.withMessageBase(
|
||||
helpers.withRequiredMessageTarget(
|
||||
message.command("poll").description("Send a poll"),
|
||||
),
|
||||
helpers.withRequiredMessageTarget(message.command("poll").description("Send a poll")),
|
||||
)
|
||||
.requiredOption("--poll-question <text>", "Poll question")
|
||||
.option(
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import type { Command } from "commander";
|
||||
import type { MessageCliHelpers } from "./helpers.js";
|
||||
|
||||
export function registerMessageReactionsCommands(
|
||||
message: Command,
|
||||
helpers: MessageCliHelpers,
|
||||
) {
|
||||
export function registerMessageReactionsCommands(message: Command, helpers: MessageCliHelpers) {
|
||||
helpers
|
||||
.withMessageBase(
|
||||
helpers.withMessageTarget(
|
||||
message.command("react").description("Add or remove a reaction"),
|
||||
),
|
||||
helpers.withMessageTarget(message.command("react").description("Add or remove a reaction")),
|
||||
)
|
||||
.requiredOption("--message-id <id>", "Message id")
|
||||
.option("--emoji <emoji>", "Emoji for reactions")
|
||||
|
||||
@@ -7,9 +7,7 @@ export function registerMessageReadEditDeleteCommands(
|
||||
) {
|
||||
helpers
|
||||
.withMessageBase(
|
||||
helpers.withMessageTarget(
|
||||
message.command("read").description("Read recent messages"),
|
||||
),
|
||||
helpers.withMessageTarget(message.command("read").description("Read recent messages")),
|
||||
)
|
||||
.option("--limit <n>", "Result limit")
|
||||
.option("--before <id>", "Read/search before id")
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { Command } from "commander";
|
||||
import type { MessageCliHelpers } from "./helpers.js";
|
||||
|
||||
export function registerMessageSendCommand(
|
||||
message: Command,
|
||||
helpers: MessageCliHelpers,
|
||||
) {
|
||||
export function registerMessageSendCommand(message: Command, helpers: MessageCliHelpers) {
|
||||
helpers
|
||||
.withMessageBase(
|
||||
helpers
|
||||
@@ -24,11 +21,7 @@ export function registerMessageSendCommand(
|
||||
)
|
||||
.option("--reply-to <id>", "Reply-to message id")
|
||||
.option("--thread-id <id>", "Thread id (Telegram forum thread)")
|
||||
.option(
|
||||
"--gif-playback",
|
||||
"Treat video media as GIF playback (WhatsApp only).",
|
||||
false,
|
||||
),
|
||||
.option("--gif-playback", "Treat video media as GIF playback (WhatsApp only).", false),
|
||||
)
|
||||
.action(async (opts) => {
|
||||
await helpers.runMessageAction("send", opts);
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { Command } from "commander";
|
||||
import type { MessageCliHelpers } from "./helpers.js";
|
||||
|
||||
export function registerMessageThreadCommands(
|
||||
message: Command,
|
||||
helpers: MessageCliHelpers,
|
||||
) {
|
||||
export function registerMessageThreadCommands(message: Command, helpers: MessageCliHelpers) {
|
||||
const thread = message.command("thread").description("Thread actions");
|
||||
|
||||
helpers
|
||||
|
||||
@@ -11,10 +11,7 @@ import { autoMigrateLegacyState } from "../../infra/state-migrations.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { emitCliBanner } from "../banner.js";
|
||||
|
||||
export function registerPreActionHooks(
|
||||
program: Command,
|
||||
programVersion: string,
|
||||
) {
|
||||
export function registerPreActionHooks(program: Command, programVersion: string) {
|
||||
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
||||
emitCliBanner(programVersion);
|
||||
if (actionCommand.name() === "doctor") return;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import type { Command } from "commander";
|
||||
import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js";
|
||||
import { agentCliCommand } from "../../commands/agent-via-gateway.js";
|
||||
import {
|
||||
agentsAddCommand,
|
||||
agentsDeleteCommand,
|
||||
agentsListCommand,
|
||||
} from "../../commands/agents.js";
|
||||
import { agentsAddCommand, agentsDeleteCommand, agentsListCommand } from "../../commands/agents.js";
|
||||
import { setVerbose } from "../../globals.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
@@ -14,23 +10,14 @@ import { hasExplicitOptions } from "../command-options.js";
|
||||
import { createDefaultDeps } from "../deps.js";
|
||||
import { collectOption } from "./helpers.js";
|
||||
|
||||
export function registerAgentCommands(
|
||||
program: Command,
|
||||
args: { agentChannelOptions: string },
|
||||
) {
|
||||
export function registerAgentCommands(program: Command, args: { agentChannelOptions: string }) {
|
||||
program
|
||||
.command("agent")
|
||||
.description("Run an agent turn via the Gateway (use --local for embedded)")
|
||||
.requiredOption("-m, --message <text>", "Message body for the agent")
|
||||
.option(
|
||||
"-t, --to <number>",
|
||||
"Recipient number in E.164 used to derive the session key",
|
||||
)
|
||||
.option("-t, --to <number>", "Recipient number in E.164 used to derive the session key")
|
||||
.option("--session-id <id>", "Use an explicit session id")
|
||||
.option(
|
||||
"--thinking <level>",
|
||||
"Thinking level: off | minimal | low | medium | high",
|
||||
)
|
||||
.option("--thinking <level>", "Thinking level: off | minimal | low | medium | high")
|
||||
.option("--verbose <on|off>", "Persist agent verbose level for the session")
|
||||
.option(
|
||||
"--channel <channel>",
|
||||
@@ -61,14 +48,10 @@ Examples:
|
||||
clawdbot agent --to +15555550123 --message "Trace logs" --verbose on --json
|
||||
clawdbot agent --to +15555550123 --message "Summon reply" --deliver
|
||||
|
||||
${theme.muted("Docs:")} ${formatDocsLink(
|
||||
"/agent-send",
|
||||
"docs.clawd.bot/agent-send",
|
||||
)}`,
|
||||
${theme.muted("Docs:")} ${formatDocsLink("/agent-send", "docs.clawd.bot/agent-send")}`,
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const verboseLevel =
|
||||
typeof opts.verbose === "string" ? opts.verbose.toLowerCase() : "";
|
||||
const verboseLevel = typeof opts.verbose === "string" ? opts.verbose.toLowerCase() : "";
|
||||
setVerbose(verboseLevel === "on");
|
||||
// Build default deps (keeps parity with other commands; future-proofing).
|
||||
const deps = createDefaultDeps();
|
||||
@@ -107,12 +90,7 @@ ${theme.muted("Docs:")} ${formatDocsLink(
|
||||
.option("--workspace <dir>", "Workspace directory for the new agent")
|
||||
.option("--model <id>", "Model id for this agent")
|
||||
.option("--agent-dir <dir>", "Agent state directory for this agent")
|
||||
.option(
|
||||
"--bind <channel[:accountId]>",
|
||||
"Route channel binding (repeatable)",
|
||||
collectOption,
|
||||
[],
|
||||
)
|
||||
.option("--bind <channel[:accountId]>", "Route channel binding (repeatable)", collectOption, [])
|
||||
.option("--non-interactive", "Disable prompts; requires --workspace", false)
|
||||
.option("--json", "Output JSON summary", false)
|
||||
.action(async (name, opts, command) => {
|
||||
@@ -130,9 +108,7 @@ ${theme.muted("Docs:")} ${formatDocsLink(
|
||||
workspace: opts.workspace as string | undefined,
|
||||
model: opts.model as string | undefined,
|
||||
agentDir: opts.agentDir as string | undefined,
|
||||
bind: Array.isArray(opts.bind)
|
||||
? (opts.bind as string[])
|
||||
: undefined,
|
||||
bind: Array.isArray(opts.bind) ? (opts.bind as string[]) : undefined,
|
||||
nonInteractive: Boolean(opts.nonInteractive),
|
||||
json: Boolean(opts.json),
|
||||
},
|
||||
|
||||
@@ -25,9 +25,7 @@ export function registerConfigureCommand(program: Command) {
|
||||
try {
|
||||
const sections: string[] = Array.isArray(opts.section)
|
||||
? opts.section
|
||||
.map((value: unknown) =>
|
||||
typeof value === "string" ? value.trim() : "",
|
||||
)
|
||||
.map((value: unknown) => (typeof value === "string" ? value.trim() : ""))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
if (sections.length === 0) {
|
||||
@@ -35,9 +33,7 @@ export function registerConfigureCommand(program: Command) {
|
||||
return;
|
||||
}
|
||||
|
||||
const invalid = sections.filter(
|
||||
(s) => !CONFIGURE_WIZARD_SECTIONS.includes(s as never),
|
||||
);
|
||||
const invalid = sections.filter((s) => !CONFIGURE_WIZARD_SECTIONS.includes(s as never));
|
||||
if (invalid.length > 0) {
|
||||
defaultRuntime.error(
|
||||
`Invalid --section: ${invalid.join(", ")}. Expected one of: ${CONFIGURE_WIZARD_SECTIONS.join(", ")}.`,
|
||||
|
||||
@@ -9,28 +9,12 @@ export function registerMaintenanceCommands(program: Command) {
|
||||
program
|
||||
.command("doctor")
|
||||
.description("Health checks + quick fixes for the gateway and channels")
|
||||
.option(
|
||||
"--no-workspace-suggestions",
|
||||
"Disable workspace memory system suggestions",
|
||||
false,
|
||||
)
|
||||
.option("--no-workspace-suggestions", "Disable workspace memory system suggestions", false)
|
||||
.option("--yes", "Accept defaults without prompting", false)
|
||||
.option("--repair", "Apply recommended repairs without prompting", false)
|
||||
.option(
|
||||
"--force",
|
||||
"Apply aggressive repairs (overwrites custom service config)",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--non-interactive",
|
||||
"Run without prompts (safe migrations only)",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--generate-gateway-token",
|
||||
"Generate and configure a gateway token",
|
||||
false,
|
||||
)
|
||||
.option("--force", "Apply aggressive repairs (overwrites custom service config)", false)
|
||||
.option("--non-interactive", "Run without prompts (safe migrations only)", false)
|
||||
.option("--generate-gateway-token", "Generate and configure a gateway token", false)
|
||||
.option("--deep", "Scan system services for extra gateway installs", false)
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
@@ -67,16 +51,9 @@ export function registerMaintenanceCommands(program: Command) {
|
||||
program
|
||||
.command("reset")
|
||||
.description("Reset local config/state (keeps the CLI installed)")
|
||||
.option(
|
||||
"--scope <scope>",
|
||||
"config|config+creds+sessions|full (default: interactive prompt)",
|
||||
)
|
||||
.option("--scope <scope>", "config|config+creds+sessions|full (default: interactive prompt)")
|
||||
.option("--yes", "Skip confirmation prompts", false)
|
||||
.option(
|
||||
"--non-interactive",
|
||||
"Disable prompts (requires --scope + --yes)",
|
||||
false,
|
||||
)
|
||||
.option("--non-interactive", "Disable prompts (requires --scope + --yes)", false)
|
||||
.option("--dry-run", "Print actions without removing files", false)
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
|
||||
@@ -33,10 +33,7 @@ Examples:
|
||||
clawdbot message poll --channel discord --to channel:123 --poll-question "Snack?" --poll-option Pizza --poll-option Sushi
|
||||
clawdbot message react --channel discord --to 123 --message-id 456 --emoji "✅"
|
||||
|
||||
${theme.muted("Docs:")} ${formatDocsLink(
|
||||
"/cli/message",
|
||||
"docs.clawd.bot/cli/message",
|
||||
)}`,
|
||||
${theme.muted("Docs:")} ${formatDocsLink("/cli/message", "docs.clawd.bot/cli/message")}`,
|
||||
)
|
||||
.action(() => {
|
||||
message.help({ error: true });
|
||||
|
||||
@@ -16,9 +16,7 @@ function resolveInstallDaemonFlag(
|
||||
): boolean | undefined {
|
||||
if (!command || typeof command !== "object") return undefined;
|
||||
const getOptionValueSource =
|
||||
"getOptionValueSource" in command
|
||||
? command.getOptionValueSource
|
||||
: undefined;
|
||||
"getOptionValueSource" in command ? command.getOptionValueSource : undefined;
|
||||
if (typeof getOptionValueSource !== "function") return undefined;
|
||||
|
||||
// Commander doesn't support option conflicts natively; keep original behavior.
|
||||
@@ -33,14 +31,9 @@ function resolveInstallDaemonFlag(
|
||||
export function registerOnboardCommand(program: Command) {
|
||||
program
|
||||
.command("onboard")
|
||||
.description(
|
||||
"Interactive wizard to set up the gateway, workspace, and skills",
|
||||
)
|
||||
.description("Interactive wizard to set up the gateway, workspace, and skills")
|
||||
.option("--workspace <dir>", "Agent workspace directory (default: ~/clawd)")
|
||||
.option(
|
||||
"--reset",
|
||||
"Reset config + credentials + sessions + workspace before running wizard",
|
||||
)
|
||||
.option("--reset", "Reset config + credentials + sessions + workspace before running wizard")
|
||||
.option("--non-interactive", "Run without prompts", false)
|
||||
.option("--flow <flow>", "Wizard flow: quickstart|advanced")
|
||||
.option("--mode <mode>", "Wizard mode: local|remote")
|
||||
@@ -52,18 +45,12 @@ export function registerOnboardCommand(program: Command) {
|
||||
"--token-provider <id>",
|
||||
"Token provider id (non-interactive; used with --auth-choice token)",
|
||||
)
|
||||
.option(
|
||||
"--token <token>",
|
||||
"Token value (non-interactive; used with --auth-choice token)",
|
||||
)
|
||||
.option("--token <token>", "Token value (non-interactive; used with --auth-choice token)")
|
||||
.option(
|
||||
"--token-profile-id <id>",
|
||||
"Auth profile id (non-interactive; default: <provider>:manual)",
|
||||
)
|
||||
.option(
|
||||
"--token-expires-in <duration>",
|
||||
"Optional token expiry duration (e.g. 365d, 12h)",
|
||||
)
|
||||
.option("--token-expires-in <duration>", "Optional token expiry duration (e.g. 365d, 12h)")
|
||||
.option("--anthropic-api-key <key>", "Anthropic API key")
|
||||
.option("--openai-api-key <key>", "OpenAI API key")
|
||||
.option("--openrouter-api-key <key>", "OpenRouter API key")
|
||||
@@ -98,9 +85,7 @@ export function registerOnboardCommand(program: Command) {
|
||||
installDaemon: Boolean(opts.installDaemon),
|
||||
});
|
||||
const gatewayPort =
|
||||
typeof opts.gatewayPort === "string"
|
||||
? Number.parseInt(opts.gatewayPort, 10)
|
||||
: undefined;
|
||||
typeof opts.gatewayPort === "string" ? Number.parseInt(opts.gatewayPort, 10) : undefined;
|
||||
await onboardCommand(
|
||||
{
|
||||
workspace: opts.workspace as string | undefined,
|
||||
@@ -135,9 +120,7 @@ export function registerOnboardCommand(program: Command) {
|
||||
tailscaleResetOnExit: Boolean(opts.tailscaleResetOnExit),
|
||||
reset: Boolean(opts.reset),
|
||||
installDaemon,
|
||||
daemonRuntime: opts.daemonRuntime as
|
||||
| GatewayDaemonRuntime
|
||||
| undefined,
|
||||
daemonRuntime: opts.daemonRuntime as GatewayDaemonRuntime | undefined,
|
||||
skipChannels: Boolean(opts.skipChannels),
|
||||
skipSkills: Boolean(opts.skipSkills),
|
||||
skipHealth: Boolean(opts.skipHealth),
|
||||
|
||||
@@ -39,10 +39,7 @@ export function registerSetupCommand(program: Command) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
await setupCommand(
|
||||
{ workspace: opts.workspace as string | undefined },
|
||||
defaultRuntime,
|
||||
);
|
||||
await setupCommand({ workspace: opts.workspace as string | undefined }, defaultRuntime);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(String(err));
|
||||
defaultRuntime.exit(1);
|
||||
|
||||
@@ -13,11 +13,7 @@ export function registerStatusHealthSessionsCommands(program: Command) {
|
||||
.option("--json", "Output JSON instead of text", false)
|
||||
.option("--all", "Full diagnosis (read-only, pasteable)", false)
|
||||
.option("--usage", "Show model provider usage/quota snapshots", false)
|
||||
.option(
|
||||
"--deep",
|
||||
"Probe channels (WhatsApp Web + Telegram + Discord + Slack + Signal)",
|
||||
false,
|
||||
)
|
||||
.option("--deep", "Probe channels (WhatsApp Web + Telegram + Discord + Slack + Signal)", false)
|
||||
.option("--timeout <ms>", "Probe timeout in milliseconds", "10000")
|
||||
.option("--verbose", "Verbose logging", false)
|
||||
.option("--debug", "Alias for --verbose", false)
|
||||
@@ -38,9 +34,7 @@ Examples:
|
||||
setVerbose(verbose);
|
||||
const timeout = parsePositiveIntOrUndefined(opts.timeout);
|
||||
if (opts.timeout !== undefined && timeout === undefined) {
|
||||
defaultRuntime.error(
|
||||
"--timeout must be a positive integer (milliseconds)",
|
||||
);
|
||||
defaultRuntime.error("--timeout must be a positive integer (milliseconds)");
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
@@ -74,9 +68,7 @@ Examples:
|
||||
setVerbose(verbose);
|
||||
const timeout = parsePositiveIntOrUndefined(opts.timeout);
|
||||
if (opts.timeout !== undefined && timeout === undefined) {
|
||||
defaultRuntime.error(
|
||||
"--timeout must be a positive integer (milliseconds)",
|
||||
);
|
||||
defaultRuntime.error("--timeout must be a positive integer (milliseconds)");
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
@@ -100,14 +92,8 @@ Examples:
|
||||
.description("List stored conversation sessions")
|
||||
.option("--json", "Output as JSON", false)
|
||||
.option("--verbose", "Verbose logging", false)
|
||||
.option(
|
||||
"--store <path>",
|
||||
"Path to session store (default: resolved from config)",
|
||||
)
|
||||
.option(
|
||||
"--active <minutes>",
|
||||
"Only show sessions updated within the past N minutes",
|
||||
)
|
||||
.option("--store <path>", "Path to session store (default: resolved from config)")
|
||||
.option("--active <minutes>", "Only show sessions updated within the past N minutes")
|
||||
.addHelpText(
|
||||
"after",
|
||||
`
|
||||
|
||||
@@ -42,11 +42,9 @@ export function createCliProgress(options: ProgressOptions): ProgressReporter {
|
||||
const stream = options.stream ?? process.stderr;
|
||||
if (!stream.isTTY) return noopReporter;
|
||||
|
||||
const delayMs =
|
||||
typeof options.delayMs === "number" ? options.delayMs : DEFAULT_DELAY_MS;
|
||||
const delayMs = typeof options.delayMs === "number" ? options.delayMs : DEFAULT_DELAY_MS;
|
||||
const canOsc = supportsOscProgress(process.env, stream.isTTY);
|
||||
const allowSpinner =
|
||||
options.fallback === undefined || options.fallback === "spinner";
|
||||
const allowSpinner = options.fallback === undefined || options.fallback === "spinner";
|
||||
|
||||
let started = false;
|
||||
let label = options.label;
|
||||
@@ -54,8 +52,7 @@ export function createCliProgress(options: ProgressOptions): ProgressReporter {
|
||||
let completed = 0;
|
||||
let percent = 0;
|
||||
let indeterminate =
|
||||
options.indeterminate ??
|
||||
(options.total === undefined || options.total === null);
|
||||
options.indeterminate ?? (options.total === undefined || options.total === null);
|
||||
|
||||
activeProgress += 1;
|
||||
|
||||
@@ -145,10 +142,7 @@ export async function withProgress<T>(
|
||||
|
||||
export async function withProgressTotals<T>(
|
||||
options: ProgressOptions,
|
||||
work: (
|
||||
update: (update: ProgressTotalsUpdate) => void,
|
||||
progress: ProgressReporter,
|
||||
) => Promise<T>,
|
||||
work: (update: (update: ProgressTotalsUpdate) => void, progress: ProgressReporter) => Promise<T>,
|
||||
): Promise<T> {
|
||||
return await withProgress(options, async (progress) => {
|
||||
const update = ({ completed, total, label }: ProgressTotalsUpdate) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user