Files
openclaw/extensions/tlon/src/setup-core.ts
2026-03-16 22:51:46 -07:00

211 lines
6.4 KiB
TypeScript

import {
DEFAULT_ACCOUNT_ID,
formatDocsLink,
normalizeAccountId,
patchScopedAccountConfig,
prepareScopedSetupConfig,
type ChannelSetupAdapter,
type ChannelSetupInput,
type ChannelSetupWizard,
type OpenClawConfig,
} from "openclaw/plugin-sdk/setup";
import { buildTlonAccountFields } from "./account-fields.js";
import { normalizeShip } from "./targets.js";
import { listTlonAccountIds, resolveTlonAccount, type TlonResolvedAccount } from "./types.js";
import { validateUrbitBaseUrl } from "./urbit/base-url.js";
const channel = "tlon" as const;
export type TlonSetupInput = ChannelSetupInput & {
ship?: string;
url?: string;
code?: string;
allowPrivateNetwork?: boolean;
groupChannels?: string[];
dmAllowlist?: string[];
autoDiscoverChannels?: boolean;
ownerShip?: string;
};
function isConfigured(account: TlonResolvedAccount): boolean {
return Boolean(account.ship && account.url && account.code);
}
type TlonSetupWizardBaseParams = {
resolveConfigured: (params: { cfg: OpenClawConfig }) => boolean | Promise<boolean>;
resolveStatusLines?: (params: {
cfg: OpenClawConfig;
configured: boolean;
}) => string[] | Promise<string[]>;
finalize: NonNullable<ChannelSetupWizard["finalize"]>;
};
export function createTlonSetupWizardBase(params: TlonSetupWizardBaseParams): ChannelSetupWizard {
return {
channel,
status: {
configuredLabel: "configured",
unconfiguredLabel: "needs setup",
configuredHint: "configured",
unconfiguredHint: "urbit messenger",
configuredScore: 1,
unconfiguredScore: 4,
resolveConfigured: ({ cfg }) => params.resolveConfigured({ cfg }),
resolveStatusLines: ({ cfg, configured }) =>
params.resolveStatusLines?.({ cfg, configured }) ?? [],
},
introNote: {
title: "Tlon setup",
lines: [
"You need your Urbit ship URL and login code.",
"Example URL: https://your-ship-host",
"Example ship: ~sampel-palnet",
"If your ship URL is on a private network (LAN/localhost), you must explicitly allow it during setup.",
`Docs: ${formatDocsLink("/channels/tlon", "channels/tlon")}`,
],
},
credentials: [],
textInputs: [
{
inputKey: "ship",
message: "Ship name",
placeholder: "~sampel-palnet",
currentValue: ({ cfg, accountId }) => resolveTlonAccount(cfg, accountId).ship ?? undefined,
validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"),
normalizeValue: ({ value }) => normalizeShip(String(value).trim()),
applySet: async ({ cfg, accountId, value }) =>
applyTlonSetupConfig({
cfg,
accountId,
input: { ship: value },
}),
},
{
inputKey: "url",
message: "Ship URL",
placeholder: "https://your-ship-host",
currentValue: ({ cfg, accountId }) => resolveTlonAccount(cfg, accountId).url ?? undefined,
validate: ({ value }) => {
const next = validateUrbitBaseUrl(String(value ?? ""));
if (!next.ok) {
return next.error;
}
return undefined;
},
normalizeValue: ({ value }) => String(value).trim(),
applySet: async ({ cfg, accountId, value }) =>
applyTlonSetupConfig({
cfg,
accountId,
input: { url: value },
}),
},
{
inputKey: "code",
message: "Login code",
placeholder: "lidlut-tabwed-pillex-ridrup",
currentValue: ({ cfg, accountId }) => resolveTlonAccount(cfg, accountId).code ?? undefined,
validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"),
normalizeValue: ({ value }) => String(value).trim(),
applySet: async ({ cfg, accountId, value }) =>
applyTlonSetupConfig({
cfg,
accountId,
input: { code: value },
}),
},
],
finalize: params.finalize,
};
}
export async function resolveTlonSetupConfigured(cfg: OpenClawConfig): Promise<boolean> {
const accountIds = listTlonAccountIds(cfg);
return accountIds.length > 0
? accountIds.some((accountId) => isConfigured(resolveTlonAccount(cfg, accountId)))
: isConfigured(resolveTlonAccount(cfg, DEFAULT_ACCOUNT_ID));
}
export async function resolveTlonSetupStatusLines(cfg: OpenClawConfig): Promise<string[]> {
const configured = await resolveTlonSetupConfigured(cfg);
return [`Tlon: ${configured ? "configured" : "needs setup"}`];
}
export function applyTlonSetupConfig(params: {
cfg: OpenClawConfig;
accountId: string;
input: TlonSetupInput;
}): OpenClawConfig {
const { cfg, accountId, input } = params;
const useDefault = accountId === DEFAULT_ACCOUNT_ID;
const namedConfig = prepareScopedSetupConfig({
cfg,
channelKey: channel,
accountId,
name: input.name,
});
const base = namedConfig.channels?.tlon ?? {};
const payload = buildTlonAccountFields(input);
if (useDefault) {
return {
...namedConfig,
channels: {
...namedConfig.channels,
tlon: {
...base,
enabled: true,
...payload,
},
},
};
}
return patchScopedAccountConfig({
cfg: namedConfig,
channelKey: channel,
accountId,
patch: { enabled: base.enabled ?? true },
accountPatch: {
enabled: true,
...payload,
},
ensureChannelEnabled: false,
ensureAccountEnabled: false,
});
}
export const tlonSetupAdapter: ChannelSetupAdapter = {
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
applyAccountName: ({ cfg, accountId, name }) =>
prepareScopedSetupConfig({
cfg,
channelKey: channel,
accountId,
name,
}),
validateInput: ({ cfg, accountId, input }) => {
const setupInput = input as TlonSetupInput;
const resolved = resolveTlonAccount(cfg, accountId ?? undefined);
const ship = setupInput.ship?.trim() || resolved.ship;
const url = setupInput.url?.trim() || resolved.url;
const code = setupInput.code?.trim() || resolved.code;
if (!ship) {
return "Tlon requires --ship.";
}
if (!url) {
return "Tlon requires --url.";
}
if (!code) {
return "Tlon requires --code.";
}
return null;
},
applyAccountConfig: ({ cfg, accountId, input }) =>
applyTlonSetupConfig({
cfg,
accountId,
input: input as TlonSetupInput,
}),
};