chore: format and sync protocol outputs
This commit is contained in:
@@ -1679,6 +1679,27 @@ public struct ChatAbortParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatInjectParams: Codable, Sendable {
|
||||
public let sessionkey: String
|
||||
public let message: String
|
||||
public let label: String?
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
message: String,
|
||||
label: String?
|
||||
) {
|
||||
self.sessionkey = sessionkey
|
||||
self.message = message
|
||||
self.label = label
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionkey = "sessionKey"
|
||||
case message
|
||||
case label
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatEvent: Codable, Sendable {
|
||||
public let runid: String
|
||||
public let sessionkey: String
|
||||
|
||||
@@ -126,15 +126,7 @@ describe("cleanupSuspendedCliProcesses", () => {
|
||||
await cleanupSuspendedCliProcesses(
|
||||
{
|
||||
command: "codex",
|
||||
resumeArgs: [
|
||||
"exec",
|
||||
"resume",
|
||||
"{sessionId}",
|
||||
"--color",
|
||||
"never",
|
||||
"--sandbox",
|
||||
"read-only",
|
||||
],
|
||||
resumeArgs: ["exec", "resume", "{sessionId}", "--color", "never", "--sandbox", "read-only"],
|
||||
} as CliBackendConfig,
|
||||
1,
|
||||
);
|
||||
|
||||
@@ -25,8 +25,6 @@ describe("sanitizeUserFacingText", () => {
|
||||
|
||||
it("sanitizes raw API error payloads", () => {
|
||||
const raw = '{"type":"error","error":{"message":"Something exploded","type":"server_error"}}';
|
||||
expect(sanitizeUserFacingText(raw)).toBe(
|
||||
"The AI service returned an error. Please try again.",
|
||||
);
|
||||
expect(sanitizeUserFacingText(raw)).toBe("The AI service returned an error. Please try again.");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -230,7 +230,11 @@ export function formatAssistantErrorText(
|
||||
}
|
||||
|
||||
// Catch role ordering errors - including JSON-wrapped and "400" prefix variants
|
||||
if (/incorrect role information|roles must alternate|400.*role|"message".*role.*information/i.test(raw)) {
|
||||
if (
|
||||
/incorrect role information|roles must alternate|400.*role|"message".*role.*information/i.test(
|
||||
raw,
|
||||
)
|
||||
) {
|
||||
return (
|
||||
"Message ordering conflict - please try again. " +
|
||||
"If this persists, use /new to start a fresh session."
|
||||
|
||||
@@ -422,7 +422,8 @@ export async function runEmbeddedAttempt(
|
||||
|
||||
// Check if last message is a user message to prevent consecutive user turns
|
||||
const lastMsg = activeSession.messages[activeSession.messages.length - 1];
|
||||
const lastMsgRole = lastMsg && typeof lastMsg === "object" ? (lastMsg as { role?: unknown }).role : undefined;
|
||||
const lastMsgRole =
|
||||
lastMsg && typeof lastMsg === "object" ? (lastMsg as { role?: unknown }).role : undefined;
|
||||
|
||||
if (lastMsgRole === "user") {
|
||||
// Last message was a user message. Adding another user message would create
|
||||
@@ -433,9 +434,11 @@ export async function runEmbeddedAttempt(
|
||||
// Skip this prompt to prevent "400 Incorrect role information" error.
|
||||
log.warn(
|
||||
`Skipping prompt because last message is a user message (would create consecutive user turns). ` +
|
||||
`runId=${params.runId} sessionId=${params.sessionId}`
|
||||
`runId=${params.runId} sessionId=${params.sessionId}`,
|
||||
);
|
||||
promptError = new Error(
|
||||
"Incorrect role information: consecutive user messages would violate role ordering",
|
||||
);
|
||||
promptError = new Error("Incorrect role information: consecutive user messages would violate role ordering");
|
||||
} else {
|
||||
try {
|
||||
await activeSession.prompt(params.prompt, { images: params.images });
|
||||
|
||||
@@ -27,10 +27,7 @@ function buildSkillsSection(params: {
|
||||
];
|
||||
}
|
||||
|
||||
function buildMemorySection(params: {
|
||||
isMinimal: boolean;
|
||||
availableTools: Set<string>;
|
||||
}) {
|
||||
function buildMemorySection(params: { isMinimal: boolean; availableTools: Set<string> }) {
|
||||
if (params.isMinimal) return [];
|
||||
if (!params.availableTools.has("memory_search") && !params.availableTools.has("memory_get")) {
|
||||
return [];
|
||||
|
||||
@@ -156,10 +156,7 @@ export async function buildStatusReply(params: {
|
||||
usageProviders.add(currentUsageProvider);
|
||||
}
|
||||
const usageByProvider = new Map<string, string>();
|
||||
let usageSummaryCache:
|
||||
| Awaited<ReturnType<typeof loadProviderUsageSummary>>
|
||||
| null
|
||||
| undefined;
|
||||
let usageSummaryCache: Awaited<ReturnType<typeof loadProviderUsageSummary>> | null | undefined;
|
||||
if (usageProviders.size > 0) {
|
||||
try {
|
||||
usageSummaryCache = await loadProviderUsageSummary({
|
||||
|
||||
@@ -43,9 +43,9 @@ export function applyReplyTagsToPayload(
|
||||
export function isRenderablePayload(payload: ReplyPayload): boolean {
|
||||
return Boolean(
|
||||
payload.text ||
|
||||
payload.mediaUrl ||
|
||||
(payload.mediaUrls && payload.mediaUrls.length > 0) ||
|
||||
payload.audioAsVoice,
|
||||
payload.mediaUrl ||
|
||||
(payload.mediaUrls && payload.mediaUrls.length > 0) ||
|
||||
payload.audioAsVoice,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,10 @@ export function findChromeExecutableMac(): BrowserExecutable | null {
|
||||
},
|
||||
{
|
||||
kind: "edge",
|
||||
path: path.join(os.homedir(), "Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"),
|
||||
path: path.join(
|
||||
os.homedir(),
|
||||
"Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
||||
),
|
||||
},
|
||||
{
|
||||
kind: "chromium",
|
||||
|
||||
@@ -19,14 +19,7 @@ import { getActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
// - add an entry to `src/channels/dock.ts` for shared behavior (capabilities, allowFrom, threading, …)
|
||||
// - add ids/aliases in `src/channels/registry.ts`
|
||||
function resolveCoreChannels(): ChannelPlugin[] {
|
||||
return [
|
||||
telegramPlugin,
|
||||
whatsappPlugin,
|
||||
discordPlugin,
|
||||
slackPlugin,
|
||||
signalPlugin,
|
||||
imessagePlugin,
|
||||
];
|
||||
return [telegramPlugin, whatsappPlugin, discordPlugin, slackPlugin, signalPlugin, imessagePlugin];
|
||||
}
|
||||
|
||||
function listPluginChannels(): ChannelPlugin[] {
|
||||
@@ -80,12 +73,5 @@ export function normalizeChannelId(raw?: string | null): ChannelId | null {
|
||||
return plugin?.id ?? null;
|
||||
}
|
||||
|
||||
export {
|
||||
discordPlugin,
|
||||
imessagePlugin,
|
||||
signalPlugin,
|
||||
slackPlugin,
|
||||
telegramPlugin,
|
||||
whatsappPlugin,
|
||||
};
|
||||
export { discordPlugin, imessagePlugin, signalPlugin, slackPlugin, telegramPlugin, whatsappPlugin };
|
||||
export type { ChannelId, ChannelPlugin } from "./types.js";
|
||||
|
||||
@@ -4,14 +4,14 @@ import {
|
||||
githubCopilotLoginCommand,
|
||||
modelsAliasesAddCommand,
|
||||
modelsAliasesListCommand,
|
||||
modelsAliasesRemoveCommand,
|
||||
modelsAuthAddCommand,
|
||||
modelsAuthLoginCommand,
|
||||
modelsAuthOrderClearCommand,
|
||||
modelsAuthOrderGetCommand,
|
||||
modelsAuthOrderSetCommand,
|
||||
modelsAuthPasteTokenCommand,
|
||||
modelsAuthSetupTokenCommand,
|
||||
modelsAliasesRemoveCommand,
|
||||
modelsAuthAddCommand,
|
||||
modelsAuthLoginCommand,
|
||||
modelsAuthOrderClearCommand,
|
||||
modelsAuthOrderGetCommand,
|
||||
modelsAuthOrderSetCommand,
|
||||
modelsAuthPasteTokenCommand,
|
||||
modelsAuthSetupTokenCommand,
|
||||
modelsFallbacksAddCommand,
|
||||
modelsFallbacksClearCommand,
|
||||
modelsFallbacksListCommand,
|
||||
|
||||
@@ -119,11 +119,14 @@ describe("runConfigureWizard", () => {
|
||||
mocks.clackText.mockResolvedValue("");
|
||||
mocks.clackConfirm.mockResolvedValue(false);
|
||||
|
||||
await runConfigureWizard({ command: "configure" }, {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
});
|
||||
await runConfigureWizard(
|
||||
{ command: "configure" },
|
||||
{
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
},
|
||||
);
|
||||
|
||||
expect(mocks.writeConfigFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -8,10 +8,18 @@ import {
|
||||
upsertAuthProfile,
|
||||
} from "../../agents/auth-profiles.js";
|
||||
import { normalizeProviderId } from "../../agents/model-selection.js";
|
||||
import { resolveAgentDir, resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||
import {
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js";
|
||||
import { parseDurationMs } from "../../cli/parse-duration.js";
|
||||
import { CONFIG_PATH_CLAWDBOT, readConfigFileSnapshot, type ClawdbotConfig } from "../../config/config.js";
|
||||
import {
|
||||
CONFIG_PATH_CLAWDBOT,
|
||||
readConfigFileSnapshot,
|
||||
type ClawdbotConfig,
|
||||
} from "../../config/config.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import { stylePromptHint, stylePromptMessage } from "../../terminal/prompt-style.js";
|
||||
import { applyAuthProfileConfig } from "../onboard-auth.js";
|
||||
@@ -21,7 +29,11 @@ import { createVpsAwareOAuthHandlers } from "../oauth-flow.js";
|
||||
import { updateConfig } from "./shared.js";
|
||||
import { resolvePluginProviders } from "../../plugins/providers.js";
|
||||
import { createClackPrompter } from "../../wizard/clack-prompter.js";
|
||||
import type { ProviderAuthMethod, ProviderAuthResult, ProviderPlugin } from "../../plugins/types.js";
|
||||
import type {
|
||||
ProviderAuthMethod,
|
||||
ProviderAuthResult,
|
||||
ProviderPlugin,
|
||||
} from "../../plugins/types.js";
|
||||
import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js";
|
||||
|
||||
const confirm = (params: Parameters<typeof clackConfirm>[0]) =>
|
||||
@@ -334,14 +346,16 @@ export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: Runtim
|
||||
const prompter = createClackPrompter();
|
||||
const selectedProvider =
|
||||
resolveProviderMatch(providers, opts.provider) ??
|
||||
(await prompter.select({
|
||||
message: "Select a provider",
|
||||
options: providers.map((provider) => ({
|
||||
value: provider.id,
|
||||
label: provider.label,
|
||||
hint: provider.docsPath ? `Docs: ${provider.docsPath}` : undefined,
|
||||
})),
|
||||
}).then((id) => resolveProviderMatch(providers, String(id))));
|
||||
(await prompter
|
||||
.select({
|
||||
message: "Select a provider",
|
||||
options: providers.map((provider) => ({
|
||||
value: provider.id,
|
||||
label: provider.label,
|
||||
hint: provider.docsPath ? `Docs: ${provider.docsPath}` : undefined,
|
||||
})),
|
||||
})
|
||||
.then((id) => resolveProviderMatch(providers, String(id))));
|
||||
|
||||
if (!selectedProvider) {
|
||||
throw new Error("Unknown provider. Use --provider <id> to pick a provider plugin.");
|
||||
@@ -351,16 +365,16 @@ export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: Runtim
|
||||
pickAuthMethod(selectedProvider, opts.method) ??
|
||||
(selectedProvider.auth.length === 1
|
||||
? selectedProvider.auth[0]
|
||||
: await prompter.select({
|
||||
message: `Auth method for ${selectedProvider.label}`,
|
||||
options: selectedProvider.auth.map((method) => ({
|
||||
value: method.id,
|
||||
label: method.label,
|
||||
hint: method.hint,
|
||||
})),
|
||||
}).then((id) =>
|
||||
selectedProvider.auth.find((method) => method.id === String(id)),
|
||||
));
|
||||
: await prompter
|
||||
.select({
|
||||
message: `Auth method for ${selectedProvider.label}`,
|
||||
options: selectedProvider.auth.map((method) => ({
|
||||
value: method.id,
|
||||
label: method.label,
|
||||
hint: method.hint,
|
||||
})),
|
||||
})
|
||||
.then((id) => selectedProvider.auth.find((method) => method.id === String(id))));
|
||||
|
||||
if (!chosenMethod) {
|
||||
throw new Error("Unknown auth method. Use --method <id> to select one.");
|
||||
|
||||
@@ -177,9 +177,7 @@ describe("setupChannels", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: "Select a channel" }),
|
||||
);
|
||||
expect(select).toHaveBeenCalledWith(expect.objectContaining({ message: "Select a channel" }));
|
||||
expect(multiselect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -317,7 +317,10 @@ export async function setupChannels(
|
||||
};
|
||||
|
||||
const buildSelectionOptions = (
|
||||
entries: Array<{ id: ChannelChoice; meta: { id: string; label: string; selectionLabel?: string } }>,
|
||||
entries: Array<{
|
||||
id: ChannelChoice;
|
||||
meta: { id: string; label: string; selectionLabel?: string };
|
||||
}>,
|
||||
) =>
|
||||
entries.map((entry) => {
|
||||
const status = statusByChannel.get(entry.id);
|
||||
|
||||
@@ -108,9 +108,7 @@ export function resolveHeartbeatIntervalMs(
|
||||
}
|
||||
|
||||
export function resolveHeartbeatPrompt(cfg: ClawdbotConfig, heartbeat?: HeartbeatConfig) {
|
||||
return resolveHeartbeatPromptText(
|
||||
heartbeat?.prompt ?? cfg.agents?.defaults?.heartbeat?.prompt,
|
||||
);
|
||||
return resolveHeartbeatPromptText(heartbeat?.prompt ?? cfg.agents?.defaults?.heartbeat?.prompt);
|
||||
}
|
||||
|
||||
function resolveHeartbeatAckMaxChars(cfg: ClawdbotConfig, heartbeat?: HeartbeatConfig) {
|
||||
@@ -127,9 +125,7 @@ function resolveHeartbeatSession(cfg: ClawdbotConfig, agentId?: string) {
|
||||
const scope = sessionCfg?.scope ?? "per-sender";
|
||||
const resolvedAgentId = normalizeAgentId(agentId ?? resolveDefaultAgentId(cfg));
|
||||
const sessionKey =
|
||||
scope === "global"
|
||||
? "global"
|
||||
: resolveAgentMainSessionKey({ cfg, agentId: resolvedAgentId });
|
||||
scope === "global" ? "global" : resolveAgentMainSessionKey({ cfg, agentId: resolvedAgentId });
|
||||
const storeAgentId = scope === "global" ? resolveDefaultAgentId(cfg) : resolvedAgentId;
|
||||
const storePath = resolveStorePath(sessionCfg?.store, { agentId: storeAgentId });
|
||||
const store = loadSessionStore(storePath);
|
||||
@@ -337,8 +333,10 @@ export async function runHeartbeatOnce(opts: {
|
||||
|
||||
// Suppress duplicate heartbeats (same payload) within a short window.
|
||||
// This prevents "nagging" when nothing changed but the model repeats the same items.
|
||||
const prevHeartbeatText = typeof entry?.lastHeartbeatText === "string" ? entry.lastHeartbeatText : "";
|
||||
const prevHeartbeatAt = typeof entry?.lastHeartbeatSentAt === "number" ? entry.lastHeartbeatSentAt : undefined;
|
||||
const prevHeartbeatText =
|
||||
typeof entry?.lastHeartbeatText === "string" ? entry.lastHeartbeatText : "";
|
||||
const prevHeartbeatAt =
|
||||
typeof entry?.lastHeartbeatSentAt === "number" ? entry.lastHeartbeatSentAt : undefined;
|
||||
const isDuplicateMain =
|
||||
!shouldSkipMain &&
|
||||
!mediaUrls.length &&
|
||||
|
||||
@@ -210,10 +210,11 @@ export async function runMessageAction(
|
||||
const to = readStringParam(params, "to", { required: true });
|
||||
// Allow message to be omitted when sending media-only (e.g., voice notes)
|
||||
const mediaHint = readStringParam(params, "media", { trim: false });
|
||||
let message = readStringParam(params, "message", {
|
||||
required: !mediaHint, // Only require message if no media hint
|
||||
allowEmpty: true,
|
||||
}) ?? "";
|
||||
let message =
|
||||
readStringParam(params, "message", {
|
||||
required: !mediaHint, // Only require message if no media hint
|
||||
allowEmpty: true,
|
||||
}) ?? "";
|
||||
|
||||
const parsed = parseReplyDirectives(message);
|
||||
message = parsed.text;
|
||||
|
||||
@@ -34,7 +34,11 @@ export type GatewaySessionList = {
|
||||
ts: number;
|
||||
path: string;
|
||||
count: number;
|
||||
defaults?: { model?: string | null; modelProvider?: string | null; contextTokens?: number | null };
|
||||
defaults?: {
|
||||
model?: string | null;
|
||||
modelProvider?: string | null;
|
||||
contextTokens?: number | null;
|
||||
};
|
||||
sessions: Array<{
|
||||
key: string;
|
||||
sessionId?: string;
|
||||
|
||||
Reference in New Issue
Block a user