@kodelyth/tlon 2026.5.42 → 2026.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/klaw.plugin.json +203 -3
  2. package/package.json +19 -6
  3. package/api.ts +0 -16
  4. package/channel-plugin-api.ts +0 -1
  5. package/doctor-contract-api.ts +0 -1
  6. package/index.ts +0 -16
  7. package/runtime-api.ts +0 -17
  8. package/setup-api.ts +0 -2
  9. package/setup-entry.ts +0 -9
  10. package/src/account-fields.ts +0 -31
  11. package/src/channel.message-adapter.test.ts +0 -145
  12. package/src/channel.runtime.ts +0 -259
  13. package/src/channel.ts +0 -192
  14. package/src/config-schema.ts +0 -54
  15. package/src/core.test.ts +0 -298
  16. package/src/doctor-contract.ts +0 -9
  17. package/src/doctor.test.ts +0 -46
  18. package/src/doctor.ts +0 -10
  19. package/src/logger-runtime.ts +0 -1
  20. package/src/monitor/approval-runtime.ts +0 -363
  21. package/src/monitor/approval.test.ts +0 -33
  22. package/src/monitor/approval.ts +0 -283
  23. package/src/monitor/authorization.ts +0 -30
  24. package/src/monitor/cites.ts +0 -54
  25. package/src/monitor/discovery.ts +0 -68
  26. package/src/monitor/history.ts +0 -226
  27. package/src/monitor/index.ts +0 -1523
  28. package/src/monitor/media.test.ts +0 -80
  29. package/src/monitor/media.ts +0 -156
  30. package/src/monitor/processed-messages.test.ts +0 -58
  31. package/src/monitor/processed-messages.ts +0 -89
  32. package/src/monitor/settings-helpers.test.ts +0 -113
  33. package/src/monitor/settings-helpers.ts +0 -158
  34. package/src/monitor/utils.ts +0 -402
  35. package/src/runtime.ts +0 -9
  36. package/src/security.test.ts +0 -658
  37. package/src/session-route.ts +0 -40
  38. package/src/settings.ts +0 -391
  39. package/src/setup-core.ts +0 -231
  40. package/src/setup-surface.ts +0 -99
  41. package/src/targets.ts +0 -102
  42. package/src/tlon-api.test.ts +0 -572
  43. package/src/tlon-api.ts +0 -389
  44. package/src/types.ts +0 -160
  45. package/src/urbit/auth.ssrf.test.ts +0 -45
  46. package/src/urbit/auth.ts +0 -48
  47. package/src/urbit/base-url.test.ts +0 -48
  48. package/src/urbit/base-url.ts +0 -61
  49. package/src/urbit/channel-ops.test.ts +0 -36
  50. package/src/urbit/channel-ops.ts +0 -149
  51. package/src/urbit/context.ts +0 -50
  52. package/src/urbit/errors.ts +0 -51
  53. package/src/urbit/fetch.ts +0 -38
  54. package/src/urbit/foreigns.ts +0 -49
  55. package/src/urbit/send.test.ts +0 -83
  56. package/src/urbit/send.ts +0 -228
  57. package/src/urbit/sse-client.test.ts +0 -234
  58. package/src/urbit/sse-client.ts +0 -492
  59. package/src/urbit/story.ts +0 -332
  60. package/src/urbit/upload.test.ts +0 -155
  61. package/src/urbit/upload.ts +0 -60
  62. package/test-api.ts +0 -1
  63. package/tsconfig.json +0 -16
package/src/settings.ts DELETED
@@ -1,391 +0,0 @@
1
- /**
2
- * Settings Store integration for hot-reloading Tlon plugin config.
3
- *
4
- * Settings are stored in Urbit's %settings agent under:
5
- * desk: "moltbot"
6
- * bucket: "tlon"
7
- *
8
- * This allows config changes via poke from any Landscape client
9
- * without requiring a gateway restart.
10
- */
11
-
12
- import type { UrbitSSEClient } from "./urbit/sse-client.js";
13
-
14
- /** Pending approval request stored for persistence */
15
- export type PendingApproval = {
16
- id: string;
17
- type: "dm" | "channel" | "group";
18
- requestingShip: string;
19
- channelNest?: string;
20
- groupFlag?: string;
21
- messagePreview?: string;
22
- /** Full message context for processing after approval */
23
- originalMessage?: {
24
- messageId: string;
25
- messageText: string;
26
- messageContent: unknown;
27
- timestamp: number;
28
- parentId?: string;
29
- isThreadReply?: boolean;
30
- };
31
- timestamp: number;
32
- };
33
-
34
- export type TlonSettingsStore = {
35
- groupChannels?: string[];
36
- dmAllowlist?: string[];
37
- autoDiscover?: boolean;
38
- showModelSig?: boolean;
39
- autoAcceptDmInvites?: boolean;
40
- autoDiscoverChannels?: boolean;
41
- autoAcceptGroupInvites?: boolean;
42
- /** Ships allowed to invite us to groups (when autoAcceptGroupInvites is true) */
43
- groupInviteAllowlist?: string[];
44
- channelRules?: Record<
45
- string,
46
- {
47
- mode?: "restricted" | "open";
48
- allowedShips?: string[];
49
- }
50
- >;
51
- defaultAuthorizedShips?: string[];
52
- /** Ship that receives approval requests for DMs, channel mentions, and group invites */
53
- ownerShip?: string;
54
- /** Pending approval requests awaiting owner response */
55
- pendingApprovals?: PendingApproval[];
56
- };
57
-
58
- type TlonSettingsState = {
59
- current: TlonSettingsStore;
60
- loaded: boolean;
61
- };
62
-
63
- const SETTINGS_DESK = "moltbot";
64
- const SETTINGS_BUCKET = "tlon";
65
-
66
- /**
67
- * Parse channelRules - handles both JSON string and object formats.
68
- * Settings-store doesn't support nested objects, so we store as JSON string.
69
- */
70
- function parseChannelRules(
71
- value: unknown,
72
- ): Record<string, { mode?: "restricted" | "open"; allowedShips?: string[] }> | undefined {
73
- if (!value) {
74
- return undefined;
75
- }
76
-
77
- // If it's a string, try to parse as JSON
78
- if (typeof value === "string") {
79
- try {
80
- const parsed = JSON.parse(value);
81
- if (isChannelRulesObject(parsed)) {
82
- return parsed;
83
- }
84
- } catch {
85
- return undefined;
86
- }
87
- }
88
-
89
- // If it's already an object, use directly
90
- if (isChannelRulesObject(value)) {
91
- return value;
92
- }
93
-
94
- return undefined;
95
- }
96
-
97
- /**
98
- * Parse settings from the raw Urbit settings-store response.
99
- * The response shape is: { [bucket]: { [key]: value } }
100
- */
101
- function parseSettingsResponse(raw: unknown): TlonSettingsStore {
102
- if (!raw || typeof raw !== "object") {
103
- return {};
104
- }
105
-
106
- const desk = raw as Record<string, unknown>;
107
- const bucket = desk[SETTINGS_BUCKET];
108
- if (!bucket || typeof bucket !== "object") {
109
- return {};
110
- }
111
-
112
- const settings = bucket as Record<string, unknown>;
113
-
114
- return {
115
- groupChannels: Array.isArray(settings.groupChannels)
116
- ? settings.groupChannels.filter((x): x is string => typeof x === "string")
117
- : undefined,
118
- dmAllowlist: Array.isArray(settings.dmAllowlist)
119
- ? settings.dmAllowlist.filter((x): x is string => typeof x === "string")
120
- : undefined,
121
- autoDiscover: typeof settings.autoDiscover === "boolean" ? settings.autoDiscover : undefined,
122
- showModelSig: typeof settings.showModelSig === "boolean" ? settings.showModelSig : undefined,
123
- autoAcceptDmInvites:
124
- typeof settings.autoAcceptDmInvites === "boolean" ? settings.autoAcceptDmInvites : undefined,
125
- autoAcceptGroupInvites:
126
- typeof settings.autoAcceptGroupInvites === "boolean"
127
- ? settings.autoAcceptGroupInvites
128
- : undefined,
129
- groupInviteAllowlist: Array.isArray(settings.groupInviteAllowlist)
130
- ? settings.groupInviteAllowlist.filter((x): x is string => typeof x === "string")
131
- : undefined,
132
- channelRules: parseChannelRules(settings.channelRules),
133
- defaultAuthorizedShips: Array.isArray(settings.defaultAuthorizedShips)
134
- ? settings.defaultAuthorizedShips.filter((x): x is string => typeof x === "string")
135
- : undefined,
136
- ownerShip: typeof settings.ownerShip === "string" ? settings.ownerShip : undefined,
137
- pendingApprovals: parsePendingApprovals(settings.pendingApprovals),
138
- };
139
- }
140
-
141
- function isChannelRulesObject(
142
- val: unknown,
143
- ): val is Record<string, { mode?: "restricted" | "open"; allowedShips?: string[] }> {
144
- if (!val || typeof val !== "object" || Array.isArray(val)) {
145
- return false;
146
- }
147
- for (const [, rule] of Object.entries(val)) {
148
- if (!rule || typeof rule !== "object") {
149
- return false;
150
- }
151
- }
152
- return true;
153
- }
154
-
155
- /**
156
- * Parse pendingApprovals - handles both JSON string and array formats.
157
- * Settings-store stores complex objects as JSON strings.
158
- */
159
- function parsePendingApprovals(value: unknown): PendingApproval[] | undefined {
160
- if (!value) {
161
- return undefined;
162
- }
163
-
164
- // If it's a string, try to parse as JSON
165
- let parsed: unknown = value;
166
- if (typeof value === "string") {
167
- try {
168
- parsed = JSON.parse(value);
169
- } catch {
170
- return undefined;
171
- }
172
- }
173
-
174
- // Validate it's an array
175
- if (!Array.isArray(parsed)) {
176
- return undefined;
177
- }
178
-
179
- // Filter to valid PendingApproval objects
180
- return parsed.filter((item): item is PendingApproval => {
181
- if (!item || typeof item !== "object") {
182
- return false;
183
- }
184
- const obj = item as Record<string, unknown>;
185
- return (
186
- typeof obj.id === "string" &&
187
- (obj.type === "dm" || obj.type === "channel" || obj.type === "group") &&
188
- typeof obj.requestingShip === "string" &&
189
- typeof obj.timestamp === "number"
190
- );
191
- });
192
- }
193
-
194
- /**
195
- * Parse a single settings entry update event.
196
- */
197
- function parseSettingsEvent(event: unknown): { key: string; value: unknown } | null {
198
- if (!event || typeof event !== "object") {
199
- return null;
200
- }
201
-
202
- const evt = event as Record<string, unknown>;
203
-
204
- // Handle put-entry events
205
- if (evt["put-entry"]) {
206
- const put = evt["put-entry"] as Record<string, unknown>;
207
- if (put.desk !== SETTINGS_DESK || put["bucket-key"] !== SETTINGS_BUCKET) {
208
- return null;
209
- }
210
- return {
211
- key: typeof put["entry-key"] === "string" ? put["entry-key"] : "",
212
- value: put.value,
213
- };
214
- }
215
-
216
- // Handle del-entry events
217
- if (evt["del-entry"]) {
218
- const del = evt["del-entry"] as Record<string, unknown>;
219
- if (del.desk !== SETTINGS_DESK || del["bucket-key"] !== SETTINGS_BUCKET) {
220
- return null;
221
- }
222
- return {
223
- key: typeof del["entry-key"] === "string" ? del["entry-key"] : "",
224
- value: undefined,
225
- };
226
- }
227
-
228
- return null;
229
- }
230
-
231
- /**
232
- * Apply a single settings update to the current state.
233
- */
234
- function applySettingsUpdate(
235
- current: TlonSettingsStore,
236
- key: string,
237
- value: unknown,
238
- ): TlonSettingsStore {
239
- const next = { ...current };
240
-
241
- switch (key) {
242
- case "groupChannels":
243
- next.groupChannels = Array.isArray(value)
244
- ? value.filter((x): x is string => typeof x === "string")
245
- : undefined;
246
- break;
247
- case "dmAllowlist":
248
- next.dmAllowlist = Array.isArray(value)
249
- ? value.filter((x): x is string => typeof x === "string")
250
- : undefined;
251
- break;
252
- case "autoDiscover":
253
- next.autoDiscover = typeof value === "boolean" ? value : undefined;
254
- break;
255
- case "showModelSig":
256
- next.showModelSig = typeof value === "boolean" ? value : undefined;
257
- break;
258
- case "autoAcceptDmInvites":
259
- next.autoAcceptDmInvites = typeof value === "boolean" ? value : undefined;
260
- break;
261
- case "autoAcceptGroupInvites":
262
- next.autoAcceptGroupInvites = typeof value === "boolean" ? value : undefined;
263
- break;
264
- case "groupInviteAllowlist":
265
- next.groupInviteAllowlist = Array.isArray(value)
266
- ? value.filter((x): x is string => typeof x === "string")
267
- : undefined;
268
- break;
269
- case "channelRules":
270
- next.channelRules = parseChannelRules(value);
271
- break;
272
- case "defaultAuthorizedShips":
273
- next.defaultAuthorizedShips = Array.isArray(value)
274
- ? value.filter((x): x is string => typeof x === "string")
275
- : undefined;
276
- break;
277
- case "ownerShip":
278
- next.ownerShip = typeof value === "string" ? value : undefined;
279
- break;
280
- case "pendingApprovals":
281
- next.pendingApprovals = parsePendingApprovals(value);
282
- break;
283
- }
284
-
285
- return next;
286
- }
287
-
288
- type SettingsLogger = {
289
- log?: (msg: string) => void;
290
- error?: (msg: string) => void;
291
- };
292
-
293
- /**
294
- * Create a settings store subscription manager.
295
- *
296
- * Usage:
297
- * const settings = createSettingsManager(api, logger);
298
- * await settings.load();
299
- * settings.subscribe((newSettings) => { ... });
300
- */
301
- export function createSettingsManager(api: UrbitSSEClient, logger?: SettingsLogger) {
302
- let state: TlonSettingsState = {
303
- current: {},
304
- loaded: false,
305
- };
306
-
307
- const listeners = new Set<(settings: TlonSettingsStore) => void>();
308
-
309
- const notify = () => {
310
- for (const listener of listeners) {
311
- try {
312
- listener(state.current);
313
- } catch (err) {
314
- logger?.error?.(`[settings] Listener error: ${String(err)}`);
315
- }
316
- }
317
- };
318
-
319
- return {
320
- /**
321
- * Get current settings (may be empty if not loaded yet).
322
- */
323
- get current(): TlonSettingsStore {
324
- return state.current;
325
- },
326
-
327
- /**
328
- * Whether initial settings have been loaded.
329
- */
330
- get loaded(): boolean {
331
- return state.loaded;
332
- },
333
-
334
- /**
335
- * Load initial settings via scry.
336
- */
337
- async load(): Promise<TlonSettingsStore> {
338
- try {
339
- const raw = await api.scry("/settings/all.json");
340
- // Response shape: { all: { [desk]: { [bucket]: { [key]: value } } } }
341
- const allData = raw as { all?: Record<string, Record<string, unknown>> };
342
- const deskData = allData?.all?.[SETTINGS_DESK];
343
- state.current = parseSettingsResponse(deskData ?? {});
344
- state.loaded = true;
345
- logger?.log?.(`[settings] Loaded: ${JSON.stringify(state.current)}`);
346
- return state.current;
347
- } catch (err) {
348
- // Settings desk may not exist yet - that's fine, use defaults
349
- logger?.log?.(`[settings] No settings found (using defaults): ${String(err)}`);
350
- state.current = {};
351
- state.loaded = true;
352
- return state.current;
353
- }
354
- },
355
-
356
- /**
357
- * Subscribe to settings changes.
358
- */
359
- async startSubscription(): Promise<void> {
360
- await api.subscribe({
361
- app: "settings",
362
- path: "/desk/" + SETTINGS_DESK,
363
- event: (event) => {
364
- const update = parseSettingsEvent(event);
365
- if (!update) {
366
- return;
367
- }
368
-
369
- logger?.log?.(`[settings] Update: ${update.key} = ${JSON.stringify(update.value)}`);
370
- state.current = applySettingsUpdate(state.current, update.key, update.value);
371
- notify();
372
- },
373
- err: (error) => {
374
- logger?.error?.(`[settings] Subscription error: ${String(error)}`);
375
- },
376
- quit: () => {
377
- logger?.log?.("[settings] Subscription ended");
378
- },
379
- });
380
- logger?.log?.("[settings] Subscribed to settings updates");
381
- },
382
-
383
- /**
384
- * Register a listener for settings changes.
385
- */
386
- onChange(listener: (settings: TlonSettingsStore) => void): () => void {
387
- listeners.add(listener);
388
- return () => listeners.delete(listener);
389
- },
390
- };
391
- }
package/src/setup-core.ts DELETED
@@ -1,231 +0,0 @@
1
- import {
2
- DEFAULT_ACCOUNT_ID,
3
- formatDocsLink,
4
- normalizeAccountId,
5
- patchScopedAccountConfig,
6
- prepareScopedSetupConfig,
7
- createSetupTranslator,
8
- createSetupInputPresenceValidator,
9
- type ChannelSetupAdapter,
10
- type ChannelSetupInput,
11
- type ChannelSetupWizard,
12
- type KlawConfig,
13
- } from "klaw/plugin-sdk/setup";
14
- import {
15
- normalizeOptionalString,
16
- normalizeStringifiedOptionalString,
17
- } from "klaw/plugin-sdk/string-coerce-runtime";
18
- import { buildTlonAccountFields, type TlonAccountFieldsInput } from "./account-fields.js";
19
- import { normalizeShip } from "./targets.js";
20
- import { listTlonAccountIds, resolveTlonAccount, type TlonResolvedAccount } from "./types.js";
21
- import { validateUrbitBaseUrl } from "./urbit/base-url.js";
22
-
23
- const t = createSetupTranslator();
24
-
25
- function tlonChannelId() {
26
- return "tlon" as const;
27
- }
28
-
29
- type TlonSetupInput = ChannelSetupInput & TlonAccountFieldsInput;
30
-
31
- function isConfigured(account: TlonResolvedAccount): boolean {
32
- return Boolean(account.ship && account.url && account.code);
33
- }
34
-
35
- type TlonSetupWizardBaseParams = {
36
- resolveConfigured: (params: {
37
- cfg: KlawConfig;
38
- accountId?: string;
39
- }) => boolean | Promise<boolean>;
40
- resolveStatusLines?: (params: {
41
- cfg: KlawConfig;
42
- accountId?: string;
43
- configured: boolean;
44
- }) => string[] | Promise<string[]>;
45
- finalize: NonNullable<ChannelSetupWizard["finalize"]>;
46
- };
47
-
48
- export function createTlonSetupWizardBase(params: TlonSetupWizardBaseParams): ChannelSetupWizard {
49
- return {
50
- channel: tlonChannelId(),
51
- status: {
52
- configuredLabel: t("wizard.channels.statusConfigured"),
53
- unconfiguredLabel: t("wizard.channels.statusNeedsSetup"),
54
- configuredHint: t("wizard.channels.statusConfigured"),
55
- unconfiguredHint: t("wizard.channels.statusUrbitMessenger"),
56
- configuredScore: 1,
57
- unconfiguredScore: 4,
58
- resolveConfigured: ({ cfg, accountId }) => params.resolveConfigured({ cfg, accountId }),
59
- resolveStatusLines: ({ cfg, accountId, configured }) =>
60
- params.resolveStatusLines?.({ cfg, accountId, configured }) ?? [],
61
- },
62
- introNote: {
63
- title: t("wizard.tlon.setupTitle"),
64
- lines: [
65
- t("wizard.tlon.helpNeedsUrlCode"),
66
- t("wizard.tlon.helpExampleUrl"),
67
- t("wizard.tlon.helpExampleShip"),
68
- t("wizard.tlon.helpPrivateNetwork"),
69
- `Docs: ${formatDocsLink("/channels/tlon", "channels/tlon")}`,
70
- ],
71
- },
72
- credentials: [],
73
- textInputs: [
74
- {
75
- inputKey: "ship",
76
- message: t("wizard.tlon.shipPrompt"),
77
- placeholder: "~sampel-palnet",
78
- currentValue: ({ cfg, accountId }) => resolveTlonAccount(cfg, accountId).ship ?? undefined,
79
- validate: ({ value }) =>
80
- normalizeStringifiedOptionalString(value) ? undefined : "Required",
81
- normalizeValue: ({ value }) =>
82
- normalizeShip(normalizeStringifiedOptionalString(value) ?? ""),
83
- applySet: async ({ cfg, accountId, value }) =>
84
- applyTlonSetupConfig({
85
- cfg,
86
- accountId,
87
- input: { ship: value },
88
- }),
89
- },
90
- {
91
- inputKey: "url",
92
- message: t("wizard.tlon.shipUrlPrompt"),
93
- placeholder: "https://your-ship-host",
94
- currentValue: ({ cfg, accountId }) => resolveTlonAccount(cfg, accountId).url ?? undefined,
95
- validate: ({ value }) => {
96
- const next = validateUrbitBaseUrl(value ?? "");
97
- if (!next.ok) {
98
- return next.error;
99
- }
100
- return undefined;
101
- },
102
- normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "",
103
- applySet: async ({ cfg, accountId, value }) =>
104
- applyTlonSetupConfig({
105
- cfg,
106
- accountId,
107
- input: { url: value },
108
- }),
109
- },
110
- {
111
- inputKey: "code",
112
- message: t("wizard.tlon.loginCodePrompt"),
113
- placeholder: "lidlut-tabwed-pillex-ridrup",
114
- currentValue: ({ cfg, accountId }) => resolveTlonAccount(cfg, accountId).code ?? undefined,
115
- validate: ({ value }) =>
116
- normalizeStringifiedOptionalString(value) ? undefined : "Required",
117
- normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "",
118
- applySet: async ({ cfg, accountId, value }) =>
119
- applyTlonSetupConfig({
120
- cfg,
121
- accountId,
122
- input: { code: value },
123
- }),
124
- },
125
- ],
126
- finalize: params.finalize,
127
- };
128
- }
129
-
130
- export async function resolveTlonSetupConfigured(
131
- cfg: KlawConfig,
132
- accountId?: string,
133
- ): Promise<boolean> {
134
- if (accountId) {
135
- return isConfigured(resolveTlonAccount(cfg, accountId));
136
- }
137
- const accountIds = listTlonAccountIds(cfg);
138
- return accountIds.length > 0
139
- ? accountIds.some((resolvedAccountId) =>
140
- isConfigured(resolveTlonAccount(cfg, resolvedAccountId)),
141
- )
142
- : isConfigured(resolveTlonAccount(cfg, DEFAULT_ACCOUNT_ID));
143
- }
144
-
145
- export async function resolveTlonSetupStatusLines(
146
- cfg: KlawConfig,
147
- accountId?: string,
148
- ): Promise<string[]> {
149
- const configured = await resolveTlonSetupConfigured(cfg, accountId);
150
- const label = accountId && accountId !== DEFAULT_ACCOUNT_ID ? `Tlon (${accountId})` : "Tlon";
151
- return [`${label}: ${configured ? "configured" : "needs setup"}`];
152
- }
153
-
154
- export function applyTlonSetupConfig(params: {
155
- cfg: KlawConfig;
156
- accountId: string;
157
- input: TlonSetupInput;
158
- }): KlawConfig {
159
- const { cfg, accountId, input } = params;
160
- const useDefault = accountId === DEFAULT_ACCOUNT_ID;
161
- const namedConfig = prepareScopedSetupConfig({
162
- cfg,
163
- channelKey: tlonChannelId(),
164
- accountId,
165
- name: input.name,
166
- });
167
- const base = namedConfig.channels?.tlon ?? {};
168
- const payload = buildTlonAccountFields(input);
169
-
170
- if (useDefault) {
171
- return {
172
- ...namedConfig,
173
- channels: {
174
- ...namedConfig.channels,
175
- tlon: {
176
- ...base,
177
- enabled: true,
178
- ...payload,
179
- },
180
- },
181
- };
182
- }
183
-
184
- return patchScopedAccountConfig({
185
- cfg: namedConfig,
186
- channelKey: tlonChannelId(),
187
- accountId,
188
- patch: { enabled: base.enabled ?? true },
189
- accountPatch: {
190
- enabled: true,
191
- ...payload,
192
- },
193
- ensureChannelEnabled: false,
194
- ensureAccountEnabled: false,
195
- });
196
- }
197
-
198
- export const tlonSetupAdapter: ChannelSetupAdapter = {
199
- resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
200
- applyAccountName: ({ cfg, accountId, name }) =>
201
- prepareScopedSetupConfig({
202
- cfg,
203
- channelKey: tlonChannelId(),
204
- accountId,
205
- name,
206
- }),
207
- validateInput: createSetupInputPresenceValidator({
208
- validate: ({ cfg, accountId, input }) => {
209
- const resolved = resolveTlonAccount(cfg, accountId ?? undefined);
210
- const ship = normalizeOptionalString(input.ship) || resolved.ship;
211
- const url = normalizeOptionalString(input.url) || resolved.url;
212
- const code = normalizeOptionalString(input.code) || resolved.code;
213
- if (!ship) {
214
- return "Tlon requires --ship.";
215
- }
216
- if (!url) {
217
- return "Tlon requires --url.";
218
- }
219
- if (!code) {
220
- return "Tlon requires --code.";
221
- }
222
- return null;
223
- },
224
- }),
225
- applyAccountConfig: ({ cfg, accountId, input }) =>
226
- applyTlonSetupConfig({
227
- cfg,
228
- accountId,
229
- input: input as TlonSetupInput,
230
- }),
231
- };