@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.
- package/README.md +5 -0
- package/api.ts +16 -0
- package/channel-plugin-api.ts +1 -0
- package/dist/api.js +4 -0
- package/dist/channel-Bvzym9ez.js +236 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.runtime-CDY2BdfM.js +3626 -0
- package/dist/doctor-contract-Ip6FcHDH.js +7 -0
- package/dist/doctor-contract-api.js +2 -0
- package/dist/index.js +18 -0
- package/dist/runtime-BmSb9A-q.js +8 -0
- package/dist/runtime-api-Dq8wkBC_.js +4 -0
- package/dist/runtime-api.js +2 -0
- package/dist/setup-api.js +3 -0
- package/dist/setup-core-CF3ryHqs.js +387 -0
- package/dist/setup-entry.js +11 -0
- package/dist/setup-surface-BM5_V_XL.js +74 -0
- package/dist/test-api.js +2 -0
- package/doctor-contract-api.ts +1 -0
- package/index.ts +16 -0
- package/klaw.plugin.json +3 -203
- package/package.json +4 -4
- package/runtime-api.ts +17 -0
- package/setup-api.ts +2 -0
- package/setup-entry.ts +9 -0
- package/src/account-fields.ts +31 -0
- package/src/channel.message-adapter.test.ts +145 -0
- package/src/channel.runtime.ts +259 -0
- package/src/channel.ts +192 -0
- package/src/config-schema.ts +54 -0
- package/src/core.test.ts +298 -0
- package/src/doctor-contract.ts +9 -0
- package/src/doctor.test.ts +46 -0
- package/src/doctor.ts +10 -0
- package/src/logger-runtime.ts +1 -0
- package/src/monitor/approval-runtime.ts +363 -0
- package/src/monitor/approval.test.ts +33 -0
- package/src/monitor/approval.ts +283 -0
- package/src/monitor/authorization.ts +30 -0
- package/src/monitor/cites.ts +54 -0
- package/src/monitor/discovery.ts +68 -0
- package/src/monitor/history.ts +226 -0
- package/src/monitor/index.ts +1523 -0
- package/src/monitor/media.test.ts +80 -0
- package/src/monitor/media.ts +156 -0
- package/src/monitor/processed-messages.test.ts +58 -0
- package/src/monitor/processed-messages.ts +89 -0
- package/src/monitor/settings-helpers.test.ts +113 -0
- package/src/monitor/settings-helpers.ts +158 -0
- package/src/monitor/utils.ts +402 -0
- package/src/runtime.ts +9 -0
- package/src/security.test.ts +658 -0
- package/src/session-route.ts +40 -0
- package/src/settings.ts +391 -0
- package/src/setup-core.ts +231 -0
- package/src/setup-surface.ts +99 -0
- package/src/targets.ts +102 -0
- package/src/tlon-api.test.ts +572 -0
- package/src/tlon-api.ts +389 -0
- package/src/types.ts +160 -0
- package/src/urbit/auth.ssrf.test.ts +45 -0
- package/src/urbit/auth.ts +48 -0
- package/src/urbit/base-url.test.ts +48 -0
- package/src/urbit/base-url.ts +61 -0
- package/src/urbit/channel-ops.test.ts +36 -0
- package/src/urbit/channel-ops.ts +149 -0
- package/src/urbit/context.ts +50 -0
- package/src/urbit/errors.ts +51 -0
- package/src/urbit/fetch.ts +38 -0
- package/src/urbit/foreigns.ts +49 -0
- package/src/urbit/send.test.ts +83 -0
- package/src/urbit/send.ts +228 -0
- package/src/urbit/sse-client.test.ts +234 -0
- package/src/urbit/sse-client.ts +492 -0
- package/src/urbit/story.ts +332 -0
- package/src/urbit/upload.test.ts +155 -0
- package/src/urbit/upload.ts +60 -0
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/api.js +0 -7
- package/bundled-skills/@tloncorp/tlon-skill/SKILL.md +0 -501
- package/bundled-skills/@tloncorp/tlon-skill/bin/tlon.js +0 -7
- package/bundled-skills/@tloncorp/tlon-skill/package.json +0 -40
- package/bundled-skills/@tloncorp/tlon-skill/scripts/postinstall.js +0 -7
- package/channel-plugin-api.js +0 -7
- package/doctor-contract-api.js +0 -7
- package/index.js +0 -7
- package/runtime-api.js +0 -7
- package/setup-api.js +0 -7
- package/setup-entry.js +0 -7
- package/test-api.js +0 -7
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { RuntimeEnv } from "klaw/plugin-sdk/runtime";
|
|
2
|
+
import { asRecord, extractCites, extractMessageText, type ParsedCite } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
type TlonScryApi = {
|
|
5
|
+
scry: (path: string) => Promise<unknown>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function createTlonCitationResolver(params: { api: TlonScryApi; runtime: RuntimeEnv }) {
|
|
9
|
+
const { api, runtime } = params;
|
|
10
|
+
|
|
11
|
+
const resolveCiteContent = async (cite: ParsedCite): Promise<string | null> => {
|
|
12
|
+
if (cite.type !== "chan" || !cite.nest || !cite.postId) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const scryPath = `/channels/v4/${cite.nest}/posts/post/${cite.postId}.json`;
|
|
18
|
+
runtime.log?.(`[tlon] Fetching cited post: ${scryPath}`);
|
|
19
|
+
|
|
20
|
+
const data = asRecord(await api.scry(scryPath));
|
|
21
|
+
const essay = asRecord(data?.essay);
|
|
22
|
+
if (essay?.content) {
|
|
23
|
+
return extractMessageText(essay.content) || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return null;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
runtime.log?.(`[tlon] Failed to fetch cited post: ${String(err)}`);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const resolveAllCites = async (content: unknown): Promise<string> => {
|
|
34
|
+
const cites = extractCites(content);
|
|
35
|
+
if (cites.length === 0) {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const resolved: string[] = [];
|
|
40
|
+
for (const cite of cites) {
|
|
41
|
+
const text = await resolveCiteContent(cite);
|
|
42
|
+
if (text) {
|
|
43
|
+
resolved.push(`> ${cite.author || "unknown"} wrote: ${text}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return resolved.length > 0 ? `${resolved.join("\n")}\n\n` : "";
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
resolveCiteContent,
|
|
52
|
+
resolveAllCites,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { RuntimeEnv } from "klaw/plugin-sdk/runtime";
|
|
2
|
+
import type { Foreigns } from "../urbit/foreigns.js";
|
|
3
|
+
import { asRecord, formatErrorMessage } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
interface InitData {
|
|
6
|
+
channels: string[];
|
|
7
|
+
foreigns: Foreigns | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fetch groups-ui init data, returning channels and foreigns.
|
|
12
|
+
* This is a single scry that provides both channel discovery and pending invites.
|
|
13
|
+
*/
|
|
14
|
+
export async function fetchInitData(
|
|
15
|
+
api: { scry: (path: string) => Promise<unknown> },
|
|
16
|
+
runtime: RuntimeEnv,
|
|
17
|
+
): Promise<InitData> {
|
|
18
|
+
try {
|
|
19
|
+
runtime.log?.("[tlon] Fetching groups-ui init data...");
|
|
20
|
+
const initData = asRecord(await api.scry("/groups-ui/v6/init.json"));
|
|
21
|
+
|
|
22
|
+
const channels: string[] = [];
|
|
23
|
+
const groups = asRecord(initData?.groups);
|
|
24
|
+
if (groups) {
|
|
25
|
+
for (const groupData of Object.values(groups)) {
|
|
26
|
+
const typedGroupData = asRecord(groupData);
|
|
27
|
+
const groupChannels = asRecord(typedGroupData?.channels);
|
|
28
|
+
if (groupChannels) {
|
|
29
|
+
for (const channelNest of Object.keys(groupChannels)) {
|
|
30
|
+
if (channelNest.startsWith("chat/")) {
|
|
31
|
+
channels.push(channelNest);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (channels.length > 0) {
|
|
39
|
+
runtime.log?.(`[tlon] Auto-discovered ${channels.length} chat channel(s)`);
|
|
40
|
+
} else {
|
|
41
|
+
runtime.log?.("[tlon] No chat channels found via auto-discovery");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const foreignsValue = asRecord(initData?.foreigns);
|
|
45
|
+
const foreigns = foreignsValue ? (foreignsValue as Foreigns) : null;
|
|
46
|
+
if (foreigns) {
|
|
47
|
+
const pendingCount = Object.values(foreigns).filter((f) =>
|
|
48
|
+
f.invites?.some((i) => i.valid),
|
|
49
|
+
).length;
|
|
50
|
+
if (pendingCount > 0) {
|
|
51
|
+
runtime.log?.(`[tlon] Found ${pendingCount} pending group invite(s)`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { channels, foreigns };
|
|
56
|
+
} catch (error: unknown) {
|
|
57
|
+
runtime.log?.(`[tlon] Init data fetch failed: ${formatErrorMessage(error)}`);
|
|
58
|
+
return { channels: [], foreigns: null };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function fetchAllChannels(
|
|
63
|
+
api: { scry: (path: string) => Promise<unknown> },
|
|
64
|
+
runtime: RuntimeEnv,
|
|
65
|
+
): Promise<string[]> {
|
|
66
|
+
const { channels } = await fetchInitData(api, runtime);
|
|
67
|
+
return channels;
|
|
68
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { RuntimeEnv } from "klaw/plugin-sdk/runtime";
|
|
2
|
+
import { asRecord, extractMessageText, formatErrorMessage } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Format a number as @ud (with dots every 3 digits from the right)
|
|
6
|
+
* e.g., 170141184507799509469114119040828178432 -> 170.141.184.507.799.509.469.114.119.040.828.178.432
|
|
7
|
+
*/
|
|
8
|
+
function formatUd(id: string | number): string {
|
|
9
|
+
const str = String(id).replace(/\./g, ""); // Remove any existing dots
|
|
10
|
+
const reversed = str.split("").toReversed();
|
|
11
|
+
const chunks: string[] = [];
|
|
12
|
+
for (let i = 0; i < reversed.length; i += 3) {
|
|
13
|
+
chunks.push(
|
|
14
|
+
reversed
|
|
15
|
+
.slice(i, i + 3)
|
|
16
|
+
.toReversed()
|
|
17
|
+
.join(""),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return chunks.toReversed().join(".");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type TlonHistoryEntry = {
|
|
24
|
+
author: string;
|
|
25
|
+
content: string;
|
|
26
|
+
timestamp: number;
|
|
27
|
+
id?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function createHistoryEntryFromMemo(params: {
|
|
31
|
+
memo?: Record<string, unknown> | null;
|
|
32
|
+
seal?: Record<string, unknown> | null;
|
|
33
|
+
fallbackId?: unknown;
|
|
34
|
+
}): TlonHistoryEntry {
|
|
35
|
+
const { memo, seal, fallbackId } = params;
|
|
36
|
+
return {
|
|
37
|
+
author: typeof memo?.author === "string" ? memo.author : "unknown",
|
|
38
|
+
content: extractMessageText(memo?.content || []),
|
|
39
|
+
timestamp: typeof memo?.sent === "number" ? memo.sent : Date.now(),
|
|
40
|
+
id:
|
|
41
|
+
typeof seal?.id === "string"
|
|
42
|
+
? seal.id
|
|
43
|
+
: typeof fallbackId === "string"
|
|
44
|
+
? fallbackId
|
|
45
|
+
: undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const messageCache = new Map<string, TlonHistoryEntry[]>();
|
|
50
|
+
const MAX_CACHED_MESSAGES = 100;
|
|
51
|
+
|
|
52
|
+
export function cacheMessage(channelNest: string, message: TlonHistoryEntry) {
|
|
53
|
+
if (!messageCache.has(channelNest)) {
|
|
54
|
+
messageCache.set(channelNest, []);
|
|
55
|
+
}
|
|
56
|
+
const cache = messageCache.get(channelNest);
|
|
57
|
+
if (!cache) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
cache.unshift(message);
|
|
61
|
+
if (cache.length > MAX_CACHED_MESSAGES) {
|
|
62
|
+
cache.pop();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function fetchChannelHistory(
|
|
67
|
+
api: { scry: (path: string) => Promise<unknown> },
|
|
68
|
+
channelNest: string,
|
|
69
|
+
count = 50,
|
|
70
|
+
runtime?: RuntimeEnv,
|
|
71
|
+
): Promise<TlonHistoryEntry[]> {
|
|
72
|
+
try {
|
|
73
|
+
const scryPath = `/channels/v4/${channelNest}/posts/newest/${count}/outline.json`;
|
|
74
|
+
runtime?.log?.(`[tlon] Fetching history: ${scryPath}`);
|
|
75
|
+
|
|
76
|
+
const data: unknown = await api.scry(scryPath);
|
|
77
|
+
if (!data) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let posts: unknown[] = [];
|
|
82
|
+
if (Array.isArray(data)) {
|
|
83
|
+
posts = data;
|
|
84
|
+
} else {
|
|
85
|
+
const dataRecord = asRecord(data);
|
|
86
|
+
const postMap = asRecord(dataRecord?.posts);
|
|
87
|
+
if (postMap) {
|
|
88
|
+
posts = Object.values(postMap);
|
|
89
|
+
} else if (dataRecord) {
|
|
90
|
+
posts = Object.values(dataRecord);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const messages = posts
|
|
95
|
+
.map((item) => {
|
|
96
|
+
const itemRecord = asRecord(item);
|
|
97
|
+
const replyPost = asRecord(itemRecord?.["r-post"]);
|
|
98
|
+
const replyPostSet = asRecord(replyPost?.set);
|
|
99
|
+
const essay = asRecord(itemRecord?.essay) ?? asRecord(replyPostSet?.essay);
|
|
100
|
+
const seal = asRecord(itemRecord?.seal) ?? asRecord(replyPostSet?.seal);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
author: typeof essay?.author === "string" ? essay.author : "unknown",
|
|
104
|
+
content: extractMessageText(essay?.content || []),
|
|
105
|
+
timestamp: typeof essay?.sent === "number" ? essay.sent : Date.now(),
|
|
106
|
+
id: typeof seal?.id === "string" ? seal.id : undefined,
|
|
107
|
+
} as TlonHistoryEntry;
|
|
108
|
+
})
|
|
109
|
+
.filter((msg) => msg.content);
|
|
110
|
+
|
|
111
|
+
runtime?.log?.(`[tlon] Extracted ${messages.length} messages from history`);
|
|
112
|
+
return messages;
|
|
113
|
+
} catch (error: unknown) {
|
|
114
|
+
runtime?.log?.(`[tlon] Error fetching channel history: ${formatErrorMessage(error)}`);
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function getChannelHistory(
|
|
120
|
+
api: { scry: (path: string) => Promise<unknown> },
|
|
121
|
+
channelNest: string,
|
|
122
|
+
count = 50,
|
|
123
|
+
runtime?: RuntimeEnv,
|
|
124
|
+
): Promise<TlonHistoryEntry[]> {
|
|
125
|
+
const cache = messageCache.get(channelNest) ?? [];
|
|
126
|
+
if (cache.length >= count) {
|
|
127
|
+
runtime?.log?.(`[tlon] Using cached messages (${cache.length} available)`);
|
|
128
|
+
return cache.slice(0, count);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
runtime?.log?.(`[tlon] Cache has ${cache.length} messages, need ${count}, fetching from scry...`);
|
|
132
|
+
return await fetchChannelHistory(api, channelNest, count, runtime);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Fetch thread/reply history for a specific parent post.
|
|
137
|
+
* Used to get context when entering a thread conversation.
|
|
138
|
+
*/
|
|
139
|
+
export async function fetchThreadHistory(
|
|
140
|
+
api: { scry: (path: string) => Promise<unknown> },
|
|
141
|
+
channelNest: string,
|
|
142
|
+
parentId: string,
|
|
143
|
+
count = 50,
|
|
144
|
+
runtime?: RuntimeEnv,
|
|
145
|
+
): Promise<TlonHistoryEntry[]> {
|
|
146
|
+
try {
|
|
147
|
+
// Tlon API: fetch replies to a specific post
|
|
148
|
+
// Format: /channels/v4/{nest}/posts/post/{parentId}/replies/newest/{count}.json
|
|
149
|
+
// parentId needs @ud formatting (dots every 3 digits)
|
|
150
|
+
const formattedParentId = formatUd(parentId);
|
|
151
|
+
runtime?.log?.(
|
|
152
|
+
`[tlon] Thread history - parentId: ${parentId} -> formatted: ${formattedParentId}`,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const scryPath = `/channels/v4/${channelNest}/posts/post/id/${formattedParentId}/replies/newest/${count}.json`;
|
|
156
|
+
runtime?.log?.(`[tlon] Fetching thread history: ${scryPath}`);
|
|
157
|
+
|
|
158
|
+
const data: unknown = await api.scry(scryPath);
|
|
159
|
+
if (!data) {
|
|
160
|
+
runtime?.log?.(`[tlon] No thread history data returned`);
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let replies: unknown[] = [];
|
|
165
|
+
if (Array.isArray(data)) {
|
|
166
|
+
replies = data;
|
|
167
|
+
} else {
|
|
168
|
+
const dataRecord = asRecord(data);
|
|
169
|
+
const replyValue = dataRecord?.replies;
|
|
170
|
+
if (Array.isArray(replyValue)) {
|
|
171
|
+
replies = replyValue;
|
|
172
|
+
} else if (typeof replyValue === "object" && replyValue) {
|
|
173
|
+
replies = Object.values(replyValue as Record<string, unknown>);
|
|
174
|
+
} else if (dataRecord) {
|
|
175
|
+
replies = Object.values(dataRecord);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const messages = replies
|
|
180
|
+
.map((item) => {
|
|
181
|
+
// Thread replies use 'memo' structure
|
|
182
|
+
const itemRecord = asRecord(item);
|
|
183
|
+
const replyRecord = asRecord(itemRecord?.["r-reply"]);
|
|
184
|
+
const replySet = asRecord(replyRecord?.set);
|
|
185
|
+
const memo = asRecord(itemRecord?.memo) ?? asRecord(replySet?.memo) ?? itemRecord;
|
|
186
|
+
const seal = asRecord(itemRecord?.seal) ?? asRecord(replySet?.seal);
|
|
187
|
+
|
|
188
|
+
return createHistoryEntryFromMemo({ memo, seal, fallbackId: itemRecord?.id });
|
|
189
|
+
})
|
|
190
|
+
.filter((msg) => msg.content);
|
|
191
|
+
|
|
192
|
+
runtime?.log?.(`[tlon] Extracted ${messages.length} thread replies from history`);
|
|
193
|
+
return messages;
|
|
194
|
+
} catch (error: unknown) {
|
|
195
|
+
runtime?.log?.(`[tlon] Error fetching thread history: ${formatErrorMessage(error)}`);
|
|
196
|
+
// Fall back to trying alternate path structure
|
|
197
|
+
try {
|
|
198
|
+
const altPath = `/channels/v4/${channelNest}/posts/post/id/${formatUd(parentId)}.json`;
|
|
199
|
+
runtime?.log?.(`[tlon] Trying alternate path: ${altPath}`);
|
|
200
|
+
const data = asRecord(await api.scry(altPath));
|
|
201
|
+
const dataSeal = asRecord(data?.seal);
|
|
202
|
+
const dataMeta = asRecord(dataSeal?.meta);
|
|
203
|
+
const repliesValue = data?.replies;
|
|
204
|
+
|
|
205
|
+
if (typeof dataMeta?.replyCount === "number" && dataMeta.replyCount > 0 && repliesValue) {
|
|
206
|
+
const replies = Array.isArray(repliesValue)
|
|
207
|
+
? repliesValue
|
|
208
|
+
: Object.values(repliesValue as Record<string, unknown>);
|
|
209
|
+
const messages = replies
|
|
210
|
+
.map((reply: unknown) => {
|
|
211
|
+
const replyRecord = asRecord(reply);
|
|
212
|
+
const memo = asRecord(replyRecord?.memo);
|
|
213
|
+
const seal = asRecord(replyRecord?.seal);
|
|
214
|
+
return createHistoryEntryFromMemo({ memo, seal });
|
|
215
|
+
})
|
|
216
|
+
.filter((msg: TlonHistoryEntry) => msg.content);
|
|
217
|
+
|
|
218
|
+
runtime?.log?.(`[tlon] Extracted ${messages.length} replies from post data`);
|
|
219
|
+
return messages;
|
|
220
|
+
}
|
|
221
|
+
} catch (altError: unknown) {
|
|
222
|
+
runtime?.log?.(`[tlon] Alternate path also failed: ${formatErrorMessage(altError)}`);
|
|
223
|
+
}
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
}
|