@kodelyth/tlon 2026.5.39 → 2026.5.42

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 (91) hide show
  1. package/README.md +5 -0
  2. package/api.ts +16 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/dist/api.js +4 -0
  5. package/dist/channel-Bvzym9ez.js +236 -0
  6. package/dist/channel-plugin-api.js +2 -0
  7. package/dist/channel.runtime-CDY2BdfM.js +3626 -0
  8. package/dist/doctor-contract-Ip6FcHDH.js +7 -0
  9. package/dist/doctor-contract-api.js +2 -0
  10. package/dist/index.js +18 -0
  11. package/dist/runtime-BmSb9A-q.js +8 -0
  12. package/dist/runtime-api-Dq8wkBC_.js +4 -0
  13. package/dist/runtime-api.js +2 -0
  14. package/dist/setup-api.js +3 -0
  15. package/dist/setup-core-CF3ryHqs.js +387 -0
  16. package/dist/setup-entry.js +11 -0
  17. package/dist/setup-surface-BM5_V_XL.js +74 -0
  18. package/dist/test-api.js +2 -0
  19. package/doctor-contract-api.ts +1 -0
  20. package/index.ts +16 -0
  21. package/klaw.plugin.json +3 -203
  22. package/package.json +4 -4
  23. package/runtime-api.ts +17 -0
  24. package/setup-api.ts +2 -0
  25. package/setup-entry.ts +9 -0
  26. package/src/account-fields.ts +31 -0
  27. package/src/channel.message-adapter.test.ts +145 -0
  28. package/src/channel.runtime.ts +259 -0
  29. package/src/channel.ts +192 -0
  30. package/src/config-schema.ts +54 -0
  31. package/src/core.test.ts +298 -0
  32. package/src/doctor-contract.ts +9 -0
  33. package/src/doctor.test.ts +46 -0
  34. package/src/doctor.ts +10 -0
  35. package/src/logger-runtime.ts +1 -0
  36. package/src/monitor/approval-runtime.ts +363 -0
  37. package/src/monitor/approval.test.ts +33 -0
  38. package/src/monitor/approval.ts +283 -0
  39. package/src/monitor/authorization.ts +30 -0
  40. package/src/monitor/cites.ts +54 -0
  41. package/src/monitor/discovery.ts +68 -0
  42. package/src/monitor/history.ts +226 -0
  43. package/src/monitor/index.ts +1523 -0
  44. package/src/monitor/media.test.ts +80 -0
  45. package/src/monitor/media.ts +156 -0
  46. package/src/monitor/processed-messages.test.ts +58 -0
  47. package/src/monitor/processed-messages.ts +89 -0
  48. package/src/monitor/settings-helpers.test.ts +113 -0
  49. package/src/monitor/settings-helpers.ts +158 -0
  50. package/src/monitor/utils.ts +402 -0
  51. package/src/runtime.ts +9 -0
  52. package/src/security.test.ts +658 -0
  53. package/src/session-route.ts +40 -0
  54. package/src/settings.ts +391 -0
  55. package/src/setup-core.ts +231 -0
  56. package/src/setup-surface.ts +99 -0
  57. package/src/targets.ts +102 -0
  58. package/src/tlon-api.test.ts +572 -0
  59. package/src/tlon-api.ts +389 -0
  60. package/src/types.ts +160 -0
  61. package/src/urbit/auth.ssrf.test.ts +45 -0
  62. package/src/urbit/auth.ts +48 -0
  63. package/src/urbit/base-url.test.ts +48 -0
  64. package/src/urbit/base-url.ts +61 -0
  65. package/src/urbit/channel-ops.test.ts +36 -0
  66. package/src/urbit/channel-ops.ts +149 -0
  67. package/src/urbit/context.ts +50 -0
  68. package/src/urbit/errors.ts +51 -0
  69. package/src/urbit/fetch.ts +38 -0
  70. package/src/urbit/foreigns.ts +49 -0
  71. package/src/urbit/send.test.ts +83 -0
  72. package/src/urbit/send.ts +228 -0
  73. package/src/urbit/sse-client.test.ts +234 -0
  74. package/src/urbit/sse-client.ts +492 -0
  75. package/src/urbit/story.ts +332 -0
  76. package/src/urbit/upload.test.ts +155 -0
  77. package/src/urbit/upload.ts +60 -0
  78. package/test-api.ts +1 -0
  79. package/tsconfig.json +16 -0
  80. package/api.js +0 -7
  81. package/bundled-skills/@tloncorp/tlon-skill/SKILL.md +0 -501
  82. package/bundled-skills/@tloncorp/tlon-skill/bin/tlon.js +0 -7
  83. package/bundled-skills/@tloncorp/tlon-skill/package.json +0 -40
  84. package/bundled-skills/@tloncorp/tlon-skill/scripts/postinstall.js +0 -7
  85. package/channel-plugin-api.js +0 -7
  86. package/doctor-contract-api.js +0 -7
  87. package/index.js +0 -7
  88. package/runtime-api.js +0 -7
  89. package/setup-api.js +0 -7
  90. package/setup-entry.js +0 -7
  91. package/test-api.js +0 -7
@@ -0,0 +1,402 @@
1
+ import {
2
+ resolveStableChannelMessageIngress,
3
+ type StableChannelIngressIdentityParams,
4
+ } from "klaw/plugin-sdk/channel-ingress-runtime";
5
+ import { formatErrorMessage as sharedFormatErrorMessage } from "klaw/plugin-sdk/error-runtime";
6
+ import { normalizeShip } from "../targets.js";
7
+
8
+ export interface ParsedCite {
9
+ type: "chan" | "group" | "desk" | "bait";
10
+ nest?: string;
11
+ author?: string;
12
+ postId?: string;
13
+ group?: string;
14
+ flag?: string;
15
+ where?: string;
16
+ }
17
+
18
+ export function extractCites(content: unknown): ParsedCite[] {
19
+ if (!content || !Array.isArray(content)) {
20
+ return [];
21
+ }
22
+
23
+ const cites: ParsedCite[] = [];
24
+
25
+ for (const verse of content) {
26
+ if (verse?.block?.cite && typeof verse.block.cite === "object") {
27
+ const cite = verse.block.cite;
28
+
29
+ if (cite.chan && typeof cite.chan === "object") {
30
+ const { nest, where } = cite.chan;
31
+ const whereMatch = where?.match(/\/msg\/(~[a-z-]+)\/(.+)/);
32
+ cites.push({
33
+ type: "chan",
34
+ nest,
35
+ where,
36
+ author: whereMatch?.[1],
37
+ postId: whereMatch?.[2],
38
+ });
39
+ } else if (cite.group && typeof cite.group === "string") {
40
+ cites.push({ type: "group", group: cite.group });
41
+ } else if (cite.desk && typeof cite.desk === "object") {
42
+ cites.push({ type: "desk", flag: cite.desk.flag, where: cite.desk.where });
43
+ } else if (cite.bait && typeof cite.bait === "object") {
44
+ cites.push({
45
+ type: "bait",
46
+ group: cite.bait.group,
47
+ nest: cite.bait.graph,
48
+ where: cite.bait.where,
49
+ });
50
+ }
51
+ }
52
+ }
53
+
54
+ return cites;
55
+ }
56
+
57
+ export function formatModelName(modelString?: string | null): string {
58
+ if (!modelString) {
59
+ return "AI";
60
+ }
61
+ const modelName = modelString.includes("/") ? modelString.split("/")[1] : modelString;
62
+ const modelMappings: Record<string, string> = {
63
+ "claude-opus-4-5": "Claude Opus 4.5",
64
+ "claude-sonnet-4-5": "Claude Sonnet 4.5",
65
+ "claude-sonnet-3-5": "Claude Sonnet 3.5",
66
+ "gpt-4o": "GPT-4o",
67
+ "gpt-4-turbo": "GPT-4 Turbo",
68
+ "gpt-4": "GPT-4",
69
+ "gemini-2.0-flash": "Gemini 2.0 Flash",
70
+ "gemini-pro": "Gemini Pro",
71
+ };
72
+
73
+ if (modelMappings[modelName]) {
74
+ return modelMappings[modelName];
75
+ }
76
+ return modelName
77
+ .replace(/-/g, " ")
78
+ .split(" ")
79
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
80
+ .join(" ");
81
+ }
82
+
83
+ export function isBotMentioned(
84
+ messageText: string,
85
+ botShipName: string,
86
+ nickname?: string,
87
+ ): boolean {
88
+ if (!messageText || !botShipName) {
89
+ return false;
90
+ }
91
+
92
+ if (/@all\b/i.test(messageText)) {
93
+ return true;
94
+ }
95
+
96
+ const normalizedBotShip = normalizeShip(botShipName);
97
+ const escapedShip = normalizedBotShip.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
98
+ const mentionPattern = new RegExp(`(^|\\s)${escapedShip}(?=\\s|$)`, "i");
99
+ if (mentionPattern.test(messageText)) {
100
+ return true;
101
+ }
102
+
103
+ if (nickname) {
104
+ const escapedNickname = nickname.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
105
+ const nicknamePattern = new RegExp(`(^|\\s)${escapedNickname}(?=\\s|$|[,!?.])`, "i");
106
+ if (nicknamePattern.test(messageText)) {
107
+ return true;
108
+ }
109
+ }
110
+
111
+ return false;
112
+ }
113
+
114
+ export function stripBotMention(messageText: string, botShipName: string): string {
115
+ if (!messageText || !botShipName) {
116
+ return messageText;
117
+ }
118
+ return messageText.replace(normalizeShip(botShipName), "").trim();
119
+ }
120
+
121
+ const tlonIngressIdentity = {
122
+ key: "sender-ship",
123
+ normalize: normalizeShip,
124
+ sensitivity: "pii",
125
+ isWildcardEntry: () => false,
126
+ entryIdPrefix: "tlon-entry",
127
+ } satisfies StableChannelIngressIdentityParams;
128
+
129
+ export async function isDmAllowedWithIngress(
130
+ senderShip: string,
131
+ allowlist: string[] | undefined,
132
+ ): Promise<boolean> {
133
+ const access = await resolveStableChannelMessageIngress({
134
+ channelId: "tlon",
135
+ accountId: "default",
136
+ identity: tlonIngressIdentity,
137
+ subject: { stableId: senderShip },
138
+ conversation: {
139
+ kind: "direct",
140
+ id: "direct",
141
+ },
142
+ dmPolicy: "allowlist",
143
+ allowFrom: allowlist ?? [],
144
+ });
145
+ return access.senderAccess.allowed;
146
+ }
147
+
148
+ export async function resolveTlonCommandAuthorizationWithIngress(params: {
149
+ senderShip: string;
150
+ ownerShip: string | null | undefined;
151
+ useAccessGroups: boolean;
152
+ }) {
153
+ const normalizedOwner = params.ownerShip ? normalizeShip(params.ownerShip) : null;
154
+ return await resolveStableChannelMessageIngress({
155
+ channelId: "tlon",
156
+ accountId: "default",
157
+ identity: tlonIngressIdentity,
158
+ useAccessGroups: params.useAccessGroups,
159
+ subject: { stableId: params.senderShip },
160
+ conversation: {
161
+ kind: "direct",
162
+ id: "command",
163
+ },
164
+ event: {
165
+ authMode: "none",
166
+ mayPair: false,
167
+ },
168
+ dmPolicy: "allowlist",
169
+ groupPolicy: "open",
170
+ allowFrom: normalizedOwner ? [normalizedOwner] : [],
171
+ command: {},
172
+ });
173
+ }
174
+
175
+ export function isGroupInviteAllowed(
176
+ inviterShip: string,
177
+ allowlist: string[] | undefined,
178
+ ): boolean {
179
+ if (!allowlist || allowlist.length === 0) {
180
+ return false;
181
+ }
182
+ const normalizedInviter = normalizeShip(inviterShip);
183
+ return allowlist.map((ship) => normalizeShip(ship)).some((ship) => ship === normalizedInviter);
184
+ }
185
+
186
+ export async function resolveAuthorizedMessageText(params: {
187
+ rawText: string;
188
+ content: unknown;
189
+ authorizedForCites: boolean;
190
+ resolveAllCites: (content: unknown) => Promise<string>;
191
+ }): Promise<string> {
192
+ const { rawText, content, authorizedForCites, resolveAllCites } = params;
193
+ if (!authorizedForCites) {
194
+ return rawText;
195
+ }
196
+ const citedContent = await resolveAllCites(content);
197
+ return citedContent + rawText;
198
+ }
199
+
200
+ export const asRecord = asNullableObjectRecord;
201
+ export const formatErrorMessage = sharedFormatErrorMessage;
202
+ export const readString = readStringField;
203
+
204
+ function asNullableObjectRecord(value: unknown): Record<string, unknown> | null {
205
+ return value && typeof value === "object" && !Array.isArray(value)
206
+ ? (value as Record<string, unknown>)
207
+ : null;
208
+ }
209
+
210
+ function readStringField(
211
+ record: Record<string, unknown> | null | undefined,
212
+ field: string,
213
+ ): string | undefined {
214
+ const value = record?.[field];
215
+ return typeof value === "string" ? value : undefined;
216
+ }
217
+
218
+ // Helper to recursively extract text from inline content
219
+ function renderInlineItem(
220
+ item: unknown,
221
+ options?: {
222
+ linkMode?: "content-or-href" | "href";
223
+ allowBreak?: boolean;
224
+ allowBlockquote?: boolean;
225
+ },
226
+ ): string {
227
+ if (typeof item === "string") {
228
+ return item;
229
+ }
230
+ const record = asRecord(item);
231
+ if (!record) {
232
+ return "";
233
+ }
234
+ const ship = readString(record, "ship");
235
+ if (ship) {
236
+ return ship;
237
+ }
238
+ if ("sect" in record) {
239
+ const sect = record.sect;
240
+ if (typeof sect === "string") {
241
+ return `@${sect || "all"}`;
242
+ }
243
+ if (sect === null) {
244
+ return "@all";
245
+ }
246
+ }
247
+ if (options?.allowBreak && "break" in record) {
248
+ return "\n";
249
+ }
250
+ const inlineCode = readString(record, "inline-code");
251
+ if (inlineCode) {
252
+ return `\`${inlineCode}\``;
253
+ }
254
+ const code = readString(record, "code");
255
+ if (code) {
256
+ return `\`${code}\``;
257
+ }
258
+ const link = asRecord(record.link);
259
+ const linkHref = link ? readString(link, "href") : undefined;
260
+ if (link && linkHref) {
261
+ const linkContent = readString(link, "content");
262
+ return options?.linkMode === "href" ? linkHref : linkContent || linkHref;
263
+ }
264
+ if (Array.isArray(record.bold)) {
265
+ return `**${extractInlineText(record.bold)}**`;
266
+ }
267
+ if (Array.isArray(record.italics)) {
268
+ return `*${extractInlineText(record.italics)}*`;
269
+ }
270
+ if (Array.isArray(record.strike)) {
271
+ return `~~${extractInlineText(record.strike)}~~`;
272
+ }
273
+ if (options?.allowBlockquote && Array.isArray(record.blockquote)) {
274
+ return `> ${extractInlineText(record.blockquote)}`;
275
+ }
276
+ return "";
277
+ }
278
+
279
+ function extractInlineText(items: readonly unknown[]): string {
280
+ return items.map((item) => renderInlineItem(item)).join("");
281
+ }
282
+
283
+ export function extractMessageText(content: unknown): string {
284
+ if (!content || !Array.isArray(content)) {
285
+ return "";
286
+ }
287
+
288
+ return content
289
+ .map((verse) => {
290
+ const verseRecord = asRecord(verse);
291
+ if (!verseRecord) {
292
+ return "";
293
+ }
294
+
295
+ // Handle inline content (text, ships, links, etc.)
296
+ if (Array.isArray(verseRecord.inline)) {
297
+ return verseRecord.inline
298
+ .map((item) =>
299
+ renderInlineItem(item, {
300
+ linkMode: "href",
301
+ allowBreak: true,
302
+ allowBlockquote: true,
303
+ }),
304
+ )
305
+ .join("");
306
+ }
307
+
308
+ // Handle block content (images, code blocks, etc.)
309
+ const block = asRecord(verseRecord.block);
310
+ if (block) {
311
+ const image = asRecord(block.image);
312
+
313
+ // Image blocks
314
+ if (image) {
315
+ const imageSrc = readString(image, "src");
316
+ if (imageSrc) {
317
+ const altText = readString(image, "alt");
318
+ const alt = altText ? ` (${altText})` : "";
319
+ return `\n${imageSrc}${alt}\n`;
320
+ }
321
+ }
322
+
323
+ // Code blocks
324
+ const codeBlock = asRecord(block.code);
325
+ if (codeBlock) {
326
+ const lang = readString(codeBlock, "lang") ?? "";
327
+ const code = readString(codeBlock, "code") ?? "";
328
+ return `\n\`\`\`${lang}\n${code}\n\`\`\`\n`;
329
+ }
330
+
331
+ // Header blocks
332
+ const header = asRecord(block.header);
333
+ if (header) {
334
+ const headerContent = Array.isArray(header.content) ? header.content : [];
335
+ const text =
336
+ headerContent.map((item) => (typeof item === "string" ? item : "")).join("") || "";
337
+ return `\n## ${text}\n`;
338
+ }
339
+
340
+ // Cite/quote blocks - parse the reference structure
341
+ const cite = asRecord(block.cite);
342
+ if (cite) {
343
+ const chanCite = asRecord(cite.chan);
344
+
345
+ // ChanCite - reference to a channel message
346
+ if (chanCite) {
347
+ const nest = readString(chanCite, "nest");
348
+ const where = readString(chanCite, "where");
349
+ // where is typically /msg/~author/timestamp
350
+ const whereMatch = where?.match(/\/msg\/(~[a-z-]+)\/(.+)/);
351
+ if (whereMatch) {
352
+ const [, author, _postId] = whereMatch;
353
+ return `\n> [quoted: ${author} in ${nest}]\n`;
354
+ }
355
+ return `\n> [quoted from ${nest}]\n`;
356
+ }
357
+
358
+ // GroupCite - reference to a group
359
+ const group = readString(cite, "group");
360
+ if (group) {
361
+ return `\n> [ref: group ${group}]\n`;
362
+ }
363
+
364
+ // DeskCite - reference to an app/desk
365
+ const desk = asRecord(cite.desk);
366
+ if (desk) {
367
+ const flag = readString(desk, "flag");
368
+ if (flag) {
369
+ return `\n> [ref: ${flag}]\n`;
370
+ }
371
+ }
372
+
373
+ // BaitCite - reference with group+graph context
374
+ const bait = asRecord(cite.bait);
375
+ if (bait) {
376
+ const graph = readString(bait, "graph");
377
+ const groupName = readString(bait, "group");
378
+ if (graph && groupName) {
379
+ return `\n> [ref: ${graph} in ${groupName}]\n`;
380
+ }
381
+ }
382
+
383
+ return `\n> [quoted message]\n`;
384
+ }
385
+ }
386
+
387
+ return "";
388
+ })
389
+ .join("\n")
390
+ .trim();
391
+ }
392
+
393
+ export function isSummarizationRequest(messageText: string): boolean {
394
+ const patterns = [
395
+ /summarize\s+(this\s+)?(channel|chat|conversation)/i,
396
+ /what\s+did\s+i\s+miss/i,
397
+ /catch\s+me\s+up/i,
398
+ /channel\s+summary/i,
399
+ /tldr/i,
400
+ ];
401
+ return patterns.some((pattern) => pattern.test(messageText));
402
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { PluginRuntime } from "klaw/plugin-sdk/plugin-runtime";
2
+ import { createPluginRuntimeStore } from "klaw/plugin-sdk/runtime-store";
3
+
4
+ const { setRuntime: setTlonRuntime, getRuntime: getTlonRuntime } =
5
+ createPluginRuntimeStore<PluginRuntime>({
6
+ pluginId: "tlon",
7
+ errorMessage: "Tlon runtime not initialized",
8
+ });
9
+ export { getTlonRuntime, setTlonRuntime };