2026-03-17 09:24:22 -07:00

225 lines
6.5 KiB
TypeScript

import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import type { ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk/tlon";
import { tlonChannelConfigSchema } from "./config-schema.js";
import {
applyTlonSetupConfig,
createTlonSetupWizardBase,
resolveTlonSetupConfigured,
tlonSetupAdapter,
} from "./setup-core.js";
import {
formatTargetHint,
normalizeShip,
parseTlonTarget,
resolveTlonOutboundTarget,
} from "./targets.js";
import { resolveTlonAccount, listTlonAccountIds } from "./types.js";
import { validateUrbitBaseUrl } from "./urbit/base-url.js";
const TLON_CHANNEL_ID = "tlon" as const;
const loadTlonChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js"));
const tlonSetupWizardProxy = createTlonSetupWizardBase({
resolveConfigured: async ({ cfg }) =>
await (await loadTlonChannelRuntime()).tlonSetupWizard.status.resolveConfigured({ cfg }),
resolveStatusLines: async ({ cfg, configured }) =>
(await (
await loadTlonChannelRuntime()
).tlonSetupWizard.status.resolveStatusLines?.({
cfg,
configured,
})) ?? [],
finalize: async (params) =>
await (
await loadTlonChannelRuntime()
).tlonSetupWizard.finalize!(params),
}) satisfies NonNullable<ChannelPlugin["setupWizard"]>;
export const tlonPlugin: ChannelPlugin = {
id: TLON_CHANNEL_ID,
meta: {
id: TLON_CHANNEL_ID,
label: "Tlon",
selectionLabel: "Tlon (Urbit)",
docsPath: "/channels/tlon",
docsLabel: "tlon",
blurb: "Decentralized messaging on Urbit",
aliases: ["urbit"],
order: 90,
},
capabilities: {
chatTypes: ["direct", "group", "thread"],
media: true,
reply: true,
threads: true,
},
setup: tlonSetupAdapter,
setupWizard: tlonSetupWizardProxy,
reload: { configPrefixes: ["channels.tlon"] },
configSchema: tlonChannelConfigSchema,
config: {
listAccountIds: (cfg) => listTlonAccountIds(cfg),
resolveAccount: (cfg, accountId) => resolveTlonAccount(cfg, accountId ?? undefined),
defaultAccountId: () => "default",
setAccountEnabled: ({ cfg, accountId, enabled }) => {
const useDefault = !accountId || accountId === "default";
if (useDefault) {
return {
...cfg,
channels: {
...cfg.channels,
tlon: {
...cfg.channels?.tlon,
enabled,
},
},
} as OpenClawConfig;
}
return {
...cfg,
channels: {
...cfg.channels,
tlon: {
...cfg.channels?.tlon,
accounts: {
...cfg.channels?.tlon?.accounts,
[accountId]: {
...cfg.channels?.tlon?.accounts?.[accountId],
enabled,
},
},
},
},
} as OpenClawConfig;
},
deleteAccount: ({ cfg, accountId }) => {
const useDefault = !accountId || accountId === "default";
if (useDefault) {
const {
ship: _ship,
code: _code,
url: _url,
name: _name,
...rest
} = cfg.channels?.tlon ?? {};
return {
...cfg,
channels: {
...cfg.channels,
tlon: rest,
},
} as OpenClawConfig;
}
const { [accountId]: _removed, ...remainingAccounts } = cfg.channels?.tlon?.accounts ?? {};
return {
...cfg,
channels: {
...cfg.channels,
tlon: {
...cfg.channels?.tlon,
accounts: remainingAccounts,
},
},
} as OpenClawConfig;
},
isConfigured: (account) => account.configured,
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.configured,
ship: account.ship,
url: account.url,
}),
},
messaging: {
normalizeTarget: (target) => {
const parsed = parseTlonTarget(target);
if (!parsed) {
return target.trim();
}
if (parsed.kind === "dm") {
return parsed.ship;
}
return parsed.nest;
},
targetResolver: {
looksLikeId: (target) => Boolean(parseTlonTarget(target)),
hint: formatTargetHint(),
},
},
outbound: {
deliveryMode: "direct",
textChunkLimit: 10000,
resolveTarget: ({ to }) => resolveTlonOutboundTarget(to),
sendText: async (params) =>
await (
await loadTlonChannelRuntime()
).tlonRuntimeOutbound.sendText!(params),
sendMedia: async (params) =>
await (
await loadTlonChannelRuntime()
).tlonRuntimeOutbound.sendMedia!(params),
},
status: {
defaultRuntime: {
accountId: "default",
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
},
collectStatusIssues: (accounts) => {
return accounts.flatMap((account) => {
if (!account.configured) {
return [
{
channel: TLON_CHANNEL_ID,
accountId: account.accountId,
kind: "config",
message: "Account not configured (missing ship, code, or url)",
},
];
}
return [];
});
},
buildChannelSummary: ({ snapshot }) => {
const s = snapshot as { configured?: boolean; ship?: string; url?: string };
return {
configured: s.configured ?? false,
ship: s.ship ?? null,
url: s.url ?? null,
};
},
probeAccount: async ({ account }) => {
if (!account.configured || !account.ship || !account.url || !account.code) {
return { ok: false, error: "Not configured" };
}
return await (await loadTlonChannelRuntime()).probeTlonAccount(account as never);
},
buildAccountSnapshot: ({ account, runtime, probe }) => {
// Tlon-specific snapshot with ship/url for status display
const snapshot = {
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.configured,
ship: account.ship,
url: account.url,
running: runtime?.running ?? false,
lastStartAt: runtime?.lastStartAt ?? null,
lastStopAt: runtime?.lastStopAt ?? null,
lastError: runtime?.lastError ?? null,
probe,
};
return snapshot as import("openclaw/plugin-sdk/tlon").ChannelAccountSnapshot;
},
},
gateway: {
startAccount: async (ctx) =>
await (await loadTlonChannelRuntime()).startTlonGatewayAccount(ctx),
},
};