fix: stabilize ci checks
This commit is contained in:
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
src/canvas-host/a2ui/a2ui.bundle.js
|
||||
@@ -424,14 +424,18 @@ public struct PollParams: Codable, Sendable {
|
||||
|
||||
public struct AgentParams: Codable, Sendable {
|
||||
public let message: String
|
||||
public let agentid: String?
|
||||
public let to: String?
|
||||
public let replyto: String?
|
||||
public let sessionid: String?
|
||||
public let sessionkey: String?
|
||||
public let thinking: String?
|
||||
public let deliver: Bool?
|
||||
public let attachments: [AnyCodable]?
|
||||
public let channel: String?
|
||||
public let replychannel: String?
|
||||
public let accountid: String?
|
||||
public let replyaccountid: String?
|
||||
public let timeout: Int?
|
||||
public let lane: String?
|
||||
public let extrasystemprompt: String?
|
||||
@@ -441,14 +445,18 @@ public struct AgentParams: Codable, Sendable {
|
||||
|
||||
public init(
|
||||
message: String,
|
||||
agentid: String?,
|
||||
to: String?,
|
||||
replyto: String?,
|
||||
sessionid: String?,
|
||||
sessionkey: String?,
|
||||
thinking: String?,
|
||||
deliver: Bool?,
|
||||
attachments: [AnyCodable]?,
|
||||
channel: String?,
|
||||
replychannel: String?,
|
||||
accountid: String?,
|
||||
replyaccountid: String?,
|
||||
timeout: Int?,
|
||||
lane: String?,
|
||||
extrasystemprompt: String?,
|
||||
@@ -457,14 +465,18 @@ public struct AgentParams: Codable, Sendable {
|
||||
spawnedby: String?
|
||||
) {
|
||||
self.message = message
|
||||
self.agentid = agentid
|
||||
self.to = to
|
||||
self.replyto = replyto
|
||||
self.sessionid = sessionid
|
||||
self.sessionkey = sessionkey
|
||||
self.thinking = thinking
|
||||
self.deliver = deliver
|
||||
self.attachments = attachments
|
||||
self.channel = channel
|
||||
self.replychannel = replychannel
|
||||
self.accountid = accountid
|
||||
self.replyaccountid = replyaccountid
|
||||
self.timeout = timeout
|
||||
self.lane = lane
|
||||
self.extrasystemprompt = extrasystemprompt
|
||||
@@ -474,14 +486,18 @@ public struct AgentParams: Codable, Sendable {
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case message
|
||||
case agentid = "agentId"
|
||||
case to
|
||||
case replyto = "replyTo"
|
||||
case sessionid = "sessionId"
|
||||
case sessionkey = "sessionKey"
|
||||
case thinking
|
||||
case deliver
|
||||
case attachments
|
||||
case channel
|
||||
case replychannel = "replyChannel"
|
||||
case accountid = "accountId"
|
||||
case replyaccountid = "replyAccountId"
|
||||
case timeout
|
||||
case lane
|
||||
case extrasystemprompt = "extraSystemPrompt"
|
||||
|
||||
@@ -22,8 +22,7 @@ import { getApiKeyForModel } from "./model-auth.js";
|
||||
import { normalizeProviderId, parseModelRef } from "./model-selection.js";
|
||||
import { ensureClawdbotModelsJson } from "./models-config.js";
|
||||
|
||||
const LIVE =
|
||||
isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
|
||||
const LIVE = isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
|
||||
const SETUP_TOKEN_RAW = process.env.CLAWDBOT_LIVE_SETUP_TOKEN?.trim() ?? "";
|
||||
const SETUP_TOKEN_VALUE = process.env.CLAWDBOT_LIVE_SETUP_TOKEN_VALUE?.trim() ?? "";
|
||||
const SETUP_TOKEN_PROFILE = process.env.CLAWDBOT_LIVE_SETUP_TOKEN_PROFILE?.trim() ?? "";
|
||||
|
||||
@@ -22,11 +22,7 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
|
||||
import { createClawdbotTools } from "./clawdbot-tools.js";
|
||||
|
||||
const waitForCalls = async (
|
||||
getCount: () => number,
|
||||
count: number,
|
||||
timeoutMs = 2000,
|
||||
) => {
|
||||
const waitForCalls = async (getCount: () => number, count: number, timeoutMs = 2000) => {
|
||||
const start = Date.now();
|
||||
while (getCount() < count) {
|
||||
if (Date.now() - start > timeoutMs) {
|
||||
@@ -254,18 +250,9 @@ describe("sessions tools", () => {
|
||||
runId: "run-1",
|
||||
delivery: { status: "pending", mode: "announce" },
|
||||
});
|
||||
await waitForCalls(
|
||||
() => calls.filter((call) => call.method === "agent").length,
|
||||
4,
|
||||
);
|
||||
await waitForCalls(
|
||||
() => calls.filter((call) => call.method === "agent.wait").length,
|
||||
4,
|
||||
);
|
||||
await waitForCalls(
|
||||
() => calls.filter((call) => call.method === "chat.history").length,
|
||||
4,
|
||||
);
|
||||
await waitForCalls(() => calls.filter((call) => call.method === "agent").length, 4);
|
||||
await waitForCalls(() => calls.filter((call) => call.method === "agent.wait").length, 4);
|
||||
await waitForCalls(() => calls.filter((call) => call.method === "chat.history").length, 4);
|
||||
|
||||
const waitPromise = tool.execute("call6", {
|
||||
sessionKey: "main",
|
||||
@@ -279,18 +266,9 @@ describe("sessions tools", () => {
|
||||
delivery: { status: "pending", mode: "announce" },
|
||||
});
|
||||
expect(typeof (waited.details as { runId?: string }).runId).toBe("string");
|
||||
await waitForCalls(
|
||||
() => calls.filter((call) => call.method === "agent").length,
|
||||
8,
|
||||
);
|
||||
await waitForCalls(
|
||||
() => calls.filter((call) => call.method === "agent.wait").length,
|
||||
8,
|
||||
);
|
||||
await waitForCalls(
|
||||
() => calls.filter((call) => call.method === "chat.history").length,
|
||||
8,
|
||||
);
|
||||
await waitForCalls(() => calls.filter((call) => call.method === "agent").length, 8);
|
||||
await waitForCalls(() => calls.filter((call) => call.method === "agent.wait").length, 8);
|
||||
await waitForCalls(() => calls.filter((call) => call.method === "chat.history").length, 8);
|
||||
|
||||
const agentCalls = calls.filter((call) => call.method === "agent");
|
||||
const waitCalls = calls.filter((call) => call.method === "agent.wait");
|
||||
|
||||
@@ -3,8 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
|
||||
const GEMINI_KEY = process.env.GEMINI_API_KEY ?? "";
|
||||
const LIVE =
|
||||
isTruthyEnvValue(process.env.GEMINI_LIVE_TEST) || isTruthyEnvValue(process.env.LIVE);
|
||||
const LIVE = isTruthyEnvValue(process.env.GEMINI_LIVE_TEST) || isTruthyEnvValue(process.env.LIVE);
|
||||
|
||||
const describeLive = LIVE && GEMINI_KEY ? describe : describe.skip;
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ import { isTruthyEnvValue } from "../infra/env.js";
|
||||
const MINIMAX_KEY = process.env.MINIMAX_API_KEY ?? "";
|
||||
const MINIMAX_BASE_URL = process.env.MINIMAX_BASE_URL?.trim() || "https://api.minimax.io/anthropic";
|
||||
const MINIMAX_MODEL = process.env.MINIMAX_MODEL?.trim() || "MiniMax-M2.1";
|
||||
const LIVE =
|
||||
isTruthyEnvValue(process.env.MINIMAX_LIVE_TEST) || isTruthyEnvValue(process.env.LIVE);
|
||||
const LIVE = isTruthyEnvValue(process.env.MINIMAX_LIVE_TEST) || isTruthyEnvValue(process.env.LIVE);
|
||||
|
||||
const describeLive = LIVE && MINIMAX_KEY ? describe : describe.skip;
|
||||
|
||||
|
||||
@@ -15,8 +15,7 @@ import { getApiKeyForModel } from "./model-auth.js";
|
||||
import { ensureClawdbotModelsJson } from "./models-config.js";
|
||||
import { isRateLimitErrorMessage } from "./pi-embedded-helpers/errors.js";
|
||||
|
||||
const LIVE =
|
||||
isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
|
||||
const LIVE = isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
|
||||
const DIRECT_ENABLED = Boolean(process.env.CLAWDBOT_LIVE_MODELS?.trim());
|
||||
const REQUIRE_PROFILE_KEYS = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_REQUIRE_PROFILE_KEYS);
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { applyExtraParamsToAgent } from "./pi-embedded-runner.js";
|
||||
|
||||
const OPENAI_KEY = process.env.OPENAI_API_KEY ?? "";
|
||||
const LIVE =
|
||||
isTruthyEnvValue(process.env.OPENAI_LIVE_TEST) || isTruthyEnvValue(process.env.LIVE);
|
||||
const LIVE = isTruthyEnvValue(process.env.OPENAI_LIVE_TEST) || isTruthyEnvValue(process.env.LIVE);
|
||||
|
||||
const describeLive = LIVE && OPENAI_KEY ? describe : describe.skip;
|
||||
|
||||
|
||||
@@ -18,10 +18,7 @@ import {
|
||||
updateSessionStore,
|
||||
} from "../../config/sessions.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import {
|
||||
emitAgentEvent,
|
||||
registerAgentRunContext,
|
||||
} from "../../infra/agent-events.js";
|
||||
import { emitAgentEvent, registerAgentRunContext } from "../../infra/agent-events.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import {
|
||||
isMarkdownCapableMessageChannel,
|
||||
@@ -32,20 +29,11 @@ import type { TemplateContext } from "../templating.js";
|
||||
import type { VerboseLevel } from "../thinking.js";
|
||||
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import {
|
||||
buildThreadingToolContext,
|
||||
resolveEnforceFinalTag,
|
||||
} from "./agent-runner-utils.js";
|
||||
import {
|
||||
createBlockReplyPayloadKey,
|
||||
type BlockReplyPipeline,
|
||||
} from "./block-reply-pipeline.js";
|
||||
import { buildThreadingToolContext, resolveEnforceFinalTag } from "./agent-runner-utils.js";
|
||||
import { createBlockReplyPayloadKey, type BlockReplyPipeline } from "./block-reply-pipeline.js";
|
||||
import type { FollowupRun } from "./queue.js";
|
||||
import { parseReplyDirectives } from "./reply-directives.js";
|
||||
import {
|
||||
applyReplyTagsToPayload,
|
||||
isRenderablePayload,
|
||||
} from "./reply-payloads.js";
|
||||
import { applyReplyTagsToPayload, isRenderablePayload } from "./reply-payloads.js";
|
||||
import type { TypingSignaler } from "./typing-mode.js";
|
||||
|
||||
export type AgentRunLoopResult =
|
||||
@@ -108,12 +96,9 @@ export async function runAgentTurnWithFallback(params: {
|
||||
while (true) {
|
||||
try {
|
||||
const allowPartialStream = !(
|
||||
params.followupRun.run.reasoningLevel === "stream" &&
|
||||
params.opts?.onReasoningStream
|
||||
params.followupRun.run.reasoningLevel === "stream" && params.opts?.onReasoningStream
|
||||
);
|
||||
const normalizeStreamingText = (
|
||||
payload: ReplyPayload,
|
||||
): { text?: string; skip: boolean } => {
|
||||
const normalizeStreamingText = (payload: ReplyPayload): { text?: string; skip: boolean } => {
|
||||
if (!allowPartialStream) return { skip: true };
|
||||
let text = payload.text;
|
||||
if (!params.isHeartbeat && text?.includes("HEARTBEAT_OK")) {
|
||||
@@ -137,9 +122,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
if (!sanitized.trim()) return { skip: true };
|
||||
return { text: sanitized, skip: false };
|
||||
};
|
||||
const handlePartialForTyping = async (
|
||||
payload: ReplyPayload,
|
||||
): Promise<string | undefined> => {
|
||||
const handlePartialForTyping = async (payload: ReplyPayload): Promise<string | undefined> => {
|
||||
const { text, skip } = normalizeStreamingText(payload);
|
||||
if (skip || !text) return undefined;
|
||||
await params.typingSignals.signalTextDelta(text);
|
||||
@@ -174,10 +157,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
startedAt,
|
||||
},
|
||||
});
|
||||
const cliSessionId = getCliSessionId(
|
||||
params.getActiveSessionEntry(),
|
||||
provider,
|
||||
);
|
||||
const cliSessionId = getCliSessionId(params.getActiveSessionEntry(), provider);
|
||||
return runCliAgent({
|
||||
sessionId: params.followupRun.run.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
@@ -227,8 +207,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
return runEmbeddedPiAgent({
|
||||
sessionId: params.followupRun.run.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
messageProvider:
|
||||
params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
||||
messageProvider: params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
||||
agentAccountId: params.sessionCtx.AccountId,
|
||||
// Provider threading context for tool auto-injection
|
||||
...buildThreadingToolContext({
|
||||
@@ -244,10 +223,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
prompt: params.commandBody,
|
||||
extraSystemPrompt: params.followupRun.run.extraSystemPrompt,
|
||||
ownerNumbers: params.followupRun.run.ownerNumbers,
|
||||
enforceFinalTag: resolveEnforceFinalTag(
|
||||
params.followupRun.run,
|
||||
provider,
|
||||
),
|
||||
enforceFinalTag: resolveEnforceFinalTag(params.followupRun.run, provider),
|
||||
provider,
|
||||
model,
|
||||
authProfileId,
|
||||
@@ -264,9 +240,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
params.sessionCtx.Provider,
|
||||
);
|
||||
if (!channel) return "markdown";
|
||||
return isMarkdownCapableMessageChannel(channel)
|
||||
? "markdown"
|
||||
: "plain";
|
||||
return isMarkdownCapableMessageChannel(channel) ? "markdown" : "plain";
|
||||
})(),
|
||||
bashElevated: params.followupRun.run.bashElevated,
|
||||
timeoutMs: params.followupRun.run.timeoutMs,
|
||||
@@ -276,11 +250,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
onPartialReply: allowPartialStream
|
||||
? async (payload) => {
|
||||
const textForTyping = await handlePartialForTyping(payload);
|
||||
if (
|
||||
!params.opts?.onPartialReply ||
|
||||
textForTyping === undefined
|
||||
)
|
||||
return;
|
||||
if (!params.opts?.onPartialReply || textForTyping === undefined) return;
|
||||
await params.opts.onPartialReply({
|
||||
text: textForTyping,
|
||||
mediaUrls: payload.mediaUrls,
|
||||
@@ -291,8 +261,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
await params.typingSignals.signalMessageStart();
|
||||
},
|
||||
onReasoningStream:
|
||||
params.typingSignals.shouldStartOnReasoning ||
|
||||
params.opts?.onReasoningStream
|
||||
params.typingSignals.shouldStartOnReasoning || params.opts?.onReasoningStream
|
||||
? async (payload) => {
|
||||
await params.typingSignals.signalReasoningDelta();
|
||||
await params.opts?.onReasoningStream?.({
|
||||
@@ -305,16 +274,14 @@ export async function runAgentTurnWithFallback(params: {
|
||||
// Trigger typing when tools start executing.
|
||||
// Must await to ensure typing indicator starts before tool summaries are emitted.
|
||||
if (evt.stream === "tool") {
|
||||
const phase =
|
||||
typeof evt.data.phase === "string" ? evt.data.phase : "";
|
||||
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
|
||||
if (phase === "start" || phase === "update") {
|
||||
await params.typingSignals.signalToolStart();
|
||||
}
|
||||
}
|
||||
// Track auto-compaction completion
|
||||
if (evt.stream === "compaction") {
|
||||
const phase =
|
||||
typeof evt.data.phase === "string" ? evt.data.phase : "";
|
||||
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
|
||||
const willRetry = Boolean(evt.data.willRetry);
|
||||
if (phase === "end" && !willRetry) {
|
||||
autoCompactionCompleted = true;
|
||||
@@ -338,22 +305,14 @@ export async function runAgentTurnWithFallback(params: {
|
||||
params.sessionCtx.MessageSid,
|
||||
);
|
||||
// Let through payloads with audioAsVoice flag even if empty (need to track it)
|
||||
if (
|
||||
!isRenderablePayload(taggedPayload) &&
|
||||
!payload.audioAsVoice
|
||||
)
|
||||
return;
|
||||
const parsed = parseReplyDirectives(
|
||||
taggedPayload.text ?? "",
|
||||
{
|
||||
currentMessageId: params.sessionCtx.MessageSid,
|
||||
silentToken: SILENT_REPLY_TOKEN,
|
||||
},
|
||||
);
|
||||
if (!isRenderablePayload(taggedPayload) && !payload.audioAsVoice) return;
|
||||
const parsed = parseReplyDirectives(taggedPayload.text ?? "", {
|
||||
currentMessageId: params.sessionCtx.MessageSid,
|
||||
silentToken: SILENT_REPLY_TOKEN,
|
||||
});
|
||||
const cleaned = parsed.text || undefined;
|
||||
const hasRenderableMedia =
|
||||
Boolean(taggedPayload.mediaUrl) ||
|
||||
(taggedPayload.mediaUrls?.length ?? 0) > 0;
|
||||
Boolean(taggedPayload.mediaUrl) || (taggedPayload.mediaUrls?.length ?? 0) > 0;
|
||||
// Skip empty payloads unless they have audioAsVoice flag (need to track it)
|
||||
if (
|
||||
!cleaned &&
|
||||
@@ -367,35 +326,25 @@ export async function runAgentTurnWithFallback(params: {
|
||||
const blockPayload: ReplyPayload = params.applyReplyToMode({
|
||||
...taggedPayload,
|
||||
text: cleaned,
|
||||
audioAsVoice: Boolean(
|
||||
parsed.audioAsVoice || payload.audioAsVoice,
|
||||
),
|
||||
audioAsVoice: Boolean(parsed.audioAsVoice || payload.audioAsVoice),
|
||||
replyToId: taggedPayload.replyToId ?? parsed.replyToId,
|
||||
replyToTag: taggedPayload.replyToTag || parsed.replyToTag,
|
||||
replyToCurrent:
|
||||
taggedPayload.replyToCurrent || parsed.replyToCurrent,
|
||||
replyToCurrent: taggedPayload.replyToCurrent || parsed.replyToCurrent,
|
||||
});
|
||||
|
||||
void params.typingSignals
|
||||
.signalTextDelta(cleaned ?? taggedPayload.text)
|
||||
.catch((err) => {
|
||||
logVerbose(
|
||||
`block reply typing signal failed: ${String(err)}`,
|
||||
);
|
||||
logVerbose(`block reply typing signal failed: ${String(err)}`);
|
||||
});
|
||||
|
||||
// Use pipeline if available (block streaming enabled), otherwise send directly
|
||||
if (
|
||||
params.blockStreamingEnabled &&
|
||||
params.blockReplyPipeline
|
||||
) {
|
||||
if (params.blockStreamingEnabled && params.blockReplyPipeline) {
|
||||
params.blockReplyPipeline.enqueue(blockPayload);
|
||||
} else {
|
||||
// Send directly when flushing before tool execution (no streaming).
|
||||
// Track sent key to avoid duplicate in final payloads.
|
||||
directlySentBlockKeys.add(
|
||||
createBlockReplyPayloadKey(blockPayload),
|
||||
);
|
||||
directlySentBlockKeys.add(createBlockReplyPayloadKey(blockPayload));
|
||||
await params.opts?.onBlockReply?.(blockPayload);
|
||||
}
|
||||
}
|
||||
@@ -456,9 +405,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
};
|
||||
}
|
||||
if (embeddedError?.kind === "role_ordering") {
|
||||
const didReset = await params.resetSessionAfterRoleOrderingConflict(
|
||||
embeddedError.message,
|
||||
);
|
||||
const didReset = await params.resetSessionAfterRoleOrderingConflict(embeddedError.message);
|
||||
if (didReset) {
|
||||
return {
|
||||
kind: "final",
|
||||
@@ -476,10 +423,8 @@ export async function runAgentTurnWithFallback(params: {
|
||||
isContextOverflowError(message) ||
|
||||
/context.*overflow|too large|context window/i.test(message);
|
||||
const isCompactionFailure = isCompactionFailureError(message);
|
||||
const isSessionCorruption =
|
||||
/function call turn comes immediately after/i.test(message);
|
||||
const isRoleOrderingError =
|
||||
/incorrect role information|roles must alternate/i.test(message);
|
||||
const isSessionCorruption = /function call turn comes immediately after/i.test(message);
|
||||
const isRoleOrderingError = /incorrect role information|roles must alternate/i.test(message);
|
||||
|
||||
if (
|
||||
isCompactionFailure &&
|
||||
@@ -495,8 +440,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
};
|
||||
}
|
||||
if (isRoleOrderingError) {
|
||||
const didReset =
|
||||
await params.resetSessionAfterRoleOrderingConflict(message);
|
||||
const didReset = await params.resetSessionAfterRoleOrderingConflict(message);
|
||||
if (didReset) {
|
||||
return {
|
||||
kind: "final",
|
||||
@@ -523,8 +467,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
try {
|
||||
// Delete transcript file if it exists
|
||||
if (corruptedSessionId) {
|
||||
const transcriptPath =
|
||||
resolveSessionTranscriptPath(corruptedSessionId);
|
||||
const transcriptPath = resolveSessionTranscriptPath(corruptedSessionId);
|
||||
try {
|
||||
fs.unlinkSync(transcriptPath);
|
||||
} catch {
|
||||
@@ -574,7 +517,6 @@ export async function runAgentTurnWithFallback(params: {
|
||||
fallbackModel,
|
||||
didLogHeartbeatStrip,
|
||||
autoCompactionCompleted,
|
||||
directlySentBlockKeys:
|
||||
directlySentBlockKeys.size > 0 ? directlySentBlockKeys : undefined,
|
||||
directlySentBlockKeys: directlySentBlockKeys.size > 0 ? directlySentBlockKeys : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,8 +196,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
durationMs: 1,
|
||||
error: {
|
||||
kind: "context_overflow",
|
||||
message:
|
||||
'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}',
|
||||
message: 'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe, it } from "vitest";
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
|
||||
const LIVE =
|
||||
isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
|
||||
const LIVE = isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
|
||||
const CDP_URL = process.env.CLAWDBOT_LIVE_BROWSER_CDP_URL?.trim() || "";
|
||||
const describeLive = LIVE && CDP_URL ? describe : describe.skip;
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ async function promptDiscordAllowFrom(params: {
|
||||
}): Promise<ClawdbotConfig> {
|
||||
const accountId =
|
||||
params.accountId && normalizeAccountId(params.accountId)
|
||||
? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID
|
||||
? (normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID)
|
||||
: resolveDefaultDiscordAccountId(params.cfg);
|
||||
const resolved = resolveDiscordAccount({ cfg: params.cfg, accountId });
|
||||
const token = resolved.token;
|
||||
@@ -249,9 +249,7 @@ async function promptDiscordAllowFrom(params: {
|
||||
continue;
|
||||
}
|
||||
const ids = results.map((res) => res.id as string);
|
||||
const unique = [
|
||||
...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids]),
|
||||
];
|
||||
const unique = [...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids])];
|
||||
return setDiscordAllowFrom(params.cfg, unique);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ async function promptIMessageAllowFrom(params: {
|
||||
}): Promise<ClawdbotConfig> {
|
||||
const accountId =
|
||||
params.accountId && normalizeAccountId(params.accountId)
|
||||
? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID
|
||||
? (normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID)
|
||||
: resolveDefaultIMessageAccountId(params.cfg);
|
||||
const resolved = resolveIMessageAccount({ cfg: params.cfg, accountId });
|
||||
const existing = resolved.config.allowFrom ?? [];
|
||||
|
||||
@@ -85,7 +85,7 @@ async function promptSignalAllowFrom(params: {
|
||||
}): Promise<ClawdbotConfig> {
|
||||
const accountId =
|
||||
params.accountId && normalizeAccountId(params.accountId)
|
||||
? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID
|
||||
? (normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID)
|
||||
: resolveDefaultSignalAccountId(params.cfg);
|
||||
const resolved = resolveSignalAccount({ cfg: params.cfg, accountId });
|
||||
const existing = resolved.config.allowFrom ?? [];
|
||||
|
||||
@@ -232,7 +232,7 @@ async function promptSlackAllowFrom(params: {
|
||||
}): Promise<ClawdbotConfig> {
|
||||
const accountId =
|
||||
params.accountId && normalizeAccountId(params.accountId)
|
||||
? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID
|
||||
? (normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID)
|
||||
: resolveDefaultSlackAccountId(params.cfg);
|
||||
const resolved = resolveSlackAccount({ cfg: params.cfg, accountId });
|
||||
const token = resolved.config.userToken ?? resolved.config.botToken ?? "";
|
||||
@@ -299,9 +299,7 @@ async function promptSlackAllowFrom(params: {
|
||||
continue;
|
||||
}
|
||||
const ids = results.map((res) => res.id as string);
|
||||
const unique = [
|
||||
...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids]),
|
||||
];
|
||||
const unique = [...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids])];
|
||||
return setSlackAllowFrom(params.cfg, unique);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,9 +80,10 @@ async function promptTelegramAllowFrom(params: {
|
||||
const username = stripped.startsWith("@") ? stripped : `@${stripped}`;
|
||||
const url = `https://api.telegram.org/bot${token}/getChat?chat_id=${encodeURIComponent(username)}`;
|
||||
const res = await fetch(url);
|
||||
const data = (await res.json().catch(() => null)) as
|
||||
| { ok?: boolean; result?: { id?: number | string } }
|
||||
| null;
|
||||
const data = (await res.json().catch(() => null)) as {
|
||||
ok?: boolean;
|
||||
result?: { id?: number | string };
|
||||
} | null;
|
||||
const id = data?.ok ? data?.result?.id : undefined;
|
||||
if (typeof id === "number" || typeof id === "string") return String(id);
|
||||
return null;
|
||||
@@ -164,7 +165,7 @@ async function promptTelegramAllowFromForAccount(params: {
|
||||
}): Promise<ClawdbotConfig> {
|
||||
const accountId =
|
||||
params.accountId && normalizeAccountId(params.accountId)
|
||||
? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID
|
||||
? (normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID)
|
||||
: resolveDefaultTelegramAccountId(params.cfg);
|
||||
return promptTelegramAllowFrom({
|
||||
cfg: params.cfg,
|
||||
|
||||
@@ -17,16 +17,9 @@ describe("argv helpers", () => {
|
||||
});
|
||||
|
||||
it("extracts command path ignoring flags and terminator", () => {
|
||||
expect(getCommandPath(["node", "clawdbot", "status", "--json"], 2)).toEqual([
|
||||
"status",
|
||||
]);
|
||||
expect(getCommandPath(["node", "clawdbot", "agents", "list"], 2)).toEqual([
|
||||
"agents",
|
||||
"list",
|
||||
]);
|
||||
expect(getCommandPath(["node", "clawdbot", "status", "--", "ignored"], 2)).toEqual([
|
||||
"status",
|
||||
]);
|
||||
expect(getCommandPath(["node", "clawdbot", "status", "--json"], 2)).toEqual(["status"]);
|
||||
expect(getCommandPath(["node", "clawdbot", "agents", "list"], 2)).toEqual(["agents", "list"]);
|
||||
expect(getCommandPath(["node", "clawdbot", "status", "--", "ignored"], 2)).toEqual(["status"]);
|
||||
});
|
||||
|
||||
it("returns primary command", () => {
|
||||
|
||||
@@ -87,37 +87,34 @@ describe("gateway SIGTERM", () => {
|
||||
const out: string[] = [];
|
||||
const err: string[] = [];
|
||||
|
||||
const bunBin = process.env.BUN_INSTALL
|
||||
? path.join(process.env.BUN_INSTALL, "bin", "bun")
|
||||
: "bun";
|
||||
const nodeBin = process.execPath;
|
||||
const args = [
|
||||
"--import",
|
||||
"tsx",
|
||||
"src/entry.ts",
|
||||
"gateway",
|
||||
"--port",
|
||||
String(port),
|
||||
"--bind",
|
||||
"loopback",
|
||||
"--allow-unconfigured",
|
||||
];
|
||||
|
||||
child = spawn(
|
||||
bunBin,
|
||||
[
|
||||
"src/entry.ts",
|
||||
"gateway",
|
||||
"--port",
|
||||
String(port),
|
||||
"--bind",
|
||||
"loopback",
|
||||
"--allow-unconfigured",
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
CLAWDBOT_STATE_DIR: stateDir,
|
||||
CLAWDBOT_CONFIG_PATH: configPath,
|
||||
CLAWDBOT_SKIP_CHANNELS: "1",
|
||||
CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER: "1",
|
||||
CLAWDBOT_SKIP_CANVAS_HOST: "1",
|
||||
// Avoid port collisions with other test processes that may also start a bridge server.
|
||||
CLAWDBOT_BRIDGE_HOST: "127.0.0.1",
|
||||
CLAWDBOT_BRIDGE_PORT: "0",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
child = spawn(nodeBin, args, {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
CLAWDBOT_STATE_DIR: stateDir,
|
||||
CLAWDBOT_CONFIG_PATH: configPath,
|
||||
CLAWDBOT_SKIP_CHANNELS: "1",
|
||||
CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER: "1",
|
||||
CLAWDBOT_SKIP_CANVAS_HOST: "1",
|
||||
// Avoid port collisions with other test processes that may also start a bridge server.
|
||||
CLAWDBOT_BRIDGE_HOST: "127.0.0.1",
|
||||
CLAWDBOT_BRIDGE_PORT: "0",
|
||||
},
|
||||
);
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const proc = child;
|
||||
if (!proc) throw new Error("failed to spawn gateway");
|
||||
|
||||
@@ -149,9 +149,7 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
|
||||
`(requested: ${status.requestedProvider})`,
|
||||
)}`,
|
||||
`${label("Model")} ${info(status.model)}`,
|
||||
status.sources?.length
|
||||
? `${label("Sources")} ${info(status.sources.join(", "))}`
|
||||
: null,
|
||||
status.sources?.length ? `${label("Sources")} ${info(status.sources.join(", "))}` : null,
|
||||
`${label("Indexed")} ${success(`${status.files} files · ${status.chunks} chunks`)}`,
|
||||
`${label("Dirty")} ${status.dirty ? warn("yes") : muted("no")}`,
|
||||
`${label("Store")} ${info(status.dbPath)}`,
|
||||
@@ -222,9 +220,7 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
|
||||
status.cache.enabled && typeof status.cache.entries === "number"
|
||||
? ` (${status.cache.entries} entries)`
|
||||
: "";
|
||||
lines.push(
|
||||
`${label("Embedding cache")} ${colorize(rich, cacheColor, cacheState)}${suffix}`,
|
||||
);
|
||||
lines.push(`${label("Embedding cache")} ${colorize(rich, cacheColor, cacheState)}${suffix}`);
|
||||
if (status.cache.enabled && typeof status.cache.maxEntries === "number") {
|
||||
lines.push(`${label("Cache cap")} ${info(String(status.cache.maxEntries))}`);
|
||||
}
|
||||
|
||||
@@ -33,11 +33,7 @@ export function registerAgentCommands(program: Command, args: { agentChannelOpti
|
||||
"Run the embedded agent locally (requires model provider API keys in your shell)",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--deliver",
|
||||
"Send the agent's reply back to the selected channel",
|
||||
false,
|
||||
)
|
||||
.option("--deliver", "Send the agent's reply back to the selected channel", false)
|
||||
.option("--json", "Output result as JSON", false)
|
||||
.option(
|
||||
"--timeout <seconds>",
|
||||
|
||||
@@ -78,4 +78,4 @@ describe("registerSubCliCommands", () => {
|
||||
expect(registerNodesCli).toHaveBeenCalledTimes(1);
|
||||
expect(nodesAction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,7 +51,6 @@ export type AgentCliOpts = {
|
||||
local?: boolean;
|
||||
};
|
||||
|
||||
|
||||
function parseTimeoutSeconds(opts: { cfg: ReturnType<typeof loadConfig>; timeout?: string }) {
|
||||
const raw =
|
||||
opts.timeout !== undefined
|
||||
|
||||
@@ -586,13 +586,11 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
// module scope. `CLAWDBOT_CONFIG_PATH` (and friends) are expected to work even
|
||||
// when set after the module has been imported (tests, one-off scripts, etc.).
|
||||
const DEFAULT_CONFIG_CACHE_MS = 200;
|
||||
let configCache:
|
||||
| {
|
||||
configPath: string;
|
||||
expiresAt: number;
|
||||
config: ClawdbotConfig;
|
||||
}
|
||||
| null = null;
|
||||
let configCache: {
|
||||
configPath: string;
|
||||
expiresAt: number;
|
||||
config: ClawdbotConfig;
|
||||
} | null = null;
|
||||
|
||||
function resolveConfigCacheMs(env: NodeJS.ProcessEnv): number {
|
||||
const raw = env.CLAWDBOT_CONFIG_CACHE_MS?.trim();
|
||||
|
||||
@@ -12,8 +12,7 @@ import { GatewayClient } from "./client.js";
|
||||
import { renderCatNoncePngBase64 } from "./live-image-probe.js";
|
||||
import { startGatewayServer } from "./server.js";
|
||||
|
||||
const LIVE =
|
||||
isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
|
||||
const LIVE = isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
|
||||
const CLI_LIVE = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_CLI_BACKEND);
|
||||
const CLI_IMAGE = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_CLI_BACKEND_IMAGE_PROBE);
|
||||
const CLI_RESUME = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_CLI_BACKEND_RESUME_PROBE);
|
||||
|
||||
@@ -31,8 +31,7 @@ import { GatewayClient } from "./client.js";
|
||||
import { renderCatNoncePngBase64 } from "./live-image-probe.js";
|
||||
import { startGatewayServer } from "./server.js";
|
||||
|
||||
const LIVE =
|
||||
isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
|
||||
const LIVE = isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
|
||||
const GATEWAY_LIVE = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_GATEWAY);
|
||||
const ZAI_FALLBACK = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_GATEWAY_ZAI_FALLBACK);
|
||||
const PROVIDERS = parseFilter(process.env.CLAWDBOT_LIVE_GATEWAY_PROVIDERS);
|
||||
|
||||
@@ -427,11 +427,15 @@ vi.mock("../config/config.js", async () => {
|
||||
}
|
||||
|
||||
const fileAgents =
|
||||
fileConfig.agents && typeof fileConfig.agents === "object" && !Array.isArray(fileConfig.agents)
|
||||
fileConfig.agents &&
|
||||
typeof fileConfig.agents === "object" &&
|
||||
!Array.isArray(fileConfig.agents)
|
||||
? (fileConfig.agents as Record<string, unknown>)
|
||||
: {};
|
||||
const fileDefaults =
|
||||
fileAgents.defaults && typeof fileAgents.defaults === "object" && !Array.isArray(fileAgents.defaults)
|
||||
fileAgents.defaults &&
|
||||
typeof fileAgents.defaults === "object" &&
|
||||
!Array.isArray(fileAgents.defaults)
|
||||
? (fileAgents.defaults as Record<string, unknown>)
|
||||
: {};
|
||||
const defaults = {
|
||||
@@ -449,7 +453,9 @@ vi.mock("../config/config.js", async () => {
|
||||
: undefined;
|
||||
|
||||
const fileChannels =
|
||||
fileConfig.channels && typeof fileConfig.channels === "object" && !Array.isArray(fileConfig.channels)
|
||||
fileConfig.channels &&
|
||||
typeof fileConfig.channels === "object" &&
|
||||
!Array.isArray(fileConfig.channels)
|
||||
? ({ ...(fileConfig.channels as Record<string, unknown>) } as Record<string, unknown>)
|
||||
: {};
|
||||
const overrideChannels =
|
||||
@@ -459,7 +465,9 @@ vi.mock("../config/config.js", async () => {
|
||||
const mergedChannels = { ...fileChannels, ...overrideChannels };
|
||||
if (testState.allowFrom !== undefined) {
|
||||
const existing =
|
||||
mergedChannels.whatsapp && typeof mergedChannels.whatsapp === "object" && !Array.isArray(mergedChannels.whatsapp)
|
||||
mergedChannels.whatsapp &&
|
||||
typeof mergedChannels.whatsapp === "object" &&
|
||||
!Array.isArray(mergedChannels.whatsapp)
|
||||
? (mergedChannels.whatsapp as Record<string, unknown>)
|
||||
: {};
|
||||
mergedChannels.whatsapp = {
|
||||
@@ -470,18 +478,23 @@ vi.mock("../config/config.js", async () => {
|
||||
const channels = Object.keys(mergedChannels).length > 0 ? mergedChannels : undefined;
|
||||
|
||||
const fileSession =
|
||||
fileConfig.session && typeof fileConfig.session === "object" && !Array.isArray(fileConfig.session)
|
||||
fileConfig.session &&
|
||||
typeof fileConfig.session === "object" &&
|
||||
!Array.isArray(fileConfig.session)
|
||||
? (fileConfig.session as Record<string, unknown>)
|
||||
: {};
|
||||
const session: Record<string, unknown> = {
|
||||
...fileSession,
|
||||
mainKey: fileSession.mainKey ?? "main",
|
||||
};
|
||||
if (typeof testState.sessionStorePath === "string") session.store = testState.sessionStorePath;
|
||||
if (typeof testState.sessionStorePath === "string")
|
||||
session.store = testState.sessionStorePath;
|
||||
if (testState.sessionConfig) Object.assign(session, testState.sessionConfig);
|
||||
|
||||
const fileGateway =
|
||||
fileConfig.gateway && typeof fileConfig.gateway === "object" && !Array.isArray(fileConfig.gateway)
|
||||
fileConfig.gateway &&
|
||||
typeof fileConfig.gateway === "object" &&
|
||||
!Array.isArray(fileConfig.gateway)
|
||||
? ({ ...(fileConfig.gateway as Record<string, unknown>) } as Record<string, unknown>)
|
||||
: {};
|
||||
if (testState.gatewayBind) fileGateway.bind = testState.gatewayBind;
|
||||
@@ -489,14 +502,16 @@ vi.mock("../config/config.js", async () => {
|
||||
const gateway = Object.keys(fileGateway).length > 0 ? fileGateway : undefined;
|
||||
|
||||
const fileCanvasHost =
|
||||
fileConfig.canvasHost && typeof fileConfig.canvasHost === "object" && !Array.isArray(fileConfig.canvasHost)
|
||||
fileConfig.canvasHost &&
|
||||
typeof fileConfig.canvasHost === "object" &&
|
||||
!Array.isArray(fileConfig.canvasHost)
|
||||
? ({ ...(fileConfig.canvasHost as Record<string, unknown>) } as Record<string, unknown>)
|
||||
: {};
|
||||
if (typeof testState.canvasHostPort === "number") fileCanvasHost.port = testState.canvasHostPort;
|
||||
if (typeof testState.canvasHostPort === "number")
|
||||
fileCanvasHost.port = testState.canvasHostPort;
|
||||
const canvasHost = Object.keys(fileCanvasHost).length > 0 ? fileCanvasHost : undefined;
|
||||
|
||||
const hooks =
|
||||
testState.hooksConfig ?? (fileConfig.hooks as HooksConfig | undefined);
|
||||
const hooks = testState.hooksConfig ?? (fileConfig.hooks as HooksConfig | undefined);
|
||||
|
||||
const fileCron =
|
||||
fileConfig.cron && typeof fileConfig.cron === "object" && !Array.isArray(fileConfig.cron)
|
||||
|
||||
@@ -93,7 +93,8 @@ const parseUsageEntry = (entry: Record<string, unknown>): ParsedUsageEntry | nul
|
||||
const role = message?.role;
|
||||
if (role !== "assistant") return null;
|
||||
|
||||
const usageRaw = (message?.usage as UsageLike | undefined) ?? (entry.usage as UsageLike | undefined);
|
||||
const usageRaw =
|
||||
(message?.usage as UsageLike | undefined) ?? (entry.usage as UsageLike | undefined);
|
||||
const usage = normalizeUsage(usageRaw);
|
||||
if (!usage) return null;
|
||||
|
||||
@@ -123,10 +124,7 @@ const applyUsageTotals = (totals: CostUsageTotals, usage: NormalizedUsage) => {
|
||||
totals.cacheWrite += usage.cacheWrite ?? 0;
|
||||
const totalTokens =
|
||||
usage.total ??
|
||||
(usage.input ?? 0) +
|
||||
(usage.output ?? 0) +
|
||||
(usage.cacheRead ?? 0) +
|
||||
(usage.cacheWrite ?? 0);
|
||||
(usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
||||
totals.totalTokens += totalTokens;
|
||||
};
|
||||
|
||||
|
||||
@@ -45,10 +45,7 @@ async function main() {
|
||||
{ startGatewayServer },
|
||||
{ setGatewayWsLogStyle },
|
||||
{ setVerbose },
|
||||
{
|
||||
consumeGatewaySigusr1RestartAuthorization,
|
||||
isGatewaySigusr1RestartExternallyAllowed,
|
||||
},
|
||||
{ consumeGatewaySigusr1RestartAuthorization, isGatewaySigusr1RestartExternallyAllowed },
|
||||
{ defaultRuntime },
|
||||
{ enableConsoleCapture, setConsoleTimestampPrefix },
|
||||
] = await Promise.all([
|
||||
|
||||
@@ -75,7 +75,7 @@ describe("loadClawdbotPlugins", () => {
|
||||
expect(enabled?.status).toBe("loaded");
|
||||
});
|
||||
|
||||
it("loads bundled telegram plugin when enabled", () => {
|
||||
it("loads bundled telegram plugin when enabled", { timeout: 120_000 }, () => {
|
||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = path.join(process.cwd(), "extensions");
|
||||
|
||||
const registry = loadClawdbotPlugins({
|
||||
|
||||
@@ -327,18 +327,7 @@ export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
const pluginSdkAlias = resolvePluginSdkAlias();
|
||||
const jiti = createJiti(import.meta.url, {
|
||||
interopDefault: true,
|
||||
extensions: [
|
||||
".ts",
|
||||
".tsx",
|
||||
".mts",
|
||||
".cts",
|
||||
".mtsx",
|
||||
".ctsx",
|
||||
".js",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".json",
|
||||
],
|
||||
extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"],
|
||||
...(pluginSdkAlias ? { alias: { "clawdbot/plugin-sdk": pluginSdkAlias } } : {}),
|
||||
});
|
||||
|
||||
@@ -393,9 +382,7 @@ export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
try {
|
||||
mod = jiti(candidate.source) as ClawdbotPluginModule;
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`[plugins] ${record.id} failed to load from ${record.source}: ${String(err)}`,
|
||||
);
|
||||
logger.error(`[plugins] ${record.id} failed to load from ${record.source}: ${String(err)}`);
|
||||
record.status = "error";
|
||||
record.error = String(err);
|
||||
registry.plugins.push(record);
|
||||
@@ -480,9 +467,7 @@ export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
});
|
||||
|
||||
if (!validatedConfig.ok) {
|
||||
logger.error(
|
||||
`[plugins] ${record.id} invalid config: ${validatedConfig.errors?.join(", ")}`,
|
||||
);
|
||||
logger.error(`[plugins] ${record.id} invalid config: ${validatedConfig.errors?.join(", ")}`);
|
||||
record.status = "error";
|
||||
record.error = `invalid config: ${validatedConfig.errors?.join(", ")}`;
|
||||
registry.plugins.push(record);
|
||||
|
||||
@@ -28,10 +28,9 @@ export const normalizeAllowFromWithStore = (params: {
|
||||
allowFrom?: Array<string | number>;
|
||||
storeAllowFrom?: string[];
|
||||
}): NormalizedAllowFrom => {
|
||||
const combined = [
|
||||
...(params.allowFrom ?? []),
|
||||
...(params.storeAllowFrom ?? []),
|
||||
].map((value) => String(value).trim()).filter(Boolean);
|
||||
const combined = [...(params.allowFrom ?? []), ...(params.storeAllowFrom ?? [])]
|
||||
.map((value) => String(value).trim())
|
||||
.filter(Boolean);
|
||||
return normalizeAllowFrom(combined);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,8 +13,16 @@ const mockTheme: SearchableSelectListTheme = {
|
||||
};
|
||||
|
||||
const testItems = [
|
||||
{ value: "anthropic/claude-3-opus", label: "anthropic/claude-3-opus", description: "Claude 3 Opus" },
|
||||
{ value: "anthropic/claude-3-sonnet", label: "anthropic/claude-3-sonnet", description: "Claude 3 Sonnet" },
|
||||
{
|
||||
value: "anthropic/claude-3-opus",
|
||||
label: "anthropic/claude-3-opus",
|
||||
description: "Claude 3 Opus",
|
||||
},
|
||||
{
|
||||
value: "anthropic/claude-3-sonnet",
|
||||
label: "anthropic/claude-3-sonnet",
|
||||
description: "Claude 3 Sonnet",
|
||||
},
|
||||
{ value: "openai/gpt-4", label: "openai/gpt-4", description: "GPT-4" },
|
||||
{ value: "openai/gpt-4-turbo", label: "openai/gpt-4-turbo", description: "GPT-4 Turbo" },
|
||||
{ value: "google/gemini-pro", label: "google/gemini-pro", description: "Gemini Pro" },
|
||||
@@ -50,7 +58,11 @@ describe("SearchableSelectList", () => {
|
||||
const items = [
|
||||
{ value: "openrouter/auto", label: "openrouter/auto", description: "Routes to best" },
|
||||
{ value: "opus-direct", label: "opus-direct", description: "Direct opus model" },
|
||||
{ value: "anthropic/claude-3-opus", label: "anthropic/claude-3-opus", description: "Claude 3 Opus" },
|
||||
{
|
||||
value: "anthropic/claude-3-opus",
|
||||
label: "anthropic/claude-3-opus",
|
||||
description: "Claude 3 Opus",
|
||||
},
|
||||
];
|
||||
const list = new SearchableSelectList(items, 5, mockTheme);
|
||||
|
||||
@@ -66,7 +78,11 @@ describe("SearchableSelectList", () => {
|
||||
|
||||
it("exact label match beats description match", () => {
|
||||
const items = [
|
||||
{ value: "provider/other", label: "provider/other", description: "This mentions opus in description" },
|
||||
{
|
||||
value: "provider/other",
|
||||
label: "provider/other",
|
||||
description: "This mentions opus in description",
|
||||
},
|
||||
{ value: "provider/opus-model", label: "provider/opus-model", description: "Something else" },
|
||||
];
|
||||
const list = new SearchableSelectList(items, 5, mockTheme);
|
||||
|
||||
@@ -98,7 +98,11 @@ export class SearchableSelectList implements Component {
|
||||
exactLabel.sort(this.compareByScore);
|
||||
wordBoundary.sort(this.compareByScore);
|
||||
descriptionMatches.sort(this.compareByScore);
|
||||
const fuzzyMatches = fuzzyFilter(fuzzyCandidates, query, (i) => `${i.label} ${i.description ?? ""}`);
|
||||
const fuzzyMatches = fuzzyFilter(
|
||||
fuzzyCandidates,
|
||||
query,
|
||||
(i) => `${i.label} ${i.description ?? ""}`,
|
||||
);
|
||||
return [
|
||||
...exactLabel.map((s) => s.item),
|
||||
...wordBoundary.map((s) => s.item),
|
||||
@@ -133,7 +137,10 @@ export class SearchableSelectList implements Component {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
private compareByScore = (a: { item: SelectItem; score: number }, b: { item: SelectItem; score: number }) => {
|
||||
private compareByScore = (
|
||||
a: { item: SelectItem; score: number },
|
||||
b: { item: SelectItem; score: number },
|
||||
) => {
|
||||
if (a.score !== b.score) return a.score - b.score;
|
||||
return this.getItemLabel(a.item).localeCompare(this.getItemLabel(b.item));
|
||||
};
|
||||
@@ -190,7 +197,10 @@ export class SearchableSelectList implements Component {
|
||||
// Calculate visible range with scrolling
|
||||
const startIndex = Math.max(
|
||||
0,
|
||||
Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible),
|
||||
Math.min(
|
||||
this.selectedIndex - Math.floor(this.maxVisible / 2),
|
||||
this.filteredItems.length - this.maxVisible,
|
||||
),
|
||||
);
|
||||
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);
|
||||
|
||||
@@ -211,7 +221,12 @@ export class SearchableSelectList implements Component {
|
||||
return lines;
|
||||
}
|
||||
|
||||
private renderItemLine(item: SelectItem, isSelected: boolean, width: number, query: string): string {
|
||||
private renderItemLine(
|
||||
item: SelectItem,
|
||||
isSelected: boolean,
|
||||
width: number,
|
||||
query: string,
|
||||
): string {
|
||||
const prefix = isSelected ? "→ " : " ";
|
||||
const prefixWidth = prefix.length;
|
||||
const displayValue = this.getItemLabel(item);
|
||||
|
||||
@@ -7,7 +7,11 @@ import {
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
import { helpText, parseCommand } from "./commands.js";
|
||||
import type { ChatLog } from "./components/chat-log.js";
|
||||
import { createSearchableSelectList, createSelectList, createSettingsList } from "./components/selectors.js";
|
||||
import {
|
||||
createSearchableSelectList,
|
||||
createSelectList,
|
||||
createSettingsList,
|
||||
} from "./components/selectors.js";
|
||||
import type { GatewayChatClient } from "./gateway-chat.js";
|
||||
import { formatStatusSummary } from "./tui-status-summary.js";
|
||||
import type {
|
||||
|
||||
@@ -22,10 +22,7 @@ import { editorTheme, theme } from "./theme/theme.js";
|
||||
import { createCommandHandlers } from "./tui-command-handlers.js";
|
||||
import { createEventHandlers } from "./tui-event-handlers.js";
|
||||
import { formatTokens } from "./tui-formatters.js";
|
||||
import {
|
||||
buildWaitingStatusMessage,
|
||||
defaultWaitingPhrases,
|
||||
} from "./tui-waiting.js";
|
||||
import { buildWaitingStatusMessage, defaultWaitingPhrases } from "./tui-waiting.js";
|
||||
import { createOverlayHandlers } from "./tui-overlays.js";
|
||||
import { createSessionActions } from "./tui-session-actions.js";
|
||||
import type {
|
||||
@@ -335,8 +332,7 @@ export async function runTui(opts: TuiOptions) {
|
||||
// Pick a phrase once per waiting session.
|
||||
if (!waitingPhrase) {
|
||||
const idx = Math.floor(Math.random() * defaultWaitingPhrases.length);
|
||||
waitingPhrase =
|
||||
defaultWaitingPhrases[idx] ?? defaultWaitingPhrases[0] ?? "waiting";
|
||||
waitingPhrase = defaultWaitingPhrases[idx] ?? defaultWaitingPhrases[0] ?? "waiting";
|
||||
}
|
||||
|
||||
waitingTick = 0;
|
||||
|
||||
Reference in New Issue
Block a user