@justfortytwo/telegram 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ // @justfortytwo/telegram — public API.
2
+ //
3
+ // The Telegram channel adapter for fortytwo: a long-polling bridge that
4
+ // drives a headless `claude` turn loop per chat, surfaces gate approvals as
5
+ // inline keyboards, and gates inbound access behind a login/pairing binding flow.
6
+ //
7
+ // Peer packages (declared in peerDependencies, NOT bundled):
8
+ // - @justfortytwo/memory (MEMORY_TOOL_CONTRACT_VERSION, tools mcp__fortytwo-memory__*)
9
+ // - @justfortytwo/gate (POLICY_SCHEMA_VERSION)
10
+ // See src/bridge.ts for the `// TODO(wire):` seams where these are wired.
11
+ // --- Channel transport (owned) ---
12
+ export { Telegram, parseAllowed, approvalKeyboard, messageAttachmentSpecs, largestPhoto, formatTelegramHtml, } from './telegram.js';
13
+ // --- Attachments (owned; depends on @justfortytwo/gate's provenance contract) ---
14
+ export { buildAttachment, kindFromMime, extensionFor, inboxRelPath, withinSizeLimit, } from './attachments.js';
15
+ // --- Adapter config (owned) ---
16
+ export { loadAdaptersConfig, } from './adapters-config.js';
17
+ // --- Binding contract + store (owned) ---
18
+ export { TelegramAdapter, } from './adapter.js';
19
+ export { MemoryBindingStore, SqliteBindingStore, } from './bindings.js';
20
+ // --- Login / authorization flow (owned) ---
21
+ export { handleAuthCommand, isAuthorized, isAuthCommand, } from './login.js';
22
+ // --- Bridge turn-loop primitives (owned; pure decision logic is testable) ---
23
+ export { interpretResult, formatApprovalCard, telegramSourceKind, hasHandleableContent, telegramCommandReply, buildInboundPrompt, ingestMessageAttachments, dueSchedules, SCHEDULES, } from './bridge.js';
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,EAAE;AACF,wEAAwE;AACxE,4EAA4E;AAC5E,kFAAkF;AAClF,EAAE;AACF,6DAA6D;AAC7D,0FAA0F;AAC1F,kDAAkD;AAClD,0EAA0E;AAE1E,oCAAoC;AACpC,OAAO,EACL,QAAQ,EACR,YAAY,EACZ,gBAAgB,EAChB,sBAAsB,EACtB,YAAY,EACZ,kBAAkB,GAOnB,MAAM,eAAe,CAAC;AAEvB,mFAAmF;AACnF,OAAO,EACL,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,eAAe,GAGhB,MAAM,kBAAkB,CAAC;AAE1B,iCAAiC;AACjC,OAAO,EACL,kBAAkB,GAGnB,MAAM,sBAAsB,CAAC;AAE9B,2CAA2C;AAC3C,OAAO,EACL,eAAe,GAIhB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,EAClB,kBAAkB,GAInB,MAAM,eAAe,CAAC;AAEvB,6CAA6C;AAC7C,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,aAAa,GAGd,MAAM,YAAY,CAAC;AAEpB,+EAA+E;AAC/E,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,wBAAwB,EACxB,YAAY,EACZ,SAAS,GAKV,MAAM,aAAa,CAAC"}
@@ -0,0 +1,40 @@
1
+ import type { TelegramAdapter } from './adapter.js';
2
+ import type { Binding } from './bindings.js';
3
+ export type LoginAction = {
4
+ kind: 'login_ok';
5
+ binding: Binding;
6
+ reply: string;
7
+ } | {
8
+ kind: 'login_failed';
9
+ reply: string;
10
+ } | {
11
+ kind: 'logout';
12
+ reply: string;
13
+ } | {
14
+ kind: 'ignore';
15
+ };
16
+ export interface AuthContext {
17
+ /** Telegram adapter that owns the challenge/verify + binding lookups. */
18
+ adapter: TelegramAdapter;
19
+ /** Static bootstrap allowlist (from ALLOWED_CHAT_IDS). May be empty. */
20
+ bootstrap: Set<number>;
21
+ }
22
+ /**
23
+ * Is this chat currently authorized to talk to the assistant?
24
+ * True iff it has a persisted binding OR is in the bootstrap allowlist.
25
+ */
26
+ export declare function isAuthorized(ctx: AuthContext, chatId: number): boolean;
27
+ /**
28
+ * Classify an inbound text from a chat and, for `/login` / `/logout`, perform
29
+ * the binding mutation. Returns the action the bridge should take.
30
+ *
31
+ * Lockdown contract:
32
+ * - authorized chat: caller proceeds to the normal turn loop (this returns
33
+ * `ignore` for non-command text so the bridge handles it itself — callers
34
+ * should only consult this for command handling + the unbound gate).
35
+ * - unbound chat presenting `/login <code>`: verify + maybe bind.
36
+ * - unbound chat, anything else: `ignore` (NO response).
37
+ */
38
+ export declare function handleAuthCommand(ctx: AuthContext, chatId: number, text: string): LoginAction;
39
+ /** Convenience: does this inbound look like an auth command at all? */
40
+ export declare function isAuthCommand(text: string): boolean;
package/dist/login.js ADDED
@@ -0,0 +1,88 @@
1
+ // Login / pairing flow + inbound authorization.
2
+ //
3
+ // The lockdown rule (carried over from the monolith, spec §14) is: unbound
4
+ // senders get NO response. The ONLY exception is a sender presenting `/login
5
+ // <code>` — that is how an unknown chat earns a binding. Everything else from an
6
+ // unbound chat is silently dropped.
7
+ //
8
+ // Authorization set = persisted bindings ∪ optional ALLOWED_CHAT_IDS bootstrap.
9
+ // The bootstrap set lets the very first owner chat reach the bridge before any
10
+ // binding exists (e.g. to issue the first challenge), without leaving the bot
11
+ // open. Once bindings exist, they are the durable source of truth.
12
+ const LOGIN_RE = /^\/login(?:@[A-Za-z0-9_]+)?(?:\s+(\S+))?\s*$/;
13
+ const LOGOUT_RE = /^\/logout(?:@[A-Za-z0-9_]+)?\s*$/;
14
+ /**
15
+ * Is this chat currently authorized to talk to the assistant?
16
+ * True iff it has a persisted binding OR is in the bootstrap allowlist.
17
+ */
18
+ export function isAuthorized(ctx, chatId) {
19
+ // Groups/channels (negative Telegram chat ids) are NEVER the owner: a group
20
+ // chat id identifies the ROOM, not the speaker, so authorizing one would let
21
+ // any member drive the assistant. Only private chats (chatId === the user id)
22
+ // can be authorized/bound.
23
+ if (chatId < 0)
24
+ return false;
25
+ if (ctx.bootstrap.has(chatId))
26
+ return true;
27
+ return ctx.adapter.bindingFor(String(chatId)) !== null;
28
+ }
29
+ /**
30
+ * Classify an inbound text from a chat and, for `/login` / `/logout`, perform
31
+ * the binding mutation. Returns the action the bridge should take.
32
+ *
33
+ * Lockdown contract:
34
+ * - authorized chat: caller proceeds to the normal turn loop (this returns
35
+ * `ignore` for non-command text so the bridge handles it itself — callers
36
+ * should only consult this for command handling + the unbound gate).
37
+ * - unbound chat presenting `/login <code>`: verify + maybe bind.
38
+ * - unbound chat, anything else: `ignore` (NO response).
39
+ */
40
+ export function handleAuthCommand(ctx, chatId, text) {
41
+ const trimmed = (text ?? '').trim();
42
+ const chatKey = String(chatId);
43
+ const authorized = isAuthorized(ctx, chatId);
44
+ // Groups/channels can never pair or be authorized (see isAuthorized): the chat
45
+ // id is the room, not a person. Drop silently.
46
+ if (chatId < 0)
47
+ return { kind: 'ignore' };
48
+ const loginMatch = trimmed.match(LOGIN_RE);
49
+ if (loginMatch) {
50
+ const code = loginMatch[1];
51
+ if (!code) {
52
+ // Only hint at the format if the chat is already authorized; an unbound
53
+ // chat that fat-fingers `/login` learns nothing about the bot's existence.
54
+ return authorized
55
+ ? { kind: 'login_failed', reply: 'Usage: /login <code>' }
56
+ : { kind: 'ignore' };
57
+ }
58
+ // A chat that already holds a binding cannot re-point it with a fresh code:
59
+ // that would be an owner-reassignment primitive (and would consume a code
60
+ // minted for a different chat). Require an explicit /logout first.
61
+ if (ctx.adapter.bindingFor(chatKey)) {
62
+ return { kind: 'login_failed', reply: 'This chat is already paired. Send /logout first to re-pair.' };
63
+ }
64
+ const binding = ctx.adapter.verify(chatKey, code);
65
+ if (binding) {
66
+ return { kind: 'login_ok', binding, reply: `Paired. This chat is now bound to "${binding.owner}". Send /logout to unbind.` };
67
+ }
68
+ // Bad/expired/used code from an unbound chat: stay dark to avoid an oracle.
69
+ return authorized
70
+ ? { kind: 'login_failed', reply: 'That code is invalid or expired.' }
71
+ : { kind: 'ignore' };
72
+ }
73
+ if (LOGOUT_RE.test(trimmed)) {
74
+ if (!authorized)
75
+ return { kind: 'ignore' };
76
+ ctx.adapter.unbind(chatKey);
77
+ return { kind: 'logout', reply: 'Unbound. This chat can no longer reach the assistant until you /login again.' };
78
+ }
79
+ // Not an auth command. Authorized chats fall through to the normal turn loop
80
+ // (the bridge re-derives authorization); unbound chats are dropped (lockdown).
81
+ return { kind: 'ignore' };
82
+ }
83
+ /** Convenience: does this inbound look like an auth command at all? */
84
+ export function isAuthCommand(text) {
85
+ const trimmed = (text ?? '').trim();
86
+ return LOGIN_RE.test(trimmed) || LOGOUT_RE.test(trimmed);
87
+ }
88
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../src/login.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ export declare function formatTelegramHtml(text: string): string;
2
+ export interface PhotoSize {
3
+ file_id: string;
4
+ file_unique_id: string;
5
+ width: number;
6
+ height: number;
7
+ file_size?: number;
8
+ }
9
+ export interface TgDocument {
10
+ file_id: string;
11
+ file_name?: string;
12
+ mime_type?: string;
13
+ file_size?: number;
14
+ }
15
+ export interface AttachmentSpec {
16
+ file_id: string;
17
+ mime: string;
18
+ file_name?: string;
19
+ file_size?: number;
20
+ source_kind: 'telegram_photo' | 'telegram_document';
21
+ }
22
+ export interface TgMessage {
23
+ message_id: number;
24
+ chat: {
25
+ id: number;
26
+ };
27
+ from?: {
28
+ id: number;
29
+ is_bot: boolean;
30
+ };
31
+ text?: string;
32
+ caption?: string;
33
+ photo?: PhotoSize[];
34
+ document?: TgDocument;
35
+ forward_origin?: unknown;
36
+ forward_from?: unknown;
37
+ forward_from_chat?: unknown;
38
+ reply_to_message?: TgMessage;
39
+ }
40
+ export interface TgUpdate {
41
+ update_id: number;
42
+ message?: TgMessage;
43
+ callback_query?: {
44
+ id: string;
45
+ data?: string;
46
+ from?: {
47
+ id: number;
48
+ };
49
+ message?: {
50
+ message_id: number;
51
+ chat: {
52
+ id: number;
53
+ };
54
+ };
55
+ };
56
+ }
57
+ export declare function largestPhoto(sizes: PhotoSize[]): PhotoSize | undefined;
58
+ export declare function messageAttachmentSpecs(message: TgMessage): AttachmentSpec[];
59
+ export declare class Telegram {
60
+ private token;
61
+ private allowedChatIds;
62
+ constructor(token: string, allowedChatIds: Set<number>);
63
+ /**
64
+ * True if the chat is on the static bootstrap allowlist (ALLOWED_CHAT_IDS).
65
+ *
66
+ * NOTE (post-extraction): in the monolith this was the sole authorization
67
+ * check (lockdown). In @justfortytwo/telegram, authorization is owned by the
68
+ * login/binding flow (see src/login.ts `authorize`). This allowlist is now a
69
+ * BOOTSTRAP set unioned with persisted bindings, not the only gate. The poller
70
+ * no longer hard-filters on it; see the bridge poll loop.
71
+ */
72
+ allowed(chatId: number): boolean;
73
+ /** Long-poll for updates. Pass offset = lastUpdateId + 1 to acknowledge. */
74
+ getUpdates(offset: number, timeoutSec?: number): Promise<TgUpdate[]>;
75
+ sendMessage(chatId: number, text: string, replyMarkup?: Record<string, unknown>): Promise<{
76
+ message_id: number;
77
+ }>;
78
+ editMessageText(chatId: number, messageId: number, text: string): Promise<void>;
79
+ react(chatId: number, messageId: number, emoji: string): Promise<void>;
80
+ /** Native "Bot is typing…" chat action (shows ~5s in the header; refresh to keep alive). */
81
+ sendChatAction(chatId: number, action?: string): Promise<void>;
82
+ /** Answer a callback_query (button tap) — stops the button's loading spinner. */
83
+ answerCallbackQuery(callbackQueryId: string, text?: string): Promise<void>;
84
+ /** Resolve a file_id to a downloadable file_path (Bot API getFile). */
85
+ getFilePath(fileId: string): Promise<string>;
86
+ /** Download raw file bytes from the Telegram file API. */
87
+ downloadFile(filePath: string): Promise<Buffer>;
88
+ }
89
+ export interface ApprovalKeyboardOptions {
90
+ bashTtlHours?: number;
91
+ }
92
+ /** Inline approval keyboard, keyed by tool_use_id. Bash gets a TTL allowlist option. */
93
+ export declare function approvalKeyboard(toolUseId: string, opts?: ApprovalKeyboardOptions): Record<string, unknown>;
94
+ /** Parse "12345, -100999" → Set<number>. */
95
+ export declare function parseAllowed(raw: string | undefined): Set<number>;
@@ -0,0 +1,152 @@
1
+ // Minimal Telegram Bot API client (long-poll + send) for the bridge. No SDK —
2
+ // just fetch against https://api.telegram.org/bot<token>/. The bridge is the
3
+ // ONLY long-poller; nothing else may call getUpdates on this bot.
4
+ const API = (token) => `https://api.telegram.org/bot${token}`;
5
+ async function tg(token, method, body) {
6
+ const res = await fetch(`${API(token)}/${method}`, {
7
+ method: 'POST',
8
+ headers: { 'content-type': 'application/json' },
9
+ body: JSON.stringify(body),
10
+ });
11
+ const json = await res.json();
12
+ if (!json.ok)
13
+ throw new Error(`telegram ${method} failed: ${json.description ?? res.status}`);
14
+ return json.result;
15
+ }
16
+ function escapeHtml(text) {
17
+ return text
18
+ .replace(/&/g, '&amp;')
19
+ .replace(/</g, '&lt;')
20
+ .replace(/>/g, '&gt;');
21
+ }
22
+ function formatInlineMarkdown(text) {
23
+ return escapeHtml(text)
24
+ .replace(/`([^`\n]+)`/g, '<code>$1</code>')
25
+ .replace(/\*\*([^*\n]+)\*\*/g, '<b>$1</b>')
26
+ .replace(/__([^_\n]+)__/g, '<b>$1</b>');
27
+ }
28
+ function formatMarkdownBlock(text) {
29
+ return text.split(/\r?\n/).map((line) => {
30
+ const heading = line.match(/^#{1,6}\s+(.+)$/);
31
+ if (heading)
32
+ return `<b>${formatInlineMarkdown(heading[1])}</b>`;
33
+ const bullet = line.match(/^(\s*)[-*]\s+(.+)$/);
34
+ if (bullet)
35
+ return `${bullet[1]}• ${formatInlineMarkdown(bullet[2])}`;
36
+ return formatInlineMarkdown(line);
37
+ }).join('\n');
38
+ }
39
+ export function formatTelegramHtml(text) {
40
+ return text.split('```').map((part, index) => {
41
+ if (index % 2 === 0)
42
+ return formatMarkdownBlock(part);
43
+ return `<pre>${escapeHtml(part.replace(/^\s*\w+\n/, '').trim())}</pre>`;
44
+ }).join('');
45
+ }
46
+ export function largestPhoto(sizes) {
47
+ const score = (p) => p.file_size ?? p.width * p.height;
48
+ return sizes.reduce((best, s) => (!best || score(s) > score(best) ? s : best), undefined);
49
+ }
50
+ export function messageAttachmentSpecs(message) {
51
+ const specs = [];
52
+ if (message.photo && message.photo.length) {
53
+ const p = largestPhoto(message.photo);
54
+ if (p)
55
+ specs.push({ file_id: p.file_id, mime: 'image/jpeg', file_size: p.file_size, source_kind: 'telegram_photo' });
56
+ }
57
+ if (message.document) {
58
+ specs.push({
59
+ file_id: message.document.file_id,
60
+ mime: message.document.mime_type ?? 'application/octet-stream',
61
+ file_name: message.document.file_name,
62
+ file_size: message.document.file_size,
63
+ source_kind: 'telegram_document',
64
+ });
65
+ }
66
+ return specs;
67
+ }
68
+ export class Telegram {
69
+ token;
70
+ allowedChatIds;
71
+ constructor(token, allowedChatIds) {
72
+ this.token = token;
73
+ this.allowedChatIds = allowedChatIds;
74
+ }
75
+ /**
76
+ * True if the chat is on the static bootstrap allowlist (ALLOWED_CHAT_IDS).
77
+ *
78
+ * NOTE (post-extraction): in the monolith this was the sole authorization
79
+ * check (lockdown). In @justfortytwo/telegram, authorization is owned by the
80
+ * login/binding flow (see src/login.ts `authorize`). This allowlist is now a
81
+ * BOOTSTRAP set unioned with persisted bindings, not the only gate. The poller
82
+ * no longer hard-filters on it; see the bridge poll loop.
83
+ */
84
+ allowed(chatId) {
85
+ return this.allowedChatIds.has(chatId);
86
+ }
87
+ /** Long-poll for updates. Pass offset = lastUpdateId + 1 to acknowledge. */
88
+ getUpdates(offset, timeoutSec = 30) {
89
+ return tg(this.token, 'getUpdates', { offset, timeout: timeoutSec, allowed_updates: ['message', 'callback_query'] });
90
+ }
91
+ sendMessage(chatId, text, replyMarkup) {
92
+ const body = { chat_id: chatId, text: formatTelegramHtml(text), parse_mode: 'HTML' };
93
+ if (replyMarkup)
94
+ body['reply_markup'] = replyMarkup;
95
+ return tg(this.token, 'sendMessage', body);
96
+ }
97
+ editMessageText(chatId, messageId, text) {
98
+ return tg(this.token, 'editMessageText', { chat_id: chatId, message_id: messageId, text: formatTelegramHtml(text), parse_mode: 'HTML' }).then(() => undefined);
99
+ }
100
+ react(chatId, messageId, emoji) {
101
+ return tg(this.token, 'setMessageReaction', {
102
+ chat_id: chatId, message_id: messageId,
103
+ reaction: [{ type: 'emoji', emoji }],
104
+ }).then(() => undefined);
105
+ }
106
+ /** Native "Bot is typing…" chat action (shows ~5s in the header; refresh to keep alive). */
107
+ sendChatAction(chatId, action = 'typing') {
108
+ return tg(this.token, 'sendChatAction', { chat_id: chatId, action }).then(() => undefined);
109
+ }
110
+ /** Answer a callback_query (button tap) — stops the button's loading spinner. */
111
+ answerCallbackQuery(callbackQueryId, text) {
112
+ const body = { callback_query_id: callbackQueryId };
113
+ if (text)
114
+ body['text'] = text;
115
+ return tg(this.token, 'answerCallbackQuery', body).then(() => undefined);
116
+ }
117
+ /** Resolve a file_id to a downloadable file_path (Bot API getFile). */
118
+ async getFilePath(fileId) {
119
+ const r = await tg(this.token, 'getFile', { file_id: fileId });
120
+ if (!r || typeof r.file_path !== 'string')
121
+ throw new Error('telegram getFile returned no file_path');
122
+ return r.file_path;
123
+ }
124
+ /** Download raw file bytes from the Telegram file API. */
125
+ async downloadFile(filePath) {
126
+ const res = await fetch(`https://api.telegram.org/file/bot${this.token}/${filePath}`);
127
+ if (!res.ok)
128
+ throw new Error(`telegram file download failed: ${res.status}`);
129
+ return Buffer.from(await res.arrayBuffer());
130
+ }
131
+ }
132
+ /** Inline approval keyboard, keyed by tool_use_id. Bash gets a TTL allowlist option. */
133
+ export function approvalKeyboard(toolUseId, opts = {}) {
134
+ const row = opts.bashTtlHours
135
+ ? [
136
+ { text: 'Run once', callback_data: `approve_once:${toolUseId}` },
137
+ { text: `Allow ${opts.bashTtlHours}h`, callback_data: `approve_ttl:${toolUseId}` },
138
+ { text: 'Deny', callback_data: `deny:${toolUseId}` },
139
+ ]
140
+ : [
141
+ { text: 'Approve', callback_data: `approve_once:${toolUseId}` },
142
+ { text: 'Deny', callback_data: `deny:${toolUseId}` },
143
+ ];
144
+ return { inline_keyboard: [row] };
145
+ }
146
+ /** Parse "12345, -100999" → Set<number>. */
147
+ export function parseAllowed(raw) {
148
+ if (!raw)
149
+ return new Set();
150
+ return new Set(raw.split(',').map((s) => Number(s.trim())).filter((n) => !Number.isNaN(n)));
151
+ }
152
+ //# sourceMappingURL=telegram.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telegram.js","sourceRoot":"","sources":["../src/telegram.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@justfortytwo/telegram",
3
+ "version": "0.1.0",
4
+ "description": "Telegram channel adapter for fortytwo: a long-polling bridge that drives a headless `claude` turn loop per chat, surfaces gate approvals as inline keyboards, and gates inbound access behind a login/pairing binding flow.",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Enrico Deleo",
8
+ "url": "https://enricodeleo.com"
9
+ },
10
+ "homepage": "https://forty-two.it",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/justfortytwo/telegram.git"
14
+ },
15
+ "type": "module",
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ },
26
+ "./bridge": "./dist/bridge.js"
27
+ },
28
+ "bin": {
29
+ "fortytwo-telegram-bridge": "./dist/bridge.js"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "test": "vitest run",
39
+ "prepublishOnly": "npm run build"
40
+ },
41
+ "keywords": [
42
+ "claude-code",
43
+ "telegram",
44
+ "channel-adapter",
45
+ "bridge",
46
+ "agent",
47
+ "bot",
48
+ "pairing",
49
+ "login"
50
+ ],
51
+ "peerDependencies": {
52
+ "@justfortytwo/memory": "^0.1.0",
53
+ "@justfortytwo/gate": "^0.1.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@justfortytwo/memory": {
57
+ "optional": true
58
+ },
59
+ "@justfortytwo/gate": {
60
+ "optional": true
61
+ }
62
+ },
63
+ "dependencies": {
64
+ "better-sqlite3": "^11.0.0"
65
+ },
66
+ "devDependencies": {
67
+ "@types/better-sqlite3": "^7.6.0",
68
+ "@types/node": "^20.0.0",
69
+ "typescript": "^5.4.0",
70
+ "vitest": "^1.6.0"
71
+ },
72
+ "publishConfig": {
73
+ "access": "public"
74
+ }
75
+ }