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, enabled: overrides?.remote?.batch?.enabled ?? defaults?.remote?.batch?.enabled ?? true,
wait: overrides?.remote?.batch?.wait ?? defaults?.remote?.batch?.wait ?? true, wait: overrides?.remote?.batch?.wait ?? defaults?.remote?.batch?.wait ?? true,
pollIntervalMs: pollIntervalMs:
overrides?.remote?.batch?.pollIntervalMs ?? overrides?.remote?.batch?.pollIntervalMs ?? defaults?.remote?.batch?.pollIntervalMs ?? 5000,
defaults?.remote?.batch?.pollIntervalMs ??
5000,
timeoutMinutes: timeoutMinutes:
overrides?.remote?.batch?.timeoutMinutes ?? overrides?.remote?.batch?.timeoutMinutes ?? defaults?.remote?.batch?.timeoutMinutes ?? 60,
defaults?.remote?.batch?.timeoutMinutes ??
60,
}; };
const remote = includeRemote const remote = includeRemote
? { ? {

View File

@@ -112,8 +112,9 @@ export function installSessionToolResultGuard(sessionManager: SessionManager): {
const result = originalAppend(sanitized as never); const result = originalAppend(sanitized as never);
const sessionFile = (sessionManager as { getSessionFile?: () => string | null }) const sessionFile = (
.getSessionFile?.(); sessionManager as { getSessionFile?: () => string | null }
).getSessionFile?.();
if (sessionFile) { if (sessionFile) {
emitSessionTranscriptUpdate(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", () => { it("keeps exec flags outside markdown and moves them to the front", () => {
vi.stubEnv("HOME", "/Users/test"); vi.stubEnv("HOME", "/Users/test");
const out = formatToolAggregate( const out = formatToolAggregate("exec", ["cd /Users/test/dir && gemini 2>&1 · elevated"], {
"exec", markdown: true,
["cd /Users/test/dir && gemini 2>&1 · elevated"], });
{ markdown: true },
);
expect(out).toBe("🛠️ exec: elevated · `cd ~/dir && gemini 2>&1`"); expect(out).toBe("🛠️ exec: elevated · `cd ~/dir && gemini 2>&1`");
}); });

View File

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

View File

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

View File

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

View File

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

View File

@@ -110,7 +110,9 @@ export function registerMemoryCli(program: Command) {
return; return;
} }
if (opts.index) { 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); defaultRuntime.log(line);
} }
const rich = isRich(); const rich = isRich();
@@ -127,7 +129,9 @@ export function registerMemoryCli(program: Command) {
`(requested: ${status.requestedProvider})`, `(requested: ${status.requestedProvider})`,
)}`, )}`,
`${label("Model")} ${info(status.model)}`, `${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("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)}`,

View File

@@ -5,18 +5,15 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ChannelPlugin } from "../../channels/plugins/types.js"; import type { ChannelPlugin } from "../../channels/plugins/types.js";
import { channelsCapabilitiesCommand } from "./capabilities.js"; import { channelsCapabilitiesCommand } from "./capabilities.js";
import { fetchSlackScopes } from "../../slack/scopes.js"; import { fetchSlackScopes } from "../../slack/scopes.js";
import { import { getChannelPlugin, listChannelPlugins } from "../../channels/plugins/index.js";
getChannelPlugin,
listChannelPlugins,
} from "../../channels/plugins/index.js";
const logs: string[] = []; const logs: string[] = [];
const errors: string[] = []; const errors: string[] = [];
vi.mock("./shared.js", () => ({ vi.mock("./shared.js", () => ({
requireValidConfig: vi.fn(async () => ({ channels: {} })), requireValidConfig: vi.fn(async () => ({ channels: {} })),
formatChannelAccountLabel: vi.fn(({ channel, accountId }: { channel: string; accountId: string }) => formatChannelAccountLabel: vi.fn(
`${channel}:${accountId}`, ({ 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 flags: string[] = [];
const canJoinGroups = (bot as { canJoinGroups?: boolean | null })?.canJoinGroups; const canJoinGroups = (bot as { canJoinGroups?: boolean | null })?.canJoinGroups;
const canReadAll = (bot as { canReadAllGroupMessages?: boolean | null })?.canReadAllGroupMessages; const canReadAll = (bot as { canReadAllGroupMessages?: boolean | null })
const inlineQueries = (bot as { supportsInlineQueries?: boolean | null })?.supportsInlineQueries; ?.canReadAllGroupMessages;
const inlineQueries = (bot as { supportsInlineQueries?: boolean | null })
?.supportsInlineQueries;
if (typeof canJoinGroups === "boolean") flags.push(`joinGroups=${canJoinGroups}`); if (typeof canJoinGroups === "boolean") flags.push(`joinGroups=${canJoinGroups}`);
if (typeof canReadAll === "boolean") flags.push(`readAllGroupMessages=${canReadAll}`); if (typeof canReadAll === "boolean") flags.push(`readAllGroupMessages=${canReadAll}`);
if (typeof inlineQueries === "boolean") flags.push(`inlineQueries=${inlineQueries}`); 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) const roles = Array.isArray(graph.roles)
? graph.roles.map((role) => String(role).trim()).filter(Boolean) ? graph.roles.map((role) => String(role).trim()).filter(Boolean)
: []; : [];
const scopes = typeof graph.scopes === "string" const scopes =
? graph.scopes typeof graph.scopes === "string"
.split(/\s+/) ? graph.scopes
.map((scope) => scope.trim()) .split(/\s+/)
.filter(Boolean) .map((scope) => scope.trim())
: Array.isArray(graph.scopes) .filter(Boolean)
? graph.scopes.map((scope) => String(scope).trim()).filter(Boolean) : Array.isArray(graph.scopes)
: []; ? graph.scopes.map((scope) => String(scope).trim()).filter(Boolean)
: [];
if (graph.ok === false) { if (graph.ok === false) {
lines.push(`Graph: ${theme.error(graph.error ?? "failed")}`); lines.push(`Graph: ${theme.error(graph.error ?? "failed")}`);
} else if (roles.length > 0 || scopes.length > 0) { } else if (roles.length > 0 || scopes.length > 0) {
@@ -219,7 +222,8 @@ function formatProbeLines(channelId: string, probe: unknown): string[] {
lines.push("Probe: ok"); lines.push("Probe: ok");
} }
if (ok === false) { 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}`)}`); lines.push(`Probe: ${theme.error(`failed${error}`)}`);
} }
return lines; return lines;
@@ -388,8 +392,7 @@ export async function channelsCapabilitiesCommand(
const cfg = await requireValidConfig(runtime); const cfg = await requireValidConfig(runtime);
if (!cfg) return; if (!cfg) return;
const timeoutMs = normalizeTimeout(opts.timeout, 10_000); const timeoutMs = normalizeTimeout(opts.timeout, 10_000);
const rawChannel = const rawChannel = typeof opts.channel === "string" ? opts.channel.trim().toLowerCase() : "";
typeof opts.channel === "string" ? opts.channel.trim().toLowerCase() : "";
const rawTarget = typeof opts.target === "string" ? opts.target.trim() : ""; const rawTarget = typeof opts.target === "string" ? opts.target.trim() : "";
if (opts.account && (!rawChannel || rawChannel === "all")) { if (opts.account && (!rawChannel || rawChannel === "all")) {
@@ -483,9 +486,7 @@ export async function channelsCapabilitiesCommand(
const label = perms.channelId ? ` (${perms.channelId})` : ""; const label = perms.channelId ? ` (${perms.channelId})` : "";
lines.push(`Permissions${label}: ${list}`); lines.push(`Permissions${label}: ${list}`);
if (perms.missingRequired && perms.missingRequired.length > 0) { if (perms.missingRequired && perms.missingRequired.length > 0) {
lines.push( lines.push(`${theme.warn("Missing required:")} ${perms.missingRequired.join(", ")}`);
`${theme.warn("Missing required:")} ${perms.missingRequired.join(", ")}`,
);
} else { } else {
lines.push(theme.success("Missing required: none")); lines.push(theme.success("Missing required: none"));
} }

View File

@@ -34,9 +34,10 @@ afterEach(() => {
describe("resolveGatewayDevMode", () => { describe("resolveGatewayDevMode", () => {
it("detects dev mode for src ts entrypoints", () => { it("detects dev mode for src ts entrypoints", () => {
expect( expect(resolveGatewayDevMode(["node", "/Users/me/clawdbot/src/cli/index.ts"])).toBe(true);
resolveGatewayDevMode(["node", "/Users/me/clawdbot/src/cli/index.ts"]), expect(resolveGatewayDevMode(["node", "C:\\Users\\me\\clawdbot\\src\\cli\\index.ts"])).toBe(
).toBe(true); true,
);
expect(resolveGatewayDevMode(["node", "/Users/me/clawdbot/dist/cli/index.js"])).toBe(false); 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 { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
import { resolveGatewayProgramArguments } from "../daemon/program-args.js"; import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
import { import {
@@ -20,7 +18,8 @@ export type GatewayInstallPlan = {
export function resolveGatewayDevMode(argv: string[] = process.argv): boolean { export function resolveGatewayDevMode(argv: string[] = process.argv): boolean {
const entry = argv[1]; 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: { export async function buildGatewayInstallPlan(params: {

View File

@@ -15,10 +15,7 @@ import {
GATEWAY_DAEMON_RUNTIME_OPTIONS, GATEWAY_DAEMON_RUNTIME_OPTIONS,
type GatewayDaemonRuntime, type GatewayDaemonRuntime,
} from "./daemon-runtime.js"; } from "./daemon-runtime.js";
import { import { buildGatewayInstallPlan, gatewayInstallErrorHint } from "./daemon-install-helpers.js";
buildGatewayInstallPlan,
gatewayInstallErrorHint,
} from "./daemon-install-helpers.js";
import { buildGatewayRuntimeHints, formatGatewayRuntimeSummary } from "./doctor-format.js"; import { buildGatewayRuntimeHints, formatGatewayRuntimeSummary } from "./doctor-format.js";
import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js"; import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js";
import { healthCommand } from "./health.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 { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js";
import { findExtraGatewayServices, renderGatewayServiceCleanupHints } from "../daemon/inspect.js"; import { findExtraGatewayServices, renderGatewayServiceCleanupHints } from "../daemon/inspect.js";
import { findLegacyGatewayServices, uninstallLegacyGatewayServices } from "../daemon/legacy.js"; import { findLegacyGatewayServices, uninstallLegacyGatewayServices } from "../daemon/legacy.js";
import { import { renderSystemNodeWarning, resolveSystemNodeInfo } from "../daemon/runtime-paths.js";
renderSystemNodeWarning,
resolveSystemNodeInfo,
} from "../daemon/runtime-paths.js";
import { resolveGatewayService } from "../daemon/service.js"; import { resolveGatewayService } from "../daemon/service.js";
import { import {
auditGatewayServiceConfig, auditGatewayServiceConfig,
@@ -16,10 +13,7 @@ import {
} from "../daemon/service-audit.js"; } from "../daemon/service-audit.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import { note } from "../terminal/note.js"; import { note } from "../terminal/note.js";
import { import { buildGatewayInstallPlan, gatewayInstallErrorHint } from "./daemon-install-helpers.js";
buildGatewayInstallPlan,
gatewayInstallErrorHint,
} from "./daemon-install-helpers.js";
import { import {
DEFAULT_GATEWAY_DAEMON_RUNTIME, DEFAULT_GATEWAY_DAEMON_RUNTIME,
GATEWAY_DAEMON_RUNTIME_OPTIONS, GATEWAY_DAEMON_RUNTIME_OPTIONS,

View File

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

View File

@@ -1,6 +1,9 @@
import type { Guild, User } from "@buape/carbon"; 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"; import { formatDiscordUserTag } from "./format.js";
export type DiscordAllowList = { export type DiscordAllowList = {

View File

@@ -266,7 +266,9 @@ export async function preflightDiscordMessage(
channelConfig?.matchSource ?? "none" channelConfig?.matchSource ?? "none"
}`; }`;
if (isGuildMessage && channelConfig?.enabled === false) { 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; return null;
} }

View File

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

View File

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

View File

@@ -71,10 +71,7 @@ describe("memory embedding batches", () => {
await manager.sync({ force: true }); await manager.sync({ force: true });
const status = manager.status(); const status = manager.status();
const totalTexts = embedBatch.mock.calls.reduce( const totalTexts = embedBatch.mock.calls.reduce((sum, call) => sum + (call[0]?.length ?? 0), 0);
(sum, call) => sum + (call[0]?.length ?? 0),
0,
);
expect(totalTexts).toBe(status.chunks); expect(totalTexts).toBe(status.chunks);
expect(embedBatch.mock.calls.length).toBeGreaterThan(1); 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 (!this.sources.has("sessions")) return false;
if (params?.force) return true; if (params?.force) return true;
const reason = params?.reason; const reason = params?.reason;
@@ -876,9 +879,7 @@ export class MemoryIndexManager {
force?: boolean; force?: boolean;
progress?: (update: MemorySyncProgressUpdate) => void; progress?: (update: MemorySyncProgressUpdate) => void;
}) { }) {
const progress = params?.progress const progress = params?.progress ? this.createSyncProgress(params.progress) : undefined;
? this.createSyncProgress(params.progress)
: undefined;
const vectorReady = await this.ensureVectorReady(); const vectorReady = await this.ensureVectorReady();
const meta = this.readMeta(); const meta = this.readMeta();
const needsFullReindex = const needsFullReindex =
@@ -972,7 +973,10 @@ export class MemoryIndexManager {
} }
private normalizeSessionText(value: string): string { 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 { private extractSessionText(content: unknown): string | null {

View File

@@ -17,7 +17,14 @@ export function normalizeAllowListLower(list?: Array<string | number>) {
export type SlackAllowListMatch = { export type SlackAllowListMatch = {
allowed: boolean; allowed: boolean;
matchKey?: string; 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: { export function resolveSlackAllowListMatch(params: {

View File

@@ -1,6 +1,9 @@
import type { SlackReactionNotificationMode } from "../../config/config.js"; import type { SlackReactionNotificationMode } from "../../config/config.js";
import type { SlackMessageEvent } from "../types.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"; import { allowListMatches, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js";
export type SlackChannelConfigResolved = { 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 client = new WebClient(token, { timeout: timeoutMs });
const attempts: SlackScopesSource[] = ["auth.scopes", "apps.permissions.info"]; const attempts: SlackScopesSource[] = ["auth.scopes", "apps.permissions.info"];
const errors: string[] = []; const errors: string[] = [];

View File

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

View File

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

View File

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

View File

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

View File

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