fix(channels): keep bundled setup entries dependency-light

This commit is contained in:
Peter Steinberger
2026-04-24 06:09:34 +01:00
parent d91f6a05c6
commit d32fdcebc1
16 changed files with 605 additions and 18 deletions

View File

@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
- Codex harness: route native `request_user_input` prompts back to the originating chat, preserve queued follow-up answers, and honor newer app-server command approval amendment decisions.
- Codex harness/context-engine: redact context-engine assembly failures before logging, so fallback warnings do not serialize raw error objects. (#70809) Thanks @jalehman.
- WhatsApp/onboarding: keep first-run setup entry loading off the Baileys runtime dependency path, so packaged QuickStart installs can show WhatsApp setup before runtime deps are staged. Fixes #70932.
- Block streaming: suppress final assembled text after partial block-delivery aborts when the already-sent text chunks exactly cover the final reply, preventing duplicate replies without dropping unrelated short messages. Fixes #70921.
- Codex harness/Windows: resolve npm-installed `codex.cmd` shims through PATHEXT before starting the native app-server, so `codex/*` models work without a manual `.exe` shim. Fixes #70913.
- Slack/groups: classify MPIM group DMs as group chat context and suppress verbose tool/plan progress on Slack non-DM surfaces, so internal "Working…" traces no longer leak into rooms. Fixes #70912.

View File

@@ -3,8 +3,8 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr
export default defineBundledChannelSetupEntry({
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
exportName: "googlechatPlugin",
specifier: "./setup-plugin-api.js",
exportName: "googlechatSetupPlugin",
},
secrets: {
specifier: "./secret-contract-api.js",

View File

@@ -0,0 +1,3 @@
// Keep bundled setup entry imports narrow so setup loads do not pull the
// broader Google Chat runtime plugin surface.
export { googlechatSetupPlugin } from "./src/channel.setup.js";

View File

@@ -0,0 +1,92 @@
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
import {
adaptScopedAccountAccessor,
createScopedChannelConfigAdapter,
} from "openclaw/plugin-sdk/channel-config-helpers";
import type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
listGoogleChatAccountIds,
resolveDefaultGoogleChatAccountId,
resolveGoogleChatAccount,
type ResolvedGoogleChatAccount,
} from "./accounts.js";
import { googlechatSetupAdapter } from "./setup-core.js";
import { googlechatSetupWizard } from "./setup-surface.js";
const formatGoogleChatAllowFromEntry = (entry: string) =>
normalizeLowercaseStringOrEmpty(
entry
.trim()
.replace(/^(googlechat|google-chat|gchat):/i, "")
.replace(/^user:/i, "")
.replace(/^users\//i, ""),
);
const googleChatConfigAdapter = createScopedChannelConfigAdapter<ResolvedGoogleChatAccount>({
sectionKey: "googlechat",
listAccountIds: listGoogleChatAccountIds,
resolveAccount: adaptScopedAccountAccessor(resolveGoogleChatAccount),
defaultAccountId: resolveDefaultGoogleChatAccountId,
clearBaseFields: [
"serviceAccount",
"serviceAccountFile",
"audienceType",
"audience",
"webhookPath",
"webhookUrl",
"botUser",
"name",
],
resolveAllowFrom: (account) => account.config.dm?.allowFrom,
formatAllowFrom: (allowFrom) =>
formatNormalizedAllowFromEntries({
allowFrom,
normalizeEntry: formatGoogleChatAllowFromEntry,
}),
resolveDefaultTo: (account) => account.config.defaultTo,
});
export const googlechatSetupPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
id: "googlechat",
meta: {
id: "googlechat",
label: "Google Chat",
selectionLabel: "Google Chat (Chat API)",
docsPath: "/channels/googlechat",
docsLabel: "googlechat",
blurb: "Google Workspace Chat app with HTTP webhook.",
aliases: ["gchat", "google-chat"],
order: 55,
detailLabel: "Google Chat",
systemImage: "message.badge",
markdownCapable: true,
},
setup: googlechatSetupAdapter,
setupWizard: googlechatSetupWizard,
capabilities: {
chatTypes: ["direct", "group", "thread"],
reactions: true,
threads: true,
media: true,
nativeCommands: false,
blockStreaming: true,
},
streaming: {
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
},
reload: { configPrefixes: ["channels.googlechat"] },
config: {
...googleChatConfigAdapter,
isConfigured: (account) => account.credentialSource !== "none",
describeAccount: (account) =>
describeAccountSnapshot({
account,
configured: account.credentialSource !== "none",
extra: {
credentialSource: account.credentialSource,
},
}),
},
};

View File

@@ -3,8 +3,8 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr
export default defineBundledChannelSetupEntry({
importMetaUrl: import.meta.url,
plugin: {
specifier: "./channel-plugin-api.js",
exportName: "matrixPlugin",
specifier: "./setup-plugin-api.js",
exportName: "matrixSetupPlugin",
},
secrets: {
specifier: "./secret-contract-api.js",

View File

@@ -0,0 +1,3 @@
// Keep bundled setup entry imports narrow so setup loads do not pull the
// broader Matrix runtime plugin surface.
export { matrixSetupPlugin } from "./src/channel.setup.js";

View File

@@ -0,0 +1,49 @@
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-primitives";
import type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
import { matrixConfigAdapter } from "./config-adapter.js";
import { MatrixConfigSchema } from "./config-schema.js";
import { resolveMatrixAccount, type ResolvedMatrixAccount } from "./matrix/accounts.js";
import { createMatrixSetupWizardProxy, matrixSetupAdapter } from "./setup-core.js";
const matrixSetupWizard = createMatrixSetupWizardProxy(async () => ({
matrixSetupWizard: (await import("./setup-surface.js")).matrixSetupWizard,
}));
export const matrixSetupPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
id: "matrix",
meta: {
id: "matrix",
label: "Matrix",
selectionLabel: "Matrix (plugin)",
docsPath: "/channels/matrix",
docsLabel: "matrix",
blurb: "open protocol; configure a homeserver + access token.",
order: 70,
quickstartAllowFrom: true,
},
setupWizard: matrixSetupWizard,
setup: matrixSetupAdapter,
capabilities: {
chatTypes: ["direct", "group", "thread"],
polls: true,
reactions: true,
threads: true,
media: true,
},
reload: { configPrefixes: ["channels.matrix"] },
configSchema: buildChannelConfigSchema(MatrixConfigSchema),
config: {
...matrixConfigAdapter,
isConfigured: (account) => account.configured,
describeAccount: (account) =>
describeAccountSnapshot({
account,
configured: account.configured,
extra: {
baseUrl: account.homeserver,
},
}),
hasConfiguredState: ({ cfg }) => resolveMatrixAccount({ cfg }).configured,
},
};

View File

@@ -3,7 +3,7 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr
export default defineBundledChannelSetupEntry({
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
exportName: "nostrPlugin",
specifier: "./setup-plugin-api.js",
exportName: "nostrSetupPlugin",
},
});

View File

@@ -0,0 +1,3 @@
// Keep bundled setup entry imports narrow so setup loads do not pull the
// broader Nostr runtime plugin surface.
export { nostrSetupPlugin } from "./src/channel.setup.js";

View File

@@ -0,0 +1,231 @@
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { patchTopLevelChannelConfigSection } from "openclaw/plugin-sdk/setup";
import {
createDelegatedSetupWizardProxy,
createStandardChannelSetupStatus,
DEFAULT_ACCOUNT_ID,
type ChannelSetupAdapter,
} from "openclaw/plugin-sdk/setup-runtime";
import { buildChannelConfigSchema, type ChannelPlugin } from "./channel-api.js";
import { NostrConfigSchema } from "./config-schema.js";
import { DEFAULT_RELAYS } from "./default-relays.js";
const channel = "nostr" as const;
type NostrAccountConfig = {
enabled?: boolean;
name?: string;
defaultAccount?: string;
privateKey?: unknown;
relays?: string[];
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
allowFrom?: Array<string | number>;
profile?: unknown;
};
type ResolvedNostrSetupAccount = {
accountId: string;
name?: string;
enabled: boolean;
configured: boolean;
privateKey: string;
publicKey: string;
relays: string[];
profile?: unknown;
config: NostrAccountConfig;
};
function getNostrConfig(cfg: OpenClawConfig): NostrAccountConfig | undefined {
return (cfg.channels as Record<string, unknown> | undefined)?.nostr as
| NostrAccountConfig
| undefined;
}
function listSetupNostrAccountIds(cfg: OpenClawConfig): string[] {
const nostrCfg = getNostrConfig(cfg);
const privateKey = typeof nostrCfg?.privateKey === "string" ? nostrCfg.privateKey.trim() : "";
if (!privateKey) {
return [];
}
return [resolveDefaultSetupNostrAccountId(cfg)];
}
function resolveDefaultSetupNostrAccountId(cfg: OpenClawConfig): string {
const configured = getNostrConfig(cfg)?.defaultAccount;
return typeof configured === "string" && configured.trim()
? configured.trim()
: DEFAULT_ACCOUNT_ID;
}
function resolveSetupNostrAccount(params: {
cfg: OpenClawConfig;
accountId?: string | null;
}): ResolvedNostrSetupAccount {
const nostrCfg = getNostrConfig(params.cfg);
const accountId = params.accountId?.trim() || resolveDefaultSetupNostrAccountId(params.cfg);
const privateKey = typeof nostrCfg?.privateKey === "string" ? nostrCfg.privateKey.trim() : "";
const configured = Boolean(privateKey);
return {
accountId,
name: typeof nostrCfg?.name === "string" ? nostrCfg.name : undefined,
enabled: nostrCfg?.enabled !== false,
configured,
privateKey,
publicKey: "",
relays: nostrCfg?.relays ?? DEFAULT_RELAYS,
profile: nostrCfg?.profile,
config: {
enabled: nostrCfg?.enabled,
name: nostrCfg?.name,
privateKey: nostrCfg?.privateKey,
relays: nostrCfg?.relays,
dmPolicy: nostrCfg?.dmPolicy,
allowFrom: nostrCfg?.allowFrom,
profile: nostrCfg?.profile,
},
};
}
function buildNostrSetupPatch(accountId: string, patch: Record<string, unknown>) {
return {
...(accountId !== DEFAULT_ACCOUNT_ID ? { defaultAccount: accountId } : {}),
...patch,
};
}
function parseRelayUrls(raw: string): { relays: string[]; error?: string } {
const entries = raw
.split(/[,\n]/)
.map((entry) => entry.trim())
.filter(Boolean);
const relays: string[] = [];
for (const entry of entries) {
try {
const parsed = new URL(entry);
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
return { relays: [], error: `Relay must use ws:// or wss:// (${entry})` };
}
} catch {
return { relays: [], error: `Invalid relay URL: ${entry}` };
}
relays.push(entry);
}
return { relays: [...new Set(relays)] };
}
function looksLikeNostrPrivateKey(privateKey: string): boolean {
return privateKey.startsWith("nsec1") || /^[0-9a-fA-F]{64}$/.test(privateKey);
}
const nostrSetupAdapter: ChannelSetupAdapter = {
resolveAccountId: ({ cfg, accountId }) =>
accountId?.trim() || resolveDefaultSetupNostrAccountId(cfg),
applyAccountName: ({ cfg, accountId, name }) =>
patchTopLevelChannelConfigSection({
cfg,
channel,
patch: buildNostrSetupPatch(accountId, name?.trim() ? { name: name.trim() } : {}),
}),
validateInput: ({ input }) => {
const typedInput = input as {
useEnv?: boolean;
privateKey?: string;
relayUrls?: string;
};
if (!typedInput.useEnv) {
const privateKey = typedInput.privateKey?.trim();
if (!privateKey) {
return "Nostr requires --private-key or --use-env.";
}
if (!looksLikeNostrPrivateKey(privateKey)) {
return "Nostr private key must be valid nsec or 64-character hex.";
}
}
if (typedInput.relayUrls?.trim()) {
return parseRelayUrls(typedInput.relayUrls).error ?? null;
}
return null;
},
applyAccountConfig: ({ cfg, accountId, input }) => {
const typedInput = input as {
useEnv?: boolean;
privateKey?: string;
relayUrls?: string;
};
const relayResult = typedInput.relayUrls?.trim()
? parseRelayUrls(typedInput.relayUrls)
: { relays: [] };
return patchTopLevelChannelConfigSection({
cfg,
channel,
enabled: true,
clearFields: typedInput.useEnv ? ["privateKey"] : undefined,
patch: buildNostrSetupPatch(accountId, {
...(typedInput.useEnv ? {} : { privateKey: typedInput.privateKey?.trim() }),
...(relayResult.relays.length > 0 ? { relays: relayResult.relays } : {}),
}),
});
},
};
const nostrSetupWizard = createDelegatedSetupWizardProxy({
channel,
loadWizard: async () => (await import("./setup-surface.js")).nostrSetupWizard,
status: {
...createStandardChannelSetupStatus({
channelLabel: "Nostr",
configuredLabel: "configured",
unconfiguredLabel: "needs private key",
configuredHint: "configured",
unconfiguredHint: "needs private key",
configuredScore: 1,
unconfiguredScore: 0,
includeStatusLine: true,
resolveConfigured: ({ cfg, accountId }) =>
resolveSetupNostrAccount({ cfg, accountId }).configured,
resolveExtraStatusLines: ({ cfg }) => {
const account = resolveSetupNostrAccount({ cfg });
return [`Relays: ${account.relays.length || DEFAULT_RELAYS.length}`];
},
}),
},
resolveShouldPromptAccountIds: () => false,
delegatePrepare: true,
delegateFinalize: true,
});
export const nostrSetupPlugin: ChannelPlugin<ResolvedNostrSetupAccount> = {
id: channel,
meta: {
id: channel,
label: "Nostr",
selectionLabel: "Nostr",
docsPath: "/channels/nostr",
docsLabel: "nostr",
blurb: "Decentralized DMs via Nostr relays (NIP-04)",
order: 100,
},
capabilities: {
chatTypes: ["direct"],
media: false,
},
reload: { configPrefixes: ["channels.nostr"] },
configSchema: buildChannelConfigSchema(NostrConfigSchema),
setup: nostrSetupAdapter,
setupWizard: nostrSetupWizard,
config: {
listAccountIds: listSetupNostrAccountIds,
resolveAccount: (cfg, accountId) => resolveSetupNostrAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultSetupNostrAccountId,
isConfigured: (account) => account.configured,
describeAccount: (account) =>
describeAccountSnapshot({
account,
configured: account.configured,
extra: {
publicKey: account.publicKey,
},
}),
},
};

View File

@@ -3,7 +3,7 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr
export default defineBundledChannelSetupEntry({
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./setup-plugin-api.js",
exportName: "qqbotSetupPlugin",
},
});

View File

@@ -0,0 +1,3 @@
// Keep bundled setup entry imports narrow so setup loads do not pull the
// broader QQ Bot runtime plugin surface.
export { qqbotSetupPlugin } from "./src/channel.setup.js";

View File

@@ -1,12 +1,79 @@
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import {
adaptScopedAccountAccessor,
createScopedChannelConfigAdapter,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { type ResolvedSlackAccount } from "./accounts.js";
import {
listSlackAccountIds,
resolveDefaultSlackAccountId,
resolveSlackAccount,
} from "./accounts.js";
import { type ChannelPlugin } from "./channel-api.js";
import { slackSetupAdapter } from "./setup-core.js";
import { slackSetupWizard } from "./setup-surface.js";
import { createSlackPluginBase } from "./shared.js";
import { SlackChannelConfigSchema } from "./config-schema.js";
import { slackSetupAdapter, createSlackSetupWizardProxy } from "./setup-core.js";
import {
describeSlackSetupAccount,
isSlackSetupAccountConfigured,
SLACK_CHANNEL,
} from "./setup-shared.js";
const slackSetupWizard = createSlackSetupWizardProxy(async () => ({
slackSetupWizard: (await import("./setup-surface.js")).slackSetupWizard,
}));
const slackSetupConfigAdapter = createScopedChannelConfigAdapter<ResolvedSlackAccount>({
sectionKey: SLACK_CHANNEL,
listAccountIds: listSlackAccountIds,
resolveAccount: adaptScopedAccountAccessor(resolveSlackAccount),
defaultAccountId: resolveDefaultSlackAccountId,
clearBaseFields: ["botToken", "appToken", "name"],
resolveAllowFrom: (account) => account.dm?.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
resolveDefaultTo: (account) => account.config.defaultTo,
});
export const slackSetupPlugin: ChannelPlugin<ResolvedSlackAccount> = {
...createSlackPluginBase({
setupWizard: slackSetupWizard,
setup: slackSetupAdapter,
}),
id: SLACK_CHANNEL,
meta: {
id: SLACK_CHANNEL,
label: "Slack",
selectionLabel: "Slack (Socket Mode)",
detailLabel: "Slack Bot",
docsPath: "/channels/slack",
docsLabel: "slack",
blurb: "supported (Socket Mode).",
systemImage: "number",
markdownCapable: true,
preferSessionLookupForAnnounceTarget: true,
},
setupWizard: slackSetupWizard,
capabilities: {
chatTypes: ["direct", "channel", "thread"],
reactions: true,
threads: true,
media: true,
nativeCommands: true,
},
commands: {
nativeCommandsAutoEnabled: false,
nativeSkillsAutoEnabled: false,
resolveNativeCommandName: ({ commandKey, defaultName }) =>
commandKey === "status" ? "agentstatus" : defaultName,
},
streaming: {
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
},
reload: { configPrefixes: ["channels.slack"] },
configSchema: SlackChannelConfigSchema,
config: {
...slackSetupConfigAdapter,
hasConfiguredState: ({ env }) =>
["SLACK_APP_TOKEN", "SLACK_BOT_TOKEN", "SLACK_USER_TOKEN"].some(
(key) => typeof env?.[key] === "string" && env[key]?.trim().length > 0,
),
isConfigured: (account) => isSlackSetupAccountConfigured(account),
describeAccount: (account) => describeSlackSetupAccount(account),
},
setup: slackSetupAdapter,
};

View File

@@ -24,10 +24,10 @@ import { inspectSlackAccount } from "./account-inspect.js";
import { resolveSlackAccount } from "./accounts.js";
import {
buildSlackSetupLines,
SLACK_CHANNEL as channel,
isSlackSetupAccountConfigured,
SLACK_CHANNEL as channel,
setSlackChannelAllowlist,
} from "./shared.js";
} from "./setup-shared.js";
function enableSlackAccount(cfg: OpenClawConfig, accountId: string): OpenClawConfig {
return patchChannelConfigForAccount({

View File

@@ -0,0 +1,131 @@
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input";
import { patchChannelConfigForAccount } from "openclaw/plugin-sdk/setup-runtime";
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
import type { ResolvedSlackAccount } from "./accounts.js";
import type { OpenClawConfig } from "./channel-api.js";
export const SLACK_CHANNEL = "slack" as const;
function buildSlackManifest(botName: string) {
const safeName = botName.trim() || "OpenClaw";
const manifest = {
display_information: {
name: safeName,
description: `${safeName} connector for OpenClaw`,
},
features: {
bot_user: {
display_name: safeName,
always_online: true,
},
app_home: {
messages_tab_enabled: true,
messages_tab_read_only_enabled: false,
},
slash_commands: [
{
command: "/openclaw",
description: "Send a message to OpenClaw",
should_escape: false,
},
],
},
oauth_config: {
scopes: {
bot: [
"app_mentions:read",
"assistant:write",
"channels:history",
"channels:read",
"chat:write",
"commands",
"emoji:read",
"files:read",
"files:write",
"groups:history",
"groups:read",
"im:history",
"im:read",
"im:write",
"mpim:history",
"mpim:read",
"mpim:write",
"pins:read",
"pins:write",
"reactions:read",
"reactions:write",
"users:read",
],
},
},
settings: {
socket_mode_enabled: true,
event_subscriptions: {
bot_events: [
"app_mention",
"channel_rename",
"member_joined_channel",
"member_left_channel",
"message.channels",
"message.groups",
"message.im",
"message.mpim",
"pin_added",
"pin_removed",
"reaction_added",
"reaction_removed",
],
},
},
};
return JSON.stringify(manifest, null, 2);
}
export function buildSlackSetupLines(botName = "OpenClaw"): string[] {
return [
"1) Slack API -> Create App -> From scratch or From manifest (with the JSON below)",
"2) Add Socket Mode + enable it to get the app-level token (xapp-...)",
"3) Install App to workspace to get the xoxb- bot token",
"4) Enable Event Subscriptions (socket) for message events",
"5) App Home -> enable the Messages tab for DMs",
"Tip: set SLACK_BOT_TOKEN + SLACK_APP_TOKEN in your env.",
`Docs: ${formatDocsLink("/slack", "slack")}`,
"",
"Manifest (JSON):",
buildSlackManifest(botName),
];
}
export function setSlackChannelAllowlist(
cfg: OpenClawConfig,
accountId: string,
channelKeys: string[],
): OpenClawConfig {
const channels = Object.fromEntries(channelKeys.map((key) => [key, { enabled: true }]));
return patchChannelConfigForAccount({
cfg,
channel: SLACK_CHANNEL,
accountId,
patch: { channels },
});
}
export function isSlackSetupAccountConfigured(account: ResolvedSlackAccount): boolean {
const hasConfiguredBotToken =
Boolean(account.botToken?.trim()) || hasConfiguredSecretInput(account.config.botToken);
const hasConfiguredAppToken =
Boolean(account.appToken?.trim()) || hasConfiguredSecretInput(account.config.appToken);
return hasConfiguredBotToken && hasConfiguredAppToken;
}
export function describeSlackSetupAccount(account: ResolvedSlackAccount) {
return describeAccountSnapshot({
account,
configured: isSlackSetupAccountConfigured(account),
extra: {
botTokenSource: account.botTokenSource,
appTokenSource: account.appTokenSource,
},
});
}

View File

@@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { resolveWebCredsPath } from "./creds-files.js";
import { BufferJSON } from "./session.runtime.js";
const CREDS_FILE_MODE = 0o600;
const CREDS_SAVE_FLUSH_TIMEOUT_MS = 15_000;
@@ -11,6 +10,11 @@ const credsSaveQueues = new Map<string, Promise<void>>();
export type CredsQueueWaitResult = "drained" | "timed_out";
async function stringifyCreds(creds: unknown): Promise<string> {
const { BufferJSON } = await import("./session.runtime.js");
return JSON.stringify(creds, BufferJSON.replacer);
}
async function syncDirectory(dirPath: string): Promise<void> {
let handle: Awaited<ReturnType<typeof fs.open>> | undefined;
try {
@@ -28,7 +32,7 @@ async function syncDirectory(dirPath: string): Promise<void> {
export async function writeCredsJsonAtomically(authDir: string, creds: unknown): Promise<void> {
const credsPath = resolveWebCredsPath(authDir);
const tempPath = path.join(authDir, `.creds.${process.pid}.${randomUUID()}.tmp`);
const json = JSON.stringify(creds, BufferJSON.replacer);
const json = await stringifyCreds(creds);
let handle: Awaited<ReturnType<typeof fs.open>> | undefined;
try {