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