polygram 0.8.0-rc.12 → 0.8.0-rc.13
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/agent-loader.js +150 -65
- package/package.json +1 -1
- package/polygram.js +16 -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.8.0-rc.
|
|
4
|
+
"version": "0.8.0-rc.13",
|
|
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/lib/agent-loader.js
CHANGED
|
@@ -3,18 +3,26 @@
|
|
|
3
3
|
* v4 plan §6.5.5).
|
|
4
4
|
*
|
|
5
5
|
* Background: today's CLI pm passes `--agent <name>` on spawn; the
|
|
6
|
-
* Claude CLI then loads that agent's
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
10
|
-
*
|
|
6
|
+
* Claude CLI then loads that agent's content. Phase 0 gate 15 was
|
|
7
|
+
* DEFER — the SDK's `Options.agents` is for in-memory subagent
|
|
8
|
+
* definitions (the Task tool), NOT a "run THIS query AS this agent"
|
|
9
|
+
* mechanism. So polygram reads the agent file itself and passes its
|
|
10
|
+
* content as `systemPrompt`.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* Search order (rc.13+ — supports BOTH Claude Code's standard
|
|
13
|
+
* single-file convention AND polygram's pre-0.8.0 directory layout):
|
|
14
|
+
*
|
|
15
|
+
* 1. `<cwd>/.claude/agents/<name>.md` — Claude Code project-level
|
|
16
|
+
* 2. `<homeDir>/.claude/agents/<name>.md` — Claude Code user-level
|
|
17
|
+
* 3. `<cwd>/.claude/agents/<name>/CLAUDE.md` — polygram convention
|
|
18
|
+
* (also `AGENTS.md`, `system-prompt.txt`)
|
|
19
|
+
* 4. `<homeDir>/.claude/agents/<name>/CLAUDE.md` — polygram legacy
|
|
20
|
+
* (also `AGENTS.md`, `system-prompt.txt`)
|
|
21
|
+
*
|
|
22
|
+
* Single-file Claude Code agents may have YAML frontmatter; we strip
|
|
23
|
+
* it before using the body as systemPrompt. Frontmatter `model` /
|
|
24
|
+
* `effort` are merged into the bundle.raw so composeSdkOptions can
|
|
25
|
+
* use them as agent-level defaults.
|
|
18
26
|
*
|
|
19
27
|
* Used by `polygram.js` `buildSdkOptions(sessionKey, ctx)` —
|
|
20
28
|
* Phase 1 step 14.
|
|
@@ -31,92 +39,160 @@
|
|
|
31
39
|
const fs = require('fs');
|
|
32
40
|
const path = require('path');
|
|
33
41
|
|
|
34
|
-
const cache = new Map(); //
|
|
42
|
+
const cache = new Map(); // cacheKey → AgentBundle
|
|
43
|
+
|
|
44
|
+
// Resolve agent file by checking each search path in order.
|
|
45
|
+
// Returns { kind: 'file'|'dir', path, dir | null } or null.
|
|
46
|
+
function resolveAgentLocation(agentName, homeDir, cwd) {
|
|
47
|
+
const fileCandidates = [];
|
|
48
|
+
if (cwd) fileCandidates.push(path.join(cwd, '.claude', 'agents', agentName + '.md'));
|
|
49
|
+
fileCandidates.push(path.join(homeDir, '.claude', 'agents', agentName + '.md'));
|
|
50
|
+
for (const p of fileCandidates) {
|
|
51
|
+
if (fs.existsSync(p)) return { kind: 'file', path: p, dir: null };
|
|
52
|
+
}
|
|
53
|
+
const dirCandidates = [];
|
|
54
|
+
if (cwd) dirCandidates.push(path.join(cwd, '.claude', 'agents', agentName));
|
|
55
|
+
dirCandidates.push(path.join(homeDir, '.claude', 'agents', agentName));
|
|
56
|
+
for (const d of dirCandidates) {
|
|
57
|
+
if (fs.existsSync(d) && fs.statSync(d).isDirectory()) {
|
|
58
|
+
return { kind: 'dir', path: d, dir: d };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Strip leading YAML frontmatter (---\n...\n---\n) from markdown.
|
|
65
|
+
function stripFrontmatter(content) {
|
|
66
|
+
if (typeof content !== 'string' || !content.startsWith('---\n')) return content;
|
|
67
|
+
const end = content.indexOf('\n---\n', 4);
|
|
68
|
+
if (end === -1) return content;
|
|
69
|
+
return content.slice(end + 5);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Parse a tiny subset of YAML frontmatter (key: value lines).
|
|
73
|
+
function parseFrontmatter(content) {
|
|
74
|
+
if (typeof content !== 'string' || !content.startsWith('---\n')) return {};
|
|
75
|
+
const end = content.indexOf('\n---\n', 4);
|
|
76
|
+
if (end === -1) return {};
|
|
77
|
+
const block = content.slice(4, end);
|
|
78
|
+
const out = {};
|
|
79
|
+
for (const line of block.split('\n')) {
|
|
80
|
+
const m = /^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/.exec(line);
|
|
81
|
+
if (!m) continue;
|
|
82
|
+
let v = m[2].trim();
|
|
83
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
84
|
+
v = v.slice(1, -1);
|
|
85
|
+
}
|
|
86
|
+
out[m[1]] = v;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
35
90
|
|
|
36
91
|
/**
|
|
37
92
|
* Load an agent bundle from disk.
|
|
38
93
|
*
|
|
39
|
-
* @param {string} agentName
|
|
94
|
+
* @param {string} agentName
|
|
40
95
|
* @param {object} opts
|
|
41
96
|
* @param {string} [opts.homeDir] — defaults to process.env.HOME.
|
|
42
|
-
*
|
|
97
|
+
* @param {string} [opts.cwd] — chat's working directory; checked
|
|
98
|
+
* FIRST for Claude Code project-level agent discovery.
|
|
43
99
|
* @param {object} [opts.logger] — error logger.
|
|
44
|
-
*
|
|
45
|
-
* @returns {AgentBundle}
|
|
46
|
-
* { agentName, agentDir, systemPrompt, skills: string[],
|
|
47
|
-
* mcpServers: object, raw: settingsJson }
|
|
48
|
-
*
|
|
49
|
-
* Throws `{ code: 'AGENT_NOT_FOUND' }` if the agent dir doesn't
|
|
50
|
-
* exist. Does NOT throw on partial agents (missing CLAUDE.md or
|
|
51
|
-
* skills/ etc — fields just default to null/empty).
|
|
52
100
|
*/
|
|
53
|
-
function loadAgent(agentName, { homeDir = process.env.HOME, logger = console } = {}) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
101
|
+
function loadAgent(agentName, { homeDir = process.env.HOME, cwd = null, logger = console } = {}) {
|
|
102
|
+
// Cache key includes cwd because the same agentName can resolve
|
|
103
|
+
// to different files when called from different chats with
|
|
104
|
+
// different cwds (e.g. shumabit-claude vs shumabit-partners).
|
|
105
|
+
const cacheKey = agentName + '\x00' + (cwd || '');
|
|
106
|
+
if (cache.has(cacheKey)) return cache.get(cacheKey);
|
|
107
|
+
|
|
108
|
+
const loc = resolveAgentLocation(agentName, homeDir, cwd);
|
|
109
|
+
if (!loc) {
|
|
110
|
+
const looked = [
|
|
111
|
+
cwd ? cwd + '/.claude/agents/' + agentName + '.md' : null,
|
|
112
|
+
homeDir + '/.claude/agents/' + agentName + '.md',
|
|
113
|
+
cwd ? cwd + '/.claude/agents/' + agentName + '/' : null,
|
|
114
|
+
homeDir + '/.claude/agents/' + agentName + '/',
|
|
115
|
+
].filter(Boolean).join(', ');
|
|
58
116
|
throw Object.assign(
|
|
59
|
-
new Error(
|
|
60
|
-
{ code: 'AGENT_NOT_FOUND',
|
|
117
|
+
new Error('agent not found: ' + agentName + ' (looked in ' + looked + ')'),
|
|
118
|
+
{ code: 'AGENT_NOT_FOUND', searchPaths: looked },
|
|
61
119
|
);
|
|
62
120
|
}
|
|
63
121
|
|
|
64
|
-
// System prompt: prefer CLAUDE.md (the standard polygram convention),
|
|
65
|
-
// fall back to AGENTS.md (OpenClaw legacy), then to a single-line
|
|
66
|
-
// file `system-prompt.txt` if either of the markdown files is
|
|
67
|
-
// absent. Whichever is present, read as UTF-8 string.
|
|
68
122
|
let systemPrompt = null;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
123
|
+
let frontmatter = {};
|
|
124
|
+
let agentPath = loc.path;
|
|
125
|
+
|
|
126
|
+
if (loc.kind === 'file') {
|
|
127
|
+
// Claude Code single-file format. Read whole file, parse and
|
|
128
|
+
// strip frontmatter, body becomes systemPrompt.
|
|
129
|
+
try {
|
|
130
|
+
const raw = fs.readFileSync(loc.path, 'utf8');
|
|
131
|
+
frontmatter = parseFrontmatter(raw);
|
|
132
|
+
systemPrompt = stripFrontmatter(raw);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
logger.error?.('[agent-loader] reading ' + loc.path + ': ' + err.message);
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
// polygram directory layout. CLAUDE.md > AGENTS.md > system-prompt.txt.
|
|
138
|
+
for (const fname of ['CLAUDE.md', 'AGENTS.md', 'system-prompt.txt']) {
|
|
139
|
+
const p = path.join(loc.dir, fname);
|
|
140
|
+
if (fs.existsSync(p)) {
|
|
141
|
+
try {
|
|
142
|
+
systemPrompt = fs.readFileSync(p, 'utf8');
|
|
143
|
+
agentPath = p;
|
|
144
|
+
break;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
logger.error?.('[agent-loader] reading ' + p + ': ' + err.message);
|
|
147
|
+
}
|
|
77
148
|
}
|
|
78
149
|
}
|
|
79
150
|
}
|
|
80
151
|
|
|
81
|
-
// Settings
|
|
82
|
-
// (mcpServers, model, effort defaults, etc.).
|
|
152
|
+
// Settings.json — only meaningful for directory-layout agents.
|
|
83
153
|
let settings = {};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
154
|
+
if (loc.dir) {
|
|
155
|
+
const settingsPath = path.join(loc.dir, 'settings.json');
|
|
156
|
+
if (fs.existsSync(settingsPath)) {
|
|
157
|
+
try {
|
|
158
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
159
|
+
} catch (err) {
|
|
160
|
+
logger.error?.('[agent-loader] parsing ' + settingsPath + ': ' + err.message);
|
|
161
|
+
}
|
|
90
162
|
}
|
|
91
163
|
}
|
|
92
164
|
|
|
93
|
-
// Skills
|
|
94
|
-
// `Options.skills` accepts a string[] of skill names.
|
|
95
|
-
const skillsDir = path.join(agentDir, 'skills');
|
|
165
|
+
// Skills (only for directory layout).
|
|
96
166
|
let skills = [];
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
|
|
167
|
+
if (loc.dir) {
|
|
168
|
+
const skillsDir = path.join(loc.dir, 'skills');
|
|
169
|
+
if (fs.existsSync(skillsDir)) {
|
|
170
|
+
try {
|
|
171
|
+
skills = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
172
|
+
.filter((d) => d.isDirectory())
|
|
173
|
+
.map((d) => d.name);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
logger.error?.('[agent-loader] enumerating ' + skillsDir + ': ' + err.message);
|
|
176
|
+
}
|
|
104
177
|
}
|
|
105
178
|
}
|
|
106
179
|
|
|
107
180
|
const mcpServers = settings.mcpServers ?? {};
|
|
108
181
|
|
|
182
|
+
// Frontmatter merged with settings — composeSdkOptions can pick up
|
|
183
|
+
// model/effort overrides from either source.
|
|
184
|
+
const raw = { ...frontmatter, ...settings };
|
|
185
|
+
|
|
109
186
|
const bundle = {
|
|
110
187
|
agentName,
|
|
111
|
-
|
|
188
|
+
agentPath,
|
|
189
|
+
agentDir: loc.dir,
|
|
112
190
|
systemPrompt,
|
|
113
191
|
skills,
|
|
114
192
|
mcpServers,
|
|
115
|
-
|
|
116
|
-
// (e.g. agent-level model/effort defaults).
|
|
117
|
-
raw: settings,
|
|
193
|
+
raw,
|
|
118
194
|
};
|
|
119
|
-
cache.set(
|
|
195
|
+
cache.set(cacheKey, bundle);
|
|
120
196
|
return bundle;
|
|
121
197
|
}
|
|
122
198
|
|
|
@@ -166,4 +242,13 @@ function clearCache() {
|
|
|
166
242
|
cache.clear();
|
|
167
243
|
}
|
|
168
244
|
|
|
169
|
-
module.exports = {
|
|
245
|
+
module.exports = {
|
|
246
|
+
loadAgent,
|
|
247
|
+
composeSdkOptions,
|
|
248
|
+
clearCache,
|
|
249
|
+
// Internals for tests.
|
|
250
|
+
_resolveAgentLocation: resolveAgentLocation,
|
|
251
|
+
_stripFrontmatter: stripFrontmatter,
|
|
252
|
+
_parseFrontmatter: parseFrontmatter,
|
|
253
|
+
_cache: cache,
|
|
254
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.8.0-rc.
|
|
3
|
+
"version": "0.8.0-rc.13",
|
|
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
|
@@ -793,6 +793,10 @@ function buildSdkOptions(sessionKey, ctx) {
|
|
|
793
793
|
try {
|
|
794
794
|
agentBundle = agentLoader.loadAgent(chatConfig.agent, {
|
|
795
795
|
homeDir: CHILD_HOME,
|
|
796
|
+
// Pass cwd so the loader checks Claude Code's project-level
|
|
797
|
+
// path (`<cwd>/.claude/agents/<name>.md`) before the
|
|
798
|
+
// user-level path or polygram's directory convention.
|
|
799
|
+
cwd: chatConfig.cwd,
|
|
796
800
|
logger: console,
|
|
797
801
|
});
|
|
798
802
|
} catch (err) {
|
|
@@ -2782,7 +2786,13 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2782
2786
|
const abortedByUser = isSessionRecentlyAborted(sessionKey);
|
|
2783
2787
|
if (abortedByUser) {
|
|
2784
2788
|
await streamer.finalize('').catch(() => {});
|
|
2785
|
-
//
|
|
2789
|
+
// 0.8.0-rc.13: clear the in-flight emoji on abort so the user
|
|
2790
|
+
// sees a clean message after their /stop ack — pre-rc.13 the
|
|
2791
|
+
// last 👀 / 🤔 / ✍ stayed stuck on the message indefinitely
|
|
2792
|
+
// because reactor.stop() (in finally) only kills timers, not
|
|
2793
|
+
// the visible reaction. We DON'T set 🤯/😨 (those are for
|
|
2794
|
+
// unexpected errors); the user just wants their stop honored.
|
|
2795
|
+
await reactor.clear().catch(() => {});
|
|
2786
2796
|
} else {
|
|
2787
2797
|
await streamer.finalize('', { errorSuffix: 'stream interrupted' }).catch(() => {});
|
|
2788
2798
|
if (/wall-clock ceiling|idle with no Claude activity/i.test(err?.message || '')) {
|
|
@@ -2992,6 +3002,11 @@ function createBot(token) {
|
|
|
2992
3002
|
await stopTarget.kill(sessionKey).catch((err) =>
|
|
2993
3003
|
console.error(`[${BOT_NAME}] abort kill failed: ${err.message}`));
|
|
2994
3004
|
}
|
|
3005
|
+
// 0.8.0-rc.13: drop any buffered autosteer follow-ups for this
|
|
3006
|
+
// session — otherwise they'd be injected into the NEXT turn
|
|
3007
|
+
// (stale steer leak across abort boundary, which is what the
|
|
3008
|
+
// user just asked us not to do).
|
|
3009
|
+
autosteerBuffer.clear(sessionKey);
|
|
2995
3010
|
logEvent('abort-requested', {
|
|
2996
3011
|
chat_id: chatId, user_id: msg.from?.id || null,
|
|
2997
3012
|
had_active: hadActive,
|