polygram 0.3.4 → 0.3.6
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/.claude-plugin/plugin.json +1 -1
- package/config.example.json +8 -0
- package/lib/process-manager.js +28 -13
- package/package.json +1 -1
- package/polygram.js +100 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.6",
|
|
5
5
|
"description": "Telegram integration for Claude Code that preserves the OpenClaw per-chat session model. Migration target for OpenClaw users. Multi-bot, multi-chat, per-topic isolation; SQLite transcripts; inline-keyboard approvals. Bundles /polygram:status|logs|pair-code|approvals admin commands and a history skill.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"telegram",
|
package/config.example.json
CHANGED
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
"streamReplies": true,
|
|
12
12
|
"streamMinChars": 30,
|
|
13
13
|
"streamThrottleMs": 500,
|
|
14
|
+
"_comment_pairedChatDefaults": "When a user sends /pair <CODE> from a private chat that isn't in config.chats yet, polygram auto-creates a chat entry using these defaults (merged over top-level `defaults`). Leave out `cwd` at your peril — without it, auto-onboarded DMs have no working directory and pairing will fail.",
|
|
15
|
+
"pairedChatDefaults": {
|
|
16
|
+
"agent": "admin",
|
|
17
|
+
"cwd": "/Users/you/admin-agent",
|
|
18
|
+
"model": "sonnet",
|
|
19
|
+
"effort": "medium",
|
|
20
|
+
"timeout": 600
|
|
21
|
+
},
|
|
14
22
|
"voice": {
|
|
15
23
|
"enabled": true,
|
|
16
24
|
"provider": "openai",
|
package/lib/process-manager.js
CHANGED
|
@@ -173,14 +173,20 @@ class ProcessManager {
|
|
|
173
173
|
entry.sessionId = event.session_id;
|
|
174
174
|
if (this.onInit) this.onInit(sessionKey, event, entry);
|
|
175
175
|
}
|
|
176
|
-
if (event.type === 'assistant' &&
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
176
|
+
if (event.type === 'assistant' && entry.pending) {
|
|
177
|
+
// Any assistant step (text block, tool_use, tool_result) counts as
|
|
178
|
+
// Claude activity — reset the idle timeout so long turns don't
|
|
179
|
+
// wall-clock out.
|
|
180
|
+
entry.pending.resetIdleTimer?.();
|
|
181
|
+
if (this.onStreamChunk) {
|
|
182
|
+
const added = extractAssistantText(event);
|
|
183
|
+
if (added) {
|
|
184
|
+
entry.streamText = entry.streamText
|
|
185
|
+
? `${entry.streamText}\n\n${added}`
|
|
186
|
+
: added;
|
|
187
|
+
try { this.onStreamChunk(sessionKey, entry.streamText, entry); }
|
|
188
|
+
catch (err) { this.logger.error(`[${entry.label}] onStreamChunk: ${err.message}`); }
|
|
189
|
+
}
|
|
184
190
|
}
|
|
185
191
|
}
|
|
186
192
|
if (event.type === 'result' && entry.pending) {
|
|
@@ -250,18 +256,27 @@ class ProcessManager {
|
|
|
250
256
|
entry.pending = { resolve, reject };
|
|
251
257
|
entry.streamText = '';
|
|
252
258
|
|
|
253
|
-
|
|
259
|
+
// Idle timeout: counts N seconds of SILENCE from Claude, not total
|
|
260
|
+
// wall-clock. Long multi-tool turns that produce visible progress
|
|
261
|
+
// (streaming chunks, tool_use events) should not time out as long as
|
|
262
|
+
// they're actively working. onStreamChunk resets this below.
|
|
263
|
+
const arm = () => setTimeout(() => {
|
|
254
264
|
if (entry.pending) {
|
|
255
265
|
entry.pending = null;
|
|
256
266
|
entry.inFlight = false;
|
|
257
|
-
reject(new Error(`Timeout
|
|
267
|
+
reject(new Error(`Timeout: ${timeoutMs / 1000}s idle with no Claude activity`));
|
|
258
268
|
}
|
|
259
269
|
}, timeoutMs);
|
|
270
|
+
entry.pending.timer = arm();
|
|
271
|
+
entry.pending.resetIdleTimer = () => {
|
|
272
|
+
if (entry.pending?.timer) clearTimeout(entry.pending.timer);
|
|
273
|
+
if (entry.pending) entry.pending.timer = arm();
|
|
274
|
+
};
|
|
260
275
|
|
|
261
276
|
const wrappedResolve = entry.pending.resolve;
|
|
262
277
|
const wrappedReject = entry.pending.reject;
|
|
263
|
-
entry.pending.resolve = (r) => { clearTimeout(timer); wrappedResolve(r); };
|
|
264
|
-
entry.pending.reject = (e) => { clearTimeout(timer); wrappedReject(e); };
|
|
278
|
+
entry.pending.resolve = (r) => { if (entry.pending?.timer) clearTimeout(entry.pending.timer); wrappedResolve(r); };
|
|
279
|
+
entry.pending.reject = (e) => { if (entry.pending?.timer) clearTimeout(entry.pending.timer); wrappedReject(e); };
|
|
265
280
|
|
|
266
281
|
try {
|
|
267
282
|
entry.proc.stdin.write(JSON.stringify({
|
|
@@ -269,9 +284,9 @@ class ProcessManager {
|
|
|
269
284
|
message: { role: 'user', content: prompt },
|
|
270
285
|
}) + '\n');
|
|
271
286
|
} catch (err) {
|
|
287
|
+
if (entry.pending?.timer) clearTimeout(entry.pending.timer);
|
|
272
288
|
entry.pending = null;
|
|
273
289
|
entry.inFlight = false;
|
|
274
|
-
clearTimeout(timer);
|
|
275
290
|
reject(err);
|
|
276
291
|
}
|
|
277
292
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Telegram daemon for Claude Code that preserves the OpenClaw per-chat session model. Migration path for OpenClaw users moving to Claude Code.",
|
|
5
5
|
"main": "lib/ipc-client.js",
|
|
6
6
|
"bin": {
|
package/polygram.js
CHANGED
|
@@ -1271,6 +1271,87 @@ function createBot(token) {
|
|
|
1271
1271
|
// not another bot's problem.
|
|
1272
1272
|
const knownChat = (chatId) => !!config.chats[chatId];
|
|
1273
1273
|
|
|
1274
|
+
// Claim a pair code from an unconfigured private chat and persist a new
|
|
1275
|
+
// chat entry so subsequent messages go through the normal flow. Replies
|
|
1276
|
+
// to the user on both success and failure. Returns the new chatConfig on
|
|
1277
|
+
// success, null on any failure.
|
|
1278
|
+
//
|
|
1279
|
+
// The new chat inherits cwd/agent from bot-level pairedChatDefaults if
|
|
1280
|
+
// present, otherwise from the first existing chat the bot owns — on the
|
|
1281
|
+
// reasonable assumption that paired DMs should behave like other DMs for
|
|
1282
|
+
// this bot. Operator can override by setting config.bots.<bot>.pairedChatDefaults.
|
|
1283
|
+
async function onboardPairedChat(ctx, code) {
|
|
1284
|
+
const chatId = ctx.chat.id.toString();
|
|
1285
|
+
const userId = ctx.message.from?.id;
|
|
1286
|
+
const send = (text) => bot.api.sendMessage(chatId, text).catch(() => {});
|
|
1287
|
+
|
|
1288
|
+
if (!userId) {
|
|
1289
|
+
await send('No user id on request.');
|
|
1290
|
+
return null;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const res = pairings.claimCode({
|
|
1294
|
+
code, claimer_user_id: userId,
|
|
1295
|
+
chat_id: chatId, bot_name: BOT_NAME,
|
|
1296
|
+
});
|
|
1297
|
+
dbWrite(() => db.logEvent('pair-claim-attempt', {
|
|
1298
|
+
bot: BOT_NAME, user_id: userId, chat_id: chatId,
|
|
1299
|
+
ok: res.ok, reason: res.reason, via: 'auto-onboard',
|
|
1300
|
+
}), 'log pair-claim-attempt');
|
|
1301
|
+
|
|
1302
|
+
if (!res.ok) {
|
|
1303
|
+
const reply = res.reason === 'rate-limited'
|
|
1304
|
+
? 'Too many attempts. Try again later.'
|
|
1305
|
+
: 'Invalid or expired code.';
|
|
1306
|
+
await send(reply);
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
const paired = config.bot?.pairedChatDefaults || {};
|
|
1311
|
+
const globals = config.defaults || {};
|
|
1312
|
+
const firstChat = Object.values(config.chats)[0] || {};
|
|
1313
|
+
const chatName = paired.name
|
|
1314
|
+
|| (ctx.chat.username && `@${ctx.chat.username}`)
|
|
1315
|
+
|| ctx.chat.first_name
|
|
1316
|
+
|| `User ${userId}`;
|
|
1317
|
+
|
|
1318
|
+
const cwd = paired.cwd || firstChat.cwd;
|
|
1319
|
+
if (!cwd) {
|
|
1320
|
+
dbWrite(() => db.logEvent('auto-onboard-failed', {
|
|
1321
|
+
bot: BOT_NAME, chat_id: chatId, user_id: userId,
|
|
1322
|
+
reason: 'no-cwd',
|
|
1323
|
+
}), 'log auto-onboard-failed');
|
|
1324
|
+
await send('Paired, but no working directory is configured. Ask the operator to set pairedChatDefaults.cwd.');
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const newChat = {
|
|
1329
|
+
name: chatName,
|
|
1330
|
+
bot: BOT_NAME,
|
|
1331
|
+
agent: paired.agent || firstChat.agent,
|
|
1332
|
+
model: paired.model || globals.model || 'sonnet',
|
|
1333
|
+
effort: paired.effort || globals.effort || 'medium',
|
|
1334
|
+
cwd,
|
|
1335
|
+
timeout: paired.timeout || globals.timeout || 600,
|
|
1336
|
+
};
|
|
1337
|
+
if (paired.requireMention != null) newChat.requireMention = paired.requireMention;
|
|
1338
|
+
|
|
1339
|
+
config.chats[chatId] = newChat;
|
|
1340
|
+
try { saveConfig(); }
|
|
1341
|
+
catch (err) {
|
|
1342
|
+
console.error(`[${BOT_NAME}] saveConfig on auto-onboard failed: ${err.message}`);
|
|
1343
|
+
}
|
|
1344
|
+
dbWrite(() => db.logEvent('chat-auto-created', {
|
|
1345
|
+
bot: BOT_NAME, chat_id: chatId, user_id: userId,
|
|
1346
|
+
source: 'pair-claim', model: newChat.model, effort: newChat.effort,
|
|
1347
|
+
}), 'log chat-auto-created');
|
|
1348
|
+
|
|
1349
|
+
const chatLabel = res.chat_id ? `chat ${res.chat_id}` : `every chat ${BOT_NAME} is in`;
|
|
1350
|
+
const suffix = res.note ? `\n(${res.note})` : '';
|
|
1351
|
+
await send(`Paired. You can use me in ${chatLabel}.${suffix}`);
|
|
1352
|
+
return newChat;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1274
1355
|
bot.on('message', async (ctx) => {
|
|
1275
1356
|
if (!isWellFormedMessage(ctx.message)) {
|
|
1276
1357
|
dbWrite(() => db.logEvent('malformed-update', {
|
|
@@ -1281,7 +1362,25 @@ function createBot(token) {
|
|
|
1281
1362
|
return;
|
|
1282
1363
|
}
|
|
1283
1364
|
const chatId = ctx.chat.id.toString();
|
|
1284
|
-
|
|
1365
|
+
let chatConfig = config.chats[chatId];
|
|
1366
|
+
|
|
1367
|
+
// Auto-onboarding: /pair <CODE> from an unconfigured private chat.
|
|
1368
|
+
// Without this, the !chatConfig drop below would silently eat pair
|
|
1369
|
+
// claims from DMs the operator hasn't pre-listed — defeating the
|
|
1370
|
+
// whole point of pair codes (which exist to grant access without
|
|
1371
|
+
// pre-configuration). Group chats are not auto-onboarded: they must
|
|
1372
|
+
// still be added to config.json by the operator, because adding a
|
|
1373
|
+
// group can affect multiple users.
|
|
1374
|
+
if (!chatConfig && ctx.chat.type === 'private') {
|
|
1375
|
+
const probe = (ctx.message.text || '').trim();
|
|
1376
|
+
const pairMatch = /^\/pair(?:@\S+)?\s+(\S+)\s*$/.exec(probe);
|
|
1377
|
+
if (pairMatch) {
|
|
1378
|
+
chatConfig = await onboardPairedChat(ctx, pairMatch[1]);
|
|
1379
|
+
if (!chatConfig) return;
|
|
1380
|
+
recordInbound(ctx.message);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1285
1384
|
if (!chatConfig) return;
|
|
1286
1385
|
|
|
1287
1386
|
// Record every inbound msg, even unaddressed ones — needed for reply-to
|