mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 04:51:25 +00:00
chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
@@ -13,9 +13,7 @@ export async function readLatestAssistantReply(params: {
|
||||
method: "chat.history",
|
||||
params: { sessionKey: params.sessionKey, limit: params.limit ?? 50 },
|
||||
})) as { messages?: unknown[] };
|
||||
const filtered = stripToolMessages(
|
||||
Array.isArray(history?.messages) ? history.messages : [],
|
||||
);
|
||||
const filtered = stripToolMessages(Array.isArray(history?.messages) ? history.messages : []);
|
||||
const last = filtered.length > 0 ? filtered[filtered.length - 1] : undefined;
|
||||
return last ? extractAssistantText(last) : undefined;
|
||||
}
|
||||
@@ -43,8 +41,7 @@ export async function runAgentStep(params: {
|
||||
timeoutMs: 10_000,
|
||||
})) as { runId?: string; acceptedAt?: number };
|
||||
|
||||
const stepRunId =
|
||||
typeof response?.runId === "string" && response.runId ? response.runId : "";
|
||||
const stepRunId = typeof response?.runId === "string" && response.runId ? response.runId : "";
|
||||
const resolvedRunId = stepRunId || stepIdem;
|
||||
const stepWaitMs = Math.min(params.timeoutMs, 60_000);
|
||||
const wait = (await callGateway({
|
||||
|
||||
@@ -9,10 +9,7 @@ import {
|
||||
import { resolveAgentConfig } from "../agent-scope.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
import { jsonResult } from "./common.js";
|
||||
import {
|
||||
resolveInternalSessionKey,
|
||||
resolveMainSessionAlias,
|
||||
} from "./sessions-helpers.js";
|
||||
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js";
|
||||
|
||||
const AgentsListToolSchema = Type.Object({});
|
||||
|
||||
@@ -22,14 +19,11 @@ type AgentListEntry = {
|
||||
configured: boolean;
|
||||
};
|
||||
|
||||
export function createAgentsListTool(opts?: {
|
||||
agentSessionKey?: string;
|
||||
}): AnyAgentTool {
|
||||
export function createAgentsListTool(opts?: { agentSessionKey?: string }): AnyAgentTool {
|
||||
return {
|
||||
label: "Agents",
|
||||
name: "agents_list",
|
||||
description:
|
||||
"List agent ids you can target with sessions_spawn (based on allowlists).",
|
||||
description: "List agent ids you can target with sessions_spawn (based on allowlists).",
|
||||
parameters: AgentsListToolSchema,
|
||||
execute: async () => {
|
||||
const cfg = loadConfig();
|
||||
@@ -46,8 +40,7 @@ export function createAgentsListTool(opts?: {
|
||||
parseAgentSessionKey(requesterInternalKey)?.agentId ?? DEFAULT_AGENT_ID,
|
||||
);
|
||||
|
||||
const allowAgents =
|
||||
resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ?? [];
|
||||
const allowAgents = resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ?? [];
|
||||
const allowAny = allowAgents.some((value) => value.trim() === "*");
|
||||
const allowSet = new Set(
|
||||
allowAgents
|
||||
@@ -55,12 +48,8 @@ export function createAgentsListTool(opts?: {
|
||||
.map((value) => normalizeAgentId(value)),
|
||||
);
|
||||
|
||||
const configuredAgents = Array.isArray(cfg.agents?.list)
|
||||
? cfg.agents?.list
|
||||
: [];
|
||||
const configuredIds = configuredAgents.map((entry) =>
|
||||
normalizeAgentId(entry.id),
|
||||
);
|
||||
const configuredAgents = Array.isArray(cfg.agents?.list) ? cfg.agents?.list : [];
|
||||
const configuredIds = configuredAgents.map((entry) => normalizeAgentId(entry.id));
|
||||
const configuredNameMap = new Map<string, string>();
|
||||
for (const entry of configuredAgents) {
|
||||
const name = entry?.name?.trim() ?? "";
|
||||
@@ -77,9 +66,7 @@ export function createAgentsListTool(opts?: {
|
||||
}
|
||||
|
||||
const all = Array.from(allowed);
|
||||
const rest = all
|
||||
.filter((id) => id !== requesterAgentId)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
const rest = all.filter((id) => id !== requesterAgentId).sort((a, b) => a.localeCompare(b));
|
||||
const ordered = [requesterAgentId, ...rest];
|
||||
const agents: AgentListEntry[] = ordered.map((id) => ({
|
||||
id,
|
||||
|
||||
@@ -64,9 +64,7 @@ const BrowserActSchema = Type.Object({
|
||||
// select
|
||||
values: Type.Optional(Type.Array(Type.String())),
|
||||
// fill - use permissive array of objects
|
||||
fields: Type.Optional(
|
||||
Type.Array(Type.Object({}, { additionalProperties: true })),
|
||||
),
|
||||
fields: Type.Optional(Type.Array(Type.Object({}, { additionalProperties: true }))),
|
||||
// resize
|
||||
width: Type.Optional(Type.Number()),
|
||||
height: Type.Optional(Type.Number()),
|
||||
|
||||
@@ -21,12 +21,7 @@ import { resolveBrowserConfig } from "../../browser/config.js";
|
||||
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import { BrowserToolSchema } from "./browser-tool.schema.js";
|
||||
import {
|
||||
type AnyAgentTool,
|
||||
imageResultFromFile,
|
||||
jsonResult,
|
||||
readStringParam,
|
||||
} from "./common.js";
|
||||
import { type AnyAgentTool, imageResultFromFile, jsonResult, readStringParam } from "./common.js";
|
||||
|
||||
function resolveBrowserBaseUrl(params: {
|
||||
target?: "sandbox" | "host" | "custom";
|
||||
@@ -42,22 +37,13 @@ function resolveBrowserBaseUrl(params: {
|
||||
const normalizedControlUrl = params.controlUrl?.trim() ?? "";
|
||||
const normalizedDefault = params.defaultControlUrl?.trim() ?? "";
|
||||
const target =
|
||||
params.target ??
|
||||
(normalizedControlUrl ? "custom" : normalizedDefault ? "sandbox" : "host");
|
||||
params.target ?? (normalizedControlUrl ? "custom" : normalizedDefault ? "sandbox" : "host");
|
||||
|
||||
const assertAllowedControlUrl = (url: string) => {
|
||||
const allowedUrls = params.allowedControlUrls?.map((entry) =>
|
||||
entry.trim().replace(/\/$/, ""),
|
||||
);
|
||||
const allowedHosts = params.allowedControlHosts?.map((entry) =>
|
||||
entry.trim().toLowerCase(),
|
||||
);
|
||||
const allowedUrls = params.allowedControlUrls?.map((entry) => entry.trim().replace(/\/$/, ""));
|
||||
const allowedHosts = params.allowedControlHosts?.map((entry) => entry.trim().toLowerCase());
|
||||
const allowedPorts = params.allowedControlPorts;
|
||||
if (
|
||||
!allowedUrls?.length &&
|
||||
!allowedHosts?.length &&
|
||||
!allowedPorts?.length
|
||||
) {
|
||||
if (!allowedUrls?.length && !allowedHosts?.length && !allowedPorts?.length) {
|
||||
return;
|
||||
}
|
||||
let parsed: URL;
|
||||
@@ -71,21 +57,13 @@ function resolveBrowserBaseUrl(params: {
|
||||
throw new Error("Browser controlUrl is not in the allowed URL list.");
|
||||
}
|
||||
if (allowedHosts?.length && !allowedHosts.includes(parsed.hostname)) {
|
||||
throw new Error(
|
||||
"Browser controlUrl hostname is not in the allowed host list.",
|
||||
);
|
||||
throw new Error("Browser controlUrl hostname is not in the allowed host list.");
|
||||
}
|
||||
if (allowedPorts?.length) {
|
||||
const port =
|
||||
parsed.port?.trim() !== ""
|
||||
? Number(parsed.port)
|
||||
: parsed.protocol === "https:"
|
||||
? 443
|
||||
: 80;
|
||||
parsed.port?.trim() !== "" ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
|
||||
if (!Number.isFinite(port) || !allowedPorts.includes(port)) {
|
||||
throw new Error(
|
||||
"Browser controlUrl port is not in the allowed port list.",
|
||||
);
|
||||
throw new Error("Browser controlUrl port is not in the allowed port list.");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -134,9 +112,7 @@ export function createBrowserTool(opts?: {
|
||||
}): AnyAgentTool {
|
||||
const targetDefault = opts?.defaultControlUrl ? "sandbox" : "host";
|
||||
const hostHint =
|
||||
opts?.allowHostControl === false
|
||||
? "Host target blocked by policy."
|
||||
: "Host target allowed.";
|
||||
opts?.allowHostControl === false ? "Host target blocked by policy." : "Host target allowed.";
|
||||
const allowlistHint =
|
||||
opts?.allowedControlUrls?.length ||
|
||||
opts?.allowedControlHosts?.length ||
|
||||
@@ -159,11 +135,7 @@ export function createBrowserTool(opts?: {
|
||||
const params = args as Record<string, unknown>;
|
||||
const action = readStringParam(params, "action", { required: true });
|
||||
const controlUrl = readStringParam(params, "controlUrl");
|
||||
const target = readStringParam(params, "target") as
|
||||
| "sandbox"
|
||||
| "host"
|
||||
| "custom"
|
||||
| undefined;
|
||||
const target = readStringParam(params, "target") as "sandbox" | "host" | "custom" | undefined;
|
||||
const profile = readStringParam(params, "profile");
|
||||
const baseUrl = resolveBrowserBaseUrl({
|
||||
target,
|
||||
@@ -190,9 +162,7 @@ export function createBrowserTool(opts?: {
|
||||
const targetUrl = readStringParam(params, "targetUrl", {
|
||||
required: true,
|
||||
});
|
||||
return jsonResult(
|
||||
await browserOpenTab(baseUrl, targetUrl, { profile }),
|
||||
);
|
||||
return jsonResult(await browserOpenTab(baseUrl, targetUrl, { profile }));
|
||||
}
|
||||
case "focus": {
|
||||
const targetId = readStringParam(params, "targetId", {
|
||||
@@ -213,10 +183,7 @@ export function createBrowserTool(opts?: {
|
||||
? (params.format as "ai" | "aria")
|
||||
: "ai";
|
||||
const hasMaxChars = Object.hasOwn(params, "maxChars");
|
||||
const targetId =
|
||||
typeof params.targetId === "string"
|
||||
? params.targetId.trim()
|
||||
: undefined;
|
||||
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
||||
const limit =
|
||||
typeof params.limit === "number" && Number.isFinite(params.limit)
|
||||
? params.limit
|
||||
@@ -228,34 +195,21 @@ export function createBrowserTool(opts?: {
|
||||
? Math.floor(params.maxChars)
|
||||
: undefined;
|
||||
const resolvedMaxChars =
|
||||
format === "ai"
|
||||
? hasMaxChars
|
||||
? maxChars
|
||||
: DEFAULT_AI_SNAPSHOT_MAX_CHARS
|
||||
: undefined;
|
||||
format === "ai" ? (hasMaxChars ? maxChars : DEFAULT_AI_SNAPSHOT_MAX_CHARS) : undefined;
|
||||
const interactive =
|
||||
typeof params.interactive === "boolean"
|
||||
? params.interactive
|
||||
: undefined;
|
||||
const compact =
|
||||
typeof params.compact === "boolean" ? params.compact : undefined;
|
||||
typeof params.interactive === "boolean" ? params.interactive : undefined;
|
||||
const compact = typeof params.compact === "boolean" ? params.compact : undefined;
|
||||
const depth =
|
||||
typeof params.depth === "number" && Number.isFinite(params.depth)
|
||||
? params.depth
|
||||
: undefined;
|
||||
const selector =
|
||||
typeof params.selector === "string"
|
||||
? params.selector.trim()
|
||||
: undefined;
|
||||
const frame =
|
||||
typeof params.frame === "string" ? params.frame.trim() : undefined;
|
||||
const selector = typeof params.selector === "string" ? params.selector.trim() : undefined;
|
||||
const frame = typeof params.frame === "string" ? params.frame.trim() : undefined;
|
||||
const snapshot = await browserSnapshot(baseUrl, {
|
||||
format,
|
||||
targetId,
|
||||
limit,
|
||||
...(typeof resolvedMaxChars === "number"
|
||||
? { maxChars: resolvedMaxChars }
|
||||
: {}),
|
||||
...(typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {}),
|
||||
interactive,
|
||||
compact,
|
||||
depth,
|
||||
@@ -305,21 +259,12 @@ export function createBrowserTool(opts?: {
|
||||
);
|
||||
}
|
||||
case "console": {
|
||||
const level =
|
||||
typeof params.level === "string" ? params.level.trim() : undefined;
|
||||
const targetId =
|
||||
typeof params.targetId === "string"
|
||||
? params.targetId.trim()
|
||||
: undefined;
|
||||
return jsonResult(
|
||||
await browserConsoleMessages(baseUrl, { level, targetId, profile }),
|
||||
);
|
||||
const level = typeof params.level === "string" ? params.level.trim() : undefined;
|
||||
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
||||
return jsonResult(await browserConsoleMessages(baseUrl, { level, targetId, profile }));
|
||||
}
|
||||
case "pdf": {
|
||||
const targetId =
|
||||
typeof params.targetId === "string"
|
||||
? params.targetId.trim()
|
||||
: undefined;
|
||||
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
||||
const result = await browserPdfSave(baseUrl, { targetId, profile });
|
||||
return {
|
||||
content: [{ type: "text", text: `FILE:${result.path}` }],
|
||||
@@ -327,20 +272,14 @@ export function createBrowserTool(opts?: {
|
||||
};
|
||||
}
|
||||
case "upload": {
|
||||
const paths = Array.isArray(params.paths)
|
||||
? params.paths.map((p) => String(p))
|
||||
: [];
|
||||
const paths = Array.isArray(params.paths) ? params.paths.map((p) => String(p)) : [];
|
||||
if (paths.length === 0) throw new Error("paths required");
|
||||
const ref = readStringParam(params, "ref");
|
||||
const inputRef = readStringParam(params, "inputRef");
|
||||
const element = readStringParam(params, "element");
|
||||
const targetId =
|
||||
typeof params.targetId === "string"
|
||||
? params.targetId.trim()
|
||||
: undefined;
|
||||
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
||||
const timeoutMs =
|
||||
typeof params.timeoutMs === "number" &&
|
||||
Number.isFinite(params.timeoutMs)
|
||||
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
||||
? params.timeoutMs
|
||||
: undefined;
|
||||
return jsonResult(
|
||||
@@ -357,17 +296,10 @@ export function createBrowserTool(opts?: {
|
||||
}
|
||||
case "dialog": {
|
||||
const accept = Boolean(params.accept);
|
||||
const promptText =
|
||||
typeof params.promptText === "string"
|
||||
? params.promptText
|
||||
: undefined;
|
||||
const targetId =
|
||||
typeof params.targetId === "string"
|
||||
? params.targetId.trim()
|
||||
: undefined;
|
||||
const promptText = typeof params.promptText === "string" ? params.promptText : undefined;
|
||||
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
||||
const timeoutMs =
|
||||
typeof params.timeoutMs === "number" &&
|
||||
Number.isFinite(params.timeoutMs)
|
||||
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
||||
? params.timeoutMs
|
||||
: undefined;
|
||||
return jsonResult(
|
||||
@@ -385,11 +317,9 @@ export function createBrowserTool(opts?: {
|
||||
if (!request || typeof request !== "object") {
|
||||
throw new Error("request required");
|
||||
}
|
||||
const result = await browserAct(
|
||||
baseUrl,
|
||||
request as Parameters<typeof browserAct>[1],
|
||||
{ profile },
|
||||
);
|
||||
const result = await browserAct(baseUrl, request as Parameters<typeof browserAct>[1], {
|
||||
profile,
|
||||
});
|
||||
return jsonResult(result);
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -3,18 +3,10 @@ import fs from "node:fs/promises";
|
||||
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { writeBase64ToFile } from "../../cli/nodes-camera.js";
|
||||
import {
|
||||
canvasSnapshotTempPath,
|
||||
parseCanvasSnapshotPayload,
|
||||
} from "../../cli/nodes-canvas.js";
|
||||
import { canvasSnapshotTempPath, parseCanvasSnapshotPayload } from "../../cli/nodes-canvas.js";
|
||||
import { imageMimeFromFormat } from "../../media/mime.js";
|
||||
import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
|
||||
import {
|
||||
type AnyAgentTool,
|
||||
imageResult,
|
||||
jsonResult,
|
||||
readStringParam,
|
||||
} from "./common.js";
|
||||
import { type AnyAgentTool, imageResult, jsonResult, readStringParam } from "./common.js";
|
||||
import { callGatewayTool, type GatewayCallOptions } from "./gateway.js";
|
||||
import { resolveNodeId } from "./nodes-utils.js";
|
||||
|
||||
@@ -70,8 +62,7 @@ export function createCanvasTool(): AnyAgentTool {
|
||||
const gatewayOpts: GatewayCallOptions = {
|
||||
gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }),
|
||||
gatewayToken: readStringParam(params, "gatewayToken", { trim: false }),
|
||||
timeoutMs:
|
||||
typeof params.timeoutMs === "number" ? params.timeoutMs : undefined,
|
||||
timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined,
|
||||
};
|
||||
|
||||
const nodeId = await resolveNodeId(
|
||||
@@ -80,10 +71,7 @@ export function createCanvasTool(): AnyAgentTool {
|
||||
true,
|
||||
);
|
||||
|
||||
const invoke = async (
|
||||
command: string,
|
||||
invokeParams?: Record<string, unknown>,
|
||||
) =>
|
||||
const invoke = async (command: string, invokeParams?: Record<string, unknown>) =>
|
||||
await callGatewayTool("node.invoke", gatewayOpts, {
|
||||
nodeId,
|
||||
command,
|
||||
@@ -97,8 +85,7 @@ export function createCanvasTool(): AnyAgentTool {
|
||||
x: typeof params.x === "number" ? params.x : undefined,
|
||||
y: typeof params.y === "number" ? params.y : undefined,
|
||||
width: typeof params.width === "number" ? params.width : undefined,
|
||||
height:
|
||||
typeof params.height === "number" ? params.height : undefined,
|
||||
height: typeof params.height === "number" ? params.height : undefined,
|
||||
};
|
||||
const invokeParams: Record<string, unknown> = {};
|
||||
if (typeof params.target === "string" && params.target.trim()) {
|
||||
@@ -140,20 +127,14 @@ export function createCanvasTool(): AnyAgentTool {
|
||||
return jsonResult({ ok: true });
|
||||
}
|
||||
case "snapshot": {
|
||||
const formatRaw =
|
||||
typeof params.format === "string"
|
||||
? params.format.toLowerCase()
|
||||
: "png";
|
||||
const format =
|
||||
formatRaw === "jpg" || formatRaw === "jpeg" ? "jpeg" : "png";
|
||||
const formatRaw = typeof params.format === "string" ? params.format.toLowerCase() : "png";
|
||||
const format = formatRaw === "jpg" || formatRaw === "jpeg" ? "jpeg" : "png";
|
||||
const maxWidth =
|
||||
typeof params.maxWidth === "number" &&
|
||||
Number.isFinite(params.maxWidth)
|
||||
typeof params.maxWidth === "number" && Number.isFinite(params.maxWidth)
|
||||
? params.maxWidth
|
||||
: undefined;
|
||||
const quality =
|
||||
typeof params.quality === "number" &&
|
||||
Number.isFinite(params.quality)
|
||||
typeof params.quality === "number" && Number.isFinite(params.quality)
|
||||
? params.quality
|
||||
: undefined;
|
||||
const raw = (await invoke("canvas.snapshot", {
|
||||
|
||||
@@ -38,9 +38,9 @@ describe("readStringOrNumberParam", () => {
|
||||
});
|
||||
|
||||
it("throws when required and missing", () => {
|
||||
expect(() =>
|
||||
readStringOrNumberParam({}, "chatId", { required: true }),
|
||||
).toThrow(/chatId required/);
|
||||
expect(() => readStringOrNumberParam({}, "chatId", { required: true })).toThrow(
|
||||
/chatId required/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -45,12 +45,7 @@ export function readStringParam(
|
||||
key: string,
|
||||
options: StringParamOptions = {},
|
||||
) {
|
||||
const {
|
||||
required = false,
|
||||
trim = true,
|
||||
label = key,
|
||||
allowEmpty = false,
|
||||
} = options;
|
||||
const { required = false, trim = true, label = key, allowEmpty = false } = options;
|
||||
const raw = params[key];
|
||||
if (typeof raw !== "string") {
|
||||
if (required) throw new Error(`${label} required`);
|
||||
@@ -162,8 +157,7 @@ export function readReactionParams(
|
||||
): ReactionParams {
|
||||
const emojiKey = options.emojiKey ?? "emoji";
|
||||
const removeKey = options.removeKey ?? "remove";
|
||||
const remove =
|
||||
typeof params[removeKey] === "boolean" ? params[removeKey] : false;
|
||||
const remove = typeof params[removeKey] === "boolean" ? params[removeKey] : false;
|
||||
const emoji = readStringParam(params, emojiKey, {
|
||||
required: true,
|
||||
allowEmpty: true,
|
||||
@@ -219,8 +213,7 @@ export async function imageResultFromFile(params: {
|
||||
details?: Record<string, unknown>;
|
||||
}): Promise<AgentToolResult<unknown>> {
|
||||
const buf = await fs.readFile(params.path);
|
||||
const mimeType =
|
||||
(await detectMime({ buffer: buf.slice(0, 256) })) ?? "image/png";
|
||||
const mimeType = (await detectMime({ buffer: buf.slice(0, 256) })) ?? "image/png";
|
||||
return await imageResult({
|
||||
label: params.label,
|
||||
path: params.path,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import {
|
||||
normalizeCronJobCreate,
|
||||
normalizeCronJobPatch,
|
||||
} from "../../cron/normalize.js";
|
||||
import { normalizeCronJobCreate, normalizeCronJobPatch } from "../../cron/normalize.js";
|
||||
import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
|
||||
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
|
||||
import { callGatewayTool, type GatewayCallOptions } from "./gateway.js";
|
||||
@@ -12,16 +9,7 @@ import { callGatewayTool, type GatewayCallOptions } from "./gateway.js";
|
||||
// contain nested unions. Tool schemas need to stay provider-friendly, so we
|
||||
// accept "any object" here and validate at runtime.
|
||||
|
||||
const CRON_ACTIONS = [
|
||||
"status",
|
||||
"list",
|
||||
"add",
|
||||
"update",
|
||||
"remove",
|
||||
"run",
|
||||
"runs",
|
||||
"wake",
|
||||
] as const;
|
||||
const CRON_ACTIONS = ["status", "list", "add", "update", "remove", "run", "runs", "wake"] as const;
|
||||
|
||||
const CRON_WAKE_MODES = ["now", "next-heartbeat"] as const;
|
||||
|
||||
@@ -53,15 +41,12 @@ export function createCronTool(): AnyAgentTool {
|
||||
const gatewayOpts: GatewayCallOptions = {
|
||||
gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }),
|
||||
gatewayToken: readStringParam(params, "gatewayToken", { trim: false }),
|
||||
timeoutMs:
|
||||
typeof params.timeoutMs === "number" ? params.timeoutMs : undefined,
|
||||
timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined,
|
||||
};
|
||||
|
||||
switch (action) {
|
||||
case "status":
|
||||
return jsonResult(
|
||||
await callGatewayTool("cron.status", gatewayOpts, {}),
|
||||
);
|
||||
return jsonResult(await callGatewayTool("cron.status", gatewayOpts, {}));
|
||||
case "list":
|
||||
return jsonResult(
|
||||
await callGatewayTool("cron.list", gatewayOpts, {
|
||||
@@ -73,17 +58,12 @@ export function createCronTool(): AnyAgentTool {
|
||||
throw new Error("job required");
|
||||
}
|
||||
const job = normalizeCronJobCreate(params.job) ?? params.job;
|
||||
return jsonResult(
|
||||
await callGatewayTool("cron.add", gatewayOpts, job),
|
||||
);
|
||||
return jsonResult(await callGatewayTool("cron.add", gatewayOpts, job));
|
||||
}
|
||||
case "update": {
|
||||
const id =
|
||||
readStringParam(params, "jobId") ?? readStringParam(params, "id");
|
||||
const id = readStringParam(params, "jobId") ?? readStringParam(params, "id");
|
||||
if (!id) {
|
||||
throw new Error(
|
||||
"jobId required (id accepted for backward compatibility)",
|
||||
);
|
||||
throw new Error("jobId required (id accepted for backward compatibility)");
|
||||
}
|
||||
if (!params.patch || typeof params.patch !== "object") {
|
||||
throw new Error("patch required");
|
||||
@@ -97,40 +77,25 @@ export function createCronTool(): AnyAgentTool {
|
||||
);
|
||||
}
|
||||
case "remove": {
|
||||
const id =
|
||||
readStringParam(params, "jobId") ?? readStringParam(params, "id");
|
||||
const id = readStringParam(params, "jobId") ?? readStringParam(params, "id");
|
||||
if (!id) {
|
||||
throw new Error(
|
||||
"jobId required (id accepted for backward compatibility)",
|
||||
);
|
||||
throw new Error("jobId required (id accepted for backward compatibility)");
|
||||
}
|
||||
return jsonResult(
|
||||
await callGatewayTool("cron.remove", gatewayOpts, { id }),
|
||||
);
|
||||
return jsonResult(await callGatewayTool("cron.remove", gatewayOpts, { id }));
|
||||
}
|
||||
case "run": {
|
||||
const id =
|
||||
readStringParam(params, "jobId") ?? readStringParam(params, "id");
|
||||
const id = readStringParam(params, "jobId") ?? readStringParam(params, "id");
|
||||
if (!id) {
|
||||
throw new Error(
|
||||
"jobId required (id accepted for backward compatibility)",
|
||||
);
|
||||
throw new Error("jobId required (id accepted for backward compatibility)");
|
||||
}
|
||||
return jsonResult(
|
||||
await callGatewayTool("cron.run", gatewayOpts, { id }),
|
||||
);
|
||||
return jsonResult(await callGatewayTool("cron.run", gatewayOpts, { id }));
|
||||
}
|
||||
case "runs": {
|
||||
const id =
|
||||
readStringParam(params, "jobId") ?? readStringParam(params, "id");
|
||||
const id = readStringParam(params, "jobId") ?? readStringParam(params, "id");
|
||||
if (!id) {
|
||||
throw new Error(
|
||||
"jobId required (id accepted for backward compatibility)",
|
||||
);
|
||||
throw new Error("jobId required (id accepted for backward compatibility)");
|
||||
}
|
||||
return jsonResult(
|
||||
await callGatewayTool("cron.runs", gatewayOpts, { id }),
|
||||
);
|
||||
return jsonResult(await callGatewayTool("cron.runs", gatewayOpts, { id }));
|
||||
}
|
||||
case "wake": {
|
||||
const text = readStringParam(params, "text", { required: true });
|
||||
@@ -139,12 +104,7 @@ export function createCronTool(): AnyAgentTool {
|
||||
? params.mode
|
||||
: "next-heartbeat";
|
||||
return jsonResult(
|
||||
await callGatewayTool(
|
||||
"wake",
|
||||
gatewayOpts,
|
||||
{ mode, text },
|
||||
{ expectFinal: false },
|
||||
),
|
||||
await callGatewayTool("wake", gatewayOpts, { mode, text }, { expectFinal: false }),
|
||||
);
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -28,9 +28,7 @@ import {
|
||||
readStringParam,
|
||||
} from "./common.js";
|
||||
|
||||
function readParentIdParam(
|
||||
params: Record<string, unknown>,
|
||||
): string | null | undefined {
|
||||
function readParentIdParam(params: Record<string, unknown>): string | null | undefined {
|
||||
if (params.clearParent === true) return null;
|
||||
if (params.parentId === null) return null;
|
||||
return readStringParam(params, "parentId");
|
||||
@@ -206,8 +204,7 @@ export async function handleDiscordGuildAction(
|
||||
const channelId = readStringParam(params, "channelId");
|
||||
const location = readStringParam(params, "location");
|
||||
const entityTypeRaw = readStringParam(params, "entityType");
|
||||
const entityType =
|
||||
entityTypeRaw === "stage" ? 1 : entityTypeRaw === "external" ? 3 : 2;
|
||||
const entityType = entityTypeRaw === "stage" ? 1 : entityTypeRaw === "external" ? 3 : 2;
|
||||
const payload = {
|
||||
name,
|
||||
description,
|
||||
@@ -215,8 +212,7 @@ export async function handleDiscordGuildAction(
|
||||
scheduled_end_time: endTime,
|
||||
entity_type: entityType,
|
||||
channel_id: channelId,
|
||||
entity_metadata:
|
||||
entityType === 3 && location ? { location } : undefined,
|
||||
entity_metadata: entityType === 3 && location ? { location } : undefined,
|
||||
privacy_level: 2,
|
||||
};
|
||||
const event = await createScheduledEventDiscord(guildId, payload);
|
||||
|
||||
@@ -113,9 +113,7 @@ export async function handleDiscordMessagingAction(
|
||||
});
|
||||
const limitRaw = params.limit;
|
||||
const limit =
|
||||
typeof limitRaw === "number" && Number.isFinite(limitRaw)
|
||||
? limitRaw
|
||||
: undefined;
|
||||
typeof limitRaw === "number" && Number.isFinite(limitRaw) ? limitRaw : undefined;
|
||||
const reactions = await fetchReactionsDiscord(channelId, messageId, {
|
||||
limit,
|
||||
});
|
||||
@@ -149,14 +147,10 @@ export async function handleDiscordMessagingAction(
|
||||
});
|
||||
const allowMultiselectRaw = params.allowMultiselect;
|
||||
const allowMultiselect =
|
||||
typeof allowMultiselectRaw === "boolean"
|
||||
? allowMultiselectRaw
|
||||
: undefined;
|
||||
typeof allowMultiselectRaw === "boolean" ? allowMultiselectRaw : undefined;
|
||||
const durationRaw = params.durationHours;
|
||||
const durationHours =
|
||||
typeof durationRaw === "number" && Number.isFinite(durationRaw)
|
||||
? durationRaw
|
||||
: undefined;
|
||||
typeof durationRaw === "number" && Number.isFinite(durationRaw) ? durationRaw : undefined;
|
||||
const maxSelections = allowMultiselect ? Math.max(2, answers.length) : 1;
|
||||
await sendPollDiscord(
|
||||
to,
|
||||
@@ -215,8 +209,7 @@ export async function handleDiscordMessagingAction(
|
||||
});
|
||||
const formattedMessages = messages.map((message) => ({
|
||||
...message,
|
||||
timestamp:
|
||||
formatDiscordTimestamp(message.timestamp) ?? message.timestamp,
|
||||
timestamp: formatDiscordTimestamp(message.timestamp) ?? message.timestamp,
|
||||
}));
|
||||
return jsonResult({ ok: true, messages: formattedMessages });
|
||||
}
|
||||
@@ -278,8 +271,7 @@ export async function handleDiscordMessagingAction(
|
||||
const messageId = readStringParam(params, "messageId");
|
||||
const autoArchiveMinutesRaw = params.autoArchiveMinutes;
|
||||
const autoArchiveMinutes =
|
||||
typeof autoArchiveMinutesRaw === "number" &&
|
||||
Number.isFinite(autoArchiveMinutesRaw)
|
||||
typeof autoArchiveMinutesRaw === "number" && Number.isFinite(autoArchiveMinutesRaw)
|
||||
? autoArchiveMinutesRaw
|
||||
: undefined;
|
||||
const thread = await createThreadDiscord(channelId, {
|
||||
@@ -298,9 +290,7 @@ export async function handleDiscordMessagingAction(
|
||||
});
|
||||
const channelId = readStringParam(params, "channelId");
|
||||
const includeArchived =
|
||||
typeof params.includeArchived === "boolean"
|
||||
? params.includeArchived
|
||||
: undefined;
|
||||
typeof params.includeArchived === "boolean" ? params.includeArchived : undefined;
|
||||
const before = readStringParam(params, "before");
|
||||
const limit =
|
||||
typeof params.limit === "number" && Number.isFinite(params.limit)
|
||||
@@ -387,14 +377,8 @@ export async function handleDiscordMessagingAction(
|
||||
typeof params.limit === "number" && Number.isFinite(params.limit)
|
||||
? params.limit
|
||||
: undefined;
|
||||
const channelIdList = [
|
||||
...(channelIds ?? []),
|
||||
...(channelId ? [channelId] : []),
|
||||
];
|
||||
const authorIdList = [
|
||||
...(authorIds ?? []),
|
||||
...(authorId ? [authorId] : []),
|
||||
];
|
||||
const channelIdList = [...(channelIds ?? []), ...(channelId ? [channelId] : [])];
|
||||
const authorIdList = [...(authorIds ?? []), ...(authorId ? [authorId] : [])];
|
||||
const results = await searchMessagesDiscord({
|
||||
guildId,
|
||||
content,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { DiscordActionConfig } from "../../config/config.js";
|
||||
import {
|
||||
banMemberDiscord,
|
||||
kickMemberDiscord,
|
||||
timeoutMemberDiscord,
|
||||
} from "../../discord/send.js";
|
||||
import { banMemberDiscord, kickMemberDiscord, timeoutMemberDiscord } from "../../discord/send.js";
|
||||
import { type ActionGate, jsonResult, readStringParam } from "./common.js";
|
||||
|
||||
export async function handleDiscordModerationAction(
|
||||
@@ -24,8 +20,7 @@ export async function handleDiscordModerationAction(
|
||||
required: true,
|
||||
});
|
||||
const durationMinutes =
|
||||
typeof params.durationMinutes === "number" &&
|
||||
Number.isFinite(params.durationMinutes)
|
||||
typeof params.durationMinutes === "number" && Number.isFinite(params.durationMinutes)
|
||||
? params.durationMinutes
|
||||
: undefined;
|
||||
const until = readStringParam(params, "until");
|
||||
@@ -65,8 +60,7 @@ export async function handleDiscordModerationAction(
|
||||
});
|
||||
const reason = readStringParam(params, "reason");
|
||||
const deleteMessageDays =
|
||||
typeof params.deleteMessageDays === "number" &&
|
||||
Number.isFinite(params.deleteMessageDays)
|
||||
typeof params.deleteMessageDays === "number" && Number.isFinite(params.deleteMessageDays)
|
||||
? params.deleteMessageDays
|
||||
: undefined;
|
||||
await banMemberDiscord({
|
||||
|
||||
@@ -42,8 +42,7 @@ vi.mock("../../discord/send.js", () => ({
|
||||
deleteMessageDiscord: (...args: unknown[]) => deleteMessageDiscord(...args),
|
||||
editChannelDiscord: (...args: unknown[]) => editChannelDiscord(...args),
|
||||
editMessageDiscord: (...args: unknown[]) => editMessageDiscord(...args),
|
||||
fetchChannelPermissionsDiscord: (...args: unknown[]) =>
|
||||
fetchChannelPermissionsDiscord(...args),
|
||||
fetchChannelPermissionsDiscord: (...args: unknown[]) => fetchChannelPermissionsDiscord(...args),
|
||||
fetchReactionsDiscord: (...args: unknown[]) => fetchReactionsDiscord(...args),
|
||||
listPinsDiscord: (...args: unknown[]) => listPinsDiscord(...args),
|
||||
listThreadsDiscord: (...args: unknown[]) => listThreadsDiscord(...args),
|
||||
@@ -51,17 +50,14 @@ vi.mock("../../discord/send.js", () => ({
|
||||
pinMessageDiscord: (...args: unknown[]) => pinMessageDiscord(...args),
|
||||
reactMessageDiscord: (...args: unknown[]) => reactMessageDiscord(...args),
|
||||
readMessagesDiscord: (...args: unknown[]) => readMessagesDiscord(...args),
|
||||
removeChannelPermissionDiscord: (...args: unknown[]) =>
|
||||
removeChannelPermissionDiscord(...args),
|
||||
removeOwnReactionsDiscord: (...args: unknown[]) =>
|
||||
removeOwnReactionsDiscord(...args),
|
||||
removeChannelPermissionDiscord: (...args: unknown[]) => removeChannelPermissionDiscord(...args),
|
||||
removeOwnReactionsDiscord: (...args: unknown[]) => removeOwnReactionsDiscord(...args),
|
||||
removeReactionDiscord: (...args: unknown[]) => removeReactionDiscord(...args),
|
||||
searchMessagesDiscord: (...args: unknown[]) => searchMessagesDiscord(...args),
|
||||
sendMessageDiscord: (...args: unknown[]) => sendMessageDiscord(...args),
|
||||
sendPollDiscord: (...args: unknown[]) => sendPollDiscord(...args),
|
||||
sendStickerDiscord: (...args: unknown[]) => sendStickerDiscord(...args),
|
||||
setChannelPermissionDiscord: (...args: unknown[]) =>
|
||||
setChannelPermissionDiscord(...args),
|
||||
setChannelPermissionDiscord: (...args: unknown[]) => setChannelPermissionDiscord(...args),
|
||||
unpinMessageDiscord: (...args: unknown[]) => unpinMessageDiscord(...args),
|
||||
}));
|
||||
|
||||
@@ -169,11 +165,7 @@ describe("handleDiscordGuildAction - channel management", () => {
|
||||
|
||||
it("respects channel gating for channelCreate", async () => {
|
||||
await expect(
|
||||
handleDiscordGuildAction(
|
||||
"channelCreate",
|
||||
{ guildId: "G1", name: "test" },
|
||||
channelsDisabled,
|
||||
),
|
||||
handleDiscordGuildAction("channelCreate", { guildId: "G1", name: "test" }, channelsDisabled),
|
||||
).rejects.toThrow(/Discord channel management is disabled/);
|
||||
});
|
||||
|
||||
@@ -239,11 +231,7 @@ describe("handleDiscordGuildAction - channel management", () => {
|
||||
});
|
||||
|
||||
it("deletes a channel", async () => {
|
||||
await handleDiscordGuildAction(
|
||||
"channelDelete",
|
||||
{ channelId: "C1" },
|
||||
channelsEnabled,
|
||||
);
|
||||
await handleDiscordGuildAction("channelDelete", { channelId: "C1" }, channelsEnabled);
|
||||
expect(deleteChannelDiscord).toHaveBeenCalledWith("C1");
|
||||
});
|
||||
|
||||
@@ -330,11 +318,7 @@ describe("handleDiscordGuildAction - channel management", () => {
|
||||
});
|
||||
|
||||
it("deletes a category", async () => {
|
||||
await handleDiscordGuildAction(
|
||||
"categoryDelete",
|
||||
{ categoryId: "CAT1" },
|
||||
channelsEnabled,
|
||||
);
|
||||
await handleDiscordGuildAction("categoryDelete", { categoryId: "CAT1" }, channelsEnabled);
|
||||
expect(deleteChannelDiscord).toHaveBeenCalledWith("CAT1");
|
||||
});
|
||||
|
||||
|
||||
@@ -58,9 +58,7 @@ export function createGatewayTool(opts?: {
|
||||
const action = readStringParam(params, "action", { required: true });
|
||||
if (action === "restart") {
|
||||
if (opts?.config?.commands?.restart !== true) {
|
||||
throw new Error(
|
||||
"Gateway restart is disabled. Set commands.restart=true to enable.",
|
||||
);
|
||||
throw new Error("Gateway restart is disabled. Set commands.restart=true to enable.");
|
||||
}
|
||||
const sessionKey =
|
||||
typeof params.sessionKey === "string" && params.sessionKey.trim()
|
||||
@@ -75,9 +73,7 @@ export function createGatewayTool(opts?: {
|
||||
? params.reason.trim().slice(0, 200)
|
||||
: undefined;
|
||||
const note =
|
||||
typeof params.note === "string" && params.note.trim()
|
||||
? params.note.trim()
|
||||
: undefined;
|
||||
typeof params.note === "string" && params.note.trim() ? params.note.trim() : undefined;
|
||||
const payload: RestartSentinelPayload = {
|
||||
kind: "restart",
|
||||
status: "ok",
|
||||
@@ -114,8 +110,7 @@ export function createGatewayTool(opts?: {
|
||||
? params.gatewayToken.trim()
|
||||
: undefined;
|
||||
const timeoutMs =
|
||||
typeof params.timeoutMs === "number" &&
|
||||
Number.isFinite(params.timeoutMs)
|
||||
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
||||
? Math.max(1, Math.floor(params.timeoutMs))
|
||||
: undefined;
|
||||
const gatewayOpts = { gatewayUrl, gatewayToken, timeoutMs };
|
||||
@@ -135,12 +130,9 @@ export function createGatewayTool(opts?: {
|
||||
? params.sessionKey.trim()
|
||||
: opts?.agentSessionKey?.trim() || undefined;
|
||||
const note =
|
||||
typeof params.note === "string" && params.note.trim()
|
||||
? params.note.trim()
|
||||
: undefined;
|
||||
typeof params.note === "string" && params.note.trim() ? params.note.trim() : undefined;
|
||||
const restartDelayMs =
|
||||
typeof params.restartDelayMs === "number" &&
|
||||
Number.isFinite(params.restartDelayMs)
|
||||
typeof params.restartDelayMs === "number" && Number.isFinite(params.restartDelayMs)
|
||||
? Math.floor(params.restartDelayMs)
|
||||
: undefined;
|
||||
const result = await callGatewayTool("config.apply", gatewayOpts, {
|
||||
@@ -157,12 +149,9 @@ export function createGatewayTool(opts?: {
|
||||
? params.sessionKey.trim()
|
||||
: opts?.agentSessionKey?.trim() || undefined;
|
||||
const note =
|
||||
typeof params.note === "string" && params.note.trim()
|
||||
? params.note.trim()
|
||||
: undefined;
|
||||
typeof params.note === "string" && params.note.trim() ? params.note.trim() : undefined;
|
||||
const restartDelayMs =
|
||||
typeof params.restartDelayMs === "number" &&
|
||||
Number.isFinite(params.restartDelayMs)
|
||||
typeof params.restartDelayMs === "number" && Number.isFinite(params.restartDelayMs)
|
||||
? Math.floor(params.restartDelayMs)
|
||||
: undefined;
|
||||
const result = await callGatewayTool("update.run", gatewayOpts, {
|
||||
|
||||
@@ -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";
|
||||
|
||||
export const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789";
|
||||
|
||||
|
||||
@@ -40,15 +40,11 @@ export function coerceImageAssistantText(params: {
|
||||
);
|
||||
}
|
||||
if (errorMessage) {
|
||||
throw new Error(
|
||||
`Image model failed (${params.provider}/${params.model}): ${errorMessage}`,
|
||||
);
|
||||
throw new Error(`Image model failed (${params.provider}/${params.model}): ${errorMessage}`);
|
||||
}
|
||||
const text = extractAssistantText(params.message);
|
||||
if (text.trim()) return text.trim();
|
||||
throw new Error(
|
||||
`Image model returned no text (${params.provider}/${params.model}).`,
|
||||
);
|
||||
throw new Error(`Image model returned no text (${params.provider}/${params.model}).`);
|
||||
}
|
||||
|
||||
export function coerceImageModelConfig(cfg?: ClawdbotConfig): ImageModelConfig {
|
||||
@@ -56,10 +52,8 @@ export function coerceImageModelConfig(cfg?: ClawdbotConfig): ImageModelConfig {
|
||||
| { primary?: string; fallbacks?: string[] }
|
||||
| string
|
||||
| undefined;
|
||||
const primary =
|
||||
typeof imageModel === "string" ? imageModel.trim() : imageModel?.primary;
|
||||
const fallbacks =
|
||||
typeof imageModel === "object" ? (imageModel?.fallbacks ?? []) : [];
|
||||
const primary = typeof imageModel === "string" ? imageModel.trim() : imageModel?.primary;
|
||||
const fallbacks = typeof imageModel === "object" ? (imageModel?.fallbacks ?? []) : [];
|
||||
return {
|
||||
...(primary?.trim() ? { primary: primary.trim() } : {}),
|
||||
...(fallbacks.length > 0 ? { fallbacks } : {}),
|
||||
@@ -70,9 +64,7 @@ export function resolveProviderVisionModelFromConfig(params: {
|
||||
cfg?: ClawdbotConfig;
|
||||
provider: string;
|
||||
}): string | null {
|
||||
const providerCfg = params.cfg?.models?.providers?.[
|
||||
params.provider
|
||||
] as unknown as
|
||||
const providerCfg = params.cfg?.models?.providers?.[params.provider] as unknown as
|
||||
| { models?: Array<{ id?: string; input?: string[] }> }
|
||||
| undefined;
|
||||
const models = providerCfg?.models ?? [];
|
||||
@@ -87,9 +79,7 @@ export function resolveProviderVisionModelFromConfig(params: {
|
||||
: null;
|
||||
const picked =
|
||||
preferMinimaxVl ??
|
||||
models.find(
|
||||
(m) => Boolean((m?.id ?? "").trim()) && m.input?.includes("image"),
|
||||
);
|
||||
models.find((m) => Boolean((m?.id ?? "").trim()) && m.input?.includes("image"));
|
||||
const id = (picked?.id ?? "").trim();
|
||||
return id ? `${params.provider}/${id}` : null;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@ import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import {
|
||||
__testing,
|
||||
createImageTool,
|
||||
resolveImageModelConfigForTool,
|
||||
} from "./image-tool.js";
|
||||
import { __testing, createImageTool, resolveImageModelConfigForTool } from "./image-tool.js";
|
||||
|
||||
async function writeAuthProfiles(agentDir: string, profiles: unknown) {
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
@@ -41,9 +37,7 @@ describe("image tool implicit imageModel config", () => {
|
||||
});
|
||||
|
||||
it("stays disabled without auth when no pairing is possible", async () => {
|
||||
const agentDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "clawdbot-image-"),
|
||||
);
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-image-"));
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: { defaults: { model: { primary: "openai/gpt-5.2" } } },
|
||||
};
|
||||
@@ -52,9 +46,7 @@ describe("image tool implicit imageModel config", () => {
|
||||
});
|
||||
|
||||
it("pairs minimax primary with MiniMax-VL-01 (and fallbacks) when auth exists", async () => {
|
||||
const agentDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "clawdbot-image-"),
|
||||
);
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-image-"));
|
||||
vi.stubEnv("MINIMAX_API_KEY", "minimax-test");
|
||||
vi.stubEnv("OPENAI_API_KEY", "openai-test");
|
||||
vi.stubEnv("ANTHROPIC_API_KEY", "anthropic-test");
|
||||
@@ -69,9 +61,7 @@ describe("image tool implicit imageModel config", () => {
|
||||
});
|
||||
|
||||
it("pairs a custom provider when it declares an image-capable model", async () => {
|
||||
const agentDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "clawdbot-image-"),
|
||||
);
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-image-"));
|
||||
await writeAuthProfiles(agentDir, {
|
||||
version: 1,
|
||||
profiles: {
|
||||
@@ -98,9 +88,7 @@ describe("image tool implicit imageModel config", () => {
|
||||
});
|
||||
|
||||
it("prefers explicit agents.defaults.imageModel", async () => {
|
||||
const agentDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "clawdbot-image-"),
|
||||
);
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-image-"));
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -115,9 +103,7 @@ describe("image tool implicit imageModel config", () => {
|
||||
});
|
||||
|
||||
it("sandboxes image paths like the read tool", async () => {
|
||||
const stateDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "clawdbot-image-sandbox-"),
|
||||
);
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-image-sandbox-"));
|
||||
const agentDir = path.join(stateDir, "agent");
|
||||
const sandboxRoot = path.join(stateDir, "sandbox");
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
@@ -132,19 +118,17 @@ describe("image tool implicit imageModel config", () => {
|
||||
expect(tool).not.toBeNull();
|
||||
if (!tool) throw new Error("expected image tool");
|
||||
|
||||
await expect(
|
||||
tool.execute("t1", { image: "https://example.com/a.png" }),
|
||||
).rejects.toThrow(/Sandboxed image tool does not allow remote URLs/i);
|
||||
await expect(tool.execute("t1", { image: "https://example.com/a.png" })).rejects.toThrow(
|
||||
/Sandboxed image tool does not allow remote URLs/i,
|
||||
);
|
||||
|
||||
await expect(
|
||||
tool.execute("t2", { image: "../escape.png" }),
|
||||
).rejects.toThrow(/escapes sandbox root/i);
|
||||
await expect(tool.execute("t2", { image: "../escape.png" })).rejects.toThrow(
|
||||
/escapes sandbox root/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("rewrites inbound absolute paths into sandbox media/inbound", async () => {
|
||||
const stateDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "clawdbot-image-sandbox-"),
|
||||
);
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-image-sandbox-"));
|
||||
const agentDir = path.join(stateDir, "agent");
|
||||
const sandboxRoot = path.join(stateDir, "sandbox");
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
@@ -190,9 +174,7 @@ describe("image tool implicit imageModel config", () => {
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect((res.details as { rewrittenFrom?: string }).rewrittenFrom).toContain(
|
||||
"photo.png",
|
||||
);
|
||||
expect((res.details as { rewrittenFrom?: string }).rewrittenFrom).toContain("photo.png");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -207,9 +189,9 @@ describe("image tool data URL support", () => {
|
||||
});
|
||||
|
||||
it("rejects non-image data URLs", () => {
|
||||
expect(() =>
|
||||
__testing.decodeDataUrl("data:text/plain;base64,SGVsbG8="),
|
||||
).toThrow(/Unsupported data URL type/i);
|
||||
expect(() => __testing.decodeDataUrl("data:text/plain;base64,SGVsbG8=")).toThrow(
|
||||
/Unsupported data URL type/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -245,9 +227,7 @@ describe("image tool MiniMax VLM routing", () => {
|
||||
// @ts-expect-error partial global
|
||||
global.fetch = fetch;
|
||||
|
||||
const agentDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "clawdbot-minimax-vlm-"),
|
||||
);
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-minimax-vlm-"));
|
||||
vi.stubEnv("MINIMAX_API_KEY", "minimax-test");
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: { defaults: { model: { primary: "minimax/MiniMax-M2.1" } } },
|
||||
@@ -265,9 +245,9 @@ describe("image tool MiniMax VLM routing", () => {
|
||||
const [url, init] = fetch.mock.calls[0];
|
||||
expect(String(url)).toBe("https://api.minimax.io/v1/coding_plan/vlm");
|
||||
expect(init?.method).toBe("POST");
|
||||
expect(
|
||||
String((init?.headers as Record<string, string>)?.Authorization),
|
||||
).toBe("Bearer minimax-test");
|
||||
expect(String((init?.headers as Record<string, string>)?.Authorization)).toBe(
|
||||
"Bearer minimax-test",
|
||||
);
|
||||
expect(String(init?.body)).toContain('"prompt":"Describe the image."');
|
||||
expect(String(init?.body)).toContain('"image_url":"data:image/png;base64,');
|
||||
|
||||
@@ -289,9 +269,7 @@ describe("image tool MiniMax VLM routing", () => {
|
||||
// @ts-expect-error partial global
|
||||
global.fetch = fetch;
|
||||
|
||||
const agentDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "clawdbot-minimax-vlm-"),
|
||||
);
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-minimax-vlm-"));
|
||||
vi.stubEnv("MINIMAX_API_KEY", "minimax-test");
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: { defaults: { model: { primary: "minimax/MiniMax-M2.1" } } },
|
||||
|
||||
@@ -8,19 +8,13 @@ import {
|
||||
complete,
|
||||
type Model,
|
||||
} from "@mariozechner/pi-ai";
|
||||
import {
|
||||
discoverAuthStorage,
|
||||
discoverModels,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { resolveUserPath } from "../../utils.js";
|
||||
import { loadWebMedia } from "../../web/media.js";
|
||||
import {
|
||||
ensureAuthProfileStore,
|
||||
listProfilesForProvider,
|
||||
} from "../auth-profiles.js";
|
||||
import { ensureAuthProfileStore, listProfilesForProvider } from "../auth-profiles.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
|
||||
import { minimaxUnderstandImage } from "../minimax-vlm.js";
|
||||
import { getApiKeyForModel, resolveEnvApiKey } from "../model-auth.js";
|
||||
@@ -48,24 +42,15 @@ function resolveDefaultModelRef(cfg?: ClawdbotConfig): {
|
||||
provider: string;
|
||||
model: string;
|
||||
} {
|
||||
const modelConfig = cfg?.agents?.defaults?.model as
|
||||
| { primary?: string }
|
||||
| string
|
||||
| undefined;
|
||||
const raw =
|
||||
typeof modelConfig === "string"
|
||||
? modelConfig.trim()
|
||||
: modelConfig?.primary?.trim();
|
||||
const modelConfig = cfg?.agents?.defaults?.model as { primary?: string } | string | undefined;
|
||||
const raw = typeof modelConfig === "string" ? modelConfig.trim() : modelConfig?.primary?.trim();
|
||||
const parsed =
|
||||
parseModelRef(raw ?? "", DEFAULT_PROVIDER) ??
|
||||
({ provider: DEFAULT_PROVIDER, model: DEFAULT_MODEL } as const);
|
||||
return { provider: parsed.provider, model: parsed.model };
|
||||
}
|
||||
|
||||
function hasAuthForProvider(params: {
|
||||
provider: string;
|
||||
agentDir: string;
|
||||
}): boolean {
|
||||
function hasAuthForProvider(params: { provider: string; agentDir: string }): boolean {
|
||||
if (resolveEnvApiKey(params.provider)?.apiKey) return true;
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
@@ -156,33 +141,18 @@ export function resolveImageModelConfigForTool(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
function pickMaxBytes(
|
||||
cfg?: ClawdbotConfig,
|
||||
maxBytesMb?: number,
|
||||
): number | undefined {
|
||||
if (
|
||||
typeof maxBytesMb === "number" &&
|
||||
Number.isFinite(maxBytesMb) &&
|
||||
maxBytesMb > 0
|
||||
) {
|
||||
function pickMaxBytes(cfg?: ClawdbotConfig, maxBytesMb?: number): number | undefined {
|
||||
if (typeof maxBytesMb === "number" && Number.isFinite(maxBytesMb) && maxBytesMb > 0) {
|
||||
return Math.floor(maxBytesMb * 1024 * 1024);
|
||||
}
|
||||
const configured = cfg?.agents?.defaults?.mediaMaxMb;
|
||||
if (
|
||||
typeof configured === "number" &&
|
||||
Number.isFinite(configured) &&
|
||||
configured > 0
|
||||
) {
|
||||
if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {
|
||||
return Math.floor(configured * 1024 * 1024);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildImageContext(
|
||||
prompt: string,
|
||||
base64: string,
|
||||
mimeType: string,
|
||||
): Context {
|
||||
function buildImageContext(prompt: string, base64: string, mimeType: string): Context {
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
@@ -201,8 +171,7 @@ async function resolveSandboxedImagePath(params: {
|
||||
sandboxRoot: string;
|
||||
imagePath: string;
|
||||
}): Promise<{ resolved: string; rewrittenFrom?: string }> {
|
||||
const normalize = (p: string) =>
|
||||
p.startsWith("file://") ? p.slice("file://".length) : p;
|
||||
const normalize = (p: string) => (p.startsWith("file://") ? p.slice("file://".length) : p);
|
||||
const filePath = normalize(params.imagePath);
|
||||
try {
|
||||
const out = await assertSandboxPath({
|
||||
@@ -269,9 +238,7 @@ async function runImagePrompt(params: {
|
||||
throw new Error(`Unknown model: ${provider}/${modelId}`);
|
||||
}
|
||||
if (!model.input?.includes("image")) {
|
||||
throw new Error(
|
||||
`Model does not support images: ${provider}/${modelId}`,
|
||||
);
|
||||
throw new Error(`Model does not support images: ${provider}/${modelId}`);
|
||||
}
|
||||
const apiKeyInfo = await getApiKeyForModel({
|
||||
model,
|
||||
@@ -291,11 +258,7 @@ async function runImagePrompt(params: {
|
||||
return { text, provider: model.provider, model: model.id };
|
||||
}
|
||||
|
||||
const context = buildImageContext(
|
||||
params.prompt,
|
||||
params.base64,
|
||||
params.mimeType,
|
||||
);
|
||||
const context = buildImageContext(params.prompt, params.base64, params.mimeType);
|
||||
const message = (await complete(model, context, {
|
||||
apiKey: apiKeyInfo.apiKey,
|
||||
maxTokens: 512,
|
||||
@@ -351,12 +314,8 @@ export function createImageTool(options?: {
|
||||
maxBytesMb: Type.Optional(Type.Number()),
|
||||
}),
|
||||
execute: async (_toolCallId, args) => {
|
||||
const record =
|
||||
args && typeof args === "object"
|
||||
? (args as Record<string, unknown>)
|
||||
: {};
|
||||
const imageRawInput =
|
||||
typeof record.image === "string" ? record.image.trim() : "";
|
||||
const record = args && typeof args === "object" ? (args as Record<string, unknown>) : {};
|
||||
const imageRawInput = typeof record.image === "string" ? record.image.trim() : "";
|
||||
const imageRaw = imageRawInput.startsWith("@")
|
||||
? imageRawInput.slice(1).trim()
|
||||
: imageRawInput;
|
||||
@@ -372,13 +331,7 @@ export function createImageTool(options?: {
|
||||
const isFileUrl = /^file:/i.test(imageRaw);
|
||||
const isHttpUrl = /^https?:\/\//i.test(imageRaw);
|
||||
const isDataUrl = /^data:/i.test(imageRaw);
|
||||
if (
|
||||
hasScheme &&
|
||||
!looksLikeWindowsDrivePath &&
|
||||
!isFileUrl &&
|
||||
!isHttpUrl &&
|
||||
!isDataUrl
|
||||
) {
|
||||
if (hasScheme && !looksLikeWindowsDrivePath && !isFileUrl && !isHttpUrl && !isDataUrl) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@@ -397,11 +350,8 @@ export function createImageTool(options?: {
|
||||
? record.prompt.trim()
|
||||
: DEFAULT_PROMPT;
|
||||
const modelOverride =
|
||||
typeof record.model === "string" && record.model.trim()
|
||||
? record.model.trim()
|
||||
: undefined;
|
||||
const maxBytesMb =
|
||||
typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined;
|
||||
typeof record.model === "string" && record.model.trim() ? record.model.trim() : undefined;
|
||||
const maxBytesMb = typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined;
|
||||
const maxBytes = pickMaxBytes(options?.config, maxBytesMb);
|
||||
|
||||
const sandboxRoot = options?.sandboxRoot?.trim();
|
||||
@@ -415,19 +365,18 @@ export function createImageTool(options?: {
|
||||
if (imageRaw.startsWith("~")) return resolveUserPath(imageRaw);
|
||||
return imageRaw;
|
||||
})();
|
||||
const resolvedPathInfo: { resolved: string; rewrittenFrom?: string } =
|
||||
isDataUrl
|
||||
? { resolved: "" }
|
||||
: sandboxRoot
|
||||
? await resolveSandboxedImagePath({
|
||||
sandboxRoot,
|
||||
imagePath: resolvedImage,
|
||||
})
|
||||
: {
|
||||
resolved: resolvedImage.startsWith("file://")
|
||||
? resolvedImage.slice("file://".length)
|
||||
: resolvedImage,
|
||||
};
|
||||
const resolvedPathInfo: { resolved: string; rewrittenFrom?: string } = isDataUrl
|
||||
? { resolved: "" }
|
||||
: sandboxRoot
|
||||
? await resolveSandboxedImagePath({
|
||||
sandboxRoot,
|
||||
imagePath: resolvedImage,
|
||||
})
|
||||
: {
|
||||
resolved: resolvedImage.startsWith("file://")
|
||||
? resolvedImage.slice("file://".length)
|
||||
: resolvedImage,
|
||||
};
|
||||
const resolvedPath = isDataUrl ? null : resolvedPathInfo.resolved;
|
||||
|
||||
const media = isDataUrl
|
||||
|
||||
@@ -9,10 +9,7 @@ import {
|
||||
} from "../../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_IDS,
|
||||
GATEWAY_CLIENT_MODES,
|
||||
} from "../../gateway/protocol/client-info.js";
|
||||
import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../gateway/protocol/client-info.js";
|
||||
import { runMessageAction } from "../../infra/outbound/message-action-runner.js";
|
||||
import { normalizeAccountId } from "../../routing/session-key.js";
|
||||
import { stringEnum } from "../schema/typebox.js";
|
||||
@@ -132,10 +129,9 @@ type MessageToolOptions = {
|
||||
function buildMessageToolSchema(cfg: ClawdbotConfig) {
|
||||
const actions = listChannelMessageActions(cfg);
|
||||
const includeButtons = supportsChannelMessageButtons(cfg);
|
||||
return buildMessageToolSchemaFromActions(
|
||||
actions.length > 0 ? actions : ["send"],
|
||||
{ includeButtons },
|
||||
);
|
||||
return buildMessageToolSchemaFromActions(actions.length > 0 ? actions : ["send"], {
|
||||
includeButtons,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveAgentAccountId(value?: string): string | undefined {
|
||||
@@ -146,9 +142,7 @@ function resolveAgentAccountId(value?: string): string | undefined {
|
||||
|
||||
export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
|
||||
const agentAccountId = resolveAgentAccountId(options?.agentAccountId);
|
||||
const schema = options?.config
|
||||
? buildMessageToolSchema(options.config)
|
||||
: MessageToolSchema;
|
||||
const schema = options?.config ? buildMessageToolSchema(options.config) : MessageToolSchema;
|
||||
|
||||
return {
|
||||
label: "Message",
|
||||
|
||||
@@ -99,26 +99,19 @@ export function createNodesTool(): AnyAgentTool {
|
||||
const gatewayOpts: GatewayCallOptions = {
|
||||
gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }),
|
||||
gatewayToken: readStringParam(params, "gatewayToken", { trim: false }),
|
||||
timeoutMs:
|
||||
typeof params.timeoutMs === "number" ? params.timeoutMs : undefined,
|
||||
timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined,
|
||||
};
|
||||
|
||||
switch (action) {
|
||||
case "status":
|
||||
return jsonResult(
|
||||
await callGatewayTool("node.list", gatewayOpts, {}),
|
||||
);
|
||||
return jsonResult(await callGatewayTool("node.list", gatewayOpts, {}));
|
||||
case "describe": {
|
||||
const node = readStringParam(params, "node", { required: true });
|
||||
const nodeId = await resolveNodeId(gatewayOpts, node);
|
||||
return jsonResult(
|
||||
await callGatewayTool("node.describe", gatewayOpts, { nodeId }),
|
||||
);
|
||||
return jsonResult(await callGatewayTool("node.describe", gatewayOpts, { nodeId }));
|
||||
}
|
||||
case "pending":
|
||||
return jsonResult(
|
||||
await callGatewayTool("node.pair.list", gatewayOpts, {}),
|
||||
);
|
||||
return jsonResult(await callGatewayTool("node.pair.list", gatewayOpts, {}));
|
||||
case "approve": {
|
||||
const requestId = readStringParam(params, "requestId", {
|
||||
required: true,
|
||||
@@ -153,16 +146,9 @@ export function createNodesTool(): AnyAgentTool {
|
||||
params: {
|
||||
title: title.trim() || undefined,
|
||||
body: body.trim() || undefined,
|
||||
sound:
|
||||
typeof params.sound === "string" ? params.sound : undefined,
|
||||
priority:
|
||||
typeof params.priority === "string"
|
||||
? params.priority
|
||||
: undefined,
|
||||
delivery:
|
||||
typeof params.delivery === "string"
|
||||
? params.delivery
|
||||
: undefined,
|
||||
sound: typeof params.sound === "string" ? params.sound : undefined,
|
||||
priority: typeof params.priority === "string" ? params.priority : undefined,
|
||||
delivery: typeof params.delivery === "string" ? params.delivery : undefined,
|
||||
},
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
});
|
||||
@@ -172,9 +158,7 @@ export function createNodesTool(): AnyAgentTool {
|
||||
const node = readStringParam(params, "node", { required: true });
|
||||
const nodeId = await resolveNodeId(gatewayOpts, node);
|
||||
const facingRaw =
|
||||
typeof params.facing === "string"
|
||||
? params.facing.toLowerCase()
|
||||
: "both";
|
||||
typeof params.facing === "string" ? params.facing.toLowerCase() : "both";
|
||||
const facings: CameraFacing[] =
|
||||
facingRaw === "both"
|
||||
? ["front", "back"]
|
||||
@@ -184,18 +168,15 @@ export function createNodesTool(): AnyAgentTool {
|
||||
throw new Error("invalid facing (front|back|both)");
|
||||
})();
|
||||
const maxWidth =
|
||||
typeof params.maxWidth === "number" &&
|
||||
Number.isFinite(params.maxWidth)
|
||||
typeof params.maxWidth === "number" && Number.isFinite(params.maxWidth)
|
||||
? params.maxWidth
|
||||
: undefined;
|
||||
const quality =
|
||||
typeof params.quality === "number" &&
|
||||
Number.isFinite(params.quality)
|
||||
typeof params.quality === "number" && Number.isFinite(params.quality)
|
||||
? params.quality
|
||||
: undefined;
|
||||
const delayMs =
|
||||
typeof params.delayMs === "number" &&
|
||||
Number.isFinite(params.delayMs)
|
||||
typeof params.delayMs === "number" && Number.isFinite(params.delayMs)
|
||||
? params.delayMs
|
||||
: undefined;
|
||||
const deviceId =
|
||||
@@ -227,13 +208,10 @@ export function createNodesTool(): AnyAgentTool {
|
||||
normalizedFormat !== "jpeg" &&
|
||||
normalizedFormat !== "png"
|
||||
) {
|
||||
throw new Error(
|
||||
`unsupported camera.snap format: ${payload.format}`,
|
||||
);
|
||||
throw new Error(`unsupported camera.snap format: ${payload.format}`);
|
||||
}
|
||||
|
||||
const isJpeg =
|
||||
normalizedFormat === "jpg" || normalizedFormat === "jpeg";
|
||||
const isJpeg = normalizedFormat === "jpg" || normalizedFormat === "jpeg";
|
||||
const filePath = cameraTempPath({
|
||||
kind: "snap",
|
||||
facing,
|
||||
@@ -245,8 +223,7 @@ export function createNodesTool(): AnyAgentTool {
|
||||
type: "image",
|
||||
data: payload.base64,
|
||||
mimeType:
|
||||
imageMimeFromFormat(payload.format) ??
|
||||
(isJpeg ? "image/jpeg" : "image/png"),
|
||||
imageMimeFromFormat(payload.format) ?? (isJpeg ? "image/jpeg" : "image/png"),
|
||||
});
|
||||
details.push({
|
||||
facing,
|
||||
@@ -269,32 +246,24 @@ export function createNodesTool(): AnyAgentTool {
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
})) as { payload?: unknown };
|
||||
const payload =
|
||||
raw && typeof raw.payload === "object" && raw.payload !== null
|
||||
? raw.payload
|
||||
: {};
|
||||
raw && typeof raw.payload === "object" && raw.payload !== null ? raw.payload : {};
|
||||
return jsonResult(payload);
|
||||
}
|
||||
case "camera_clip": {
|
||||
const node = readStringParam(params, "node", { required: true });
|
||||
const nodeId = await resolveNodeId(gatewayOpts, node);
|
||||
const facing =
|
||||
typeof params.facing === "string"
|
||||
? params.facing.toLowerCase()
|
||||
: "front";
|
||||
const facing = typeof params.facing === "string" ? params.facing.toLowerCase() : "front";
|
||||
if (facing !== "front" && facing !== "back") {
|
||||
throw new Error("invalid facing (front|back)");
|
||||
}
|
||||
const durationMs =
|
||||
typeof params.durationMs === "number" &&
|
||||
Number.isFinite(params.durationMs)
|
||||
typeof params.durationMs === "number" && Number.isFinite(params.durationMs)
|
||||
? params.durationMs
|
||||
: typeof params.duration === "string"
|
||||
? parseDurationMs(params.duration)
|
||||
: 3000;
|
||||
const includeAudio =
|
||||
typeof params.includeAudio === "boolean"
|
||||
? params.includeAudio
|
||||
: true;
|
||||
typeof params.includeAudio === "boolean" ? params.includeAudio : true;
|
||||
const deviceId =
|
||||
typeof params.deviceId === "string" && params.deviceId.trim()
|
||||
? params.deviceId.trim()
|
||||
@@ -332,25 +301,19 @@ export function createNodesTool(): AnyAgentTool {
|
||||
const node = readStringParam(params, "node", { required: true });
|
||||
const nodeId = await resolveNodeId(gatewayOpts, node);
|
||||
const durationMs =
|
||||
typeof params.durationMs === "number" &&
|
||||
Number.isFinite(params.durationMs)
|
||||
typeof params.durationMs === "number" && Number.isFinite(params.durationMs)
|
||||
? params.durationMs
|
||||
: typeof params.duration === "string"
|
||||
? parseDurationMs(params.duration)
|
||||
: 10_000;
|
||||
const fps =
|
||||
typeof params.fps === "number" && Number.isFinite(params.fps)
|
||||
? params.fps
|
||||
: 10;
|
||||
typeof params.fps === "number" && Number.isFinite(params.fps) ? params.fps : 10;
|
||||
const screenIndex =
|
||||
typeof params.screenIndex === "number" &&
|
||||
Number.isFinite(params.screenIndex)
|
||||
typeof params.screenIndex === "number" && Number.isFinite(params.screenIndex)
|
||||
? params.screenIndex
|
||||
: 0;
|
||||
const includeAudio =
|
||||
typeof params.includeAudio === "boolean"
|
||||
? params.includeAudio
|
||||
: true;
|
||||
typeof params.includeAudio === "boolean" ? params.includeAudio : true;
|
||||
const raw = (await callGatewayTool("node.invoke", gatewayOpts, {
|
||||
nodeId,
|
||||
command: "screen.record",
|
||||
@@ -368,10 +331,7 @@ export function createNodesTool(): AnyAgentTool {
|
||||
typeof params.outPath === "string" && params.outPath.trim()
|
||||
? params.outPath.trim()
|
||||
: screenRecordTempPath({ ext: payload.format || "mp4" });
|
||||
const written = await writeScreenRecordToFile(
|
||||
filePath,
|
||||
payload.base64,
|
||||
);
|
||||
const written = await writeScreenRecordToFile(filePath, payload.base64);
|
||||
return {
|
||||
content: [{ type: "text", text: `FILE:${written.path}` }],
|
||||
details: {
|
||||
@@ -387,8 +347,7 @@ export function createNodesTool(): AnyAgentTool {
|
||||
const node = readStringParam(params, "node", { required: true });
|
||||
const nodeId = await resolveNodeId(gatewayOpts, node);
|
||||
const maxAgeMs =
|
||||
typeof params.maxAgeMs === "number" &&
|
||||
Number.isFinite(params.maxAgeMs)
|
||||
typeof params.maxAgeMs === "number" && Number.isFinite(params.maxAgeMs)
|
||||
? params.maxAgeMs
|
||||
: undefined;
|
||||
const desiredAccuracy =
|
||||
@@ -419,23 +378,17 @@ export function createNodesTool(): AnyAgentTool {
|
||||
const nodeId = await resolveNodeId(gatewayOpts, node);
|
||||
const commandRaw = params.command;
|
||||
if (!commandRaw) {
|
||||
throw new Error(
|
||||
"command required (argv array, e.g. ['echo', 'Hello'])",
|
||||
);
|
||||
throw new Error("command required (argv array, e.g. ['echo', 'Hello'])");
|
||||
}
|
||||
if (!Array.isArray(commandRaw)) {
|
||||
throw new Error(
|
||||
"command must be an array of strings (argv), e.g. ['echo', 'Hello']",
|
||||
);
|
||||
throw new Error("command must be an array of strings (argv), e.g. ['echo', 'Hello']");
|
||||
}
|
||||
const command = commandRaw.map((c) => String(c));
|
||||
if (command.length === 0) {
|
||||
throw new Error("command must not be empty");
|
||||
}
|
||||
const cwd =
|
||||
typeof params.cwd === "string" && params.cwd.trim()
|
||||
? params.cwd.trim()
|
||||
: undefined;
|
||||
typeof params.cwd === "string" && params.cwd.trim() ? params.cwd.trim() : undefined;
|
||||
const env = parseEnvPairs(params.env);
|
||||
const commandTimeoutMs = parseTimeoutMs(params.commandTimeoutMs);
|
||||
const invokeTimeoutMs = parseTimeoutMs(params.invokeTimeoutMs);
|
||||
|
||||
@@ -43,21 +43,13 @@ type PairingList = {
|
||||
};
|
||||
|
||||
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[]) : [];
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -6,10 +6,7 @@ import {
|
||||
resolveAuthProfileOrder,
|
||||
} from "../../agents/auth-profiles.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js";
|
||||
import {
|
||||
getCustomProviderApiKey,
|
||||
resolveEnvApiKey,
|
||||
} from "../../agents/model-auth.js";
|
||||
import { getCustomProviderApiKey, resolveEnvApiKey } from "../../agents/model-auth.js";
|
||||
import { loadModelCatalog } from "../../agents/model-catalog.js";
|
||||
import {
|
||||
buildAllowedModelSet,
|
||||
@@ -20,10 +17,7 @@ import {
|
||||
resolveModelRefFromString,
|
||||
} from "../../agents/model-selection.js";
|
||||
import { normalizeGroupActivation } from "../../auto-reply/group-activation.js";
|
||||
import {
|
||||
getFollowupQueueDepth,
|
||||
resolveQueueSettings,
|
||||
} from "../../auto-reply/reply/queue.js";
|
||||
import { getFollowupQueueDepth, resolveQueueSettings } from "../../auto-reply/reply/queue.js";
|
||||
import { buildStatusMessage } from "../../auto-reply/status.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
@@ -45,10 +39,7 @@ import {
|
||||
} from "../../routing/session-key.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
import { readStringParam } from "./common.js";
|
||||
import {
|
||||
resolveInternalSessionKey,
|
||||
resolveMainSessionAlias,
|
||||
} from "./sessions-helpers.js";
|
||||
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js";
|
||||
|
||||
const SessionStatusToolSchema = Type.Object({
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
@@ -179,10 +170,8 @@ async function resolveModelOverride(params: {
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
const currentProvider =
|
||||
params.sessionEntry?.providerOverride?.trim() || configDefault.provider;
|
||||
const currentModel =
|
||||
params.sessionEntry?.modelOverride?.trim() || configDefault.model;
|
||||
const currentProvider = params.sessionEntry?.providerOverride?.trim() || configDefault.provider;
|
||||
const currentModel = params.sessionEntry?.modelOverride?.trim() || configDefault.model;
|
||||
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg: params.cfg,
|
||||
@@ -209,8 +198,7 @@ async function resolveModelOverride(params: {
|
||||
throw new Error(`Model "${key}" is not allowed.`);
|
||||
}
|
||||
const isDefault =
|
||||
resolved.ref.provider === configDefault.provider &&
|
||||
resolved.ref.model === configDefault.model;
|
||||
resolved.ref.provider === configDefault.provider && resolved.ref.model === configDefault.model;
|
||||
return {
|
||||
kind: "set",
|
||||
provider: resolved.ref.provider,
|
||||
@@ -234,15 +222,12 @@ export function createSessionStatusTool(opts?: {
|
||||
const cfg = opts?.config ?? loadConfig();
|
||||
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||
|
||||
const requestedKeyRaw =
|
||||
readStringParam(params, "sessionKey") ?? opts?.agentSessionKey;
|
||||
const requestedKeyRaw = readStringParam(params, "sessionKey") ?? opts?.agentSessionKey;
|
||||
if (!requestedKeyRaw?.trim()) {
|
||||
throw new Error("sessionKey required");
|
||||
}
|
||||
|
||||
const agentId = resolveAgentIdFromSessionKey(
|
||||
opts?.agentSessionKey ?? requestedKeyRaw,
|
||||
);
|
||||
const agentId = resolveAgentIdFromSessionKey(opts?.agentSessionKey ?? requestedKeyRaw);
|
||||
const storePath = resolveStorePath(cfg.session?.store, { agentId });
|
||||
const store = loadSessionStore(storePath);
|
||||
|
||||
@@ -289,8 +274,7 @@ export function createSessionStatusTool(opts?: {
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
const providerForCard =
|
||||
resolved.entry.providerOverride?.trim() || configured.provider;
|
||||
const providerForCard = resolved.entry.providerOverride?.trim() || configured.provider;
|
||||
const usageProvider = resolveUsageProviderId(providerForCard);
|
||||
let usageLine: string | undefined;
|
||||
if (usageProvider) {
|
||||
@@ -316,22 +300,18 @@ export function createSessionStatusTool(opts?: {
|
||||
resolved.key.includes(":group:") ||
|
||||
resolved.key.includes(":channel:");
|
||||
const groupActivation = isGroup
|
||||
? (normalizeGroupActivation(resolved.entry.groupActivation) ??
|
||||
"mention")
|
||||
? (normalizeGroupActivation(resolved.entry.groupActivation) ?? "mention")
|
||||
: undefined;
|
||||
|
||||
const queueSettings = resolveQueueSettings({
|
||||
cfg,
|
||||
channel:
|
||||
resolved.entry.channel ?? resolved.entry.lastChannel ?? "unknown",
|
||||
channel: resolved.entry.channel ?? resolved.entry.lastChannel ?? "unknown",
|
||||
sessionEntry: resolved.entry,
|
||||
});
|
||||
const queueKey = resolved.key ?? resolved.entry.sessionId;
|
||||
const queueDepth = queueKey ? getFollowupQueueDepth(queueKey) : 0;
|
||||
const queueOverrides = Boolean(
|
||||
resolved.entry.queueDebounceMs ??
|
||||
resolved.entry.queueCap ??
|
||||
resolved.entry.queueDrop,
|
||||
resolved.entry.queueDebounceMs ?? resolved.entry.queueCap ?? resolved.entry.queueDrop,
|
||||
);
|
||||
|
||||
const statusText = buildStatusMessage({
|
||||
|
||||
@@ -43,9 +43,7 @@ describe("resolveAnnounceTarget", () => {
|
||||
accountId: "work",
|
||||
});
|
||||
expect(callGatewayMock).toHaveBeenCalledTimes(1);
|
||||
const first = callGatewayMock.mock.calls[0]?.[0] as
|
||||
| { method?: string }
|
||||
| undefined;
|
||||
const first = callGatewayMock.mock.calls[0]?.[0] as { method?: string } | undefined;
|
||||
expect(first).toBeDefined();
|
||||
expect(first?.method).toBe("sessions.list");
|
||||
});
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
getChannelPlugin,
|
||||
normalizeChannelId,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import type { AnnounceTarget } from "./sessions-send-helpers.js";
|
||||
import { resolveAnnounceTargetFromKey } from "./sessions-send-helpers.js";
|
||||
@@ -35,13 +32,9 @@ export async function resolveAnnounceTarget(params: {
|
||||
const match =
|
||||
sessions.find((entry) => entry?.key === params.sessionKey) ??
|
||||
sessions.find((entry) => entry?.key === params.displayKey);
|
||||
const channel =
|
||||
typeof match?.lastChannel === "string" ? match.lastChannel : undefined;
|
||||
const channel = typeof match?.lastChannel === "string" ? match.lastChannel : undefined;
|
||||
const to = typeof match?.lastTo === "string" ? match.lastTo : undefined;
|
||||
const accountId =
|
||||
typeof match?.lastAccountId === "string"
|
||||
? match.lastAccountId
|
||||
: undefined;
|
||||
const accountId = typeof match?.lastAccountId === "string" ? match.lastAccountId : undefined;
|
||||
if (channel && to) return { channel, to, accountId };
|
||||
} catch {
|
||||
// ignore
|
||||
|
||||
@@ -15,21 +15,13 @@ export function resolveMainSessionAlias(cfg: ClawdbotConfig) {
|
||||
return { mainKey, alias, scope };
|
||||
}
|
||||
|
||||
export function resolveDisplaySessionKey(params: {
|
||||
key: string;
|
||||
alias: string;
|
||||
mainKey: string;
|
||||
}) {
|
||||
export function resolveDisplaySessionKey(params: { key: string; alias: string; mainKey: string }) {
|
||||
if (params.key === params.alias) return "main";
|
||||
if (params.key === params.mainKey) return "main";
|
||||
return params.key;
|
||||
}
|
||||
|
||||
export function resolveInternalSessionKey(params: {
|
||||
key: string;
|
||||
alias: string;
|
||||
mainKey: string;
|
||||
}) {
|
||||
export function resolveInternalSessionKey(params: { key: string; alias: string; mainKey: string }) {
|
||||
if (params.key === "main") return params.alias;
|
||||
return params.key;
|
||||
}
|
||||
@@ -46,11 +38,7 @@ export function classifySessionKind(params: {
|
||||
if (key.startsWith("hook:")) return "hook";
|
||||
if (key.startsWith("node-") || key.startsWith("node:")) return "node";
|
||||
if (params.gatewayKind === "group") return "group";
|
||||
if (
|
||||
key.startsWith("group:") ||
|
||||
key.includes(":group:") ||
|
||||
key.includes(":channel:")
|
||||
) {
|
||||
if (key.startsWith("group:") || key.includes(":group:") || key.includes(":channel:")) {
|
||||
return "group";
|
||||
}
|
||||
return "other";
|
||||
@@ -62,12 +50,7 @@ export function deriveChannel(params: {
|
||||
channel?: string | null;
|
||||
lastChannel?: string | null;
|
||||
}): string {
|
||||
if (
|
||||
params.kind === "cron" ||
|
||||
params.kind === "hook" ||
|
||||
params.kind === "node"
|
||||
)
|
||||
return "internal";
|
||||
if (params.kind === "cron" || params.kind === "hook" || params.kind === "node") return "internal";
|
||||
const channel = normalizeKey(params.channel ?? undefined);
|
||||
if (channel) return channel;
|
||||
const lastChannel = normalizeKey(params.lastChannel ?? undefined);
|
||||
|
||||
@@ -22,9 +22,7 @@ const SessionsHistoryToolSchema = Type.Object({
|
||||
includeTools: Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
function resolveSandboxSessionToolsVisibility(
|
||||
cfg: ReturnType<typeof loadConfig>,
|
||||
) {
|
||||
function resolveSandboxSessionToolsVisibility(cfg: ReturnType<typeof loadConfig>) {
|
||||
return cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
}
|
||||
|
||||
@@ -99,9 +97,7 @@ export function createSessionsHistoryTool(opts?: {
|
||||
|
||||
const routingA2A = cfg.tools?.agentToAgent;
|
||||
const a2aEnabled = routingA2A?.enabled === true;
|
||||
const allowPatterns = Array.isArray(routingA2A?.allow)
|
||||
? routingA2A.allow
|
||||
: [];
|
||||
const allowPatterns = Array.isArray(routingA2A?.allow) ? routingA2A.allow : [];
|
||||
const matchesAllow = (agentId: string) => {
|
||||
if (allowPatterns.length === 0) return true;
|
||||
return allowPatterns.some((pattern) => {
|
||||
@@ -117,9 +113,7 @@ export function createSessionsHistoryTool(opts?: {
|
||||
const requesterAgentId = normalizeAgentId(
|
||||
parseAgentSessionKey(requesterInternalKey)?.agentId,
|
||||
);
|
||||
const targetAgentId = normalizeAgentId(
|
||||
parseAgentSessionKey(resolvedKey)?.agentId,
|
||||
);
|
||||
const targetAgentId = normalizeAgentId(parseAgentSessionKey(resolvedKey)?.agentId);
|
||||
const isCrossAgent = requesterAgentId !== targetAgentId;
|
||||
if (isCrossAgent) {
|
||||
if (!a2aEnabled) {
|
||||
@@ -146,12 +140,8 @@ export function createSessionsHistoryTool(opts?: {
|
||||
method: "chat.history",
|
||||
params: { sessionKey: resolvedKey, limit },
|
||||
})) as { messages?: unknown[] };
|
||||
const rawMessages = Array.isArray(result?.messages)
|
||||
? result.messages
|
||||
: [];
|
||||
const messages = includeTools
|
||||
? rawMessages
|
||||
: stripToolMessages(rawMessages);
|
||||
const rawMessages = Array.isArray(result?.messages) ? result.messages : [];
|
||||
const messages = includeTools ? rawMessages : stripToolMessages(rawMessages);
|
||||
return jsonResult({
|
||||
sessionKey: resolveDisplaySessionKey({
|
||||
key: sessionKey,
|
||||
|
||||
@@ -6,8 +6,7 @@ vi.mock("../../gateway/call.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../config/config.js", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("../../config/config.js")>();
|
||||
const actual = await importOriginal<typeof import("../../config/config.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: () =>
|
||||
|
||||
@@ -51,9 +51,7 @@ const SessionsListToolSchema = Type.Object({
|
||||
messageLimit: Type.Optional(Type.Number({ minimum: 0 })),
|
||||
});
|
||||
|
||||
function resolveSandboxSessionToolsVisibility(
|
||||
cfg: ReturnType<typeof loadConfig>,
|
||||
) {
|
||||
function resolveSandboxSessionToolsVisibility(cfg: ReturnType<typeof loadConfig>) {
|
||||
return cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
}
|
||||
|
||||
@@ -91,22 +89,18 @@ export function createSessionsListTool(opts?: {
|
||||
const allowedKindsList = (kindsRaw ?? []).filter((value) =>
|
||||
["main", "group", "cron", "hook", "node", "other"].includes(value),
|
||||
);
|
||||
const allowedKinds = allowedKindsList.length
|
||||
? new Set(allowedKindsList)
|
||||
: undefined;
|
||||
const allowedKinds = allowedKindsList.length ? new Set(allowedKindsList) : undefined;
|
||||
|
||||
const limit =
|
||||
typeof params.limit === "number" && Number.isFinite(params.limit)
|
||||
? Math.max(1, Math.floor(params.limit))
|
||||
: undefined;
|
||||
const activeMinutes =
|
||||
typeof params.activeMinutes === "number" &&
|
||||
Number.isFinite(params.activeMinutes)
|
||||
typeof params.activeMinutes === "number" && Number.isFinite(params.activeMinutes)
|
||||
? Math.max(1, Math.floor(params.activeMinutes))
|
||||
: undefined;
|
||||
const messageLimitRaw =
|
||||
typeof params.messageLimit === "number" &&
|
||||
Number.isFinite(params.messageLimit)
|
||||
typeof params.messageLimit === "number" && Number.isFinite(params.messageLimit)
|
||||
? Math.max(0, Math.floor(params.messageLimit))
|
||||
: 0;
|
||||
const messageLimit = Math.min(messageLimitRaw, 20);
|
||||
@@ -129,9 +123,7 @@ export function createSessionsListTool(opts?: {
|
||||
const storePath = typeof list?.path === "string" ? list.path : undefined;
|
||||
const routingA2A = cfg.tools?.agentToAgent;
|
||||
const a2aEnabled = routingA2A?.enabled === true;
|
||||
const allowPatterns = Array.isArray(routingA2A?.allow)
|
||||
? routingA2A.allow
|
||||
: [];
|
||||
const allowPatterns = Array.isArray(routingA2A?.allow) ? routingA2A.allow : [];
|
||||
const matchesAllow = (agentId: string) => {
|
||||
if (allowPatterns.length === 0) return true;
|
||||
return allowPatterns.some((pattern) => {
|
||||
@@ -154,21 +146,17 @@ export function createSessionsListTool(opts?: {
|
||||
const key = typeof entry.key === "string" ? entry.key : "";
|
||||
if (!key) continue;
|
||||
|
||||
const entryAgentId = normalizeAgentId(
|
||||
parseAgentSessionKey(key)?.agentId,
|
||||
);
|
||||
const entryAgentId = normalizeAgentId(parseAgentSessionKey(key)?.agentId);
|
||||
const crossAgent = entryAgentId !== requesterAgentId;
|
||||
if (crossAgent) {
|
||||
if (!a2aEnabled) continue;
|
||||
if (!matchesAllow(requesterAgentId) || !matchesAllow(entryAgentId))
|
||||
continue;
|
||||
if (!matchesAllow(requesterAgentId) || !matchesAllow(entryAgentId)) continue;
|
||||
}
|
||||
|
||||
if (key === "unknown") continue;
|
||||
if (key === "global" && alias !== "global") continue;
|
||||
|
||||
const gatewayKind =
|
||||
typeof entry.kind === "string" ? entry.kind : undefined;
|
||||
const gatewayKind = typeof entry.kind === "string" ? entry.kind : undefined;
|
||||
const kind = classifySessionKind({ key, gatewayKind, alias, mainKey });
|
||||
if (allowedKinds && !allowedKinds.has(kind)) continue;
|
||||
|
||||
@@ -178,14 +166,10 @@ export function createSessionsListTool(opts?: {
|
||||
mainKey,
|
||||
});
|
||||
|
||||
const entryChannel =
|
||||
typeof entry.channel === "string" ? entry.channel : undefined;
|
||||
const lastChannel =
|
||||
typeof entry.lastChannel === "string" ? entry.lastChannel : undefined;
|
||||
const entryChannel = typeof entry.channel === "string" ? entry.channel : undefined;
|
||||
const lastChannel = typeof entry.lastChannel === "string" ? entry.lastChannel : undefined;
|
||||
const lastAccountId =
|
||||
typeof entry.lastAccountId === "string"
|
||||
? entry.lastAccountId
|
||||
: undefined;
|
||||
typeof entry.lastAccountId === "string" ? entry.lastAccountId : undefined;
|
||||
const derivedChannel = deriveChannel({
|
||||
key,
|
||||
kind,
|
||||
@@ -193,8 +177,7 @@ export function createSessionsListTool(opts?: {
|
||||
lastChannel,
|
||||
});
|
||||
|
||||
const sessionId =
|
||||
typeof entry.sessionId === "string" ? entry.sessionId : undefined;
|
||||
const sessionId = typeof entry.sessionId === "string" ? entry.sessionId : undefined;
|
||||
const transcriptPath =
|
||||
sessionId && storePath
|
||||
? path.join(path.dirname(storePath), `${sessionId}.jsonl`)
|
||||
@@ -205,40 +188,18 @@ export function createSessionsListTool(opts?: {
|
||||
kind,
|
||||
channel: derivedChannel,
|
||||
label: typeof entry.label === "string" ? entry.label : undefined,
|
||||
displayName:
|
||||
typeof entry.displayName === "string"
|
||||
? entry.displayName
|
||||
: undefined,
|
||||
updatedAt:
|
||||
typeof entry.updatedAt === "number" ? entry.updatedAt : undefined,
|
||||
displayName: typeof entry.displayName === "string" ? entry.displayName : undefined,
|
||||
updatedAt: typeof entry.updatedAt === "number" ? entry.updatedAt : undefined,
|
||||
sessionId,
|
||||
model: typeof entry.model === "string" ? entry.model : undefined,
|
||||
contextTokens:
|
||||
typeof entry.contextTokens === "number"
|
||||
? entry.contextTokens
|
||||
: undefined,
|
||||
totalTokens:
|
||||
typeof entry.totalTokens === "number"
|
||||
? entry.totalTokens
|
||||
: undefined,
|
||||
thinkingLevel:
|
||||
typeof entry.thinkingLevel === "string"
|
||||
? entry.thinkingLevel
|
||||
: undefined,
|
||||
verboseLevel:
|
||||
typeof entry.verboseLevel === "string"
|
||||
? entry.verboseLevel
|
||||
: undefined,
|
||||
systemSent:
|
||||
typeof entry.systemSent === "boolean"
|
||||
? entry.systemSent
|
||||
: undefined,
|
||||
contextTokens: typeof entry.contextTokens === "number" ? entry.contextTokens : undefined,
|
||||
totalTokens: typeof entry.totalTokens === "number" ? entry.totalTokens : undefined,
|
||||
thinkingLevel: typeof entry.thinkingLevel === "string" ? entry.thinkingLevel : undefined,
|
||||
verboseLevel: typeof entry.verboseLevel === "string" ? entry.verboseLevel : undefined,
|
||||
systemSent: typeof entry.systemSent === "boolean" ? entry.systemSent : undefined,
|
||||
abortedLastRun:
|
||||
typeof entry.abortedLastRun === "boolean"
|
||||
? entry.abortedLastRun
|
||||
: undefined,
|
||||
sendPolicy:
|
||||
typeof entry.sendPolicy === "string" ? entry.sendPolicy : undefined,
|
||||
typeof entry.abortedLastRun === "boolean" ? entry.abortedLastRun : undefined,
|
||||
sendPolicy: typeof entry.sendPolicy === "string" ? entry.sendPolicy : undefined,
|
||||
lastChannel,
|
||||
lastTo: typeof entry.lastTo === "string" ? entry.lastTo : undefined,
|
||||
lastAccountId,
|
||||
@@ -255,14 +216,9 @@ export function createSessionsListTool(opts?: {
|
||||
method: "chat.history",
|
||||
params: { sessionKey: resolvedKey, limit: messageLimit },
|
||||
})) as { messages?: unknown[] };
|
||||
const rawMessages = Array.isArray(history?.messages)
|
||||
? history.messages
|
||||
: [];
|
||||
const rawMessages = Array.isArray(history?.messages) ? history.messages : [];
|
||||
const filtered = stripToolMessages(rawMessages);
|
||||
row.messages =
|
||||
filtered.length > messageLimit
|
||||
? filtered.slice(-messageLimit)
|
||||
: filtered;
|
||||
row.messages = filtered.length > messageLimit ? filtered.slice(-messageLimit) : filtered;
|
||||
}
|
||||
|
||||
rows.push(row);
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
getChannelPlugin,
|
||||
normalizeChannelId,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
|
||||
const ANNOUNCE_SKIP_TOKEN = "ANNOUNCE_SKIP";
|
||||
@@ -15,14 +12,9 @@ export type AnnounceTarget = {
|
||||
accountId?: string;
|
||||
};
|
||||
|
||||
export function resolveAnnounceTargetFromKey(
|
||||
sessionKey: string,
|
||||
): AnnounceTarget | null {
|
||||
export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget | null {
|
||||
const rawParts = sessionKey.split(":").filter(Boolean);
|
||||
const parts =
|
||||
rawParts.length >= 3 && rawParts[0] === "agent"
|
||||
? rawParts.slice(2)
|
||||
: rawParts;
|
||||
const parts = rawParts.length >= 3 && rawParts[0] === "agent" ? rawParts.slice(2) : rawParts;
|
||||
if (parts.length < 3) return null;
|
||||
const [channelRaw, kind, ...rest] = parts;
|
||||
if (kind !== "group" && kind !== "channel") return null;
|
||||
@@ -37,9 +29,7 @@ export function resolveAnnounceTargetFromKey(
|
||||
: `group:${id}`
|
||||
: id;
|
||||
const normalized = normalizedChannel
|
||||
? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.(
|
||||
kindTarget,
|
||||
)
|
||||
? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.(kindTarget)
|
||||
: undefined;
|
||||
return { channel, to: normalized ?? kindTarget };
|
||||
}
|
||||
@@ -72,9 +62,7 @@ export function buildAgentToAgentReplyContext(params: {
|
||||
maxTurns: number;
|
||||
}) {
|
||||
const currentLabel =
|
||||
params.currentRole === "requester"
|
||||
? "Agent 1 (requester)"
|
||||
: "Agent 2 (target)";
|
||||
params.currentRole === "requester" ? "Agent 1 (requester)" : "Agent 2 (target)";
|
||||
const lines = [
|
||||
"Agent-to-agent reply step:",
|
||||
`Current agent: ${currentLabel}.`,
|
||||
@@ -86,9 +74,7 @@ export function buildAgentToAgentReplyContext(params: {
|
||||
? `Agent 1 (requester) channel: ${params.requesterChannel}.`
|
||||
: undefined,
|
||||
`Agent 2 (target) session: ${params.targetSessionKey}.`,
|
||||
params.targetChannel
|
||||
? `Agent 2 (target) channel: ${params.targetChannel}.`
|
||||
: undefined,
|
||||
params.targetChannel ? `Agent 2 (target) channel: ${params.targetChannel}.` : undefined,
|
||||
`If you want to stop the ping-pong, reply exactly "${REPLY_SKIP_TOKEN}".`,
|
||||
].filter(Boolean);
|
||||
return lines.join("\n");
|
||||
@@ -112,16 +98,12 @@ export function buildAgentToAgentAnnounceContext(params: {
|
||||
? `Agent 1 (requester) channel: ${params.requesterChannel}.`
|
||||
: undefined,
|
||||
`Agent 2 (target) session: ${params.targetSessionKey}.`,
|
||||
params.targetChannel
|
||||
? `Agent 2 (target) channel: ${params.targetChannel}.`
|
||||
: undefined,
|
||||
params.targetChannel ? `Agent 2 (target) channel: ${params.targetChannel}.` : undefined,
|
||||
`Original request: ${params.originalMessage}`,
|
||||
params.roundOneReply
|
||||
? `Round 1 reply: ${params.roundOneReply}`
|
||||
: "Round 1 reply: (not available).",
|
||||
params.latestReply
|
||||
? `Latest reply: ${params.latestReply}`
|
||||
: "Latest reply: (not available).",
|
||||
params.latestReply ? `Latest reply: ${params.latestReply}` : "Latest reply: (not available).",
|
||||
`If you want to remain silent, reply exactly "${ANNOUNCE_SKIP_TOKEN}".`,
|
||||
"Any other reply will be posted to the target channel.",
|
||||
"After this reply, the agent-to-agent conversation is over.",
|
||||
|
||||
@@ -66,9 +66,7 @@ export async function runSessionsSendA2AFlow(params: {
|
||||
let incomingMessage = latestReply;
|
||||
for (let turn = 1; turn <= params.maxPingPongTurns; turn += 1) {
|
||||
const currentRole =
|
||||
currentSessionKey === params.requesterSessionKey
|
||||
? "requester"
|
||||
: "target";
|
||||
currentSessionKey === params.requesterSessionKey ? "requester" : "target";
|
||||
const replyPrompt = buildAgentToAgentReplyContext({
|
||||
requesterSessionKey: params.requesterSessionKey,
|
||||
requesterChannel: params.requesterChannel,
|
||||
@@ -112,12 +110,7 @@ export async function runSessionsSendA2AFlow(params: {
|
||||
timeoutMs: params.announceTimeoutMs,
|
||||
lane: AGENT_LANE_NESTED,
|
||||
});
|
||||
if (
|
||||
announceTarget &&
|
||||
announceReply &&
|
||||
announceReply.trim() &&
|
||||
!isAnnounceSkip(announceReply)
|
||||
) {
|
||||
if (announceTarget && announceReply && announceReply.trim() && !isAnnounceSkip(announceReply)) {
|
||||
try {
|
||||
await callGateway({
|
||||
method: "send",
|
||||
|
||||
@@ -6,8 +6,7 @@ vi.mock("../../gateway/call.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../config/config.js", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("../../config/config.js")>();
|
||||
const actual = await importOriginal<typeof import("../../config/config.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: () =>
|
||||
|
||||
@@ -24,17 +24,12 @@ import {
|
||||
resolveMainSessionAlias,
|
||||
stripToolMessages,
|
||||
} from "./sessions-helpers.js";
|
||||
import {
|
||||
buildAgentToAgentMessageContext,
|
||||
resolvePingPongTurns,
|
||||
} from "./sessions-send-helpers.js";
|
||||
import { buildAgentToAgentMessageContext, resolvePingPongTurns } from "./sessions-send-helpers.js";
|
||||
import { runSessionsSendA2AFlow } from "./sessions-send-tool.a2a.js";
|
||||
|
||||
const SessionsSendToolSchema = Type.Object({
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
label: Type.Optional(
|
||||
Type.String({ minLength: 1, maxLength: SESSION_LABEL_MAX_LENGTH }),
|
||||
),
|
||||
label: Type.Optional(Type.String({ minLength: 1, maxLength: SESSION_LABEL_MAX_LENGTH })),
|
||||
agentId: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),
|
||||
message: Type.String(),
|
||||
timeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
|
||||
@@ -56,8 +51,7 @@ export function createSessionsSendTool(opts?: {
|
||||
const message = readStringParam(params, "message", { required: true });
|
||||
const cfg = loadConfig();
|
||||
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||
const visibility =
|
||||
cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
const visibility = cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
const requesterInternalKey =
|
||||
typeof opts?.agentSessionKey === "string" && opts.agentSessionKey.trim()
|
||||
? resolveInternalSessionKey({
|
||||
@@ -74,9 +68,7 @@ export function createSessionsSendTool(opts?: {
|
||||
|
||||
const routingA2A = cfg.tools?.agentToAgent;
|
||||
const a2aEnabled = routingA2A?.enabled === true;
|
||||
const allowPatterns = Array.isArray(routingA2A?.allow)
|
||||
? routingA2A.allow
|
||||
: [];
|
||||
const allowPatterns = Array.isArray(routingA2A?.allow) ? routingA2A.allow : [];
|
||||
const matchesAllow = (agentId: string) => {
|
||||
if (allowPatterns.length === 0) return true;
|
||||
return allowPatterns.some((pattern) => {
|
||||
@@ -92,8 +84,7 @@ export function createSessionsSendTool(opts?: {
|
||||
|
||||
const sessionKeyParam = readStringParam(params, "sessionKey");
|
||||
const labelParam = readStringParam(params, "label")?.trim() || undefined;
|
||||
const labelAgentIdParam =
|
||||
readStringParam(params, "agentId")?.trim() || undefined;
|
||||
const labelAgentIdParam = readStringParam(params, "agentId")?.trim() || undefined;
|
||||
if (sessionKeyParam && labelParam) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
@@ -114,9 +105,7 @@ export function createSessionsSendTool(opts?: {
|
||||
let sessionKey = sessionKeyParam;
|
||||
if (!sessionKey && labelParam) {
|
||||
const requesterAgentId = requesterInternalKey
|
||||
? normalizeAgentId(
|
||||
parseAgentSessionKey(requesterInternalKey)?.agentId,
|
||||
)
|
||||
? normalizeAgentId(parseAgentSessionKey(requesterInternalKey)?.agentId)
|
||||
: undefined;
|
||||
const requestedAgentId = labelAgentIdParam
|
||||
? normalizeAgentId(labelAgentIdParam)
|
||||
@@ -131,16 +120,11 @@ export function createSessionsSendTool(opts?: {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Sandboxed sessions_send label lookup is limited to this agent",
|
||||
error: "Sandboxed sessions_send label lookup is limited to this agent",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
requesterAgentId &&
|
||||
requestedAgentId &&
|
||||
requestedAgentId !== requesterAgentId
|
||||
) {
|
||||
if (requesterAgentId && requestedAgentId && requestedAgentId !== requesterAgentId) {
|
||||
if (!a2aEnabled) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
@@ -149,15 +133,11 @@ export function createSessionsSendTool(opts?: {
|
||||
"Agent-to-agent messaging is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent sends.",
|
||||
});
|
||||
}
|
||||
if (
|
||||
!matchesAllow(requesterAgentId) ||
|
||||
!matchesAllow(requestedAgentId)
|
||||
) {
|
||||
if (!matchesAllow(requesterAgentId) || !matchesAllow(requestedAgentId)) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Agent-to-agent messaging denied by tools.agentToAgent.allow.",
|
||||
error: "Agent-to-agent messaging denied by tools.agentToAgent.allow.",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -174,8 +154,7 @@ export function createSessionsSendTool(opts?: {
|
||||
params: resolveParams,
|
||||
timeoutMs: 10_000,
|
||||
})) as { key?: unknown };
|
||||
resolvedKey =
|
||||
typeof resolved?.key === "string" ? resolved.key.trim() : "";
|
||||
resolvedKey = typeof resolved?.key === "string" ? resolved.key.trim() : "";
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (restrictToSpawned) {
|
||||
@@ -245,8 +224,7 @@ export function createSessionsSendTool(opts?: {
|
||||
}
|
||||
}
|
||||
const timeoutSeconds =
|
||||
typeof params.timeoutSeconds === "number" &&
|
||||
Number.isFinite(params.timeoutSeconds)
|
||||
typeof params.timeoutSeconds === "number" && Number.isFinite(params.timeoutSeconds)
|
||||
? Math.max(0, Math.floor(params.timeoutSeconds))
|
||||
: 30;
|
||||
const timeoutMs = timeoutSeconds * 1000;
|
||||
@@ -261,9 +239,7 @@ export function createSessionsSendTool(opts?: {
|
||||
const requesterAgentId = normalizeAgentId(
|
||||
parseAgentSessionKey(requesterInternalKey)?.agentId,
|
||||
);
|
||||
const targetAgentId = normalizeAgentId(
|
||||
parseAgentSessionKey(resolvedKey)?.agentId,
|
||||
);
|
||||
const targetAgentId = normalizeAgentId(parseAgentSessionKey(resolvedKey)?.agentId);
|
||||
const isCrossAgent = requesterAgentId !== targetAgentId;
|
||||
if (isCrossAgent) {
|
||||
if (!a2aEnabled) {
|
||||
@@ -279,8 +255,7 @@ export function createSessionsSendTool(opts?: {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Agent-to-agent messaging denied by tools.agentToAgent.allow.",
|
||||
error: "Agent-to-agent messaging denied by tools.agentToAgent.allow.",
|
||||
sessionKey: displayKey,
|
||||
});
|
||||
}
|
||||
@@ -337,11 +312,7 @@ export function createSessionsSendTool(opts?: {
|
||||
});
|
||||
} catch (err) {
|
||||
const messageText =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: typeof err === "string"
|
||||
? err
|
||||
: "error";
|
||||
err instanceof Error ? err.message : typeof err === "string" ? err : "error";
|
||||
return jsonResult({
|
||||
runId,
|
||||
status: "error",
|
||||
@@ -362,11 +333,7 @@ export function createSessionsSendTool(opts?: {
|
||||
}
|
||||
} catch (err) {
|
||||
const messageText =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: typeof err === "string"
|
||||
? err
|
||||
: "error";
|
||||
err instanceof Error ? err.message : typeof err === "string" ? err : "error";
|
||||
return jsonResult({
|
||||
runId,
|
||||
status: "error",
|
||||
@@ -390,11 +357,7 @@ export function createSessionsSendTool(opts?: {
|
||||
waitError = typeof wait?.error === "string" ? wait.error : undefined;
|
||||
} catch (err) {
|
||||
const messageText =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: typeof err === "string"
|
||||
? err
|
||||
: "error";
|
||||
err instanceof Error ? err.message : typeof err === "string" ? err : "error";
|
||||
return jsonResult({
|
||||
runId,
|
||||
status: messageText.includes("gateway timeout") ? "timeout" : "error",
|
||||
@@ -424,11 +387,8 @@ export function createSessionsSendTool(opts?: {
|
||||
method: "chat.history",
|
||||
params: { sessionKey: resolvedKey, limit: 50 },
|
||||
})) as { messages?: unknown[] };
|
||||
const filtered = stripToolMessages(
|
||||
Array.isArray(history?.messages) ? history.messages : [],
|
||||
);
|
||||
const last =
|
||||
filtered.length > 0 ? filtered[filtered.length - 1] : undefined;
|
||||
const filtered = stripToolMessages(Array.isArray(history?.messages) ? history.messages : []);
|
||||
const last = filtered.length > 0 ? filtered[filtered.length - 1] : undefined;
|
||||
const reply = last ? extractAssistantText(last) : undefined;
|
||||
startA2AFlow(reply ?? undefined);
|
||||
|
||||
|
||||
@@ -68,14 +68,12 @@ export function createSessionsSpawnTool(opts?: {
|
||||
: "keep";
|
||||
const runTimeoutSeconds = (() => {
|
||||
const explicit =
|
||||
typeof params.runTimeoutSeconds === "number" &&
|
||||
Number.isFinite(params.runTimeoutSeconds)
|
||||
typeof params.runTimeoutSeconds === "number" && Number.isFinite(params.runTimeoutSeconds)
|
||||
? Math.max(0, Math.floor(params.runTimeoutSeconds))
|
||||
: undefined;
|
||||
if (explicit !== undefined) return explicit;
|
||||
const legacy =
|
||||
typeof params.timeoutSeconds === "number" &&
|
||||
Number.isFinite(params.timeoutSeconds)
|
||||
typeof params.timeoutSeconds === "number" && Number.isFinite(params.timeoutSeconds)
|
||||
? Math.max(0, Math.floor(params.timeoutSeconds))
|
||||
: undefined;
|
||||
return legacy ?? 0;
|
||||
@@ -86,10 +84,7 @@ export function createSessionsSpawnTool(opts?: {
|
||||
const cfg = loadConfig();
|
||||
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||
const requesterSessionKey = opts?.agentSessionKey;
|
||||
if (
|
||||
typeof requesterSessionKey === "string" &&
|
||||
isSubagentSessionKey(requesterSessionKey)
|
||||
) {
|
||||
if (typeof requesterSessionKey === "string" && isSubagentSessionKey(requesterSessionKey)) {
|
||||
return jsonResult({
|
||||
status: "forbidden",
|
||||
error: "sessions_spawn is not allowed from sub-agent sessions",
|
||||
@@ -115,9 +110,7 @@ export function createSessionsSpawnTool(opts?: {
|
||||
? normalizeAgentId(requestedAgentId)
|
||||
: requesterAgentId;
|
||||
if (targetAgentId !== requesterAgentId) {
|
||||
const allowAgents =
|
||||
resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ??
|
||||
[];
|
||||
const allowAgents = resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ?? [];
|
||||
const allowAny = allowAgents.some((value) => value.trim() === "*");
|
||||
const normalizedTargetId = targetAgentId.toLowerCase();
|
||||
const allowSet = new Set(
|
||||
@@ -154,14 +147,9 @@ export function createSessionsSpawnTool(opts?: {
|
||||
modelApplied = true;
|
||||
} catch (err) {
|
||||
const messageText =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: typeof err === "string"
|
||||
? err
|
||||
: "error";
|
||||
err instanceof Error ? err.message : typeof err === "string" ? err : "error";
|
||||
const recoverable =
|
||||
messageText.includes("invalid model") ||
|
||||
messageText.includes("model not allowed");
|
||||
messageText.includes("invalid model") || messageText.includes("model not allowed");
|
||||
if (!recoverable) {
|
||||
return jsonResult({
|
||||
status: "error",
|
||||
@@ -204,11 +192,7 @@ export function createSessionsSpawnTool(opts?: {
|
||||
}
|
||||
} catch (err) {
|
||||
const messageText =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: typeof err === "string"
|
||||
? err
|
||||
: "error";
|
||||
err instanceof Error ? err.message : typeof err === "string" ? err : "error";
|
||||
return jsonResult({
|
||||
status: "error",
|
||||
error: messageText,
|
||||
|
||||
@@ -27,8 +27,7 @@ vi.mock("../../slack/actions.js", () => ({
|
||||
pinSlackMessage: (...args: unknown[]) => pinSlackMessage(...args),
|
||||
reactSlackMessage: (...args: unknown[]) => reactSlackMessage(...args),
|
||||
readSlackMessages: (...args: unknown[]) => readSlackMessages(...args),
|
||||
removeOwnSlackReactions: (...args: unknown[]) =>
|
||||
removeOwnSlackReactions(...args),
|
||||
removeOwnSlackReactions: (...args: unknown[]) => removeOwnSlackReactions(...args),
|
||||
removeSlackReaction: (...args: unknown[]) => removeSlackReaction(...args),
|
||||
sendSlackMessage: (...args: unknown[]) => sendSlackMessage(...args),
|
||||
unpinSlackMessage: (...args: unknown[]) => unpinSlackMessage(...args),
|
||||
@@ -122,14 +121,10 @@ describe("handleSlackAction", () => {
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith(
|
||||
"channel:C123",
|
||||
"Hello thread",
|
||||
{
|
||||
mediaUrl: undefined,
|
||||
threadTs: "1234567890.123456",
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "Hello thread", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: "1234567890.123456",
|
||||
});
|
||||
});
|
||||
|
||||
it("auto-injects threadTs from context when replyToMode=all", async () => {
|
||||
@@ -148,14 +143,10 @@ describe("handleSlackAction", () => {
|
||||
replyToMode: "all",
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith(
|
||||
"channel:C123",
|
||||
"Auto-threaded",
|
||||
{
|
||||
mediaUrl: undefined,
|
||||
threadTs: "1111111111.111111",
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "Auto-threaded", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: "1111111111.111111",
|
||||
});
|
||||
});
|
||||
|
||||
it("replyToMode=first threads first message then stops", async () => {
|
||||
@@ -187,14 +178,10 @@ describe("handleSlackAction", () => {
|
||||
cfg,
|
||||
context,
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenLastCalledWith(
|
||||
"channel:C123",
|
||||
"Second",
|
||||
{
|
||||
mediaUrl: undefined,
|
||||
threadTs: undefined,
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenLastCalledWith("channel:C123", "Second", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("replyToMode=first marks hasRepliedRef even when threadTs is explicit", async () => {
|
||||
@@ -218,14 +205,10 @@ describe("handleSlackAction", () => {
|
||||
cfg,
|
||||
context,
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenLastCalledWith(
|
||||
"channel:C123",
|
||||
"Explicit",
|
||||
{
|
||||
mediaUrl: undefined,
|
||||
threadTs: "2222222222.222222",
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenLastCalledWith("channel:C123", "Explicit", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: "2222222222.222222",
|
||||
});
|
||||
expect(hasRepliedRef.value).toBe(true);
|
||||
|
||||
await handleSlackAction(
|
||||
@@ -233,29 +216,21 @@ describe("handleSlackAction", () => {
|
||||
cfg,
|
||||
context,
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenLastCalledWith(
|
||||
"channel:C123",
|
||||
"Second",
|
||||
{
|
||||
mediaUrl: undefined,
|
||||
threadTs: undefined,
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenLastCalledWith("channel:C123", "Second", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("replyToMode=first without hasRepliedRef does not thread", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
sendSlackMessage.mockClear();
|
||||
await handleSlackAction(
|
||||
{ action: "sendMessage", to: "channel:C123", content: "No ref" },
|
||||
cfg,
|
||||
{
|
||||
currentChannelId: "C123",
|
||||
currentThreadTs: "1111111111.111111",
|
||||
replyToMode: "first",
|
||||
// no hasRepliedRef
|
||||
},
|
||||
);
|
||||
await handleSlackAction({ action: "sendMessage", to: "channel:C123", content: "No ref" }, cfg, {
|
||||
currentChannelId: "C123",
|
||||
currentThreadTs: "1111111111.111111",
|
||||
replyToMode: "first",
|
||||
// no hasRepliedRef
|
||||
});
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "No ref", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: undefined,
|
||||
@@ -300,14 +275,10 @@ describe("handleSlackAction", () => {
|
||||
replyToMode: "all",
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith(
|
||||
"channel:C999",
|
||||
"Different channel",
|
||||
{
|
||||
mediaUrl: undefined,
|
||||
threadTs: undefined,
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("channel:C999", "Different channel", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("explicit threadTs overrides context threadTs", async () => {
|
||||
@@ -327,14 +298,10 @@ describe("handleSlackAction", () => {
|
||||
replyToMode: "all",
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith(
|
||||
"channel:C123",
|
||||
"Explicit thread",
|
||||
{
|
||||
mediaUrl: undefined,
|
||||
threadTs: "2222222222.222222",
|
||||
},
|
||||
);
|
||||
expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "Explicit thread", {
|
||||
mediaUrl: undefined,
|
||||
threadTs: "2222222222.222222",
|
||||
});
|
||||
});
|
||||
|
||||
it("handles channel target without prefix when replyToMode=all", async () => {
|
||||
|
||||
@@ -17,19 +17,9 @@ import {
|
||||
sendSlackMessage,
|
||||
unpinSlackMessage,
|
||||
} from "../../slack/actions.js";
|
||||
import {
|
||||
createActionGate,
|
||||
jsonResult,
|
||||
readReactionParams,
|
||||
readStringParam,
|
||||
} from "./common.js";
|
||||
import { createActionGate, jsonResult, readReactionParams, readStringParam } from "./common.js";
|
||||
|
||||
const messagingActions = new Set([
|
||||
"sendMessage",
|
||||
"editMessage",
|
||||
"deleteMessage",
|
||||
"readMessages",
|
||||
]);
|
||||
const messagingActions = new Set(["sendMessage", "editMessage", "deleteMessage", "readMessages"]);
|
||||
|
||||
const reactionsActions = new Set(["react", "reactions"]);
|
||||
const pinActions = new Set(["pinMessage", "unpinMessage", "listPins"]);
|
||||
@@ -73,11 +63,7 @@ function resolveThreadTsFromContext(
|
||||
if (context.replyToMode === "all") {
|
||||
return context.currentThreadTs;
|
||||
}
|
||||
if (
|
||||
context.replyToMode === "first" &&
|
||||
context.hasRepliedRef &&
|
||||
!context.hasRepliedRef.value
|
||||
) {
|
||||
if (context.replyToMode === "first" && context.hasRepliedRef && !context.hasRepliedRef.value) {
|
||||
context.hasRepliedRef.value = true;
|
||||
return context.currentThreadTs;
|
||||
}
|
||||
@@ -157,9 +143,7 @@ export async function handleSlackAction(
|
||||
// threadTs: once we send a message to the current channel, consider the
|
||||
// first reply "used" so later tool calls don't auto-thread again.
|
||||
if (context?.hasRepliedRef && context.currentChannelId) {
|
||||
const normalizedTarget = to.startsWith("channel:")
|
||||
? to.slice("channel:".length)
|
||||
: to;
|
||||
const normalizedTarget = to.startsWith("channel:") ? to.slice("channel:".length) : to;
|
||||
if (normalizedTarget === context.currentChannelId) {
|
||||
context.hasRepliedRef.value = true;
|
||||
}
|
||||
@@ -204,9 +188,7 @@ export async function handleSlackAction(
|
||||
});
|
||||
const limitRaw = params.limit;
|
||||
const limit =
|
||||
typeof limitRaw === "number" && Number.isFinite(limitRaw)
|
||||
? limitRaw
|
||||
: undefined;
|
||||
typeof limitRaw === "number" && Number.isFinite(limitRaw) ? limitRaw : undefined;
|
||||
const before = readStringParam(params, "before");
|
||||
const after = readStringParam(params, "after");
|
||||
const result = await readSlackMessages(channelId, {
|
||||
@@ -270,9 +252,7 @@ export async function handleSlackAction(
|
||||
if (!isActionEnabled("emojiList")) {
|
||||
throw new Error("Slack emoji list is disabled.");
|
||||
}
|
||||
const emojis = accountOpts
|
||||
? await listSlackEmojis(accountOpts)
|
||||
: await listSlackEmojis();
|
||||
const emojis = accountOpts ? await listSlackEmojis(accountOpts) : await listSlackEmojis();
|
||||
return jsonResult({ ok: true, emojis });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import {
|
||||
handleTelegramAction,
|
||||
readTelegramButtons,
|
||||
} from "./telegram-actions.js";
|
||||
import { handleTelegramAction, readTelegramButtons } from "./telegram-actions.js";
|
||||
|
||||
const reactMessageTelegram = vi.fn(async () => ({ ok: true }));
|
||||
const sendMessageTelegram = vi.fn(async () => ({
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import { resolveChannelCapabilities } from "../../config/channel-capabilities.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import {
|
||||
reactMessageTelegram,
|
||||
sendMessageTelegram,
|
||||
} from "../../telegram/send.js";
|
||||
import { reactMessageTelegram, sendMessageTelegram } from "../../telegram/send.js";
|
||||
import { resolveTelegramToken } from "../../telegram/token.js";
|
||||
import {
|
||||
createActionGate,
|
||||
@@ -47,23 +44,18 @@ export function readTelegramButtons(
|
||||
}
|
||||
return row.map((button, buttonIndex) => {
|
||||
if (!button || typeof button !== "object") {
|
||||
throw new Error(
|
||||
`buttons[${rowIndex}][${buttonIndex}] must be an object`,
|
||||
);
|
||||
throw new Error(`buttons[${rowIndex}][${buttonIndex}] must be an object`);
|
||||
}
|
||||
const text =
|
||||
typeof (button as { text?: unknown }).text === "string"
|
||||
? (button as { text: string }).text.trim()
|
||||
: "";
|
||||
const callbackData =
|
||||
typeof (button as { callback_data?: unknown }).callback_data ===
|
||||
"string"
|
||||
typeof (button as { callback_data?: unknown }).callback_data === "string"
|
||||
? (button as { callback_data: string }).callback_data.trim()
|
||||
: "";
|
||||
if (!text || !callbackData) {
|
||||
throw new Error(
|
||||
`buttons[${rowIndex}][${buttonIndex}] requires text and callback_data`,
|
||||
);
|
||||
throw new Error(`buttons[${rowIndex}][${buttonIndex}] requires text and callback_data`);
|
||||
}
|
||||
if (callbackData.length > 64) {
|
||||
throw new Error(
|
||||
@@ -124,10 +116,7 @@ export async function handleTelegramAction(
|
||||
const content = readStringParam(params, "content", { required: true });
|
||||
const mediaUrl = readStringParam(params, "mediaUrl");
|
||||
const buttons = readTelegramButtons(params);
|
||||
if (
|
||||
buttons &&
|
||||
!hasInlineButtonsCapability({ cfg, accountId: accountId ?? undefined })
|
||||
) {
|
||||
if (buttons && !hasInlineButtonsCapability({ cfg, accountId: accountId ?? undefined })) {
|
||||
throw new Error(
|
||||
'Telegram inline buttons requested but not enabled. Add "inlineButtons" to channels.telegram.capabilities (or channels.telegram.accounts.<id>.capabilities).',
|
||||
);
|
||||
|
||||
@@ -24,17 +24,12 @@ describe("handleWhatsAppAction", () => {
|
||||
},
|
||||
enabledConfig,
|
||||
);
|
||||
expect(sendReactionWhatsApp).toHaveBeenCalledWith(
|
||||
"123@s.whatsapp.net",
|
||||
"msg1",
|
||||
"✅",
|
||||
{
|
||||
verbose: false,
|
||||
fromMe: undefined,
|
||||
participant: undefined,
|
||||
accountId: undefined,
|
||||
},
|
||||
);
|
||||
expect(sendReactionWhatsApp).toHaveBeenCalledWith("123@s.whatsapp.net", "msg1", "✅", {
|
||||
verbose: false,
|
||||
fromMe: undefined,
|
||||
participant: undefined,
|
||||
accountId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("removes reactions on empty emoji", async () => {
|
||||
@@ -47,17 +42,12 @@ describe("handleWhatsAppAction", () => {
|
||||
},
|
||||
enabledConfig,
|
||||
);
|
||||
expect(sendReactionWhatsApp).toHaveBeenCalledWith(
|
||||
"123@s.whatsapp.net",
|
||||
"msg1",
|
||||
"",
|
||||
{
|
||||
verbose: false,
|
||||
fromMe: undefined,
|
||||
participant: undefined,
|
||||
accountId: undefined,
|
||||
},
|
||||
);
|
||||
expect(sendReactionWhatsApp).toHaveBeenCalledWith("123@s.whatsapp.net", "msg1", "", {
|
||||
verbose: false,
|
||||
fromMe: undefined,
|
||||
participant: undefined,
|
||||
accountId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("removes reactions when remove flag set", async () => {
|
||||
@@ -71,17 +61,12 @@ describe("handleWhatsAppAction", () => {
|
||||
},
|
||||
enabledConfig,
|
||||
);
|
||||
expect(sendReactionWhatsApp).toHaveBeenCalledWith(
|
||||
"123@s.whatsapp.net",
|
||||
"msg1",
|
||||
"",
|
||||
{
|
||||
verbose: false,
|
||||
fromMe: undefined,
|
||||
participant: undefined,
|
||||
accountId: undefined,
|
||||
},
|
||||
);
|
||||
expect(sendReactionWhatsApp).toHaveBeenCalledWith("123@s.whatsapp.net", "msg1", "", {
|
||||
verbose: false,
|
||||
fromMe: undefined,
|
||||
participant: undefined,
|
||||
accountId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("passes account scope and sender flags", async () => {
|
||||
@@ -97,17 +82,12 @@ describe("handleWhatsAppAction", () => {
|
||||
},
|
||||
enabledConfig,
|
||||
);
|
||||
expect(sendReactionWhatsApp).toHaveBeenCalledWith(
|
||||
"123@s.whatsapp.net",
|
||||
"msg1",
|
||||
"🎉",
|
||||
{
|
||||
verbose: false,
|
||||
fromMe: true,
|
||||
participant: "999@s.whatsapp.net",
|
||||
accountId: "work",
|
||||
},
|
||||
);
|
||||
expect(sendReactionWhatsApp).toHaveBeenCalledWith("123@s.whatsapp.net", "msg1", "🎉", {
|
||||
verbose: false,
|
||||
fromMe: true,
|
||||
participant: "999@s.whatsapp.net",
|
||||
accountId: "work",
|
||||
});
|
||||
});
|
||||
|
||||
it("respects reaction gating", async () => {
|
||||
|
||||
@@ -2,12 +2,7 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { sendReactionWhatsApp } from "../../web/outbound.js";
|
||||
import {
|
||||
createActionGate,
|
||||
jsonResult,
|
||||
readReactionParams,
|
||||
readStringParam,
|
||||
} from "./common.js";
|
||||
import { createActionGate, jsonResult, readReactionParams, readStringParam } from "./common.js";
|
||||
|
||||
export async function handleWhatsAppAction(
|
||||
params: Record<string, unknown>,
|
||||
|
||||
Reference in New Issue
Block a user