@nodetool-ai/telegram 0.8.1
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 +49 -0
- package/dist/.nodetool-build-stamp +1 -0
- package/dist/bot-api.d.ts +115 -0
- package/dist/bot-api.d.ts.map +1 -0
- package/dist/bot-api.js +145 -0
- package/dist/bot-api.js.map +1 -0
- package/dist/chunk.d.ts +36 -0
- package/dist/chunk.d.ts.map +1 -0
- package/dist/chunk.js +280 -0
- package/dist/chunk.js.map +1 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +62 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +83 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +191 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +65 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +139 -0
- package/dist/config.js.map +1 -0
- package/dist/frame-renderer.d.ts +135 -0
- package/dist/frame-renderer.d.ts.map +1 -0
- package/dist/frame-renderer.js +407 -0
- package/dist/frame-renderer.js.map +1 -0
- package/dist/identity-client.d.ts +132 -0
- package/dist/identity-client.d.ts.map +1 -0
- package/dist/identity-client.js +204 -0
- package/dist/identity-client.js.map +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +90 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown-html.d.ts +32 -0
- package/dist/markdown-html.d.ts.map +1 -0
- package/dist/markdown-html.js +251 -0
- package/dist/markdown-html.js.map +1 -0
- package/dist/nodetool-client.d.ts +32 -0
- package/dist/nodetool-client.d.ts.map +1 -0
- package/dist/nodetool-client.js +127 -0
- package/dist/nodetool-client.js.map +1 -0
- package/dist/register-commands.d.ts +24 -0
- package/dist/register-commands.d.ts.map +1 -0
- package/dist/register-commands.js +29 -0
- package/dist/register-commands.js.map +1 -0
- package/dist/strings.d.ts +7 -0
- package/dist/strings.d.ts.map +1 -0
- package/dist/strings.js +13 -0
- package/dist/strings.js.map +1 -0
- package/dist/telegram-adapter.d.ts +100 -0
- package/dist/telegram-adapter.d.ts.map +1 -0
- package/dist/telegram-adapter.js +366 -0
- package/dist/telegram-adapter.js.map +1 -0
- package/dist/turn-router.d.ts +167 -0
- package/dist/turn-router.d.ts.map +1 -0
- package/dist/turn-router.js +360 -0
- package/dist/turn-router.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IdentityClient — the bridge's half of the identity layer (design §5).
|
|
3
|
+
*
|
|
4
|
+
* The bridge holds exactly one credential: the service token that identifies
|
|
5
|
+
* it to the NodeTool server. It never sees a user password, an OAuth code, or
|
|
6
|
+
* a long-lived user token. To act as a linked user it exchanges the Telegram
|
|
7
|
+
* user id for a short-lived delegated token, and caches that token until
|
|
8
|
+
* shortly before it expires.
|
|
9
|
+
*
|
|
10
|
+
* Every route this module calls is `packages/websocket/src/routes/integrations.ts`.
|
|
11
|
+
* All I/O goes through the injected `fetch`, so the whole flow is testable
|
|
12
|
+
* without a server.
|
|
13
|
+
*/
|
|
14
|
+
export declare const TOKEN_EXPIRY_SLACK_MS: number;
|
|
15
|
+
/** The identity layer's provider column for this bridge. */
|
|
16
|
+
export declare const TELEGRAM_PROVIDER = "telegram";
|
|
17
|
+
export interface IdentityClientOptions {
|
|
18
|
+
/** NodeTool server base URL, no trailing slash. */
|
|
19
|
+
readonly apiUrl: string;
|
|
20
|
+
/** `NODETOOL_INTEGRATION_TOKEN` — the bridge's service token. */
|
|
21
|
+
readonly integrationToken: string;
|
|
22
|
+
/** Injected so tests need no network. */
|
|
23
|
+
readonly fetch: typeof fetch;
|
|
24
|
+
/** Identity provider name. Defaults to `telegram`. */
|
|
25
|
+
readonly provider?: string;
|
|
26
|
+
/** Clock, in ms. Defaults to `Date.now`. */
|
|
27
|
+
readonly now?: () => number;
|
|
28
|
+
/** Cache slack before expiry. Defaults to {@link TOKEN_EXPIRY_SLACK_MS}. */
|
|
29
|
+
readonly expirySlackMs?: number;
|
|
30
|
+
}
|
|
31
|
+
/** A delegated token good for acting as the linked NodeTool user. */
|
|
32
|
+
export interface LinkedIdentity {
|
|
33
|
+
readonly unlinked: false;
|
|
34
|
+
/** Bearer token for `/ws`, `/trpc` and asset URLs. */
|
|
35
|
+
readonly token: string;
|
|
36
|
+
/** The NodeTool user the token authenticates as. */
|
|
37
|
+
readonly userId: string;
|
|
38
|
+
/** Absolute expiry, in ms since the epoch. */
|
|
39
|
+
readonly expiresAtMs: number;
|
|
40
|
+
}
|
|
41
|
+
/** Why no token could be minted. Both cases are answered in the chat, not thrown. */
|
|
42
|
+
export type UnlinkedReason =
|
|
43
|
+
/** No `external_identities` row: the user must link first (HTTP 404). */
|
|
44
|
+
"not-linked"
|
|
45
|
+
/** Server runs in local single-user trust mode (HTTP 409, design §9). */
|
|
46
|
+
| "local-mode";
|
|
47
|
+
export interface UnlinkedIdentity {
|
|
48
|
+
readonly unlinked: true;
|
|
49
|
+
readonly reason: UnlinkedReason;
|
|
50
|
+
/** The server's own explanation, for relaying to the user. */
|
|
51
|
+
readonly message: string;
|
|
52
|
+
}
|
|
53
|
+
export type IdentityResolution = LinkedIdentity | UnlinkedIdentity;
|
|
54
|
+
/** A link code the user turns into an `external_identities` row in the browser. */
|
|
55
|
+
export interface LinkStart {
|
|
56
|
+
readonly code: string;
|
|
57
|
+
/** Confirmation page URL to send to the user. */
|
|
58
|
+
readonly url: string;
|
|
59
|
+
/** ISO timestamp the code stops working at, when the server reported one. */
|
|
60
|
+
readonly expiresAt: string | null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Outcome of completing a `/start <code>` deep link.
|
|
64
|
+
*
|
|
65
|
+
* `unsupported` is the committed server's answer today: `/link/complete`
|
|
66
|
+
* requires a `user_id` the bridge does not know, because the code it mints is
|
|
67
|
+
* bound to an `external_id`, not to a user. See the module note below.
|
|
68
|
+
*/
|
|
69
|
+
export type DeepLinkResult = {
|
|
70
|
+
readonly ok: true;
|
|
71
|
+
} | {
|
|
72
|
+
readonly ok: false;
|
|
73
|
+
readonly reason: "expired" | "mismatch" | "unsupported" | "server-error";
|
|
74
|
+
readonly message: string;
|
|
75
|
+
};
|
|
76
|
+
/** A transport or server failure the caller must surface rather than swallow. */
|
|
77
|
+
export declare class IdentityError extends Error {
|
|
78
|
+
/** HTTP status, or null when the request never got a response. */
|
|
79
|
+
readonly status: number | null;
|
|
80
|
+
constructor(message: string, status: number | null, options?: {
|
|
81
|
+
cause?: unknown;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
export declare class IdentityClient {
|
|
85
|
+
private readonly apiUrl;
|
|
86
|
+
private readonly integrationToken;
|
|
87
|
+
private readonly fetchImpl;
|
|
88
|
+
private readonly provider;
|
|
89
|
+
private readonly now;
|
|
90
|
+
private readonly expirySlackMs;
|
|
91
|
+
private readonly cache;
|
|
92
|
+
constructor(options: IdentityClientOptions);
|
|
93
|
+
/**
|
|
94
|
+
* A delegated token for this Telegram user, from cache when one is still
|
|
95
|
+
* comfortably valid.
|
|
96
|
+
*
|
|
97
|
+
* @throws {IdentityError} on transport failure or an unexpected status —
|
|
98
|
+
* "not linked" and "local mode" are results, not errors.
|
|
99
|
+
*/
|
|
100
|
+
resolve(telegramUserId: string): Promise<IdentityResolution>;
|
|
101
|
+
/**
|
|
102
|
+
* Forget the cached token for a user. Called when the server rejects it
|
|
103
|
+
* mid-session, so the next turn mints a fresh one instead of retrying a
|
|
104
|
+
* token the server has already refused.
|
|
105
|
+
*/
|
|
106
|
+
invalidate(telegramUserId: string): void;
|
|
107
|
+
/** Mint a one-time link code and confirmation URL for the `/link` flow. */
|
|
108
|
+
linkStart(telegramUserId: string): Promise<LinkStart>;
|
|
109
|
+
/**
|
|
110
|
+
* Complete a link the *web* side started, from a `/start <code>` deep link.
|
|
111
|
+
*
|
|
112
|
+
* ROUTE GAP: against the *committed*
|
|
113
|
+
* `packages/websocket/src/routes/integrations.ts` this direction does not
|
|
114
|
+
* exist. `/link/start` binds a code to `(provider, external_id)` and
|
|
115
|
+
* `/link/complete` requires a `user_id` in the body, so only the
|
|
116
|
+
* bot-initiated direction closes — the bridge knows the Telegram id, never
|
|
117
|
+
* the NodeTool user id. The direction needs the route to accept
|
|
118
|
+
* `{external_id, code}` and read the user off a code the web side minted,
|
|
119
|
+
* which is what the M1 T1.4 change to that route adds.
|
|
120
|
+
*
|
|
121
|
+
* So the call is made in exactly the shape that direction takes, and a 400
|
|
122
|
+
* naming the missing `user_id` is reported as `unsupported` — which
|
|
123
|
+
* `/start <code>` handling turns into the ordinary `/link` flow rather than
|
|
124
|
+
* leaving the user holding a dead code.
|
|
125
|
+
*/
|
|
126
|
+
completeDeepLink(telegramUserId: string, code: string): Promise<DeepLinkResult>;
|
|
127
|
+
/** Delete the mapping for this Telegram user. Returns whether a row went. */
|
|
128
|
+
unlink(telegramUserId: string): Promise<boolean>;
|
|
129
|
+
private post;
|
|
130
|
+
private request;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=identity-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity-client.d.ts","sourceRoot":"","sources":["../src/identity-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IdentityClient — the bridge's half of the identity layer (design §5).
|
|
3
|
+
*
|
|
4
|
+
* The bridge holds exactly one credential: the service token that identifies
|
|
5
|
+
* it to the NodeTool server. It never sees a user password, an OAuth code, or
|
|
6
|
+
* a long-lived user token. To act as a linked user it exchanges the Telegram
|
|
7
|
+
* user id for a short-lived delegated token, and caches that token until
|
|
8
|
+
* shortly before it expires.
|
|
9
|
+
*
|
|
10
|
+
* Every route this module calls is `packages/websocket/src/routes/integrations.ts`.
|
|
11
|
+
* All I/O goes through the injected `fetch`, so the whole flow is testable
|
|
12
|
+
* without a server.
|
|
13
|
+
*/
|
|
14
|
+
/** How long before a token's stated expiry it stops being served from cache. */
|
|
15
|
+
import { trimTrailingSlashes } from "./strings.js";
|
|
16
|
+
export const TOKEN_EXPIRY_SLACK_MS = 5 * 60 * 1000;
|
|
17
|
+
/** The identity layer's provider column for this bridge. */
|
|
18
|
+
export const TELEGRAM_PROVIDER = "telegram";
|
|
19
|
+
/** A transport or server failure the caller must surface rather than swallow. */
|
|
20
|
+
export class IdentityError extends Error {
|
|
21
|
+
/** HTTP status, or null when the request never got a response. */
|
|
22
|
+
status;
|
|
23
|
+
constructor(message, status, options) {
|
|
24
|
+
super(message, options);
|
|
25
|
+
this.name = "IdentityError";
|
|
26
|
+
this.status = status;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function isRecord(value) {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
function stringField(body, field) {
|
|
33
|
+
if (!isRecord(body)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const value = body[field];
|
|
37
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
38
|
+
}
|
|
39
|
+
/** The server's `{error}` string, or a generic line naming the status. */
|
|
40
|
+
function errorMessage(body, status) {
|
|
41
|
+
return stringField(body, "error") ?? `NodeTool server answered ${status}`;
|
|
42
|
+
}
|
|
43
|
+
export class IdentityClient {
|
|
44
|
+
apiUrl;
|
|
45
|
+
integrationToken;
|
|
46
|
+
fetchImpl;
|
|
47
|
+
provider;
|
|
48
|
+
now;
|
|
49
|
+
expirySlackMs;
|
|
50
|
+
cache = new Map();
|
|
51
|
+
constructor(options) {
|
|
52
|
+
this.apiUrl = trimTrailingSlashes(options.apiUrl);
|
|
53
|
+
this.integrationToken = options.integrationToken;
|
|
54
|
+
this.fetchImpl = options.fetch;
|
|
55
|
+
this.provider = options.provider ?? TELEGRAM_PROVIDER;
|
|
56
|
+
this.now = options.now ?? Date.now;
|
|
57
|
+
this.expirySlackMs = options.expirySlackMs ?? TOKEN_EXPIRY_SLACK_MS;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A delegated token for this Telegram user, from cache when one is still
|
|
61
|
+
* comfortably valid.
|
|
62
|
+
*
|
|
63
|
+
* @throws {IdentityError} on transport failure or an unexpected status —
|
|
64
|
+
* "not linked" and "local mode" are results, not errors.
|
|
65
|
+
*/
|
|
66
|
+
async resolve(telegramUserId) {
|
|
67
|
+
const cached = this.cache.get(telegramUserId);
|
|
68
|
+
if (cached && cached.expiresAtMs - this.expirySlackMs > this.now()) {
|
|
69
|
+
return {
|
|
70
|
+
unlinked: false,
|
|
71
|
+
token: cached.token,
|
|
72
|
+
userId: cached.userId,
|
|
73
|
+
expiresAtMs: cached.expiresAtMs
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
this.cache.delete(telegramUserId);
|
|
77
|
+
const { status, body } = await this.post("token", { external_id: telegramUserId });
|
|
78
|
+
if (status === 404) {
|
|
79
|
+
return { unlinked: true, reason: "not-linked", message: errorMessage(body, status) };
|
|
80
|
+
}
|
|
81
|
+
if (status === 409) {
|
|
82
|
+
return { unlinked: true, reason: "local-mode", message: errorMessage(body, status) };
|
|
83
|
+
}
|
|
84
|
+
if (status !== 200) {
|
|
85
|
+
throw new IdentityError(errorMessage(body, status), status);
|
|
86
|
+
}
|
|
87
|
+
const token = stringField(body, "token");
|
|
88
|
+
const userId = stringField(body, "user_id");
|
|
89
|
+
if (token === null || userId === null) {
|
|
90
|
+
throw new IdentityError("Token response is missing `token` or `user_id`", status);
|
|
91
|
+
}
|
|
92
|
+
const expiresAt = stringField(body, "expires_at");
|
|
93
|
+
const parsed = expiresAt === null ? Number.NaN : Date.parse(expiresAt);
|
|
94
|
+
// A token whose expiry the server did not state is treated as expiring
|
|
95
|
+
// immediately after the slack window, so it is re-minted rather than used
|
|
96
|
+
// forever on a guess.
|
|
97
|
+
const expiresAtMs = Number.isFinite(parsed) ? parsed : this.now() + this.expirySlackMs;
|
|
98
|
+
this.cache.set(telegramUserId, { token, userId, expiresAtMs });
|
|
99
|
+
return { unlinked: false, token, userId, expiresAtMs };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Forget the cached token for a user. Called when the server rejects it
|
|
103
|
+
* mid-session, so the next turn mints a fresh one instead of retrying a
|
|
104
|
+
* token the server has already refused.
|
|
105
|
+
*/
|
|
106
|
+
invalidate(telegramUserId) {
|
|
107
|
+
this.cache.delete(telegramUserId);
|
|
108
|
+
}
|
|
109
|
+
/** Mint a one-time link code and confirmation URL for the `/link` flow. */
|
|
110
|
+
async linkStart(telegramUserId) {
|
|
111
|
+
const { status, body } = await this.post("link/start", {
|
|
112
|
+
external_id: telegramUserId
|
|
113
|
+
});
|
|
114
|
+
if (status !== 200) {
|
|
115
|
+
throw new IdentityError(errorMessage(body, status), status);
|
|
116
|
+
}
|
|
117
|
+
const code = stringField(body, "code");
|
|
118
|
+
const url = stringField(body, "url");
|
|
119
|
+
if (code === null || url === null) {
|
|
120
|
+
throw new IdentityError("Link response is missing `code` or `url`", status);
|
|
121
|
+
}
|
|
122
|
+
return { code, url, expiresAt: stringField(body, "expires_at") };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Complete a link the *web* side started, from a `/start <code>` deep link.
|
|
126
|
+
*
|
|
127
|
+
* ROUTE GAP: against the *committed*
|
|
128
|
+
* `packages/websocket/src/routes/integrations.ts` this direction does not
|
|
129
|
+
* exist. `/link/start` binds a code to `(provider, external_id)` and
|
|
130
|
+
* `/link/complete` requires a `user_id` in the body, so only the
|
|
131
|
+
* bot-initiated direction closes — the bridge knows the Telegram id, never
|
|
132
|
+
* the NodeTool user id. The direction needs the route to accept
|
|
133
|
+
* `{external_id, code}` and read the user off a code the web side minted,
|
|
134
|
+
* which is what the M1 T1.4 change to that route adds.
|
|
135
|
+
*
|
|
136
|
+
* So the call is made in exactly the shape that direction takes, and a 400
|
|
137
|
+
* naming the missing `user_id` is reported as `unsupported` — which
|
|
138
|
+
* `/start <code>` handling turns into the ordinary `/link` flow rather than
|
|
139
|
+
* leaving the user holding a dead code.
|
|
140
|
+
*/
|
|
141
|
+
async completeDeepLink(telegramUserId, code) {
|
|
142
|
+
const { status, body } = await this.post("link/complete", {
|
|
143
|
+
external_id: telegramUserId,
|
|
144
|
+
code
|
|
145
|
+
});
|
|
146
|
+
if (status === 200) {
|
|
147
|
+
return { ok: true };
|
|
148
|
+
}
|
|
149
|
+
if (status === 410) {
|
|
150
|
+
return { ok: false, reason: "expired", message: errorMessage(body, status) };
|
|
151
|
+
}
|
|
152
|
+
if (status === 400) {
|
|
153
|
+
const message = errorMessage(body, status);
|
|
154
|
+
const reason = message.includes("different account") ? "mismatch" : "unsupported";
|
|
155
|
+
return { ok: false, reason, message };
|
|
156
|
+
}
|
|
157
|
+
return { ok: false, reason: "server-error", message: errorMessage(body, status) };
|
|
158
|
+
}
|
|
159
|
+
/** Delete the mapping for this Telegram user. Returns whether a row went. */
|
|
160
|
+
async unlink(telegramUserId) {
|
|
161
|
+
const { status, body } = await this.request("DELETE", "link", {
|
|
162
|
+
external_id: telegramUserId
|
|
163
|
+
});
|
|
164
|
+
if (status !== 200) {
|
|
165
|
+
throw new IdentityError(errorMessage(body, status), status);
|
|
166
|
+
}
|
|
167
|
+
this.cache.delete(telegramUserId);
|
|
168
|
+
return isRecord(body) && body.unlinked === true;
|
|
169
|
+
}
|
|
170
|
+
post(path, payload) {
|
|
171
|
+
return this.request("POST", path, payload);
|
|
172
|
+
}
|
|
173
|
+
async request(method, path, payload) {
|
|
174
|
+
const url = `${this.apiUrl}/api/integrations/${this.provider}/${path}`;
|
|
175
|
+
let response;
|
|
176
|
+
try {
|
|
177
|
+
response = await this.fetchImpl(url, {
|
|
178
|
+
method,
|
|
179
|
+
headers: {
|
|
180
|
+
"content-type": "application/json",
|
|
181
|
+
authorization: `Bearer ${this.integrationToken}`
|
|
182
|
+
},
|
|
183
|
+
body: JSON.stringify(payload)
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
throw new IdentityError(`Cannot reach the NodeTool server at ${this.apiUrl}: ${err.message}`, null, { cause: err });
|
|
188
|
+
}
|
|
189
|
+
const text = await response.text();
|
|
190
|
+
let body = null;
|
|
191
|
+
if (text.length > 0) {
|
|
192
|
+
try {
|
|
193
|
+
body = JSON.parse(text);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// A non-JSON body (a proxy's HTML error page) carries no field to
|
|
197
|
+
// read; the status alone is what the caller reports.
|
|
198
|
+
body = null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { status: response.status, body };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=identity-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity-client.js","sourceRoot":"","sources":["../src/identity-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,gFAAgF;AAChF,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAEnD,4DAA4D;AAC5D,MAAM,CAAC,MAAM,iBAAiB,GAAG,UAAU,CAAC;AAoE5C,iFAAiF;AACjF,MAAM,OAAO,aAAc,SAAQ,KAAK;IACtC,kEAAkE;IACzD,MAAM,CAAgB;IAE/B,YAAY,OAAe,EAAE,MAAqB,EAAE,OAA6B;QAC/E,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAQD,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,WAAW,CAAC,IAAa,EAAE,KAAa;IAC/C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACpB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC;AAED,0EAA0E;AAC1E,SAAS,YAAY,CAAC,IAAa,EAAE,MAAc;IACjD,OAAO,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,4BAA4B,MAAM,EAAE,CAAC;AAC5E,CAAC;AAED,MAAM,OAAO,cAAc;IACR,MAAM,CAAS;IACf,gBAAgB,CAAS;IACzB,SAAS,CAAe;IACxB,QAAQ,CAAS;IACjB,GAAG,CAAe;IAClB,aAAa,CAAS;IACtB,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;IAEvD,YAAY,OAA8B;QACxC,IAAI,CAAC,MAAM,GAAG,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAClD,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;QACjD,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC;QAC/B,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,iBAAiB,CAAC;QACtD,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;QACnC,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,qBAAqB,CAAC;IACtE,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,OAAO,CAAC,cAAsB;QAClC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAC9C,IAAI,MAAM,IAAI,MAAM,CAAC,WAAW,GAAG,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YACnE,OAAO;gBACL,QAAQ,EAAE,KAAK;gBACf,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,WAAW,EAAE,MAAM,CAAC,WAAW;aAChC,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QAElC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,CAAC;QACnF,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;QACvF,CAAC;QACD,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;QACvF,CAAC;QACD,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,MAAM,IAAI,aAAa,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;QAC9D,CAAC;QAED,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QAC5C,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACtC,MAAM,IAAI,aAAa,CAAC,gDAAgD,EAAE,MAAM,CAAC,CAAC;QACpF,CAAC;QACD,MAAM,SAAS,GAAG,WAAW,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACvE,uEAAuE;QACvE,0EAA0E;QAC1E,sBAAsB;QACtB,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC;QAEvF,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,cAAc,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;QAC/D,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IACzD,CAAC;IAED;;;;OAIG;IACH,UAAU,CAAC,cAAsB;QAC/B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IACpC,CAAC;IAED,2EAA2E;IAC3E,KAAK,CAAC,SAAS,CAAC,cAAsB;QACpC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE;YACrD,WAAW,EAAE,cAAc;SAC5B,CAAC,CAAC;QACH,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,MAAM,IAAI,aAAa,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;QAC9D,CAAC;QACD,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACvC,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACrC,IAAI,IAAI,KAAK,IAAI,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YAClC,MAAM,IAAI,aAAa,CAAC,0CAA0C,EAAE,MAAM,CAAC,CAAC;QAC9E,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,WAAW,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,CAAC;IACnE,CAAC;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,KAAK,CAAC,gBAAgB,CAAC,cAAsB,EAAE,IAAY;QACzD,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE;YACxD,WAAW,EAAE,cAAc;YAC3B,IAAI;SACL,CAAC,CAAC;QACH,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACtB,CAAC;QACD,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;QAC/E,CAAC;QACD,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAC3C,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,CAAC;YAClF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;QACxC,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;IACpF,CAAC;IAED,6EAA6E;IAC7E,KAAK,CAAC,MAAM,CAAC,cAAsB;QACjC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE;YAC5D,WAAW,EAAE,cAAc;SAC5B,CAAC,CAAC;QACH,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,MAAM,IAAI,aAAa,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;QAC9D,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QAClC,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC;IAClD,CAAC;IAEO,IAAI,CAAC,IAAY,EAAE,OAA+B;QACxD,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAC7C,CAAC;IAEO,KAAK,CAAC,OAAO,CACnB,MAAyB,EACzB,IAAY,EACZ,OAA+B;QAE/B,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,MAAM,qBAAqB,IAAI,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAC;QACvE,IAAI,QAAkB,CAAC;QACvB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE;gBACnC,MAAM;gBACN,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,aAAa,EAAE,UAAU,IAAI,CAAC,gBAAgB,EAAE;iBACjD;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;aAC9B,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,aAAa,CACrB,uCAAuC,IAAI,CAAC,MAAM,KAAM,GAAa,CAAC,OAAO,EAAE,EAC/E,IAAI,EACJ,EAAE,KAAK,EAAE,GAAG,EAAE,CACf,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,IAAI,GAAY,IAAI,CAAC;QACzB,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpB,IAAI,CAAC;gBACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;YACrC,CAAC;YAAC,MAAM,CAAC;gBACP,kEAAkE;gBAClE,qDAAqD;gBACrD,IAAI,GAAG,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC;IAC3C,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nodetool-ai/telegram — the Telegram bridge.
|
|
3
|
+
*
|
|
4
|
+
* A bridge process, not a second agent runtime: it translates Telegram updates
|
|
5
|
+
* into `chat_message` commands on the NodeTool server's `/ws` chat protocol and
|
|
6
|
+
* the resulting frames back into Telegram messages. The agent loop, tools,
|
|
7
|
+
* permissions, thread persistence and cost tracking stay on the server.
|
|
8
|
+
*
|
|
9
|
+
* See `docs/telegram-bot-design.md`.
|
|
10
|
+
*/
|
|
11
|
+
export { loadConfig, TelegramConfigError, DEFAULT_API_URL, DEFAULT_CONFIG_FILE, DEFAULT_EDIT_THROTTLE_MS, DEFAULT_MAX_QUEUED_TURNS } from "./config.js";
|
|
12
|
+
export type { TelegramBotConfig, TelegramBotFileConfig, LoadConfigOptions } from "./config.js";
|
|
13
|
+
export { chunkText, splitOnce, DEFAULT_CHUNK_MAX, TELEGRAM_MESSAGE_LIMIT } from "./chunk.js";
|
|
14
|
+
export { markdownToTelegramHtml, escapeHtml } from "./markdown-html.js";
|
|
15
|
+
export type { TelegramHtmlResult } from "./markdown-html.js";
|
|
16
|
+
export { createRendererState, foldFrame, foldFrames, collectAssetRefs } from "./frame-renderer.js";
|
|
17
|
+
export type { RenderOp, RenderTarget, ParseMode, TypingOp, SendOp, EditOp, FinalizeOp, AttachOp, StopNoteOp, AssetAttachment, RenderFrame, FrameEnvelope, FinalMessageFrame, GenerationStoppedFrame, RendererState, RendererOptions, FoldResult } from "./frame-renderer.js";
|
|
18
|
+
export { IdentityClient, IdentityError, TELEGRAM_PROVIDER, TOKEN_EXPIRY_SLACK_MS } from "./identity-client.js";
|
|
19
|
+
export type { IdentityClientOptions, IdentityResolution, LinkedIdentity, UnlinkedIdentity, UnlinkedReason, LinkStart, DeepLinkResult } from "./identity-client.js";
|
|
20
|
+
export { TurnRouter, deriveThreadId, highestThreadIndex, userHash8 } from "./turn-router.js";
|
|
21
|
+
export type { BridgeChatSocket, BridgeClient, BridgeSendOptions, DeliveryContext, ExecuteOps, IdentityResolver, MakeBridgeClient, SubmitInput, SubmitResult, TurnRouterConfig, TurnRouterOptions } from "./turn-router.js";
|
|
22
|
+
export { BotApi, BotApiError, TELEGRAM_API_BASE, TELEGRAM_UPLOAD_LIMIT_BYTES } from "./bot-api.js";
|
|
23
|
+
export type { BotApiOptions, BotCommandSpec, TelegramCallbackQuery, TelegramMessage, TelegramUpdate } from "./bot-api.js";
|
|
24
|
+
export { TelegramAdapter, POLL_TIMEOUT_SECONDS, STOP_CALLBACK_DATA } from "./telegram-adapter.js";
|
|
25
|
+
export type { AdapterConfig, PollStopReason, ResolveAsset, ResolvedAsset, TelegramAdapterOptions } from "./telegram-adapter.js";
|
|
26
|
+
export { BOT_COMMANDS, MESSAGES, PUBLIC_COMMANDS, handleCommand, isAllowedUser, parseCommand } from "./commands.js";
|
|
27
|
+
export type { CommandDeps, CommandIdentity, CommandInput, CommandOutcome, CommandRouter } from "./commands.js";
|
|
28
|
+
export { registerCommands } from "./register-commands.js";
|
|
29
|
+
export type { RegisterCommandsOptions } from "./register-commands.js";
|
|
30
|
+
export { assetIdOf, createAssetResolver, createBridgeClientFactory, wrapChatSocket } from "./nodetool-client.js";
|
|
31
|
+
import type { TelegramBotConfig } from "./config.js";
|
|
32
|
+
import { type PollStopReason } from "./telegram-adapter.js";
|
|
33
|
+
/** A running bridge. Returned by {@link startTelegramBot}. */
|
|
34
|
+
export interface TelegramBotHandle {
|
|
35
|
+
/** Stop polling and close every open socket. */
|
|
36
|
+
readonly stop: () => Promise<void>;
|
|
37
|
+
/** Resolves when the poll loop ends, with why it ended. */
|
|
38
|
+
readonly finished: Promise<PollStopReason>;
|
|
39
|
+
}
|
|
40
|
+
export interface StartTelegramBotOptions {
|
|
41
|
+
/** Injected for tests; defaults to the global `fetch`. */
|
|
42
|
+
readonly fetch?: typeof fetch;
|
|
43
|
+
/** Bot API base URL, for tests. */
|
|
44
|
+
readonly botApiBaseUrl?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Start the bridge against a running NodeTool server.
|
|
48
|
+
*
|
|
49
|
+
* Long polling only: webhook mode is M3 (design §3), and config already
|
|
50
|
+
* validates the URL/secret pair, so a webhook-configured process is refused
|
|
51
|
+
* here rather than silently polling.
|
|
52
|
+
*/
|
|
53
|
+
export declare function startTelegramBot(config: TelegramBotConfig, options?: StartTelegramBotOptions): TelegramBotHandle;
|
|
54
|
+
/** Stop a running bridge. */
|
|
55
|
+
export declare function stopTelegramBot(handle: TelegramBotHandle): Promise<void>;
|
|
56
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nodetool-ai/telegram — the Telegram bridge.
|
|
3
|
+
*
|
|
4
|
+
* A bridge process, not a second agent runtime: it translates Telegram updates
|
|
5
|
+
* into `chat_message` commands on the NodeTool server's `/ws` chat protocol and
|
|
6
|
+
* the resulting frames back into Telegram messages. The agent loop, tools,
|
|
7
|
+
* permissions, thread persistence and cost tracking stay on the server.
|
|
8
|
+
*
|
|
9
|
+
* See `docs/telegram-bot-design.md`.
|
|
10
|
+
*/
|
|
11
|
+
export { loadConfig, TelegramConfigError, DEFAULT_API_URL, DEFAULT_CONFIG_FILE, DEFAULT_EDIT_THROTTLE_MS, DEFAULT_MAX_QUEUED_TURNS } from "./config.js";
|
|
12
|
+
export { chunkText, splitOnce, DEFAULT_CHUNK_MAX, TELEGRAM_MESSAGE_LIMIT } from "./chunk.js";
|
|
13
|
+
export { markdownToTelegramHtml, escapeHtml } from "./markdown-html.js";
|
|
14
|
+
export { createRendererState, foldFrame, foldFrames, collectAssetRefs } from "./frame-renderer.js";
|
|
15
|
+
export { IdentityClient, IdentityError, TELEGRAM_PROVIDER, TOKEN_EXPIRY_SLACK_MS } from "./identity-client.js";
|
|
16
|
+
export { TurnRouter, deriveThreadId, highestThreadIndex, userHash8 } from "./turn-router.js";
|
|
17
|
+
export { BotApi, BotApiError, TELEGRAM_API_BASE, TELEGRAM_UPLOAD_LIMIT_BYTES } from "./bot-api.js";
|
|
18
|
+
export { TelegramAdapter, POLL_TIMEOUT_SECONDS, STOP_CALLBACK_DATA } from "./telegram-adapter.js";
|
|
19
|
+
export { BOT_COMMANDS, MESSAGES, PUBLIC_COMMANDS, handleCommand, isAllowedUser, parseCommand } from "./commands.js";
|
|
20
|
+
export { registerCommands } from "./register-commands.js";
|
|
21
|
+
export { assetIdOf, createAssetResolver, createBridgeClientFactory, wrapChatSocket } from "./nodetool-client.js";
|
|
22
|
+
import { BotApi } from "./bot-api.js";
|
|
23
|
+
import { IdentityClient } from "./identity-client.js";
|
|
24
|
+
import { TelegramAdapter } from "./telegram-adapter.js";
|
|
25
|
+
import { TurnRouter } from "./turn-router.js";
|
|
26
|
+
import { createAssetResolver, createBridgeClientFactory } from "./nodetool-client.js";
|
|
27
|
+
/**
|
|
28
|
+
* Start the bridge against a running NodeTool server.
|
|
29
|
+
*
|
|
30
|
+
* Long polling only: webhook mode is M3 (design §3), and config already
|
|
31
|
+
* validates the URL/secret pair, so a webhook-configured process is refused
|
|
32
|
+
* here rather than silently polling.
|
|
33
|
+
*/
|
|
34
|
+
export function startTelegramBot(config, options = {}) {
|
|
35
|
+
if (config.webhookUrl !== null) {
|
|
36
|
+
throw new Error("TELEGRAM_WEBHOOK_URL is set, but webhook mode arrives in M3. Unset it " +
|
|
37
|
+
"to run the bridge in long-polling mode.");
|
|
38
|
+
}
|
|
39
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
40
|
+
const apiInit = {
|
|
41
|
+
botToken: config.botToken,
|
|
42
|
+
fetch: fetchImpl
|
|
43
|
+
};
|
|
44
|
+
if (options.botApiBaseUrl !== undefined) {
|
|
45
|
+
apiInit.baseUrl = options.botApiBaseUrl;
|
|
46
|
+
}
|
|
47
|
+
const api = new BotApi(apiInit);
|
|
48
|
+
const identity = new IdentityClient({
|
|
49
|
+
apiUrl: config.apiUrl,
|
|
50
|
+
integrationToken: config.integrationToken,
|
|
51
|
+
fetch: fetchImpl
|
|
52
|
+
});
|
|
53
|
+
// The adapter executes the router's ops, and the router routes the adapter's
|
|
54
|
+
// messages: the two are wired through a mutable slot so neither needs the
|
|
55
|
+
// other at construction.
|
|
56
|
+
let adapter = null;
|
|
57
|
+
const router = new TurnRouter({
|
|
58
|
+
identity,
|
|
59
|
+
makeClient: createBridgeClientFactory({ apiUrl: config.apiUrl, fetch: fetchImpl }),
|
|
60
|
+
config: {
|
|
61
|
+
editThrottleMs: config.editThrottleMs,
|
|
62
|
+
maxQueuedTurns: config.maxQueuedTurns,
|
|
63
|
+
permissionMode: "auto"
|
|
64
|
+
},
|
|
65
|
+
executeOps: (context, ops) => adapter?.executeOps(context, ops)
|
|
66
|
+
});
|
|
67
|
+
adapter = new TelegramAdapter({
|
|
68
|
+
api,
|
|
69
|
+
identity,
|
|
70
|
+
router,
|
|
71
|
+
config: { allowUsers: config.allowUsers, apiUrl: config.apiUrl },
|
|
72
|
+
fetch: fetchImpl,
|
|
73
|
+
resolveAsset: createAssetResolver({ apiUrl: config.apiUrl, fetch: fetchImpl })
|
|
74
|
+
});
|
|
75
|
+
const started = adapter;
|
|
76
|
+
const finished = started.poll();
|
|
77
|
+
return {
|
|
78
|
+
finished,
|
|
79
|
+
stop: async () => {
|
|
80
|
+
started.stop();
|
|
81
|
+
router.close();
|
|
82
|
+
await started.flush();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Stop a running bridge. */
|
|
87
|
+
export function stopTelegramBot(handle) {
|
|
88
|
+
return handle.stop();
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent markdown → Telegram HTML parse mode.
|
|
3
|
+
*
|
|
4
|
+
* MarkdownV2 makes every `.`, `-` and `(` in prose an escaping obligation, so
|
|
5
|
+
* one missed character fails the whole message. Telegram's HTML mode needs
|
|
6
|
+
* three characters escaped and supports the five tags this converter emits:
|
|
7
|
+
* `<b>`, `<i>`, `<code>`, `<pre>` and `<a href>`. Anything the converter
|
|
8
|
+
* cannot express — an unbalanced fence, markup it does not recognize as
|
|
9
|
+
* balanced — falls back to plain text, because a turn must never fail on
|
|
10
|
+
* formatting.
|
|
11
|
+
*
|
|
12
|
+
* Hand-rolled on purpose: the supported subset is small enough that a parser
|
|
13
|
+
* dependency would cost more than it covers.
|
|
14
|
+
*/
|
|
15
|
+
/** Converted HTML, or the plain-text fallback when conversion is not safe. */
|
|
16
|
+
export type TelegramHtmlResult = {
|
|
17
|
+
readonly ok: true;
|
|
18
|
+
readonly html: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly ok: false;
|
|
21
|
+
readonly text: string;
|
|
22
|
+
};
|
|
23
|
+
/** Escape the three characters Telegram's HTML mode reserves. */
|
|
24
|
+
export declare function escapeHtml(text: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Convert agent markdown to Telegram HTML.
|
|
27
|
+
*
|
|
28
|
+
* @returns `{ok: true, html}` when the safe subset covered the input, or
|
|
29
|
+
* `{ok: false, text}` with the original text for a plain-text send.
|
|
30
|
+
*/
|
|
31
|
+
export declare function markdownToTelegramHtml(markdown: string): TelegramHtmlResult;
|
|
32
|
+
//# sourceMappingURL=markdown-html.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-html.d.ts","sourceRoot":"","sources":["../src/markdown-html.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,8EAA8E;AAC9E,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AA6BlD,iEAAiE;AACjE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAiND;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,CAoB3E"}
|