polygram 0.23.1 → 0.23.3
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/lib/codex/runtime-profile.js +4 -2
- package/lib/ops/heartbeat.js +19 -4
- package/package.json +2 -2
- package/polygram.js +7 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.23.
|
|
4
|
+
"version": "0.23.3",
|
|
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 plus history (transcript queries) and polygram-send (out-of-turn IPC sends with file-upload validation) skills.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"telegram",
|
|
@@ -278,6 +278,7 @@ function buildOwnedConfig({
|
|
|
278
278
|
filesystem[':workspace_roots'] = { '.': 'write' };
|
|
279
279
|
return {
|
|
280
280
|
cli_auth_credentials_store: 'file',
|
|
281
|
+
model_provider: 'openai',
|
|
281
282
|
default_permissions: CODEX_PERMISSION_PROFILE_ID,
|
|
282
283
|
approval_policy: 'never',
|
|
283
284
|
approvals_reviewer: 'user',
|
|
@@ -315,6 +316,7 @@ function renderOwnedConfig(config, {
|
|
|
315
316
|
].filesystem;
|
|
316
317
|
const lines = [
|
|
317
318
|
'cli_auth_credentials_store = "file"',
|
|
319
|
+
'model_provider = "openai"',
|
|
318
320
|
`default_permissions = ${tomlString(CODEX_PERMISSION_PROFILE_ID)}`,
|
|
319
321
|
'approval_policy = "never"',
|
|
320
322
|
'approvals_reviewer = "user"',
|
|
@@ -586,8 +588,8 @@ function validateProjectedConfig(config, materialized) {
|
|
|
586
588
|
|| !isDeepStrictEqual(config.mcpServers, { count: 0, keySha256: [] })
|
|
587
589
|
|| !isDeepStrictEqual(config.plugins, { count: 0, keySha256: [] })
|
|
588
590
|
|| !isDeepStrictEqual(config.modelProviders, {
|
|
589
|
-
count:
|
|
590
|
-
keySha256: [
|
|
591
|
+
count: 0,
|
|
592
|
+
keySha256: [],
|
|
591
593
|
})
|
|
592
594
|
|| !Array.isArray(config.permissionProfiles)
|
|
593
595
|
|| config.permissionProfiles.length !== 1
|
package/lib/ops/heartbeat.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* File-only equivalent of water's lib/ops/heartbeat.js pattern — no
|
|
6
6
|
* /healthz endpoint, since this repo has no HTTP server to hang a route on.
|
|
7
|
-
* Wiring heartbeat
|
|
7
|
+
* Wiring the heartbeat file into an actual Netdata alert (netdata_watch_units /
|
|
8
8
|
* a filecheck-or-cron-parses-JSON entry) is a VPS-side ops/ansible change
|
|
9
9
|
* outside this repo's scope; this only builds the code-side signal.
|
|
10
10
|
*/
|
|
@@ -14,8 +14,24 @@
|
|
|
14
14
|
const fs = require('node:fs');
|
|
15
15
|
const path = require('node:path');
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// Every bot on a host shares one DATA_DIR (it is process.cwd(), and the fleet
|
|
18
|
+
// starts each --bot from the same directory), so the filename MUST carry the bot
|
|
19
|
+
// name. A shared heartbeat lets a live bot's beat stand in for a dead one's —
|
|
20
|
+
// which defeats the file's only purpose — and puts both daemons on one temp path,
|
|
21
|
+
// where the rename that loses the race fails ENOENT every minute. botName is
|
|
22
|
+
// required rather than defaulted so a second caller can't silently reintroduce it.
|
|
23
|
+
function createHeartbeat({ dataDir, botName, authDisabledGate, intervalMs = 60_000, now = Date.now }) {
|
|
24
|
+
if (!botName) throw new TypeError('createHeartbeat: botName required (the data dir is shared between bots)');
|
|
25
|
+
// The name lands in a path segment. Reject anything unsafe rather than rewriting
|
|
26
|
+
// it: sanitising would map two distinct names ('a/b' and 'a_b') onto one file and
|
|
27
|
+
// silently restore the very collision this split exists to prevent.
|
|
28
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(botName) || botName === '.' || botName === '..') {
|
|
29
|
+
throw new TypeError(`createHeartbeat: unsafe botName ${JSON.stringify(botName)} — allowed: A-Z a-z 0-9 . _ -`);
|
|
30
|
+
}
|
|
31
|
+
const file = path.join(dataDir, `heartbeat-${botName}.json`);
|
|
32
|
+
// Unique per process so two daemons never contend for one temp path, even if
|
|
33
|
+
// they are ever pointed at the same bot name.
|
|
34
|
+
const tmp = `${file}.tmp.${process.pid}`;
|
|
19
35
|
let timer = null;
|
|
20
36
|
|
|
21
37
|
function snapshot() {
|
|
@@ -26,7 +42,6 @@ function createHeartbeat({ dataDir, authDisabledGate, intervalMs = 60_000, now =
|
|
|
26
42
|
function beat() {
|
|
27
43
|
const snap = snapshot();
|
|
28
44
|
try {
|
|
29
|
-
const tmp = `${file}.tmp`;
|
|
30
45
|
fs.writeFileSync(tmp, JSON.stringify(snap));
|
|
31
46
|
fs.renameSync(tmp, file); // atomic
|
|
32
47
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.3",
|
|
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": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@anthropic-ai/claude-agent-sdk": "0.2.123",
|
|
65
65
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
66
|
-
"@shumkov/orchestra": "0.7.
|
|
66
|
+
"@shumkov/orchestra": "0.7.4",
|
|
67
67
|
"better-sqlite3": "^12.9.0",
|
|
68
68
|
"grammy": "^1.42.0",
|
|
69
69
|
"markdown-it": "^14.1.1",
|
package/polygram.js
CHANGED
|
@@ -772,7 +772,10 @@ let recoverCodexRequest = null;
|
|
|
772
772
|
// heartbeat.start() is called in main() once DATA_DIR is settled; the gate
|
|
773
773
|
// needs no runtime config, so it's constructed here directly.
|
|
774
774
|
const authDisabledGate = createAuthDisabledGate();
|
|
775
|
-
|
|
775
|
+
// Constructed in main(), not here: the heartbeat filename carries BOT_NAME (every
|
|
776
|
+
// bot shares DATA_DIR, which is process.cwd()), and BOT_NAME is only parsed once
|
|
777
|
+
// main() runs.
|
|
778
|
+
let authDisabledHeartbeat = null;
|
|
776
779
|
|
|
777
780
|
// rc.59: once-per-cycle gate for the contextHint. A session is added
|
|
778
781
|
// to this Set when the hint fires, removed when the SDK emits
|
|
@@ -3114,10 +3117,11 @@ async function main() {
|
|
|
3114
3117
|
setInterval(() => runAuthCheck('interval'), 30 * 60_000).unref?.();
|
|
3115
3118
|
|
|
3116
3119
|
// AUTH_DISABLED Netdata-visibility heartbeat (docs/AUTH_DISABLED_HANDLING_SPEC.md,
|
|
3117
|
-
// Layer 3.3) — writes <DATA_DIR>/heartbeat
|
|
3120
|
+
// Layer 3.3) — writes <DATA_DIR>/heartbeat-<botName>.json every 60s with the
|
|
3118
3121
|
// authDisabledGate's occurrence counter. File-only (no HTTP server in this
|
|
3119
3122
|
// repo to hang a /healthz route on); wiring the file into an actual Netdata
|
|
3120
3123
|
// alert is a separate VPS-side ops change, out of scope here.
|
|
3124
|
+
authDisabledHeartbeat = createHeartbeat({ dataDir: DATA_DIR, botName: BOT_NAME, authDisabledGate });
|
|
3121
3125
|
authDisabledHeartbeat.start();
|
|
3122
3126
|
|
|
3123
3127
|
// 0.8.0 Phase 1 step 11 + rc.50: defensive uncaughtException +
|
|
@@ -3703,7 +3707,7 @@ async function main() {
|
|
|
3703
3707
|
// 4. Remaining shutdown: approvals sweeper, IPC, resolve hook waiters,
|
|
3704
3708
|
// kill pm subprocesses, close DB.
|
|
3705
3709
|
if (approvalSweepTimer) clearInterval(approvalSweepTimer);
|
|
3706
|
-
authDisabledHeartbeat
|
|
3710
|
+
authDisabledHeartbeat?.stop(); // null if we shut down before main() armed it
|
|
3707
3711
|
if (ipcCloser) ipcCloser.close().catch(() => {});
|
|
3708
3712
|
try { fs.unlinkSync(ipcServer.secretPathFor(BOT_NAME)); } catch {}
|
|
3709
3713
|
// Reject every parked canUseTool waiter so the SDK doesn't
|