Skip to content

Commit a92e373

Browse files
ziggyclaude
andcommitted
Fix Telegram HTML chunking and improve setup docs
- Fix unclosed HTML tags when splitting long Telegram messages across the 4096-char limit. Tags (pre, code, b, i, etc.) are now properly closed at chunk boundaries and reopened in the next chunk. - Add explicit /setup-ghostclaw instruction to README getting started section — was previously unclear that users need to run the skill. - Rename /setup skill from "NanoClaw Setup" to "GhostClaw Setup" and update all nanoclaw references to ghostclaw. - Remove outdated "instead of NanoClaw's /setup" from setup-ghostclaw skill description. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c655102 commit a92e373

4 files changed

Lines changed: 52 additions & 8 deletions

File tree

.claude/skills/setup-ghostclaw/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: setup-ghostclaw
3-
description: First-time GhostClaw setup. Handles dependencies, authentication, channel configuration, main chat registration, soul building, and service setup. Run this instead of NanoClaw's /setup.
3+
description: First-time GhostClaw setup. Handles dependencies, authentication, channel configuration, main chat registration, soul building, and service setup. Use this for all new installs.
44
---
55

66
# GhostClaw Setup

.claude/skills/setup/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
---
22
name: setup
3-
description: Run initial NanoClaw setup. Use when user wants to install dependencies, authenticate WhatsApp, register their main channel, or start the background services. Triggers on "setup", "install", "configure nanoclaw", or first-time setup requests.
3+
description: Run initial GhostClaw setup. Use when user wants to install dependencies, authenticate WhatsApp, register their main channel, or start the background services. Triggers on "setup", "install", "configure ghostclaw", or first-time setup requests.
44
---
55

6-
# NanoClaw Setup
6+
# GhostClaw Setup
77

88
Run setup steps automatically. Only pause when user action is required (WhatsApp authentication, configuration choices). Setup uses `bash setup.sh` for bootstrap, then `npx tsx setup/index.ts --step <name>` for all other steps. Steps emit structured status blocks to stdout. Verbose logs go to `logs/setup.log`.
99

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ npm install
4444
claude
4545
```
4646

47-
Claude reads the setup instructions automatically and walks you through everything — connecting Telegram, adding your API keys, setting up personality. You just answer questions. About 10 minutes.
47+
Once Claude starts, run the setup skill:
48+
49+
```
50+
/setup-ghostclaw
51+
```
52+
53+
This walks you through everything — connecting Telegram, adding your API keys, setting up personality. You just answer questions. About 10 minutes.
4854

4955
**Requirements:** Node.js 20+, macOS or Linux, Claude Max or API keys.
5056

src/channels/telegram.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,19 @@ export class TelegramChannel implements Channel {
213213
await fs.promises.writeFile(filepath, buffer);
214214

215215
placeholder = `[Photo: ${filepath}]`;
216-
logger.info({ chatJid, filepath, bytes: buffer.length }, 'Downloaded Telegram photo');
216+
logger.info(
217+
{ chatJid, filepath, bytes: buffer.length },
218+
'Downloaded Telegram photo',
219+
);
217220

218221
// Also save to Desktop for easy access
219-
if (chatJid === 'tg:414798121') { // Main chat
220-
const desktopPath = path.join(process.env.HOME || '', 'Desktop', 'latest-telegram-photo.jpg');
222+
if (chatJid === 'tg:414798121') {
223+
// Main chat
224+
const desktopPath = path.join(
225+
process.env.HOME || '',
226+
'Desktop',
227+
'latest-telegram-photo.jpg',
228+
);
221229
await fs.promises.writeFile(desktopPath, buffer);
222230
logger.info({ desktopPath }, 'Saved photo to Desktop');
223231
}
@@ -335,9 +343,11 @@ export class TelegramChannel implements Channel {
335343
parse_mode: 'HTML',
336344
});
337345
} else {
338-
// Split on newlines to avoid breaking HTML tags mid-tag
346+
// Split on newlines, closing/reopening HTML tags across chunk boundaries
339347
const chunks: string[] = [];
340348
let current = '';
349+
const TELEGRAM_TAGS = ['pre', 'code', 'b', 'i', 'u', 's', 'a'];
350+
341351
for (const line of html.split('\n')) {
342352
if (current.length + line.length + 1 > MAX_LENGTH) {
343353
if (current) chunks.push(current);
@@ -348,7 +358,35 @@ export class TelegramChannel implements Channel {
348358
}
349359
if (current) chunks.push(current);
350360

361+
// Fix unclosed tags in each chunk
362+
const fixedChunks: string[] = [];
363+
let carryTags: string[] = []; // tags to reopen in next chunk
351364
for (const chunk of chunks) {
365+
let fixed = carryTags.map((t) => `<${t}>`).join('') + chunk;
366+
367+
// Track which tags are open at the end of this chunk
368+
const openTags: string[] = [];
369+
for (const tag of TELEGRAM_TAGS) {
370+
const opens = (
371+
fixed.match(new RegExp(`<${tag}(\\s|>)`, 'gi')) || []
372+
).length;
373+
const closes = (fixed.match(new RegExp(`</${tag}>`, 'gi')) || [])
374+
.length;
375+
for (let i = 0; i < opens - closes; i++) {
376+
openTags.push(tag);
377+
}
378+
}
379+
380+
// Close any unclosed tags at end of this chunk (reverse order)
381+
carryTags = [...openTags];
382+
for (const tag of [...openTags].reverse()) {
383+
fixed += `</${tag}>`;
384+
}
385+
386+
fixedChunks.push(fixed);
387+
}
388+
389+
for (const chunk of fixedChunks) {
352390
await this.bot.api.sendMessage(numericId, chunk, {
353391
parse_mode: 'HTML',
354392
});

0 commit comments

Comments
 (0)