jeopi-cli 16.5.6 → 16.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.
- package/CHANGELOG.md +21 -0
- package/README.md +100 -0
- package/dist/cli.js +3717 -3882
- package/dist/types/cli/messenger-cli.d.ts +10 -0
- package/dist/types/commands/messenger.d.ts +18 -0
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/messenger/config.d.ts +4 -0
- package/dist/types/messenger/discord.d.ts +10 -0
- package/dist/types/messenger/gateway.d.ts +9 -0
- package/dist/types/messenger/http.d.ts +9 -0
- package/dist/types/messenger/session.d.ts +17 -0
- package/dist/types/messenger/slack.d.ts +2 -0
- package/dist/types/messenger/telegram.d.ts +2 -0
- package/dist/types/messenger/types.d.ts +38 -0
- package/package.json +12 -12
- package/src/cli/messenger-cli.ts +78 -0
- package/src/cli-commands.ts +1 -0
- package/src/commands/messenger.ts +30 -0
- package/src/config/model-registry.ts +43 -8
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/messenger/config.ts +111 -0
- package/src/messenger/discord.ts +382 -0
- package/src/messenger/gateway.ts +153 -0
- package/src/messenger/http.ts +87 -0
- package/src/messenger/session.ts +219 -0
- package/src/messenger/slack.ts +333 -0
- package/src/messenger/telegram.ts +244 -0
- package/src/messenger/types.ts +47 -0
- package/src/modes/controllers/selector-controller.ts +1 -2
- package/src/modes/rpc/rpc-mode.ts +1 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +1 -2
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { logger } from "jeopi-utils";
|
|
2
|
+
import { MessengerHttpError, messengerFetchJson, splitMessengerText, waitForMessengerRetry } from "./http";
|
|
3
|
+
import type { MessengerAdapter, MessengerMessage, MessengerPlatformConfig, MessengerTransportDeps } from "./types";
|
|
4
|
+
|
|
5
|
+
interface TelegramIdentity {
|
|
6
|
+
id: number;
|
|
7
|
+
username: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class TelegramApiError extends Error {
|
|
11
|
+
constructor(
|
|
12
|
+
readonly status: number,
|
|
13
|
+
readonly retryAfterMs = 0,
|
|
14
|
+
) {
|
|
15
|
+
super("Telegram API request failed");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isInteger(value: unknown): value is number {
|
|
24
|
+
return typeof value === "number" && Number.isSafeInteger(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isPositiveId(value: unknown): value is number {
|
|
28
|
+
return isInteger(value) && value > 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseIdentity(value: unknown): TelegramIdentity {
|
|
32
|
+
if (
|
|
33
|
+
!isRecord(value) ||
|
|
34
|
+
!isPositiveId(value.id) ||
|
|
35
|
+
value.is_bot !== true ||
|
|
36
|
+
typeof value.username !== "string" ||
|
|
37
|
+
!/^[a-zA-Z0-9_]{1,32}$/.test(value.username)
|
|
38
|
+
) {
|
|
39
|
+
throw new TelegramApiError(400);
|
|
40
|
+
}
|
|
41
|
+
return { id: value.id, username: value.username };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function stripMention(
|
|
45
|
+
text: string,
|
|
46
|
+
entities: unknown,
|
|
47
|
+
identity: TelegramIdentity,
|
|
48
|
+
): { text: string; mentioned: boolean } {
|
|
49
|
+
const mention = `@${identity.username.toLowerCase()}`;
|
|
50
|
+
const ranges: { start: number; end: number }[] = [];
|
|
51
|
+
if (Array.isArray(entities)) {
|
|
52
|
+
for (const entity of entities) {
|
|
53
|
+
if (
|
|
54
|
+
!isRecord(entity) ||
|
|
55
|
+
!isInteger(entity.offset) ||
|
|
56
|
+
!isPositiveId(entity.length) ||
|
|
57
|
+
entity.offset < 0 ||
|
|
58
|
+
entity.offset + entity.length > text.length
|
|
59
|
+
) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const start = entity.offset;
|
|
63
|
+
const end = start + entity.length;
|
|
64
|
+
const value = text.slice(start, end).toLowerCase();
|
|
65
|
+
if (
|
|
66
|
+
(entity.type === "mention" && value === mention) ||
|
|
67
|
+
(entity.type === "text_mention" && isRecord(entity.user) && entity.user.id === identity.id)
|
|
68
|
+
) {
|
|
69
|
+
ranges.push({ start, end });
|
|
70
|
+
} else if (
|
|
71
|
+
entity.type === "bot_command" &&
|
|
72
|
+
/^\/[a-z0-9_]+@[a-z0-9_]+$/.test(value) &&
|
|
73
|
+
value.endsWith(mention)
|
|
74
|
+
) {
|
|
75
|
+
ranges.push({ start: end - mention.length, end });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else if (entities === undefined) {
|
|
79
|
+
// Username is validated before interpolation; reject email/name substrings.
|
|
80
|
+
const pattern = new RegExp(`(^|[^\\p{L}\\p{N}_@])@${identity.username}(?![\\p{L}\\p{N}_])`, "giu");
|
|
81
|
+
for (const match of text.matchAll(pattern)) {
|
|
82
|
+
const start = match.index + match[1].length;
|
|
83
|
+
ranges.push({ start, end: start + mention.length });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (ranges.length === 0) return { text, mentioned: false };
|
|
87
|
+
ranges.sort((left, right) => left.start - right.start);
|
|
88
|
+
let cursor = 0;
|
|
89
|
+
let stripped = "";
|
|
90
|
+
for (const range of ranges) {
|
|
91
|
+
if (range.start < cursor) continue;
|
|
92
|
+
stripped += text.slice(cursor, range.start);
|
|
93
|
+
cursor = range.end;
|
|
94
|
+
}
|
|
95
|
+
return { text: (stripped + text.slice(cursor)).trim(), mentioned: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseMessage(value: unknown, identity: TelegramIdentity): MessengerMessage | undefined {
|
|
99
|
+
if (
|
|
100
|
+
!isRecord(value) ||
|
|
101
|
+
!isPositiveId(value.message_id) ||
|
|
102
|
+
typeof value.text !== "string" ||
|
|
103
|
+
!isRecord(value.from) ||
|
|
104
|
+
!isPositiveId(value.from.id) ||
|
|
105
|
+
value.from.is_bot !== false ||
|
|
106
|
+
!isRecord(value.chat) ||
|
|
107
|
+
!isInteger(value.chat.id) ||
|
|
108
|
+
value.chat.id === 0 ||
|
|
109
|
+
(value.chat.type !== "private" && value.chat.type !== "group" && value.chat.type !== "supergroup") ||
|
|
110
|
+
(value.message_thread_id !== undefined && !isPositiveId(value.message_thread_id)) ||
|
|
111
|
+
value.edit_date !== undefined ||
|
|
112
|
+
value.sender_chat !== undefined
|
|
113
|
+
) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const content = stripMention(value.text, value.entities, identity);
|
|
117
|
+
const direct = value.chat.type === "private";
|
|
118
|
+
if (!content.text.trim()) return undefined;
|
|
119
|
+
return {
|
|
120
|
+
platform: "telegram",
|
|
121
|
+
accountId: String(identity.id),
|
|
122
|
+
channelId: String(value.chat.id),
|
|
123
|
+
senderId: String(value.from.id),
|
|
124
|
+
messageId: String(value.message_id),
|
|
125
|
+
text: content.text,
|
|
126
|
+
...(value.message_thread_id === undefined ? {} : { threadId: String(value.message_thread_id) }),
|
|
127
|
+
direct,
|
|
128
|
+
mentioned: content.mentioned,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isPermanent(error: unknown): boolean {
|
|
133
|
+
if (!(error instanceof TelegramApiError || error instanceof MessengerHttpError)) return false;
|
|
134
|
+
return error.status >= 400 && error.status < 500 && error.status !== 408 && error.status !== 429;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createTelegramAdapter(
|
|
138
|
+
config: MessengerPlatformConfig,
|
|
139
|
+
env: Record<string, string | undefined> = process.env,
|
|
140
|
+
deps: MessengerTransportDeps = {},
|
|
141
|
+
): MessengerAdapter {
|
|
142
|
+
const token = env[config.tokenEnv];
|
|
143
|
+
if (!token?.trim()) throw new Error("Telegram bot token is missing");
|
|
144
|
+
const baseUrl = `https://api.telegram.org/bot${encodeURIComponent(token).replaceAll("%3A", ":")}/`;
|
|
145
|
+
let running = false;
|
|
146
|
+
let offset = 0;
|
|
147
|
+
|
|
148
|
+
async function request(method: string, parameters: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {
|
|
149
|
+
const response = await messengerFetchJson(
|
|
150
|
+
`${baseUrl}${method}`,
|
|
151
|
+
{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(parameters) },
|
|
152
|
+
signal,
|
|
153
|
+
deps.fetch,
|
|
154
|
+
);
|
|
155
|
+
if (!isRecord(response) || typeof response.ok !== "boolean") throw new TelegramApiError(400);
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
const status = isInteger(response.error_code) ? response.error_code : 400;
|
|
158
|
+
const retryAfter = isRecord(response.parameters) ? response.parameters.retry_after : undefined;
|
|
159
|
+
throw new TelegramApiError(status, isPositiveId(retryAfter) ? Math.min(retryAfter, 300) * 1_000 : 0);
|
|
160
|
+
}
|
|
161
|
+
if (!("result" in response)) throw new TelegramApiError(400);
|
|
162
|
+
return response.result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
platform: "telegram",
|
|
167
|
+
async start(onMessage, signal) {
|
|
168
|
+
if (signal.aborted) return;
|
|
169
|
+
if (running) throw new Error("Telegram adapter is already running");
|
|
170
|
+
running = true;
|
|
171
|
+
const stopped = Promise.withResolvers<void>();
|
|
172
|
+
const onAbort = () => stopped.resolve();
|
|
173
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
174
|
+
let identity: TelegramIdentity | undefined;
|
|
175
|
+
let retryMs = 1_000;
|
|
176
|
+
try {
|
|
177
|
+
while (!signal.aborted) {
|
|
178
|
+
try {
|
|
179
|
+
identity ??= parseIdentity(await request("getMe", {}, signal));
|
|
180
|
+
if (signal.aborted) break;
|
|
181
|
+
const updates = await request(
|
|
182
|
+
"getUpdates",
|
|
183
|
+
{ offset, timeout: 25, allowed_updates: ["message"] },
|
|
184
|
+
signal,
|
|
185
|
+
);
|
|
186
|
+
if (!Array.isArray(updates)) throw new TelegramApiError(400);
|
|
187
|
+
retryMs = 1_000;
|
|
188
|
+
for (const update of updates) {
|
|
189
|
+
if (signal.aborted) break;
|
|
190
|
+
if (
|
|
191
|
+
!isRecord(update) ||
|
|
192
|
+
!isInteger(update.update_id) ||
|
|
193
|
+
update.update_id < 0 ||
|
|
194
|
+
update.update_id === Number.MAX_SAFE_INTEGER
|
|
195
|
+
) {
|
|
196
|
+
throw new TelegramApiError(400);
|
|
197
|
+
}
|
|
198
|
+
if (update.update_id < offset) continue;
|
|
199
|
+
// Acknowledge even ignored/failed callbacks: replaying can duplicate replies.
|
|
200
|
+
offset = update.update_id + 1;
|
|
201
|
+
const message = parseMessage(update.message, identity);
|
|
202
|
+
if (!message) continue;
|
|
203
|
+
const processing = Promise.resolve()
|
|
204
|
+
.then(() => onMessage(message))
|
|
205
|
+
.catch(() => {
|
|
206
|
+
logger.warn("Telegram message handler failed");
|
|
207
|
+
});
|
|
208
|
+
await Promise.race([processing, stopped.promise]);
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (signal.aborted) break;
|
|
212
|
+
if (isPermanent(error)) throw new Error("Telegram authentication or polling configuration failed");
|
|
213
|
+
const delay =
|
|
214
|
+
error instanceof TelegramApiError || error instanceof MessengerHttpError
|
|
215
|
+
? Math.max(retryMs, error.retryAfterMs ?? 0)
|
|
216
|
+
: retryMs;
|
|
217
|
+
await waitForMessengerRetry(delay, signal);
|
|
218
|
+
retryMs = Math.min(retryMs * 2, 30_000);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} finally {
|
|
222
|
+
signal.removeEventListener("abort", onAbort);
|
|
223
|
+
running = false;
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
async send(message, text, signal) {
|
|
227
|
+
const threadId = message.threadId === undefined ? undefined : Number(message.threadId);
|
|
228
|
+
if (threadId !== undefined && !isPositiveId(threadId)) throw new Error("Telegram topic identifier is invalid");
|
|
229
|
+
for (const chunk of splitMessengerText(text, 4_096)) {
|
|
230
|
+
// Never retry an ambiguous send failure: Telegram may already have delivered it.
|
|
231
|
+
const result = await request(
|
|
232
|
+
"sendMessage",
|
|
233
|
+
{
|
|
234
|
+
chat_id: message.channelId,
|
|
235
|
+
text: chunk,
|
|
236
|
+
...(threadId === undefined ? {} : { message_thread_id: threadId }),
|
|
237
|
+
},
|
|
238
|
+
signal,
|
|
239
|
+
);
|
|
240
|
+
if (!isRecord(result) || !isPositiveId(result.message_id)) throw new TelegramApiError(400);
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type MessengerPlatform = "discord" | "telegram" | "slack";
|
|
2
|
+
|
|
3
|
+
export interface MessengerMessage {
|
|
4
|
+
platform: MessengerPlatform;
|
|
5
|
+
accountId: string;
|
|
6
|
+
channelId: string;
|
|
7
|
+
senderId: string;
|
|
8
|
+
messageId: string;
|
|
9
|
+
text: string;
|
|
10
|
+
threadId?: string;
|
|
11
|
+
direct: boolean;
|
|
12
|
+
mentioned: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface MessengerAdapter {
|
|
16
|
+
platform: MessengerPlatform;
|
|
17
|
+
start(onMessage: (message: MessengerMessage) => Promise<void>, signal: AbortSignal): Promise<void>;
|
|
18
|
+
send(message: MessengerMessage, text: string, signal?: AbortSignal): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MessengerPlatformConfig {
|
|
22
|
+
tokenEnv: string;
|
|
23
|
+
appTokenEnv?: string;
|
|
24
|
+
allowedUserIds: string[];
|
|
25
|
+
allowedChannelIds?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MessengerConfig {
|
|
29
|
+
cwd: string;
|
|
30
|
+
sessionDir: string;
|
|
31
|
+
toolNames: string[];
|
|
32
|
+
platforms: Partial<Record<MessengerPlatform, MessengerPlatformConfig>>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MessengerTransportDeps {
|
|
36
|
+
fetch?: typeof fetch;
|
|
37
|
+
WebSocket?: typeof WebSocket;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type MessengerReply = (message: MessengerMessage, signal: AbortSignal) => Promise<string>;
|
|
41
|
+
/** A partially initialized SDK must not be retried inside a live gateway. */
|
|
42
|
+
export class MessengerFatalError extends Error {
|
|
43
|
+
constructor() {
|
|
44
|
+
super("Messenger session initialization failed; restart required");
|
|
45
|
+
this.name = "MessengerFatalError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1131,7 +1131,7 @@ export class SelectorController {
|
|
|
1131
1131
|
const manualInput = this.ctx.oauthManualInput;
|
|
1132
1132
|
const useManualInput = PASTE_CODE_LOGIN_PROVIDERS.has(providerId);
|
|
1133
1133
|
try {
|
|
1134
|
-
await this.ctx.session.modelRegistry.
|
|
1134
|
+
await this.ctx.session.modelRegistry.login(providerId as OAuthProvider, {
|
|
1135
1135
|
onAuth: (info: { url: string; instructions?: string }) => {
|
|
1136
1136
|
const block = new TranscriptBlock();
|
|
1137
1137
|
block.addChild(new Text(theme.fg("dim", info.url), 1, 0));
|
|
@@ -1175,7 +1175,6 @@ export class SelectorController {
|
|
|
1175
1175
|
},
|
|
1176
1176
|
onManualCodeInput: useManualInput ? () => manualInput.waitForInput(providerId) : undefined,
|
|
1177
1177
|
});
|
|
1178
|
-
await this.ctx.session.modelRegistry.refresh();
|
|
1179
1178
|
const block = new TranscriptBlock();
|
|
1180
1179
|
block.addChild(
|
|
1181
1180
|
new Text(theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`), 1, 0),
|
|
@@ -1289,7 +1289,7 @@ export async function runRpcMode(
|
|
|
1289
1289
|
// We use this ordering to self-classify at runtime — no static allowlist.
|
|
1290
1290
|
let authEmitted = false;
|
|
1291
1291
|
try {
|
|
1292
|
-
await session.modelRegistry.
|
|
1292
|
+
await session.modelRegistry.login(command.providerId, {
|
|
1293
1293
|
onAuth: info => {
|
|
1294
1294
|
authEmitted = true;
|
|
1295
1295
|
output({
|
|
@@ -1321,7 +1321,6 @@ export async function runRpcMode(
|
|
|
1321
1321
|
return new Promise<string>(() => {});
|
|
1322
1322
|
},
|
|
1323
1323
|
});
|
|
1324
|
-
await session.modelRegistry.refresh();
|
|
1325
1324
|
return success(id, "login", { providerId: command.providerId });
|
|
1326
1325
|
} catch (err: unknown) {
|
|
1327
1326
|
return error(id, "login", err instanceof Error ? err.message : String(err));
|
|
@@ -179,7 +179,7 @@ export class SignInTab implements SetupTab {
|
|
|
179
179
|
this.host.restoreFocus();
|
|
180
180
|
this.host.requestRender();
|
|
181
181
|
try {
|
|
182
|
-
await this
|
|
182
|
+
await this.host.ctx.session.modelRegistry.login(providerId as OAuthProvider, {
|
|
183
183
|
signal: this.#loginAbort.signal,
|
|
184
184
|
onAuth: info => {
|
|
185
185
|
this.#authUrl = info.url;
|
|
@@ -202,7 +202,6 @@ export class SignInTab implements SetupTab {
|
|
|
202
202
|
onManualCodeInput: () =>
|
|
203
203
|
this.#showPrompt({ message: "Paste the authorization code (or full redirect URL):" }),
|
|
204
204
|
});
|
|
205
|
-
await this.host.ctx.session.modelRegistry.refresh();
|
|
206
205
|
if (this.#disposed) return;
|
|
207
206
|
this.#statusLines = [
|
|
208
207
|
theme.fg("success", `${theme.status.success} Signed in to ${providerId}`),
|