polygram 0.2.0 → 0.3.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/.claude-plugin/plugin.json +1 -1
- package/README.md +18 -10
- package/commands/status.md +2 -2
- package/lib/attachments.js +1 -1
- package/lib/db.js +2 -2
- package/lib/inbox.js +2 -2
- package/lib/queue-utils.js +2 -2
- package/migrations/003-pairings.sql +1 -1
- package/ops/README.md +1 -1
- package/package.json +1 -1
- package/polygram.js +17 -13
- package/scripts/split-db.js +3 -3
- package/skills/history/SKILL.md +1 -1
- package/skills/history/scripts/query.js +1 -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.
|
|
4
|
+
"version": "0.3.0",
|
|
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/README.md
CHANGED
|
@@ -87,23 +87,31 @@ Practical differences that matter for migration:
|
|
|
87
87
|
Requires Node 20+.
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
# Global binary:
|
|
91
|
+
npm install -g polygram
|
|
92
|
+
|
|
93
|
+
# Data directory (config + per-bot DBs + inbox live here):
|
|
94
|
+
mkdir ~/polygram && cd ~/polygram
|
|
95
|
+
cp $(npm root -g)/polygram/config.example.json config.json
|
|
96
|
+
# edit config.json: bot tokens from @BotFather, chat IDs, cwds
|
|
95
97
|
```
|
|
96
98
|
|
|
97
99
|
## Run
|
|
98
100
|
|
|
99
101
|
```bash
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
cd ~/polygram
|
|
103
|
+
polygram --bot admin-bot # one bot, one process
|
|
104
|
+
polygram --bot partner-bot # another bot, another process
|
|
102
105
|
```
|
|
103
106
|
|
|
104
|
-
`--bot` is required.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
`--bot` is required. Paths resolve from your current working directory:
|
|
108
|
+
|
|
109
|
+
- `config.json` → `$PWD/config.json` (override with `--config` or `POLYGRAM_CONFIG`)
|
|
110
|
+
- `<bot>.db` → `$PWD/<bot>.db` (override with `--db` or `POLYGRAM_DB`)
|
|
111
|
+
- `inbox/` → `$PWD/inbox/` (override with `POLYGRAM_INBOX`)
|
|
112
|
+
|
|
113
|
+
Migrations are bundled in the package and apply automatically. The daemon
|
|
114
|
+
opens a Unix socket at `/tmp/polygram-<bot>.sock`.
|
|
107
115
|
|
|
108
116
|
For production, LaunchAgent plists are in `ops/`. See `ops/README.md`.
|
|
109
117
|
|
package/commands/status.md
CHANGED
|
@@ -21,9 +21,9 @@ a short Markdown table (one row per bot):
|
|
|
21
21
|
```
|
|
22
22
|
Interpret the result:
|
|
23
23
|
- `ping: {"id":null,"ok":true,"pong":true,"bot":"<bot>"}` → socket alive
|
|
24
|
-
- `ERR: connect ECONNREFUSED` → socket stale (
|
|
24
|
+
- `ERR: connect ECONNREFUSED` → socket stale (polygram not actually
|
|
25
25
|
serving despite plist being loaded)
|
|
26
|
-
- `ERR: ENOENT` → socket missing (
|
|
26
|
+
- `ERR: ENOENT` → socket missing (polygram never got that far at boot)
|
|
27
27
|
|
|
28
28
|
3. **Recent events in each bot's DB.** For every bot, run:
|
|
29
29
|
```
|
package/lib/attachments.js
CHANGED
|
@@ -38,7 +38,7 @@ function filterAttachments(attachments, opts = {}) {
|
|
|
38
38
|
const size = a.size || 0;
|
|
39
39
|
// Telegram sometimes reports file_size=0 or omits it. Those bypass the
|
|
40
40
|
// cap here but the download step MUST re-check Content-Length and actual
|
|
41
|
-
// bytes — see downloadAttachments in
|
|
41
|
+
// bytes — see downloadAttachments in polygram.js.
|
|
42
42
|
if (size > maxFileBytes) {
|
|
43
43
|
rejected.push({ att: a, reason: `exceeds per-file cap (${maxFileBytes} bytes, got ${size})` });
|
|
44
44
|
continue;
|
package/lib/db.js
CHANGED
|
@@ -172,7 +172,7 @@ function wrap(db) {
|
|
|
172
172
|
text: row.text || '',
|
|
173
173
|
reply_to_id: row.reply_to_id || null,
|
|
174
174
|
direction: row.direction || 'in',
|
|
175
|
-
source: row.source || '
|
|
175
|
+
source: row.source || 'polygram',
|
|
176
176
|
bot_name: row.bot_name || null,
|
|
177
177
|
attachments_json: row.attachments_json || null,
|
|
178
178
|
session_id: row.session_id || null,
|
|
@@ -192,7 +192,7 @@ function wrap(db) {
|
|
|
192
192
|
thread_id: row.thread_id ? String(row.thread_id) : null,
|
|
193
193
|
user: row.user || null,
|
|
194
194
|
text: row.text || '',
|
|
195
|
-
source: row.source || '
|
|
195
|
+
source: row.source || 'polygram',
|
|
196
196
|
bot_name: row.bot_name || null,
|
|
197
197
|
turn_id: row.turn_id || null,
|
|
198
198
|
session_id: row.session_id || null,
|
package/lib/inbox.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Inbox on-disk helpers.
|
|
3
3
|
*
|
|
4
4
|
* `sweepInbox(dir, maxAgeMs)` deletes files under each chat subdir whose
|
|
5
|
-
* mtime is older than `maxAgeMs`. Called on
|
|
6
|
-
*
|
|
5
|
+
* mtime is older than `maxAgeMs`. Called on polygram boot so a long-running
|
|
6
|
+
* polygram doesn't accumulate every file a user has ever sent.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
package/lib/queue-utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure helpers for per-chat message queues. Kept separate from
|
|
3
|
-
* they can be unit-tested without spinning up the whole
|
|
2
|
+
* Pure helpers for per-chat message queues. Kept separate from polygram.js so
|
|
3
|
+
* they can be unit-tested without spinning up the whole polygram.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// Drop queued items belonging to a chatId across all its thread-scoped
|
package/ops/README.md
CHANGED
|
@@ -89,7 +89,7 @@ launchctl load ~/Library/LaunchAgents/com.polygram.my-bot.plist
|
|
|
89
89
|
The script is idempotent; safe to re-run. It refuses to proceed if a WAL
|
|
90
90
|
file on the source DB indicates a live writer.
|
|
91
91
|
|
|
92
|
-
## Cron →
|
|
92
|
+
## Cron → polygram (IPC, not direct DB write)
|
|
93
93
|
|
|
94
94
|
Cron jobs that want to post to Telegram must address a specific bot:
|
|
95
95
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Process stays warm: no cold start, full prompt caching.
|
|
7
7
|
*
|
|
8
8
|
* Architecture:
|
|
9
|
-
* Telegram (grammy long-poll) →
|
|
9
|
+
* Telegram (grammy long-poll) → polygram receives message
|
|
10
10
|
* → looks up per-chat config (model, effort, agent, cwd)
|
|
11
11
|
* → sends to persistent claude process via stdin (stream-json)
|
|
12
12
|
* → reads response from stdout (stream-json)
|
|
@@ -41,17 +41,21 @@ const {
|
|
|
41
41
|
const ipcServer = require('./lib/ipc-server');
|
|
42
42
|
|
|
43
43
|
// ─── Config ──────────────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
//
|
|
45
|
+
// User data (config, per-bot DBs, inbox) resolves from the cwd the operator
|
|
46
|
+
// runs polygram in. Package resources (migrations/) stay under __dirname.
|
|
47
|
+
// This makes `npm install -g polygram` + `cd ~/my-data && polygram --bot X`
|
|
48
|
+
// work without symlinks or POLYGRAM_DIR gymnastics.
|
|
49
|
+
|
|
50
|
+
const DATA_DIR = process.cwd();
|
|
51
|
+
const CONFIG_PATH = process.env.POLYGRAM_CONFIG || path.join(DATA_DIR, 'config.json');
|
|
52
|
+
const SESSIONS_JSON_PATH = path.join(DATA_DIR, 'sessions.json'); // legacy, imported once on boot
|
|
53
|
+
const DB_DIR = DATA_DIR;
|
|
48
54
|
// DB_PATH is resolved in main() from --db or <bot>.db default.
|
|
49
55
|
let DB_PATH = null;
|
|
50
|
-
// Paths that vary per deployment are env-configurable. Defaults preserve
|
|
51
|
-
// the author's local layout for back-compat but the repo itself is portable.
|
|
52
56
|
const STICKERS_PATH = process.env.POLYGRAM_STICKERS
|
|
53
|
-
|| path.join(
|
|
54
|
-
const INBOX_DIR = process.env.POLYGRAM_INBOX || path.join(
|
|
57
|
+
|| path.join(DATA_DIR, 'stickers.json');
|
|
58
|
+
const INBOX_DIR = process.env.POLYGRAM_INBOX || path.join(DATA_DIR, 'inbox');
|
|
55
59
|
const CLAUDE_BIN = process.env.POLYGRAM_CLAUDE_BIN
|
|
56
60
|
|| path.join(process.env.HOME || '', '.npm-global/bin/claude');
|
|
57
61
|
const CHILD_HOME = process.env.POLYGRAM_CHILD_HOME || process.env.HOME || '';
|
|
@@ -181,7 +185,7 @@ function recordInbound(msg) {
|
|
|
181
185
|
text: msg.text || msg.caption || '',
|
|
182
186
|
reply_to_id: msg.reply_to_message?.message_id || null,
|
|
183
187
|
direction: 'in',
|
|
184
|
-
source: '
|
|
188
|
+
source: 'polygram',
|
|
185
189
|
bot_name: BOT_NAME,
|
|
186
190
|
attachments_json: attachments.length ? JSON.stringify(attachments) : null,
|
|
187
191
|
model: chatConfig?.model || null,
|
|
@@ -1507,7 +1511,7 @@ async function main() {
|
|
|
1507
1511
|
console.log(`[inbox] swept ${swept.swept} files (${(swept.bytes / 1_048_576).toFixed(1)} MiB) older than ${inboxRetentionMs / 86_400_000}d`);
|
|
1508
1512
|
db.logEvent('inbox-swept', { files: swept.swept, bytes: swept.bytes, retention_days: inboxRetentionMs / 86_400_000 });
|
|
1509
1513
|
}
|
|
1510
|
-
db.logEvent('
|
|
1514
|
+
db.logEvent('polygram-start', { migration: migration.reason, imported: migration.imported });
|
|
1511
1515
|
} catch (err) {
|
|
1512
1516
|
console.error(`[db] FATAL: ${err.message}`);
|
|
1513
1517
|
console.error('Bridge cannot run without a DB (Phase 2: DB is source of truth).');
|
|
@@ -1555,12 +1559,12 @@ async function main() {
|
|
|
1555
1559
|
try { fs.unlinkSync(ipcServer.secretPathFor(BOT_NAME)); } catch {}
|
|
1556
1560
|
// Resolve any blocked hook waiters so Claude processes don't hang.
|
|
1557
1561
|
for (const list of approvalWaiters.values()) {
|
|
1558
|
-
for (const fn of list) { try { fn('cancelled', '
|
|
1562
|
+
for (const fn of list) { try { fn('cancelled', 'polygram shutting down'); } catch {} }
|
|
1559
1563
|
}
|
|
1560
1564
|
approvalWaiters.clear();
|
|
1561
1565
|
if (pm) pm.shutdown().catch(() => {});
|
|
1562
1566
|
if (db) {
|
|
1563
|
-
try { db.logEvent('
|
|
1567
|
+
try { db.logEvent('polygram-stop'); db.raw.close(); } catch {}
|
|
1564
1568
|
}
|
|
1565
1569
|
setTimeout(() => process.exit(0), 1000);
|
|
1566
1570
|
};
|
package/scripts/split-db.js
CHANGED
|
@@ -59,7 +59,7 @@ function main() {
|
|
|
59
59
|
console.log(`[split-db] bots: ${bots.join(', ')}`);
|
|
60
60
|
if (dryRun) console.log('[split-db] DRY RUN - no files written or renamed');
|
|
61
61
|
|
|
62
|
-
// Refuse to split if a live
|
|
62
|
+
// Refuse to split if a live polygram is writing to srcPath. The WAL file's
|
|
63
63
|
// presence + recent mtime is a strong proxy: SQLite WAL checkpoints after
|
|
64
64
|
// ~1000 pages or a clean close, so a hot WAL means an active writer.
|
|
65
65
|
refuseIfActiveWriter(srcPath);
|
|
@@ -87,7 +87,7 @@ function main() {
|
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
89
|
// `transaction` runs deferred by default; we want immediate write-lock
|
|
90
|
-
// on the source to block any concurrent
|
|
90
|
+
// on the source to block any concurrent polygram from slipping in.
|
|
91
91
|
srcTx.immediate();
|
|
92
92
|
|
|
93
93
|
src.raw.close();
|
|
@@ -112,7 +112,7 @@ function refuseIfActiveWriter(srcPath) {
|
|
|
112
112
|
const wal = `${srcPath}-wal`;
|
|
113
113
|
if (!fs.existsSync(wal)) return;
|
|
114
114
|
const age = Date.now() - fs.statSync(wal).mtimeMs;
|
|
115
|
-
// 60s is generous — a clean
|
|
115
|
+
// 60s is generous — a clean polygram shutdown checkpoints the WAL.
|
|
116
116
|
// A hot WAL (< 60s) strongly suggests a live writer.
|
|
117
117
|
if (age < 60_000) {
|
|
118
118
|
console.error(`[split-db] refusing: ${wal} is active (mtime ${Math.round(age/1000)}s ago)`);
|
package/skills/history/SKILL.md
CHANGED
|
@@ -52,6 +52,6 @@ Flags: `--days 7`
|
|
|
52
52
|
|
|
53
53
|
# Notes
|
|
54
54
|
|
|
55
|
-
- DB opened read-only — safe to run alongside the live
|
|
55
|
+
- DB opened read-only — safe to run alongside the live polygram.
|
|
56
56
|
- Output capped at 500 rows. Narrow with `--since` or `--days` for wide queries.
|
|
57
57
|
- Times in `ts` are ms epoch. `formatPretty` shows local HH:MM.
|
|
@@ -106,7 +106,7 @@ function deriveBotScope(cfg) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
function openDbReadOnly(dbPath) {
|
|
109
|
-
if (!fs.existsSync(dbPath)) die(`
|
|
109
|
+
if (!fs.existsSync(dbPath)) die(`polygram DB not found at ${dbPath}`);
|
|
110
110
|
const raw = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
111
111
|
return { raw };
|
|
112
112
|
}
|