fix: normalize gateway dev mode detection

This commit is contained in:
Peter Steinberger
2026-01-18 01:08:42 +00:00
parent 2c070952e1
commit 36d88f6079
29 changed files with 95 additions and 107 deletions

View File

@@ -100,13 +100,9 @@ function mergeConfig(
enabled: overrides?.remote?.batch?.enabled ?? defaults?.remote?.batch?.enabled ?? true,
wait: overrides?.remote?.batch?.wait ?? defaults?.remote?.batch?.wait ?? true,
pollIntervalMs:
overrides?.remote?.batch?.pollIntervalMs ??
defaults?.remote?.batch?.pollIntervalMs ??
5000,
overrides?.remote?.batch?.pollIntervalMs ?? defaults?.remote?.batch?.pollIntervalMs ?? 5000,
timeoutMinutes:
overrides?.remote?.batch?.timeoutMinutes ??
defaults?.remote?.batch?.timeoutMinutes ??
60,
overrides?.remote?.batch?.timeoutMinutes ?? defaults?.remote?.batch?.timeoutMinutes ?? 60,
};
const remote = includeRemote
? {

View File

@@ -112,8 +112,9 @@ export function installSessionToolResultGuard(sessionManager: SessionManager): {
const result = originalAppend(sanitized as never);
const sessionFile = (sessionManager as { getSessionFile?: () => string | null })
.getSessionFile?.();
const sessionFile = (
sessionManager as { getSessionFile?: () => string | null }
).getSessionFile?.();
if (sessionFile) {
emitSessionTranscriptUpdate(sessionFile);
}

View File

@@ -44,11 +44,9 @@ describe("tool meta formatting", () => {
it("keeps exec flags outside markdown and moves them to the front", () => {
vi.stubEnv("HOME", "/Users/test");
const out = formatToolAggregate(
"exec",
["cd /Users/test/dir && gemini 2>&1 · elevated"],
{ markdown: true },
);
const out = formatToolAggregate("exec", ["cd /Users/test/dir && gemini 2>&1 · elevated"], {
markdown: true,
});
expect(out).toBe("🛠️ exec: elevated · `cd ~/dir && gemini 2>&1`");
});

View File

@@ -5,9 +5,7 @@ export type ChannelEntryMatch<T> = {
wildcardKey?: string;
};
export function buildChannelKeyCandidates(
...keys: Array<string | undefined | null>
): string[] {
export function buildChannelKeyCandidates(...keys: Array<string | undefined | null>): string[] {
const seen = new Set<string>();
const candidates: string[] = [];
for (const key of keys) {

View File

@@ -24,10 +24,7 @@ import {
} from "./config-helpers.js";
import { resolveDiscordGroupRequireMention } from "./group-mentions.js";
import { formatPairingApproveHint } from "./helpers.js";
import {
looksLikeDiscordTargetId,
normalizeDiscordMessagingTarget,
} from "./normalize/discord.js";
import { looksLikeDiscordTargetId, normalizeDiscordMessagingTarget } from "./normalize/discord.js";
import { discordOnboardingAdapter } from "./onboarding/discord.js";
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
import {

View File

@@ -18,10 +18,7 @@ import {
} from "./config-helpers.js";
import { formatPairingApproveHint } from "./helpers.js";
import { resolveChannelMediaMaxBytes } from "./media-limits.js";
import {
looksLikeSignalTargetId,
normalizeSignalMessagingTarget,
} from "./normalize/signal.js";
import { looksLikeSignalTargetId, normalizeSignalMessagingTarget } from "./normalize/signal.js";
import { signalOnboardingAdapter } from "./onboarding/signal.js";
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
import {

View File

@@ -31,9 +31,9 @@ describe("requireTargetKind", () => {
});
it("throws when the kind is missing or mismatched", () => {
expect(() => requireTargetKind({ platform: "Slack", target: undefined, kind: "channel" })).toThrow(
/Slack channel id is required/,
);
expect(() =>
requireTargetKind({ platform: "Slack", target: undefined, kind: "channel" }),
).toThrow(/Slack channel id is required/);
const target = buildMessagingTarget("user", "U123", "U123");
expect(() => requireTargetKind({ platform: "Slack", target, kind: "channel" })).toThrow(
/Slack channel id is required/,

View File

@@ -110,7 +110,9 @@ export function registerMemoryCli(program: Command) {
return;
}
if (opts.index) {
const line = indexError ? `Memory index failed: ${indexError}` : "Memory index complete.";
const line = indexError
? `Memory index failed: ${indexError}`
: "Memory index complete.";
defaultRuntime.log(line);
}
const rich = isRich();
@@ -127,7 +129,9 @@ export function registerMemoryCli(program: Command) {
`(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)}`,

View File

@@ -5,18 +5,15 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ChannelPlugin } from "../../channels/plugins/types.js";
import { channelsCapabilitiesCommand } from "./capabilities.js";
import { fetchSlackScopes } from "../../slack/scopes.js";
import {
getChannelPlugin,
listChannelPlugins,
} from "../../channels/plugins/index.js";
import { getChannelPlugin, listChannelPlugins } from "../../channels/plugins/index.js";
const logs: string[] = [];
const errors: string[] = [];
vi.mock("./shared.js", () => ({
requireValidConfig: vi.fn(async () => ({ channels: {} })),
formatChannelAccountLabel: vi.fn(({ channel, accountId }: { channel: string; accountId: string }) =>
`${channel}:${accountId}`,
formatChannelAccountLabel: vi.fn(
({ channel, accountId }: { channel: string; accountId: string }) => `${channel}:${accountId}`,
),
}));

View File

@@ -146,8 +146,10 @@ function formatProbeLines(channelId: string, probe: unknown): string[] {
}
const flags: string[] = [];
const canJoinGroups = (bot as { canJoinGroups?: boolean | null })?.canJoinGroups;
const canReadAll = (bot as { canReadAllGroupMessages?: boolean | null })?.canReadAllGroupMessages;
const inlineQueries = (bot as { supportsInlineQueries?: boolean | null })?.supportsInlineQueries;
const canReadAll = (bot as { canReadAllGroupMessages?: boolean | null })
?.canReadAllGroupMessages;
const inlineQueries = (bot as { supportsInlineQueries?: boolean | null })
?.supportsInlineQueries;
if (typeof canJoinGroups === "boolean") flags.push(`joinGroups=${canJoinGroups}`);
if (typeof canReadAll === "boolean") flags.push(`readAllGroupMessages=${canReadAll}`);
if (typeof inlineQueries === "boolean") flags.push(`inlineQueries=${inlineQueries}`);
@@ -187,14 +189,15 @@ function formatProbeLines(channelId: string, probe: unknown): string[] {
const roles = Array.isArray(graph.roles)
? graph.roles.map((role) => String(role).trim()).filter(Boolean)
: [];
const scopes = typeof graph.scopes === "string"
? graph.scopes
.split(/\s+/)
.map((scope) => scope.trim())
.filter(Boolean)
: Array.isArray(graph.scopes)
? graph.scopes.map((scope) => String(scope).trim()).filter(Boolean)
: [];
const scopes =
typeof graph.scopes === "string"
? graph.scopes
.split(/\s+/)
.map((scope) => scope.trim())
.filter(Boolean)
: Array.isArray(graph.scopes)
? graph.scopes.map((scope) => String(scope).trim()).filter(Boolean)
: [];
if (graph.ok === false) {
lines.push(`Graph: ${theme.error(graph.error ?? "failed")}`);
} else if (roles.length > 0 || scopes.length > 0) {
@@ -219,7 +222,8 @@ function formatProbeLines(channelId: string, probe: unknown): string[] {
lines.push("Probe: ok");
}
if (ok === false) {
const error = typeof probeObj.error === "string" && probeObj.error ? ` (${probeObj.error})` : "";
const error =
typeof probeObj.error === "string" && probeObj.error ? ` (${probeObj.error})` : "";
lines.push(`Probe: ${theme.error(`failed${error}`)}`);
}
return lines;
@@ -388,8 +392,7 @@ export async function channelsCapabilitiesCommand(
const cfg = await requireValidConfig(runtime);
if (!cfg) return;
const timeoutMs = normalizeTimeout(opts.timeout, 10_000);
const rawChannel =
typeof opts.channel === "string" ? opts.channel.trim().toLowerCase() : "";
const rawChannel = typeof opts.channel === "string" ? opts.channel.trim().toLowerCase() : "";
const rawTarget = typeof opts.target === "string" ? opts.target.trim() : "";
if (opts.account && (!rawChannel || rawChannel === "all")) {
@@ -483,9 +486,7 @@ export async function channelsCapabilitiesCommand(
const label = perms.channelId ? ` (${perms.channelId})` : "";
lines.push(`Permissions${label}: ${list}`);
if (perms.missingRequired && perms.missingRequired.length > 0) {
lines.push(
`${theme.warn("Missing required:")} ${perms.missingRequired.join(", ")}`,
);
lines.push(`${theme.warn("Missing required:")} ${perms.missingRequired.join(", ")}`);
} else {
lines.push(theme.success("Missing required: none"));
}

View File

@@ -34,9 +34,10 @@ afterEach(() => {
describe("resolveGatewayDevMode", () => {
it("detects dev mode for src ts entrypoints", () => {
expect(
resolveGatewayDevMode(["node", "/Users/me/clawdbot/src/cli/index.ts"]),
).toBe(true);
expect(resolveGatewayDevMode(["node", "/Users/me/clawdbot/src/cli/index.ts"])).toBe(true);
expect(resolveGatewayDevMode(["node", "C:\\Users\\me\\clawdbot\\src\\cli\\index.ts"])).toBe(
true,
);
expect(resolveGatewayDevMode(["node", "/Users/me/clawdbot/dist/cli/index.js"])).toBe(false);
});
});

View File

@@ -1,5 +1,3 @@
import path from "node:path";
import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
import {
@@ -20,7 +18,8 @@ export type GatewayInstallPlan = {
export function resolveGatewayDevMode(argv: string[] = process.argv): boolean {
const entry = argv[1];
return Boolean(entry?.includes(`${path.sep}src${path.sep}`) && entry.endsWith(".ts"));
const normalizedEntry = entry?.replaceAll("\\", "/");
return Boolean(normalizedEntry?.includes("/src/") && normalizedEntry.endsWith(".ts"));
}
export async function buildGatewayInstallPlan(params: {

View File

@@ -15,10 +15,7 @@ import {
GATEWAY_DAEMON_RUNTIME_OPTIONS,
type GatewayDaemonRuntime,
} from "./daemon-runtime.js";
import {
buildGatewayInstallPlan,
gatewayInstallErrorHint,
} from "./daemon-install-helpers.js";
import { buildGatewayInstallPlan, gatewayInstallErrorHint } from "./daemon-install-helpers.js";
import { buildGatewayRuntimeHints, formatGatewayRuntimeSummary } from "./doctor-format.js";
import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js";
import { healthCommand } from "./health.js";

View File

@@ -4,10 +4,7 @@ import type { ClawdbotConfig } from "../config/config.js";
import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js";
import { findExtraGatewayServices, renderGatewayServiceCleanupHints } from "../daemon/inspect.js";
import { findLegacyGatewayServices, uninstallLegacyGatewayServices } from "../daemon/legacy.js";
import {
renderSystemNodeWarning,
resolveSystemNodeInfo,
} from "../daemon/runtime-paths.js";
import { renderSystemNodeWarning, resolveSystemNodeInfo } from "../daemon/runtime-paths.js";
import { resolveGatewayService } from "../daemon/service.js";
import {
auditGatewayServiceConfig,
@@ -16,10 +13,7 @@ import {
} from "../daemon/service-audit.js";
import type { RuntimeEnv } from "../runtime.js";
import { note } from "../terminal/note.js";
import {
buildGatewayInstallPlan,
gatewayInstallErrorHint,
} from "./daemon-install-helpers.js";
import { buildGatewayInstallPlan, gatewayInstallErrorHint } from "./daemon-install-helpers.js";
import {
DEFAULT_GATEWAY_DAEMON_RUNTIME,
GATEWAY_DAEMON_RUNTIME_OPTIONS,

View File

@@ -3,10 +3,7 @@ import { resolveGatewayService } from "../../../daemon/service.js";
import { isSystemdUserServiceAvailable } from "../../../daemon/systemd.js";
import type { RuntimeEnv } from "../../../runtime.js";
import { DEFAULT_GATEWAY_DAEMON_RUNTIME, isGatewayDaemonRuntime } from "../../daemon-runtime.js";
import {
buildGatewayInstallPlan,
gatewayInstallErrorHint,
} from "../../daemon-install-helpers.js";
import { buildGatewayInstallPlan, gatewayInstallErrorHint } from "../../daemon-install-helpers.js";
import type { OnboardOptions } from "../../onboard-types.js";
import { ensureSystemdUserLingerNonInteractive } from "../../systemd-linger.js";

View File

@@ -1,6 +1,9 @@
import type { Guild, User } from "@buape/carbon";
import { buildChannelKeyCandidates, resolveChannelEntryMatch } from "../../channels/channel-config.js";
import {
buildChannelKeyCandidates,
resolveChannelEntryMatch,
} from "../../channels/channel-config.js";
import { formatDiscordUserTag } from "./format.js";
export type DiscordAllowList = {

View File

@@ -266,7 +266,9 @@ export async function preflightDiscordMessage(
channelConfig?.matchSource ?? "none"
}`;
if (isGuildMessage && channelConfig?.enabled === false) {
logVerbose(`Blocked discord channel ${message.channelId} (channel disabled, ${channelMatchMeta})`);
logVerbose(
`Blocked discord channel ${message.channelId} (channel disabled, ${channelMatchMeta})`,
);
return null;
}

View File

@@ -535,7 +535,7 @@ async function dispatchDiscordCommandInteraction(params: {
threadChannel: {
id: rawChannelId,
name: channelName,
parentId: "parentId" in channel ? channel.parentId ?? undefined : undefined,
parentId: "parentId" in channel ? (channel.parentId ?? undefined) : undefined,
parent: undefined,
},
channelInfo,

View File

@@ -55,11 +55,7 @@ describe("memory indexing with OpenAI batches", () => {
let uploadedRequests: Array<{ custom_id?: string }> = [];
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.toString()
: input.url;
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
if (url.endsWith("/files")) {
const body = init?.body;
if (!(body instanceof FormData)) {

View File

@@ -71,10 +71,7 @@ describe("memory embedding batches", () => {
await manager.sync({ force: true });
const status = manager.status();
const totalTexts = embedBatch.mock.calls.reduce(
(sum, call) => sum + (call[0]?.length ?? 0),
0,
);
const totalTexts = embedBatch.mock.calls.reduce((sum, call) => sum + (call[0]?.length ?? 0), 0);
expect(totalTexts).toBe(status.chunks);
expect(embedBatch.mock.calls.length).toBeGreaterThan(1);
});

View File

@@ -711,7 +711,10 @@ export class MemoryIndexManager {
}));
}
private shouldSyncSessions(params?: { reason?: string; force?: boolean }, needsFullReindex = false) {
private shouldSyncSessions(
params?: { reason?: string; force?: boolean },
needsFullReindex = false,
) {
if (!this.sources.has("sessions")) return false;
if (params?.force) return true;
const reason = params?.reason;
@@ -876,9 +879,7 @@ export class MemoryIndexManager {
force?: boolean;
progress?: (update: MemorySyncProgressUpdate) => void;
}) {
const progress = params?.progress
? this.createSyncProgress(params.progress)
: undefined;
const progress = params?.progress ? this.createSyncProgress(params.progress) : undefined;
const vectorReady = await this.ensureVectorReady();
const meta = this.readMeta();
const needsFullReindex =
@@ -972,7 +973,10 @@ export class MemoryIndexManager {
}
private normalizeSessionText(value: string): string {
return value.replace(/\s*\n+\s*/g, " ").replace(/\s+/g, " ").trim();
return value
.replace(/\s*\n+\s*/g, " ")
.replace(/\s+/g, " ")
.trim();
}
private extractSessionText(content: unknown): string | null {

View File

@@ -17,7 +17,14 @@ export function normalizeAllowListLower(list?: Array<string | number>) {
export type SlackAllowListMatch = {
allowed: boolean;
matchKey?: string;
matchSource?: "wildcard" | "id" | "prefixed-id" | "prefixed-user" | "name" | "prefixed-name" | "slug";
matchSource?:
| "wildcard"
| "id"
| "prefixed-id"
| "prefixed-user"
| "name"
| "prefixed-name"
| "slug";
};
export function resolveSlackAllowListMatch(params: {

View File

@@ -1,6 +1,9 @@
import type { SlackReactionNotificationMode } from "../../config/config.js";
import type { SlackMessageEvent } from "../types.js";
import { buildChannelKeyCandidates, resolveChannelEntryMatch } from "../../channels/channel-config.js";
import {
buildChannelKeyCandidates,
resolveChannelEntryMatch,
} from "../../channels/channel-config.js";
import { allowListMatches, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js";
export type SlackChannelConfigResolved = {

View File

@@ -77,7 +77,10 @@ async function callSlack(
}
}
export async function fetchSlackScopes(token: string, timeoutMs: number): Promise<SlackScopesResult> {
export async function fetchSlackScopes(
token: string,
timeoutMs: number,
): Promise<SlackScopesResult> {
const client = new WebClient(token, { timeout: timeoutMs });
const attempts: SlackScopesSource[] = ["auth.scopes", "apps.permissions.info"];
const errors: string[] = [];

View File

@@ -115,8 +115,8 @@ export async function auditTelegramGroupMembership(params: {
matchKey: chatId,
matchSource: "id",
});
continue;
}
continue;
}
const status = isRecord((json as TelegramApiOk<unknown>).result)
? ((json as TelegramApiOk<{ status?: string }>).result.status ?? null)
: null;

View File

@@ -48,8 +48,6 @@ describe("expandTextLinks", () => {
it("preserves offsets from the original string", () => {
const text = " Hello world";
const entities = [{ type: "text_link", offset: 1, length: 5, url: "https://example.com" }];
expect(expandTextLinks(text, entities)).toBe(
" [Hello](https://example.com) world",
);
expect(expandTextLinks(text, entities)).toBe(" [Hello](https://example.com) world");
});
});

View File

@@ -121,10 +121,7 @@ type TelegramTextLinkEntity = {
url?: string;
};
export function expandTextLinks(
text: string,
entities?: TelegramTextLinkEntity[] | null,
): string {
export function expandTextLinks(text: string, entities?: TelegramTextLinkEntity[] | null): string {
if (!text || !entities?.length) return text;
const textLinks = entities
@@ -140,7 +137,8 @@ export function expandTextLinks(
for (const entity of textLinks) {
const linkText = text.slice(entity.offset, entity.offset + entity.length);
const markdown = `[${linkText}](${entity.url})`;
result = result.slice(0, entity.offset) + markdown + result.slice(entity.offset + entity.length);
result =
result.slice(0, entity.offset) + markdown + result.slice(entity.offset + entity.length);
}
return result;
}

View File

@@ -70,9 +70,7 @@ export async function probeTelegram(
id: meJson.result?.id ?? null,
username: meJson.result?.username ?? null,
canJoinGroups:
typeof meJson.result?.can_join_groups === "boolean"
? meJson.result?.can_join_groups
: null,
typeof meJson.result?.can_join_groups === "boolean" ? meJson.result?.can_join_groups : null,
canReadAllGroupMessages:
typeof meJson.result?.can_read_all_group_messages === "boolean"
? meJson.result?.can_read_all_group_messages

View File

@@ -180,7 +180,9 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
} catch (err) {
installError = err instanceof Error ? err.message : String(err);
} finally {
progress.stop(installError ? "Gateway daemon install failed." : "Gateway daemon installed.");
progress.stop(
installError ? "Gateway daemon install failed." : "Gateway daemon installed.",
);
}
if (installError) {
await prompter.note(`Gateway daemon install failed: ${installError}`, "Gateway");