polygram 0.2.0 → 0.3.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.
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
+ "name": "shumkov",
4
+ "description": "Ivan Shumkov's Claude Code plugins.",
5
+ "owner": {
6
+ "name": "Ivan Shumkov",
7
+ "email": "ivanshumkov@gmail.com"
8
+ },
9
+ "plugins": [
10
+ {
11
+ "name": "polygram",
12
+ "description": "Telegram integration for Claude Code that preserves the OpenClaw per-chat session model. Migration target for OpenClaw users. Bundles /polygram:status|logs|pair-code|approvals admin commands and a history skill.",
13
+ "category": "integration",
14
+ "source": "./",
15
+ "homepage": "https://github.com/shumkov/polygram"
16
+ }
17
+ ]
18
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "polygram",
4
- "version": "0.2.0",
4
+ "version": "0.3.1",
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
- git clone https://github.com/shumkov/polygram.git
91
- cd polygram
92
- npm install
93
- cp config.example.json config.json
94
- # edit config.json: tokens from @BotFather, chat IDs, cwds
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
- node polygram.js --bot admin-bot # one bot, one process
101
- node polygram.js --bot partner-bot # another bot, another process
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. Each process creates `<bot>.db` next to `polygram.js`
105
- on first run (migrations apply automatically) and opens a Unix socket at
106
- `/tmp/polygram-<bot>.sock`.
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
 
@@ -112,23 +120,40 @@ For production, LaunchAgent plists are in `ops/`. See `ops/README.md`.
112
120
  polygram also ships as a Claude Code plugin — adds admin slash commands
113
121
  and bundles the transcript-query skill for use inside your Claude sessions.
114
122
 
123
+ The repo doubles as a single-plugin marketplace. Two commands at your
124
+ Claude Code `/` prompt:
125
+
126
+ ```
127
+ /plugin marketplace add https://github.com/shumkov/polygram.git
128
+ /plugin install polygram@shumkov
115
129
  ```
116
- /plugin install https://github.com/shumkov/polygram.git
130
+
131
+ The first registers the marketplace (`shumkov`). The second installs the
132
+ `polygram` plugin from it.
133
+
134
+ To enable in a specific Claude project, add to `.claude/settings.json`:
135
+
136
+ ```json
137
+ {
138
+ "enabledPlugins": {
139
+ "polygram@shumkov": true
140
+ }
141
+ }
117
142
  ```
118
143
 
119
- Once installed:
144
+ Once enabled:
120
145
 
121
- - `/polygram:status` — running bots, IPC health, recent events, one-line verdict
146
+ - `/polygram:status` — running bots, IPC health, recent events
122
147
  - `/polygram:logs <bot>` — tail `~/polygram/logs/<bot>.log`
123
148
  - `/polygram:pair-code` — walks you through issuing a pairing code (in-band via Telegram)
124
149
  - `/polygram:approvals [bot]` — pending and recent tool-approval rows
125
150
 
126
- The bundled **`telegram-history` skill** lets Claude query the transcript
127
- directly:
151
+ The bundled **`history` skill** lets Claude query the transcript directly
152
+ when you ask about past chat activity:
128
153
 
129
154
  ```
130
155
  "Summarise the Orders topic today" →
131
- uses skills/history to run `recent <chat> --since 24h`
156
+ Claude invokes the history skill runs `recent <chat> --since 24h`
132
157
  ```
133
158
 
134
159
  Scope is derived from `process.cwd()`: the skill refuses to run from an
@@ -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 (bridge not actually
24
+ - `ERR: connect ECONNREFUSED` → socket stale (polygram not actually
25
25
  serving despite plist being loaded)
26
- - `ERR: ENOENT` → socket missing (bridge never got that far at boot)
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
  ```
@@ -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 bridge.js.
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 || 'bridge',
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 || 'bridge',
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 bridge boot so a long-running
6
- * bridge doesn't accumulate every file a user has ever sent.
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');
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Pure helpers for per-chat message queues. Kept separate from bridge.js so
3
- * they can be unit-tested without spinning up the whole bridge.
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
@@ -1,4 +1,4 @@
1
- -- Feature 1: pairing codes for live onboarding without bridge restarts.
1
+ -- Feature 1: pairing codes for live onboarding without polygram restarts.
2
2
 
3
3
  -- Pairing codes (single-use, short-lived).
4
4
  CREATE TABLE pair_codes (
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 → bridge (IPC, not direct DB write)
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.2.0",
3
+ "version": "0.3.1",
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) → bridge receives message
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
- const CONFIG_PATH = path.join(__dirname, 'config.json');
46
- const SESSIONS_JSON_PATH = path.join(__dirname, 'sessions.json'); // legacy, imported once on boot
47
- const DB_DIR = __dirname;
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(process.env.HOME || '', 'polygram-stickers.json');
54
- const INBOX_DIR = process.env.POLYGRAM_INBOX || path.join(__dirname, 'inbox');
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: 'bridge',
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('bridge-start', { migration: migration.reason, imported: migration.imported });
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', 'bridge shutting down'); } catch {} }
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('bridge-stop'); db.raw.close(); } catch {}
1567
+ try { db.logEvent('polygram-stop'); db.raw.close(); } catch {}
1564
1568
  }
1565
1569
  setTimeout(() => process.exit(0), 1000);
1566
1570
  };
@@ -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 bridge is writing to srcPath. The WAL file's
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 bridge from slipping in.
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 bridge shutdown checkpoints the WAL.
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)`);
@@ -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 bridge.
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(`bridge DB not found at ${dbPath}`);
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
  }