@proletariat/cli 0.3.111 → 0.3.112
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/commands/gateway/connect.d.ts +33 -0
- package/dist/commands/gateway/connect.js +130 -0
- package/dist/commands/gateway/connect.js.map +1 -0
- package/dist/commands/gateway/disconnect.d.ts +21 -0
- package/dist/commands/gateway/disconnect.js +69 -0
- package/dist/commands/gateway/disconnect.js.map +1 -0
- package/dist/commands/gateway/start.d.ts +23 -0
- package/dist/commands/gateway/start.js +133 -0
- package/dist/commands/gateway/start.js.map +1 -0
- package/dist/commands/gateway/status.d.ts +16 -0
- package/dist/commands/gateway/status.js +76 -0
- package/dist/commands/gateway/status.js.map +1 -0
- package/dist/commands/gateway/test.d.ts +22 -0
- package/dist/commands/gateway/test.js +83 -0
- package/dist/commands/gateway/test.js.map +1 -0
- package/dist/commands/reconcile.d.ts +29 -0
- package/dist/commands/reconcile.js +140 -0
- package/dist/commands/reconcile.js.map +1 -0
- package/dist/commands/work/ship.js +131 -61
- package/dist/commands/work/ship.js.map +1 -1
- package/dist/lib/gateway/channel-factory.d.ts +13 -0
- package/dist/lib/gateway/channel-factory.js +37 -0
- package/dist/lib/gateway/channel-factory.js.map +1 -0
- package/dist/lib/gateway/channels/telegram.d.ts +115 -0
- package/dist/lib/gateway/channels/telegram.js +215 -0
- package/dist/lib/gateway/channels/telegram.js.map +1 -0
- package/dist/lib/gateway/router.d.ts +84 -0
- package/dist/lib/gateway/router.js +140 -0
- package/dist/lib/gateway/router.js.map +1 -0
- package/dist/lib/gateway/session-poker.d.ts +35 -0
- package/dist/lib/gateway/session-poker.js +85 -0
- package/dist/lib/gateway/session-poker.js.map +1 -0
- package/dist/lib/gateway/types.d.ts +124 -0
- package/dist/lib/gateway/types.js +17 -0
- package/dist/lib/gateway/types.js.map +1 -0
- package/dist/lib/machine-db.d.ts +87 -0
- package/dist/lib/machine-db.js +135 -0
- package/dist/lib/machine-db.js.map +1 -1
- package/dist/lib/pr/index.d.ts +34 -2
- package/dist/lib/pr/index.js +95 -4
- package/dist/lib/pr/index.js.map +1 -1
- package/dist/lib/reconcile/core.d.ts +62 -0
- package/dist/lib/reconcile/core.js +137 -0
- package/dist/lib/reconcile/core.js.map +1 -0
- package/dist/lib/reconcile/index.d.ts +54 -0
- package/dist/lib/reconcile/index.js +377 -0
- package/dist/lib/reconcile/index.js.map +1 -0
- package/dist/lib/reconcile/types.d.ts +133 -0
- package/dist/lib/reconcile/types.js +16 -0
- package/dist/lib/reconcile/types.js.map +1 -0
- package/oclif.manifest.json +2534 -2192
- package/package.json +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel factory (PRLT-1255)
|
|
3
|
+
*
|
|
4
|
+
* Single place that knows how to decode a persisted MessagingChannelRecord
|
|
5
|
+
* into a live MessagingChannel instance. Commands and the gateway daemon
|
|
6
|
+
* call this so they never import individual adapters directly.
|
|
7
|
+
*
|
|
8
|
+
* To add Slack/Discord/WhatsApp later: import the new adapter and add a
|
|
9
|
+
* case to `buildChannelFromRecord`. No other file needs to change.
|
|
10
|
+
*/
|
|
11
|
+
import type { MessagingChannelRecord } from '../machine-db.js';
|
|
12
|
+
import type { MessagingChannel } from './types.js';
|
|
13
|
+
export declare function buildChannelFromRecord(record: MessagingChannelRecord): MessagingChannel;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel factory (PRLT-1255)
|
|
3
|
+
*
|
|
4
|
+
* Single place that knows how to decode a persisted MessagingChannelRecord
|
|
5
|
+
* into a live MessagingChannel instance. Commands and the gateway daemon
|
|
6
|
+
* call this so they never import individual adapters directly.
|
|
7
|
+
*
|
|
8
|
+
* To add Slack/Discord/WhatsApp later: import the new adapter and add a
|
|
9
|
+
* case to `buildChannelFromRecord`. No other file needs to change.
|
|
10
|
+
*/
|
|
11
|
+
import { TelegramChannel } from './channels/telegram.js';
|
|
12
|
+
export function buildChannelFromRecord(record) {
|
|
13
|
+
switch (record.type) {
|
|
14
|
+
case 'telegram': {
|
|
15
|
+
const config = parseConfig(record.configJson, record.name);
|
|
16
|
+
if (!config.token) {
|
|
17
|
+
throw new Error(`Channel "${record.name}" has no Telegram token configured`);
|
|
18
|
+
}
|
|
19
|
+
if (!Array.isArray(config.allowlist)) {
|
|
20
|
+
throw new TypeError(`Channel "${record.name}" has a malformed allowlist`);
|
|
21
|
+
}
|
|
22
|
+
return new TelegramChannel({ config });
|
|
23
|
+
}
|
|
24
|
+
default:
|
|
25
|
+
throw new Error(`Unknown channel type "${record.type}" for "${record.name}"`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function parseConfig(json, channelName) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(json);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
34
|
+
throw new Error(`Channel "${channelName}" has invalid config JSON: ${msg}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=channel-factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-factory.js","sourceRoot":"","sources":["../../../src/lib/gateway/channel-factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAExD,MAAM,UAAU,sBAAsB,CAAC,MAA8B;IACnE,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,UAAU,CAAC,CAAC,CAAC;YAChB,MAAM,MAAM,GAAG,WAAW,CAAwB,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,IAAI,CAAC,CAAA;YACjF,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;gBAClB,MAAM,IAAI,KAAK,CAAC,YAAY,MAAM,CAAC,IAAI,oCAAoC,CAAC,CAAA;YAC9E,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;gBACrC,MAAM,IAAI,SAAS,CAAC,YAAY,MAAM,CAAC,IAAI,6BAA6B,CAAC,CAAA;YAC3E,CAAC;YACD,OAAO,IAAI,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC,CAAA;QACxC,CAAC;QACD;YACE,MAAM,IAAI,KAAK,CAAC,yBAAyB,MAAM,CAAC,IAAI,UAAU,MAAM,CAAC,IAAI,GAAG,CAAC,CAAA;IACjF,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAI,IAAY,EAAE,WAAmB;IACvD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAM,CAAA;IAC9B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAC5D,MAAM,IAAI,KAAK,CAAC,YAAY,WAAW,8BAA8B,GAAG,EAAE,CAAC,CAAA;IAC7E,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Channel Adapter (PRLT-1255)
|
|
3
|
+
*
|
|
4
|
+
* Polling-mode Telegram bot that implements MessagingChannel.
|
|
5
|
+
*
|
|
6
|
+
* Uses Telegram's `getUpdates` long-polling API — no webhook, no server
|
|
7
|
+
* required. Messages arrive over an outgoing HTTPS request, so the bot
|
|
8
|
+
* works fine from behind NAT, on a laptop, or in a container.
|
|
9
|
+
*
|
|
10
|
+
* This adapter is the ONLY Telegram-aware code in the gateway. To add
|
|
11
|
+
* Slack/Discord/WhatsApp later: write a new adapter next to this file,
|
|
12
|
+
* implementing the same interface. The router and commands do not need
|
|
13
|
+
* to change.
|
|
14
|
+
*
|
|
15
|
+
* @see PRLT-1251 (Messaging Gateway epic)
|
|
16
|
+
*/
|
|
17
|
+
import type { ChannelAddress, MessageHandler, MessagingChannel, TelegramChannelConfig } from '../types.js';
|
|
18
|
+
interface TelegramUser {
|
|
19
|
+
id: number;
|
|
20
|
+
is_bot?: boolean;
|
|
21
|
+
first_name?: string;
|
|
22
|
+
last_name?: string;
|
|
23
|
+
username?: string;
|
|
24
|
+
}
|
|
25
|
+
interface TelegramChat {
|
|
26
|
+
id: number;
|
|
27
|
+
type: 'private' | 'group' | 'supergroup' | 'channel';
|
|
28
|
+
title?: string;
|
|
29
|
+
username?: string;
|
|
30
|
+
first_name?: string;
|
|
31
|
+
}
|
|
32
|
+
interface TelegramMessage {
|
|
33
|
+
message_id: number;
|
|
34
|
+
from?: TelegramUser;
|
|
35
|
+
chat: TelegramChat;
|
|
36
|
+
date: number;
|
|
37
|
+
text?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface TelegramUpdate {
|
|
40
|
+
update_id: number;
|
|
41
|
+
message?: TelegramMessage;
|
|
42
|
+
edited_message?: TelegramMessage;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Minimal Telegram Bot API client. Broken out so tests can substitute a
|
|
46
|
+
* fake client without touching the network.
|
|
47
|
+
*/
|
|
48
|
+
export interface TelegramClient {
|
|
49
|
+
getUpdates(options: {
|
|
50
|
+
offset?: number;
|
|
51
|
+
timeoutSec?: number;
|
|
52
|
+
}): Promise<TelegramUpdate[]>;
|
|
53
|
+
sendMessage(chatId: string | number, text: string): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Options for constructing a TelegramChannel.
|
|
57
|
+
*
|
|
58
|
+
* - `config`: TelegramChannelConfig loaded from machine.db.
|
|
59
|
+
* - `client`: optional injectable Telegram client (tests use this).
|
|
60
|
+
* - `logger`: optional error/info sink. Defaults to console.
|
|
61
|
+
*/
|
|
62
|
+
export interface TelegramChannelOptions {
|
|
63
|
+
config: TelegramChannelConfig;
|
|
64
|
+
client?: TelegramClient;
|
|
65
|
+
logger?: {
|
|
66
|
+
info?: (msg: string) => void;
|
|
67
|
+
error?: (msg: string, err?: unknown) => void;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* How long to back off after a failed poll. Defaults to 5000ms.
|
|
71
|
+
* Tests shorten this so they don't have to sit on a 5s sleep.
|
|
72
|
+
*/
|
|
73
|
+
errorBackoffMs?: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Polling-mode Telegram adapter.
|
|
77
|
+
*
|
|
78
|
+
* The read loop is a vanilla `while (running) await getUpdates()`. We
|
|
79
|
+
* acknowledge updates by advancing the `offset` cursor to the id after
|
|
80
|
+
* the highest update seen in the batch — this is Telegram's documented
|
|
81
|
+
* delete semantics for getUpdates.
|
|
82
|
+
*/
|
|
83
|
+
export declare class TelegramChannel implements MessagingChannel {
|
|
84
|
+
readonly name = "telegram";
|
|
85
|
+
private handler;
|
|
86
|
+
private running;
|
|
87
|
+
private loopPromise;
|
|
88
|
+
private offset;
|
|
89
|
+
private readonly client;
|
|
90
|
+
private readonly config;
|
|
91
|
+
private readonly logger;
|
|
92
|
+
private readonly errorBackoffMs;
|
|
93
|
+
constructor(options: TelegramChannelOptions);
|
|
94
|
+
onMessage(handler: MessageHandler): void;
|
|
95
|
+
start(): Promise<void>;
|
|
96
|
+
stop(): Promise<void>;
|
|
97
|
+
sendMessage(to: ChannelAddress, text: string): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Main polling loop. Runs until `stop()` flips `running` false.
|
|
100
|
+
*
|
|
101
|
+
* Errors are caught, logged, and followed by a small backoff so a
|
|
102
|
+
* transient network blip doesn't hot-spin the bot against Telegram.
|
|
103
|
+
*
|
|
104
|
+
* The awaits are deliberately sequential: Telegram long-poll demands
|
|
105
|
+
* one outstanding getUpdates at a time per bot, and we must drain
|
|
106
|
+
* each batch before advancing the cursor.
|
|
107
|
+
*/
|
|
108
|
+
private runLoop;
|
|
109
|
+
/**
|
|
110
|
+
* Normalize a Telegram update into a generic `Message` and hand it to
|
|
111
|
+
* the router. Drops non-text messages silently (MVP is text-only).
|
|
112
|
+
*/
|
|
113
|
+
private handleUpdate;
|
|
114
|
+
}
|
|
115
|
+
export {};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Channel Adapter (PRLT-1255)
|
|
3
|
+
*
|
|
4
|
+
* Polling-mode Telegram bot that implements MessagingChannel.
|
|
5
|
+
*
|
|
6
|
+
* Uses Telegram's `getUpdates` long-polling API — no webhook, no server
|
|
7
|
+
* required. Messages arrive over an outgoing HTTPS request, so the bot
|
|
8
|
+
* works fine from behind NAT, on a laptop, or in a container.
|
|
9
|
+
*
|
|
10
|
+
* This adapter is the ONLY Telegram-aware code in the gateway. To add
|
|
11
|
+
* Slack/Discord/WhatsApp later: write a new adapter next to this file,
|
|
12
|
+
* implementing the same interface. The router and commands do not need
|
|
13
|
+
* to change.
|
|
14
|
+
*
|
|
15
|
+
* @see PRLT-1251 (Messaging Gateway epic)
|
|
16
|
+
*/
|
|
17
|
+
class HttpTelegramClient {
|
|
18
|
+
token;
|
|
19
|
+
constructor(token) {
|
|
20
|
+
this.token = token;
|
|
21
|
+
}
|
|
22
|
+
url(method) {
|
|
23
|
+
return `https://api.telegram.org/bot${this.token}/${method}`;
|
|
24
|
+
}
|
|
25
|
+
async getUpdates(options) {
|
|
26
|
+
const params = new URLSearchParams();
|
|
27
|
+
if (options.offset !== undefined)
|
|
28
|
+
params.set('offset', String(options.offset));
|
|
29
|
+
params.set('timeout', String(options.timeoutSec ?? 25));
|
|
30
|
+
// Only request message updates — we don't need callback queries, polls, etc.
|
|
31
|
+
params.set('allowed_updates', JSON.stringify(['message']));
|
|
32
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins -- global fetch is stable in Node 20+
|
|
33
|
+
const res = await fetch(`${this.url('getUpdates')}?${params.toString()}`);
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
throw new Error(`Telegram getUpdates failed: HTTP ${res.status}`);
|
|
36
|
+
}
|
|
37
|
+
const body = (await res.json());
|
|
38
|
+
if (!body.ok) {
|
|
39
|
+
throw new Error(`Telegram getUpdates failed: ${body.description ?? 'unknown'}`);
|
|
40
|
+
}
|
|
41
|
+
return body.result ?? [];
|
|
42
|
+
}
|
|
43
|
+
async sendMessage(chatId, text) {
|
|
44
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins -- global fetch is stable in Node 20+
|
|
45
|
+
const res = await fetch(this.url('sendMessage'), {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
body: JSON.stringify({ chat_id: chatId, text }),
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
throw new Error(`Telegram sendMessage failed: HTTP ${res.status}`);
|
|
52
|
+
}
|
|
53
|
+
const body = (await res.json());
|
|
54
|
+
if (!body.ok) {
|
|
55
|
+
throw new Error(`Telegram sendMessage failed: ${body.description ?? 'unknown'}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Polling-mode Telegram adapter.
|
|
61
|
+
*
|
|
62
|
+
* The read loop is a vanilla `while (running) await getUpdates()`. We
|
|
63
|
+
* acknowledge updates by advancing the `offset` cursor to the id after
|
|
64
|
+
* the highest update seen in the batch — this is Telegram's documented
|
|
65
|
+
* delete semantics for getUpdates.
|
|
66
|
+
*/
|
|
67
|
+
export class TelegramChannel {
|
|
68
|
+
name = 'telegram';
|
|
69
|
+
handler = null;
|
|
70
|
+
running = false;
|
|
71
|
+
loopPromise = null;
|
|
72
|
+
offset;
|
|
73
|
+
client;
|
|
74
|
+
config;
|
|
75
|
+
logger;
|
|
76
|
+
errorBackoffMs;
|
|
77
|
+
constructor(options) {
|
|
78
|
+
this.config = options.config;
|
|
79
|
+
this.client = options.client ?? new HttpTelegramClient(options.config.token);
|
|
80
|
+
this.logger = options.logger ?? {};
|
|
81
|
+
this.errorBackoffMs = options.errorBackoffMs ?? 5000;
|
|
82
|
+
}
|
|
83
|
+
onMessage(handler) {
|
|
84
|
+
this.handler = handler;
|
|
85
|
+
}
|
|
86
|
+
async start() {
|
|
87
|
+
if (this.running)
|
|
88
|
+
return;
|
|
89
|
+
if (!this.handler) {
|
|
90
|
+
throw new Error('TelegramChannel.start() called before onMessage() — no handler registered');
|
|
91
|
+
}
|
|
92
|
+
this.running = true;
|
|
93
|
+
this.loopPromise = this.runLoop();
|
|
94
|
+
}
|
|
95
|
+
async stop() {
|
|
96
|
+
if (!this.running)
|
|
97
|
+
return;
|
|
98
|
+
this.running = false;
|
|
99
|
+
if (this.loopPromise) {
|
|
100
|
+
try {
|
|
101
|
+
await this.loopPromise;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Loop errors are already logged; ignore here so stop() is always clean.
|
|
105
|
+
}
|
|
106
|
+
this.loopPromise = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async sendMessage(to, text) {
|
|
110
|
+
if (to.channel !== this.name) {
|
|
111
|
+
throw new Error(`TelegramChannel cannot send to channel=${to.channel}`);
|
|
112
|
+
}
|
|
113
|
+
await this.client.sendMessage(to.id, text);
|
|
114
|
+
}
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Internals
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
/**
|
|
119
|
+
* Main polling loop. Runs until `stop()` flips `running` false.
|
|
120
|
+
*
|
|
121
|
+
* Errors are caught, logged, and followed by a small backoff so a
|
|
122
|
+
* transient network blip doesn't hot-spin the bot against Telegram.
|
|
123
|
+
*
|
|
124
|
+
* The awaits are deliberately sequential: Telegram long-poll demands
|
|
125
|
+
* one outstanding getUpdates at a time per bot, and we must drain
|
|
126
|
+
* each batch before advancing the cursor.
|
|
127
|
+
*/
|
|
128
|
+
/* eslint-disable no-await-in-loop -- polling loop is intentionally sequential */
|
|
129
|
+
async runLoop() {
|
|
130
|
+
const pollInterval = this.config.pollIntervalMs ?? 0;
|
|
131
|
+
while (this.running) {
|
|
132
|
+
try {
|
|
133
|
+
const updates = await this.client.getUpdates({
|
|
134
|
+
offset: this.offset,
|
|
135
|
+
timeoutSec: 25,
|
|
136
|
+
});
|
|
137
|
+
for (const update of updates) {
|
|
138
|
+
await this.handleUpdate(update);
|
|
139
|
+
// Advance cursor past the highest update id we've seen.
|
|
140
|
+
this.offset = update.update_id + 1;
|
|
141
|
+
}
|
|
142
|
+
if (pollInterval > 0)
|
|
143
|
+
await sleep(pollInterval);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
this.logger.error?.('telegram: poll loop error', err);
|
|
147
|
+
// Backoff on errors so we don't hammer the API when the network
|
|
148
|
+
// is flaky or the token got revoked.
|
|
149
|
+
await sleep(this.errorBackoffMs);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/* eslint-enable no-await-in-loop */
|
|
154
|
+
/**
|
|
155
|
+
* Normalize a Telegram update into a generic `Message` and hand it to
|
|
156
|
+
* the router. Drops non-text messages silently (MVP is text-only).
|
|
157
|
+
*/
|
|
158
|
+
async handleUpdate(update) {
|
|
159
|
+
const msg = update.message ?? update.edited_message;
|
|
160
|
+
if (!msg)
|
|
161
|
+
return;
|
|
162
|
+
if (!msg.text)
|
|
163
|
+
return; // MVP: text-only
|
|
164
|
+
if (!this.handler)
|
|
165
|
+
return;
|
|
166
|
+
// Reject messages from users not on the allowlist. We match on the
|
|
167
|
+
// sender's user id (falling back to chat id for groups where there
|
|
168
|
+
// is no `from`). Allowlist is explicit — empty list means "nobody".
|
|
169
|
+
const senderId = String(msg.from?.id ?? msg.chat.id);
|
|
170
|
+
if (!this.config.allowlist.includes(senderId)) {
|
|
171
|
+
this.logger.info?.(`telegram: dropping message from non-allowlisted user ${senderId}`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const address = {
|
|
175
|
+
channel: this.name,
|
|
176
|
+
// We always send replies to the chat, not the user — that way
|
|
177
|
+
// group chats work correctly in a future multi-user mode.
|
|
178
|
+
id: String(msg.chat.id),
|
|
179
|
+
displayName: resolveDisplayName(msg),
|
|
180
|
+
};
|
|
181
|
+
const normalized = {
|
|
182
|
+
id: `telegram:${msg.message_id}`,
|
|
183
|
+
channel: this.name,
|
|
184
|
+
from: address,
|
|
185
|
+
text: msg.text,
|
|
186
|
+
timestamp: new Date(msg.date * 1000),
|
|
187
|
+
};
|
|
188
|
+
try {
|
|
189
|
+
await this.handler(normalized);
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
this.logger.error?.('telegram: handler failed', err);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// =============================================================================
|
|
197
|
+
// Helpers
|
|
198
|
+
// =============================================================================
|
|
199
|
+
function resolveDisplayName(msg) {
|
|
200
|
+
const from = msg.from;
|
|
201
|
+
if (from?.username)
|
|
202
|
+
return `@${from.username}`;
|
|
203
|
+
if (from?.first_name) {
|
|
204
|
+
return from.last_name ? `${from.first_name} ${from.last_name}` : from.first_name;
|
|
205
|
+
}
|
|
206
|
+
if (msg.chat.title)
|
|
207
|
+
return msg.chat.title;
|
|
208
|
+
if (msg.chat.username)
|
|
209
|
+
return `@${msg.chat.username}`;
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
function sleep(ms) {
|
|
213
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=telegram.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telegram.js","sourceRoot":"","sources":["../../../../src/lib/gateway/channels/telegram.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messaging Gateway Router (PRLT-1255)
|
|
3
|
+
*
|
|
4
|
+
* Sits between N `MessagingChannel` adapters and the `prlt session poke`
|
|
5
|
+
* path. The router is intentionally small — it does four things:
|
|
6
|
+
*
|
|
7
|
+
* 1. Look up (or create) the agent mapping for `(channel, user)`.
|
|
8
|
+
* 2. Forward the user's text to that agent via a `SessionPoker`.
|
|
9
|
+
* 3. Ship the agent's response back via the channel's `sendMessage`.
|
|
10
|
+
* 4. Stamp channel + route usage timestamps in machine.db.
|
|
11
|
+
*
|
|
12
|
+
* Everything Telegram-specific lives in `channels/telegram.ts`. Everything
|
|
13
|
+
* storage-specific lives in `MachineDB`. This file owns the glue.
|
|
14
|
+
*/
|
|
15
|
+
import type { MachineDB } from '../machine-db.js';
|
|
16
|
+
import type { MessagingChannel, Message } from './types.js';
|
|
17
|
+
/**
|
|
18
|
+
* Indirection over `prlt session poke`. The default implementation shells
|
|
19
|
+
* out to the globally-installed `prlt` binary, but tests and embedders can
|
|
20
|
+
* inject a custom poker.
|
|
21
|
+
*
|
|
22
|
+
* Returning `null` means "message delivered, no response captured".
|
|
23
|
+
* Throwing means "failed to reach the agent at all".
|
|
24
|
+
*/
|
|
25
|
+
export interface SessionPoker {
|
|
26
|
+
poke(agent: string, message: string, options?: {
|
|
27
|
+
waitTimeoutSec?: number;
|
|
28
|
+
}): Promise<string | null>;
|
|
29
|
+
}
|
|
30
|
+
export interface MessagingGatewayOptions {
|
|
31
|
+
/** Machine DB handle used for channel/route persistence. */
|
|
32
|
+
db: MachineDB;
|
|
33
|
+
/** How to forward messages to an agent. Defaults to the shell poker. */
|
|
34
|
+
sessionPoker: SessionPoker;
|
|
35
|
+
/**
|
|
36
|
+
* Called when there is no existing route for a user. Must return the
|
|
37
|
+
* agent session id (typically an agent name) that should own this
|
|
38
|
+
* conversation going forward. Returning `null` rejects the message —
|
|
39
|
+
* useful for "deny if no mapping" policies.
|
|
40
|
+
*/
|
|
41
|
+
resolveAgentForNewUser: (msg: Message) => Promise<string | null> | string | null;
|
|
42
|
+
/** Optional logger sink. Defaults to console. */
|
|
43
|
+
logger?: {
|
|
44
|
+
info?: (msg: string) => void;
|
|
45
|
+
error?: (msg: string, err?: unknown) => void;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* How long to wait for an agent response before giving up and sending
|
|
49
|
+
* a "still working on it" message back. Defaults to 90 seconds.
|
|
50
|
+
*/
|
|
51
|
+
waitTimeoutSec?: number;
|
|
52
|
+
}
|
|
53
|
+
export declare class MessagingGateway {
|
|
54
|
+
private channels;
|
|
55
|
+
private readonly db;
|
|
56
|
+
private readonly poker;
|
|
57
|
+
private readonly resolveAgentForNewUser;
|
|
58
|
+
private readonly logger;
|
|
59
|
+
private readonly waitTimeoutSec;
|
|
60
|
+
private started;
|
|
61
|
+
constructor(options: MessagingGatewayOptions);
|
|
62
|
+
/**
|
|
63
|
+
* Register a channel adapter. Wires the inbound handler but does NOT
|
|
64
|
+
* start the channel — the caller decides when to start() everything.
|
|
65
|
+
*/
|
|
66
|
+
registerChannel(channel: MessagingChannel): void;
|
|
67
|
+
/** Get a previously-registered channel by name. */
|
|
68
|
+
getChannel(name: string): MessagingChannel | undefined;
|
|
69
|
+
/** List names of all registered channels. */
|
|
70
|
+
listChannelNames(): string[];
|
|
71
|
+
/** Start every registered channel in parallel. */
|
|
72
|
+
start(): Promise<void>;
|
|
73
|
+
/** Stop every registered channel in parallel. Safe to call multiple times. */
|
|
74
|
+
stop(): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Route a single inbound message.
|
|
77
|
+
*
|
|
78
|
+
* This is exposed (not `private`) so tests can drive it directly
|
|
79
|
+
* without standing up a real channel adapter.
|
|
80
|
+
*/
|
|
81
|
+
routeInbound(msg: Message): Promise<void>;
|
|
82
|
+
private createRouteForNewUser;
|
|
83
|
+
private safeReply;
|
|
84
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messaging Gateway Router (PRLT-1255)
|
|
3
|
+
*
|
|
4
|
+
* Sits between N `MessagingChannel` adapters and the `prlt session poke`
|
|
5
|
+
* path. The router is intentionally small — it does four things:
|
|
6
|
+
*
|
|
7
|
+
* 1. Look up (or create) the agent mapping for `(channel, user)`.
|
|
8
|
+
* 2. Forward the user's text to that agent via a `SessionPoker`.
|
|
9
|
+
* 3. Ship the agent's response back via the channel's `sendMessage`.
|
|
10
|
+
* 4. Stamp channel + route usage timestamps in machine.db.
|
|
11
|
+
*
|
|
12
|
+
* Everything Telegram-specific lives in `channels/telegram.ts`. Everything
|
|
13
|
+
* storage-specific lives in `MachineDB`. This file owns the glue.
|
|
14
|
+
*/
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// MessagingGateway
|
|
17
|
+
// =============================================================================
|
|
18
|
+
export class MessagingGateway {
|
|
19
|
+
channels = new Map();
|
|
20
|
+
db;
|
|
21
|
+
poker;
|
|
22
|
+
resolveAgentForNewUser;
|
|
23
|
+
logger;
|
|
24
|
+
waitTimeoutSec;
|
|
25
|
+
started = false;
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.db = options.db;
|
|
28
|
+
this.poker = options.sessionPoker;
|
|
29
|
+
this.resolveAgentForNewUser = options.resolveAgentForNewUser;
|
|
30
|
+
this.logger = options.logger ?? {};
|
|
31
|
+
this.waitTimeoutSec = options.waitTimeoutSec ?? 90;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Register a channel adapter. Wires the inbound handler but does NOT
|
|
35
|
+
* start the channel — the caller decides when to start() everything.
|
|
36
|
+
*/
|
|
37
|
+
registerChannel(channel) {
|
|
38
|
+
if (this.channels.has(channel.name)) {
|
|
39
|
+
throw new Error(`Channel already registered: ${channel.name}`);
|
|
40
|
+
}
|
|
41
|
+
channel.onMessage(msg => this.routeInbound(msg));
|
|
42
|
+
this.channels.set(channel.name, channel);
|
|
43
|
+
}
|
|
44
|
+
/** Get a previously-registered channel by name. */
|
|
45
|
+
getChannel(name) {
|
|
46
|
+
return this.channels.get(name);
|
|
47
|
+
}
|
|
48
|
+
/** List names of all registered channels. */
|
|
49
|
+
listChannelNames() {
|
|
50
|
+
return [...this.channels.keys()];
|
|
51
|
+
}
|
|
52
|
+
/** Start every registered channel in parallel. */
|
|
53
|
+
async start() {
|
|
54
|
+
if (this.started)
|
|
55
|
+
return;
|
|
56
|
+
this.started = true;
|
|
57
|
+
await Promise.all([...this.channels.values()].map(ch => ch.start()));
|
|
58
|
+
}
|
|
59
|
+
/** Stop every registered channel in parallel. Safe to call multiple times. */
|
|
60
|
+
async stop() {
|
|
61
|
+
if (!this.started)
|
|
62
|
+
return;
|
|
63
|
+
this.started = false;
|
|
64
|
+
await Promise.all([...this.channels.values()].map(ch => ch.stop()));
|
|
65
|
+
}
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Core routing
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
/**
|
|
70
|
+
* Route a single inbound message.
|
|
71
|
+
*
|
|
72
|
+
* This is exposed (not `private`) so tests can drive it directly
|
|
73
|
+
* without standing up a real channel adapter.
|
|
74
|
+
*/
|
|
75
|
+
async routeInbound(msg) {
|
|
76
|
+
const channel = this.channels.get(msg.channel);
|
|
77
|
+
if (!channel) {
|
|
78
|
+
this.logger.error?.(`router: no channel registered for ${msg.channel}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!msg.text || msg.text.trim().length === 0) {
|
|
82
|
+
// MVP: text-only. Voice notes land in PRLT-1234.
|
|
83
|
+
this.logger.info?.(`router: dropping empty message ${msg.id}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// 1. Find or create the route.
|
|
87
|
+
let route = this.db.getMessagingRoute(msg.channel, msg.from.id);
|
|
88
|
+
if (!route) {
|
|
89
|
+
route = await this.createRouteForNewUser(msg);
|
|
90
|
+
if (!route) {
|
|
91
|
+
// Denied by policy — silently drop, do not leak whether the user
|
|
92
|
+
// exists to a possibly-hostile sender.
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// 2. Forward to the agent session.
|
|
97
|
+
let response = null;
|
|
98
|
+
try {
|
|
99
|
+
response = await this.poker.poke(route.agentSessionId, msg.text, {
|
|
100
|
+
waitTimeoutSec: this.waitTimeoutSec,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
this.logger.error?.(`router: poke failed for ${route.agentSessionId}`, err);
|
|
105
|
+
await this.safeReply(channel, msg, `Sorry, I couldn't reach ${route.agentSessionId}. Try again in a moment.`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// 3. Stamp usage.
|
|
109
|
+
this.db.touchMessagingRoute(msg.channel, msg.from.id);
|
|
110
|
+
this.db.touchMessagingChannel(msg.channel);
|
|
111
|
+
// 4. Reply (if we captured anything).
|
|
112
|
+
if (response && response.trim().length > 0) {
|
|
113
|
+
await this.safeReply(channel, msg, response);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Helpers
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
async createRouteForNewUser(msg) {
|
|
120
|
+
const agentSessionId = await this.resolveAgentForNewUser(msg);
|
|
121
|
+
if (!agentSessionId) {
|
|
122
|
+
this.logger.info?.(`router: no agent resolved for ${msg.channel}:${msg.from.id}`);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return this.db.upsertMessagingRoute({
|
|
126
|
+
channel: msg.channel,
|
|
127
|
+
userId: msg.from.id,
|
|
128
|
+
agentSessionId,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async safeReply(channel, msg, text) {
|
|
132
|
+
try {
|
|
133
|
+
await channel.sendMessage(msg.from, text);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
this.logger.error?.(`router: sendMessage failed for ${channel.name}`, err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../../src/lib/gateway/router.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default SessionPoker implementation (PRLT-1255)
|
|
3
|
+
*
|
|
4
|
+
* Shells out to `prlt session poke <agent> <message> --wait --timeout N --json`
|
|
5
|
+
* and extracts the captured response from the JSON output.
|
|
6
|
+
*
|
|
7
|
+
* The router code itself is agnostic to this — tests inject a fake
|
|
8
|
+
* SessionPoker. This file exists so production code can pick up the real
|
|
9
|
+
* poker with zero wiring.
|
|
10
|
+
*/
|
|
11
|
+
import type { SessionPoker } from './router.js';
|
|
12
|
+
export interface ShellSessionPokerOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Name of the binary to exec. Defaults to `prlt` on PATH. Tests can
|
|
15
|
+
* point this at a shim script.
|
|
16
|
+
*/
|
|
17
|
+
binary?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Working directory for the exec call. Defaults to the current process
|
|
20
|
+
* cwd — `prlt session poke` resolves workspace context from there.
|
|
21
|
+
*/
|
|
22
|
+
cwd?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Shell-based SessionPoker. Uses `execFile` (no shell) so nothing in the
|
|
26
|
+
* message body can trigger shell expansion or injection.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ShellSessionPoker implements SessionPoker {
|
|
29
|
+
private readonly binary;
|
|
30
|
+
private readonly cwd;
|
|
31
|
+
constructor(options?: ShellSessionPokerOptions);
|
|
32
|
+
poke(agent: string, message: string, options?: {
|
|
33
|
+
waitTimeoutSec?: number;
|
|
34
|
+
}): Promise<string | null>;
|
|
35
|
+
}
|