@foxden-app/foxclaw 0.5.31 → 0.5.33

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/dist/main.js CHANGED
@@ -11,6 +11,7 @@ import { APP_HOME, DEFAULT_CODEX_TELEGRAM_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PA
11
11
  import { createAuthRefreshNotificationAggregator, } from './auth/notifications.js';
12
12
  import { acquireProcessLock, LockHeldError } from './lock.js';
13
13
  import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
14
+ import { buildFoxclawLaunchdPlistText, extractNodePathFromLaunchdPlist, } from './launchd.js';
14
15
  import { buildFoxclawSystemdUnitText, buildSystemdRestartHelperArgs, cgroupContainsSystemdUnit, refreshFoxclawExecStartDropIns, removeFoxclawExecStartDropIns, } from './systemd.js';
15
16
  import { createSelfUpdateRuntime, inferPnpmHomeFromEntryPoint, performSelfUpdate, readSelfUpdateStatus, writeSelfUpdateStatus, } from './update.js';
16
17
  const rawCommand = process.argv[2];
@@ -113,6 +114,10 @@ async function main() {
113
114
  uninstallSystemd();
114
115
  return;
115
116
  }
117
+ if (command === 'uninstall-launchd') {
118
+ uninstallLaunchd();
119
+ return;
120
+ }
116
121
  if (command === 'install-launchd') {
117
122
  requireNode24(command);
118
123
  installLaunchd();
@@ -173,7 +178,7 @@ Usage:
173
178
  foxclaw start|restart|stop
174
179
  foxclaw update
175
180
  foxclaw install-systemd|uninstall-systemd
176
- foxclaw install-launchd
181
+ foxclaw install-launchd|uninstall-launchd
177
182
  foxclaw weixin-login [account-id]
178
183
  foxclaw --version
179
184
  foxclaw --help`);
@@ -1344,6 +1349,7 @@ function runDoctorChecks() {
1344
1349
  warnIfProxyEnvMissingFromLoadedEnv();
1345
1350
  warnIfProxyConfigNeedsAttention();
1346
1351
  warnIfInstalledServiceNodeLooksWrong();
1352
+ warnIfInstalledLaunchdNodeLooksWrong();
1347
1353
  warnIfSystemdUserLingerDisabled();
1348
1354
  return passed;
1349
1355
  }
@@ -1373,7 +1379,12 @@ function warnIfProxyConfigNeedsAttention() {
1373
1379
  return;
1374
1380
  }
1375
1381
  console.log('[WARN] Only ALL_PROXY/all_proxy is configured. Node service proxying works best with HTTP_PROXY/HTTPS_PROXY.');
1376
- console.log('[WARN] For SOCKS-only hosts, set FOXCLAW_PROXYCHAINS_CONF=/absolute/path/to/proxychains.conf and run foxclaw restart.');
1382
+ if (process.platform === 'linux') {
1383
+ console.log('[WARN] For SOCKS-only hosts, set FOXCLAW_PROXYCHAINS_CONF=/absolute/path/to/proxychains.conf and run foxclaw restart.');
1384
+ }
1385
+ else {
1386
+ console.log('[WARN] On macOS launchd, prefer HTTP_PROXY/HTTPS_PROXY; FOXCLAW_PROXYCHAINS_CONF is Linux-only.');
1387
+ }
1377
1388
  }
1378
1389
  function warnIfProxyEnvMissingFromLoadedEnv() {
1379
1390
  const envPath = serviceEnvPath();
@@ -1417,6 +1428,37 @@ function warnIfInstalledServiceNodeLooksWrong() {
1417
1428
  console.log(`[WARN] installed service node is older than 24: ${nodePath}${version ? ` (${version})` : ''}`);
1418
1429
  console.log('[WARN] Run foxclaw start from a Node 24 shell to refresh the service unit.');
1419
1430
  }
1431
+ function warnIfInstalledLaunchdNodeLooksWrong() {
1432
+ if (process.platform !== 'darwin') {
1433
+ return;
1434
+ }
1435
+ const plistPath = launchdPlistPath();
1436
+ let text = '';
1437
+ try {
1438
+ text = fs.readFileSync(plistPath, 'utf8');
1439
+ }
1440
+ catch {
1441
+ return;
1442
+ }
1443
+ const nodePath = extractNodePathFromLaunchdPlist(text);
1444
+ if (!nodePath) {
1445
+ return;
1446
+ }
1447
+ if (!fs.existsSync(nodePath)) {
1448
+ console.log(`[WARN] installed launchd node is missing: ${nodePath}`);
1449
+ console.log('[WARN] Run foxclaw start from a Node 24 shell to refresh the launchd plist.');
1450
+ return;
1451
+ }
1452
+ const result = spawnSync(nodePath, ['-p', 'process.versions.node'], { encoding: 'utf8' });
1453
+ const version = result.status === 0 ? result.stdout.trim() : '';
1454
+ const major = Number.parseInt(version.split('.')[0] ?? '', 10);
1455
+ if (Number.isFinite(major) && major >= 24) {
1456
+ console.log(`[OK] launchd node >= 24: ${nodePath}`);
1457
+ return;
1458
+ }
1459
+ console.log(`[WARN] installed launchd node is older than 24: ${nodePath}${version ? ` (${version})` : ''}`);
1460
+ console.log('[WARN] Run foxclaw start from a Node 24 shell to refresh the launchd plist.');
1461
+ }
1420
1462
  function warnIfSystemdUserLingerDisabled() {
1421
1463
  if (process.platform !== 'linux' || !hasCommand('loginctl')) {
1422
1464
  return;
@@ -1588,54 +1630,28 @@ function installLaunchd() {
1588
1630
  process.exit(1);
1589
1631
  }
1590
1632
  const home = process.env.HOME || '';
1591
- const plist = path.join(home, 'Library', 'LaunchAgents', 'app.foxden.foxclaw.plist');
1633
+ const plist = launchdPlistPath();
1592
1634
  const envPath = serviceEnvPath();
1593
1635
  const configDir = path.dirname(envPath);
1594
1636
  const nodeProxyArgs = hasStandardNodeProxyEnv() ? ['--use-env-proxy'] : [];
1595
- const nodeProxyArgXml = nodeProxyArgs.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join('\n');
1596
- const proxyEnvXml = buildLaunchdProxyEnvironmentXml();
1597
1637
  fs.mkdirSync(path.dirname(plist), { recursive: true });
1598
1638
  fs.mkdirSync(configDir, { recursive: true });
1599
1639
  fs.mkdirSync(path.join(APP_HOME, 'logs'), { recursive: true });
1600
- fs.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?>
1601
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1602
- <plist version="1.0">
1603
- <dict>
1604
- <key>Label</key>
1605
- <string>app.foxden.foxclaw</string>
1606
- <key>ProgramArguments</key>
1607
- <array>
1608
- <string>${xmlEscape(process.execPath)}</string>
1609
- ${nodeProxyArgXml ? `${nodeProxyArgXml}\n` : ''} <string>${xmlEscape(entryPoint)}</string>
1610
- <string>serve</string>
1611
- </array>
1612
- <key>WorkingDirectory</key>
1613
- <string>${xmlEscape(configDir)}</string>
1614
- <key>EnvironmentVariables</key>
1615
- <dict>
1616
- <key>PATH</key>
1617
- <string>${xmlEscape(process.env.PATH || '')}</string>
1618
- <key>HOME</key>
1619
- <string>${xmlEscape(home)}</string>
1620
- <key>USER</key>
1621
- <string>${xmlEscape(process.env.USER || '')}</string>
1622
- <key>LOGNAME</key>
1623
- <string>${xmlEscape(process.env.LOGNAME || process.env.USER || '')}</string>
1624
- <key>FOXCLAW_ENV</key>
1625
- <string>${xmlEscape(envPath)}</string>
1626
- ${proxyEnvXml}
1627
- </dict>
1628
- <key>RunAtLoad</key>
1629
- <true/>
1630
- <key>KeepAlive</key>
1631
- <true/>
1632
- <key>StandardOutPath</key>
1633
- <string>${xmlEscape(path.join(APP_HOME, 'logs', 'launchd.out.log'))}</string>
1634
- <key>StandardErrorPath</key>
1635
- <string>${xmlEscape(path.join(APP_HOME, 'logs', 'launchd.err.log'))}</string>
1636
- </dict>
1637
- </plist>
1638
- `);
1640
+ fs.writeFileSync(plist, buildFoxclawLaunchdPlistText({
1641
+ label: 'app.foxden.foxclaw',
1642
+ nodePath: process.execPath,
1643
+ nodeArgs: nodeProxyArgs,
1644
+ entryPoint,
1645
+ workingDirectory: configDir,
1646
+ pathValue: process.env.PATH || '',
1647
+ home,
1648
+ user: process.env.USER || '',
1649
+ logname: process.env.LOGNAME || process.env.USER || '',
1650
+ envPath,
1651
+ proxyEnv: launchdProxyEnvironment(),
1652
+ stdoutPath: path.join(APP_HOME, 'logs', 'launchd.out.log'),
1653
+ stderrPath: path.join(APP_HOME, 'logs', 'launchd.err.log'),
1654
+ }));
1639
1655
  spawnSync('launchctl', ['unload', plist], { stdio: 'ignore' });
1640
1656
  spawnChecked('launchctl', ['load', plist]);
1641
1657
  console.log(`Installed ${plist}`);
@@ -1648,7 +1664,7 @@ function stopLaunchd() {
1648
1664
  console.error('launchd stop is only available on macOS');
1649
1665
  process.exit(1);
1650
1666
  }
1651
- const plist = path.join(process.env.HOME || '', 'Library', 'LaunchAgents', 'app.foxden.foxclaw.plist');
1667
+ const plist = launchdPlistPath();
1652
1668
  if (!fs.existsSync(plist)) {
1653
1669
  console.error(`launchd plist not found: ${plist}`);
1654
1670
  process.exit(1);
@@ -1656,6 +1672,19 @@ function stopLaunchd() {
1656
1672
  spawnChecked('launchctl', ['unload', plist]);
1657
1673
  console.log(`Stopped ${plist}`);
1658
1674
  }
1675
+ function uninstallLaunchd() {
1676
+ if (process.platform !== 'darwin') {
1677
+ console.error('launchd uninstall is only available on macOS');
1678
+ process.exit(1);
1679
+ }
1680
+ const plist = launchdPlistPath();
1681
+ spawnSync('launchctl', ['unload', plist], { stdio: 'ignore' });
1682
+ fs.rmSync(plist, { force: true });
1683
+ console.log(`Removed ${plist}`);
1684
+ }
1685
+ function launchdPlistPath() {
1686
+ return path.join(process.env.HOME || '', 'Library', 'LaunchAgents', 'app.foxden.foxclaw.plist');
1687
+ }
1659
1688
  function buildServicePath(nodeDir) {
1660
1689
  const pnpmPath = resolveCommand('pnpm');
1661
1690
  const inferredPnpmHome = inferPnpmHomeFromEntryPoint(entryPoint) || '';
@@ -1716,16 +1745,15 @@ function hasStandardNodeProxyEnv() {
1716
1745
  function proxyEnvValue(key) {
1717
1746
  return process.env[key]?.trim() || '';
1718
1747
  }
1719
- function buildLaunchdProxyEnvironmentXml() {
1720
- const entries = [];
1748
+ function launchdProxyEnvironment() {
1749
+ const entries = {};
1721
1750
  for (const key of PROXY_ENV_KEYS) {
1722
1751
  const value = proxyEnvValue(key);
1723
1752
  if (!value)
1724
1753
  continue;
1725
- entries.push(` <key>${xmlEscape(key)}</key>`);
1726
- entries.push(` <string>${xmlEscape(value)}</string>`);
1754
+ entries[key] = value;
1727
1755
  }
1728
- return entries.length > 0 ? `${entries.join('\n')}\n` : '';
1756
+ return entries;
1729
1757
  }
1730
1758
  function spawnChecked(commandName, args) {
1731
1759
  const result = spawnSync(commandName, args, { stdio: 'inherit' });
@@ -1739,14 +1767,6 @@ function systemdEscape(value) {
1739
1767
  function systemdUnescape(value) {
1740
1768
  return value.replace(/\\x20/g, ' ').replace(/\\\\/g, '\\');
1741
1769
  }
1742
- function xmlEscape(value) {
1743
- return value
1744
- .replace(/&/g, '&amp;')
1745
- .replace(/</g, '&lt;')
1746
- .replace(/>/g, '&gt;')
1747
- .replace(/"/g, '&quot;')
1748
- .replace(/'/g, '&apos;');
1749
- }
1750
1770
  async function runWeixinLoginCli() {
1751
1771
  const [{ attachIlinkRuntimeFromBridgeLogger }, { startWeixinLoginWithQr, waitForWeixinLogin }, { accountFilePath, saveWeixinAccount }, { Logger },] = await Promise.all([
1752
1772
  import('./channels/weixin/ilink/runtime_attach.js'),
@@ -4,6 +4,7 @@ import type { BridgeStore } from '../store/database.js';
4
4
  import type { Logger } from '../logger.js';
5
5
  import type { TelegramMessageEntity } from './addressing.js';
6
6
  import type { TelegramInboundAttachment } from './media.js';
7
+ import type { TelegramInputRichMessage } from './rich.js';
7
8
  export interface TelegramTextEvent {
8
9
  chatId: string;
9
10
  topicId: number | null;
@@ -62,8 +63,13 @@ export declare class TelegramGateway extends EventEmitter {
62
63
  text: string;
63
64
  callback_data: string;
64
65
  }>>, messageThreadId?: number | null): Promise<number>;
66
+ sendRichMessage(chatId: string, richMessage: TelegramInputRichMessage, inlineKeyboard?: Array<Array<{
67
+ text: string;
68
+ callback_data: string;
69
+ }>>, messageThreadId?: number | null): Promise<number>;
65
70
  sendDocument(chatId: string, filename: string, contents: Buffer, caption?: string): Promise<number>;
66
71
  sendMessageDraft(chatId: string, draftId: number, text: string, messageThreadId?: number | null): Promise<void>;
72
+ sendRichMessageDraft(chatId: string, draftId: number, richMessage: TelegramInputRichMessage, messageThreadId?: number | null): Promise<void>;
67
73
  editMessage(chatId: string, messageId: number, text: string, inlineKeyboard?: Array<Array<{
68
74
  text: string;
69
75
  callback_data: string;
@@ -72,6 +78,10 @@ export declare class TelegramGateway extends EventEmitter {
72
78
  text: string;
73
79
  callback_data: string;
74
80
  }>>): Promise<void>;
81
+ editRichMessage(chatId: string, messageId: number, richMessage: TelegramInputRichMessage, inlineKeyboard?: Array<Array<{
82
+ text: string;
83
+ callback_data: string;
84
+ }>>): Promise<void>;
75
85
  clearMessageInlineKeyboard(chatId: string, messageId: number): Promise<void>;
76
86
  private sendMessageWithOptions;
77
87
  private editMessageWithOptions;
@@ -56,6 +56,18 @@ export class TelegramGateway extends EventEmitter {
56
56
  async sendHtmlMessage(chatId, text, inlineKeyboard, messageThreadId) {
57
57
  return this.sendMessageWithOptions(chatId, text, inlineKeyboard, 'HTML', messageThreadId);
58
58
  }
59
+ async sendRichMessage(chatId, richMessage, inlineKeyboard, messageThreadId) {
60
+ const result = await callTelegramApi(this.botToken, 'sendRichMessage', {
61
+ chat_id: chatId,
62
+ rich_message: richMessage,
63
+ ...(messageThreadId !== null && messageThreadId !== undefined ? { message_thread_id: messageThreadId } : {}),
64
+ ...(inlineKeyboard ? { reply_markup: { inline_keyboard: inlineKeyboard } } : {}),
65
+ });
66
+ if (!result.ok || !result.result) {
67
+ throw new Error(result.description || 'Failed to send Telegram rich message');
68
+ }
69
+ return result.result.message_id;
70
+ }
59
71
  async sendDocument(chatId, filename, contents, caption) {
60
72
  const result = await callTelegramMultipartApi(this.botToken, 'sendDocument', {
61
73
  chat_id: chatId,
@@ -83,12 +95,34 @@ export class TelegramGateway extends EventEmitter {
83
95
  throw new Error(result.description || 'Failed to send Telegram draft message');
84
96
  }
85
97
  }
98
+ async sendRichMessageDraft(chatId, draftId, richMessage, messageThreadId) {
99
+ const result = await callTelegramApi(this.botToken, 'sendRichMessageDraft', {
100
+ chat_id: chatId,
101
+ draft_id: draftId,
102
+ rich_message: richMessage,
103
+ ...(messageThreadId !== null && messageThreadId !== undefined ? { message_thread_id: messageThreadId } : {}),
104
+ });
105
+ if (!result.ok) {
106
+ throw new Error(result.description || 'Failed to send Telegram rich draft message');
107
+ }
108
+ }
86
109
  async editMessage(chatId, messageId, text, inlineKeyboard) {
87
110
  return this.editMessageWithOptions(chatId, messageId, text, inlineKeyboard);
88
111
  }
89
112
  async editHtmlMessage(chatId, messageId, text, inlineKeyboard) {
90
113
  return this.editMessageWithOptions(chatId, messageId, text, inlineKeyboard, 'HTML');
91
114
  }
115
+ async editRichMessage(chatId, messageId, richMessage, inlineKeyboard) {
116
+ const result = await callTelegramApi(this.botToken, 'editMessageText', {
117
+ chat_id: chatId,
118
+ message_id: messageId,
119
+ rich_message: richMessage,
120
+ ...(inlineKeyboard ? { reply_markup: { inline_keyboard: inlineKeyboard } } : {}),
121
+ });
122
+ if (!result.ok && !String(result.description || '').includes('message is not modified')) {
123
+ throw new Error(result.description || 'Failed to edit Telegram rich message');
124
+ }
125
+ }
92
126
  async clearMessageInlineKeyboard(chatId, messageId) {
93
127
  const result = await callTelegramApi(this.botToken, 'editMessageReplyMarkup', {
94
128
  chat_id: chatId,
@@ -0,0 +1,8 @@
1
+ export declare function escapeTelegramHtml(value: string): string;
2
+ export declare function telegramBold(value: string): string;
3
+ export declare function telegramCode(value: string): string;
4
+ export declare function telegramPre(value: string): string;
5
+ export declare function telegramPreCode(value: string, language?: string): string;
6
+ export declare function telegramExpandableBlockquote(value: string): string;
7
+ export declare function telegramSpoiler(value: string): string;
8
+ export declare function telegramDetails(summary: string, bodyHtml: string, open?: boolean): string;
@@ -0,0 +1,31 @@
1
+ export function escapeTelegramHtml(value) {
2
+ return value
3
+ .replaceAll('&', '&amp;')
4
+ .replaceAll('<', '&lt;')
5
+ .replaceAll('>', '&gt;');
6
+ }
7
+ function escapeTelegramHtmlAttribute(value) {
8
+ return escapeTelegramHtml(value).replaceAll('"', '&quot;');
9
+ }
10
+ export function telegramBold(value) {
11
+ return `<b>${escapeTelegramHtml(value)}</b>`;
12
+ }
13
+ export function telegramCode(value) {
14
+ return `<code>${escapeTelegramHtml(value)}</code>`;
15
+ }
16
+ export function telegramPre(value) {
17
+ return `<pre>${escapeTelegramHtml(value)}</pre>`;
18
+ }
19
+ export function telegramPreCode(value, language) {
20
+ const classAttr = language ? ` class="language-${escapeTelegramHtmlAttribute(language)}"` : '';
21
+ return `<pre><code${classAttr}>${escapeTelegramHtml(value)}</code></pre>`;
22
+ }
23
+ export function telegramExpandableBlockquote(value) {
24
+ return `<blockquote expandable>${escapeTelegramHtml(value)}</blockquote>`;
25
+ }
26
+ export function telegramSpoiler(value) {
27
+ return `<tg-spoiler>${escapeTelegramHtml(value)}</tg-spoiler>`;
28
+ }
29
+ export function telegramDetails(summary, bodyHtml, open = false) {
30
+ return `<details${open ? ' open' : ''}><summary>${escapeTelegramHtml(summary)}</summary>${bodyHtml}</details>`;
31
+ }
@@ -0,0 +1,13 @@
1
+ export declare const TELEGRAM_RICH_MESSAGE_TEXT_LIMIT = 32768;
2
+ export declare const TELEGRAM_RICH_MESSAGE_BLOCK_LIMIT = 500;
3
+ export interface TelegramInputRichMessage {
4
+ html?: string;
5
+ markdown?: string;
6
+ is_rtl?: true;
7
+ skip_entity_detection?: true;
8
+ }
9
+ export interface TelegramRichMessageOptions {
10
+ isRtl?: boolean;
11
+ skipEntityDetection?: boolean;
12
+ }
13
+ export declare function telegramRichHtml(html: string, options?: TelegramRichMessageOptions): TelegramInputRichMessage;
@@ -0,0 +1,12 @@
1
+ export const TELEGRAM_RICH_MESSAGE_TEXT_LIMIT = 32_768;
2
+ export const TELEGRAM_RICH_MESSAGE_BLOCK_LIMIT = 500;
3
+ export function telegramRichHtml(html, options = {}) {
4
+ const message = { html };
5
+ if (options.isRtl) {
6
+ message.is_rtl = true;
7
+ }
8
+ if (options.skipEntityDetection) {
9
+ message.skip_entity_detection = true;
10
+ }
11
+ return message;
12
+ }
@@ -48,6 +48,7 @@ Tasks:
48
48
  8. Ask me to send /help, /status, and /auth privately to each configured Telegram bot; confirm /auth names that bot runtime.
49
49
  9. Verify the final state:
50
50
  - foxclaw.service is active/enabled on Linux
51
+ - the app.foxden.foxclaw launchd job is loaded on macOS, with no startup errors in ~/.foxclaw/logs/launchd.err.log
51
52
  - foxclaw status works
52
53
  10. Report the commands used, the final status, and the log command I should use if something stops working. Redact TG_BOT_TOKENS and never print the full token or full .env content.
53
54
  11. If multiple bots are enabled, confirm foxclaw status lists independent app-servers; group-chat tests must mention or reply to the intended bot.
@@ -263,7 +263,12 @@ If install reports that linger could not be enabled automatically, run:
263
263
  sudo loginctl enable-linger "$USER"
264
264
  ```
265
265
 
266
- On macOS, `foxclaw start` manages launchd and starts FoxClaw when you log in.
266
+ On macOS, `foxclaw start` manages launchd and starts FoxClaw when you log in. Check state and startup logs with:
267
+
268
+ ```bash
269
+ launchctl print "gui/$(id -u)/app.foxden.foxclaw"
270
+ tail -f ~/.foxclaw/logs/launchd.err.log ~/.foxclaw/logs/service.log
271
+ ```
267
272
 
268
273
  For foreground debugging, stop the service first and then run `foxclaw serve`.
269
274
 
@@ -275,7 +280,7 @@ Check current status:
275
280
  foxclaw status
276
281
  ```
277
282
 
278
- Restart Linux service after changing `.env`:
283
+ Restart after changing `.env`:
279
284
 
280
285
  ```bash
281
286
  foxclaw restart
@@ -293,6 +298,12 @@ Uninstall Linux service:
293
298
  foxclaw uninstall-systemd
294
299
  ```
295
300
 
301
+ Uninstall macOS launchd service:
302
+
303
+ ```bash
304
+ foxclaw uninstall-launchd
305
+ ```
306
+
296
307
  Update FoxClaw later:
297
308
 
298
309
  ```bash
@@ -0,0 +1,99 @@
1
+ # Telegram Rich Message Adaptation Check
2
+
3
+ Checked on 2026-06-16 against Telegram Bot API Rich Messages, especially `RichMessage`, `sendRichMessage`, `sendRichMessageDraft`, and Rich Message Formatting Options.
4
+
5
+ Official references:
6
+
7
+ - https://core.telegram.org/bots/api#rich-message-formatting-options
8
+ - https://core.telegram.org/bots/api#sendrichmessage
9
+ - https://core.telegram.org/bots/api#sendrichmessagedraft
10
+
11
+ ## Conclusion
12
+
13
+ FoxClaw can benefit from the new rich message surface, and RichMessage is now wired into Telegram surfaces that are easy to inspect and safe to fall back.
14
+
15
+ The current strategy is to try `sendRichMessage` first on Telegram, then fall back to the existing Telegram HTML path on failure. Weixin continues to use HTML/plain fallback. Bot API 10.1 Rich Messages start with diagnostics and structured long text, then can expand to status, auth/quota, and AI draft streaming.
16
+
17
+ Already landed in this pass:
18
+
19
+ - Added `src/telegram/html.ts` for centralized Telegram HTML escaping and tag helpers.
20
+ - Added `src/telegram/rich.ts` and wired `sendRichMessage` / rich HTML through `TelegramGateway`, `TelegramMessagingPort`, and `BridgeMessagingRouter`.
21
+ - Added `/rich` as a diagnostic command for checking heading, table, details, pre/code, and list rendering in a real Telegram client.
22
+ - Changed `/diff` to prefer RichMessage with a heading plus details/pre/code diff block, falling back to HTML bold title plus expandable quote body.
23
+ - Moved existing CLI observation and archived tool-batch HTML generation onto the shared helper.
24
+
25
+ ## Official Capability
26
+
27
+ Bot API 10.1 adds Rich Messages:
28
+
29
+ - `RichText*`: bold, italic, underline, strikethrough, spoiler, code, marked, math, URL, email, phone, mention, hashtag, bot command, anchors, and references.
30
+ - `RichBlock*`: paragraph, heading, preformatted, footer, divider, math block, anchor, list, block quote, pull quote, collage, slideshow, table, details, map, media blocks, and thinking.
31
+ - `sendRichMessage`: sends a complete rich message.
32
+ - `sendRichMessageDraft`: streams an ephemeral partial rich message in private chat; the final answer must still be persisted with `sendRichMessage`.
33
+ - `editMessageText` accepts `rich_message` for editing rich messages.
34
+
35
+ Rich Message HTML also supports tags such as `<details>`, `<table>`, `<pre><code class="language-...">`, `<ul>/<ol>`, `<hr/>`, `<tg-math-block>`, and `<tg-thinking>`. `RichBlockThinking` is draft-only.
36
+
37
+ ## FoxClaw Inventory
38
+
39
+ Current Telegram send layer:
40
+
41
+ - `src/telegram/gateway.ts`: plain/html/rich send and edit are wired; regular HTML uses `parse_mode=HTML`, while rich messages use `rich_message.html`.
42
+ - `src/channels/telegram/telegram_messaging_port.ts`: controller-facing plain/html/rich-html send/edit and text draft operations.
43
+ - `src/telegram/rendering.ts`: `segmented_stream` is the default; `draft_stream` still uses the old text draft path.
44
+ - `src/controller/controller.ts`: central dispatcher for status cards, approvals, tool batches, diffs, auth, MCP, plugins, files, and runtime summaries.
45
+ - `src/controller/presentation.ts`: `/threads`, `/setup`, model, and access panels already use Telegram HTML.
46
+
47
+ Useful mapping:
48
+
49
+ | Surface | Current state | Useful rich capabilities | Recommendation |
50
+ | --- | --- | --- | --- |
51
+ | Active turn status | Short plain text, frequent edits | heading, list, thinking draft | Keep stable; use `RichBlockThinking` later only for private draft streaming |
52
+ | Codex streaming replies | Segmented plain text | rich draft, paragraph, pre, details | Feature flag only; needs fallback |
53
+ | Archived tool batches | Expandable HTML quote | details, pre, list | Keep HTML now; later use rich details |
54
+ | `/diff` | RichMessage first, HTML fallback | pre language, details | Landed |
55
+ | Approvals | Plain text plus inline keyboard | code, pre, spoiler, details | Command/path/patch fit code/pre/details; sensitive params should be hidden |
56
+ | `/status` and runtime summaries | Plain text lists | table, heading, footer | Good candidate for rich tables |
57
+ | `/auth` and `/quota` | Compact text plus buttons | table, marked, spoiler | Quota windows fit tables; abnormal candidates fit marked text |
58
+ | `/threads` and `/setup` | HTML panels | heading, list, anchor | Current HTML is enough; medium priority |
59
+ | MCP resources and plugin skills | Long plain text | details, pre, anchor/reference | Good candidate for collapsible schema/resource blocks |
60
+ | Help and setup text | Plain text | heading, list, code | Low priority |
61
+ | Media attachment feedback | Plain summary | collage/slideshow/media captions | Use only if FoxClaw starts returning media previews |
62
+
63
+ ## Rollout Plan
64
+
65
+ Phase 1: HTML-compatible enhancement.
66
+
67
+ - Centralize Telegram HTML helpers.
68
+ - Collapse long content by default: diffs, tool logs, MCP resources, plugin skill contents.
69
+ - Render commands, paths, models, and candidate names as code.
70
+ - Use spoilers or omission for secret/token-like diagnostics.
71
+
72
+ Phase 2: Rich Message builder.
73
+
74
+ Done:
75
+
76
+ - Added a minimal typed `src/telegram/rich.ts`.
77
+ - Added `sendRichMessage`, `editRichMessage`, and `sendRichMessageDraft` to `TelegramGateway`.
78
+ - Added rich HTML send/edit to `TelegramMessagingPort` and `BridgeMessagingRouter`; Weixin scopes use fallback HTML.
79
+ - `/rich` and `/diff` use rich sending first and fall back to HTML.
80
+
81
+ Next:
82
+
83
+ - Move `/status`, `/auth`, `/quota`, and MCP resources to rich table/details.
84
+ - Decide whether a global config flag is needed after real Telegram client checks.
85
+
86
+ Phase 3: Rich draft streaming.
87
+
88
+ - Enable only in Telegram private chats first; keep group/topic rendering on the current segmented stream.
89
+ - Use `RichBlockThinking` while generating and paragraph/pre/details for partial output.
90
+ - Persist the final answer with `sendRichMessage`.
91
+ - Keep the old text draft and segmented stream as fallback paths.
92
+
93
+ ## Risks
94
+
95
+ - Rich Messages landed in Bot API 10.1 on 2026-06-11, so client compatibility should be checked with `/rich` and `/diff`.
96
+ - `sendRichMessageDraft` targets private users; group, topic, and multi-bot paths must keep existing rendering.
97
+ - Rich media blocks add bot permission and media URL/upload constraints; they are not a Phase 1 target.
98
+ - Automatic entity detection can mis-detect paths, emails, URLs, and commands; rich builders should choose `skip_entity_detection` per message type.
99
+ - All Codex, shell, and file output must pass through centralized escaping before entering HTML/rich markup.
@@ -14,6 +14,13 @@ systemctl --user status foxclaw.service
14
14
  journalctl --user -u foxclaw.service -f
15
15
  ```
16
16
 
17
+ If FoxClaw is installed as a macOS launchd service, check:
18
+
19
+ ```bash
20
+ launchctl print "gui/$(id -u)/app.foxden.foxclaw"
21
+ tail -f ~/.foxclaw/logs/launchd.err.log ~/.foxclaw/logs/service.log
22
+ ```
23
+
17
24
  ## Doctor Failures
18
25
 
19
26
  | Symptom | Meaning | Fix |
@@ -136,6 +143,13 @@ systemctl --user is-active foxclaw.service
136
143
  pgrep -af foxclaw
137
144
  ```
138
145
 
146
+ On macOS, use:
147
+
148
+ ```bash
149
+ launchctl print "gui/$(id -u)/app.foxden.foxclaw"
150
+ pgrep -af foxclaw
151
+ ```
152
+
139
153
  Stop the extra process or service, then restart FoxClaw:
140
154
 
141
155
  ```bash
@@ -200,12 +214,18 @@ When multiple bots share a group, unaddressed messages intentionally do not trig
200
214
 
201
215
  If Telegram shows `ChatGPT backend 403 Forbidden`, or the app-server log contains `Unable to load site`, `cf-ray`, or `chatgpt.com/backend-api`, the auth file is not necessarily broken. The service process is usually reaching ChatGPT with the wrong network/proxy/IP.
202
216
 
203
- A common cause is that your shell or project `.env` has proxy variables, while the systemd/launchd service reads a different env file. Check the env file installed into the service:
217
+ A common cause is that your shell or project `.env` has proxy variables, while the systemd/launchd service reads a different env file. On Linux, check the env file installed into the service:
204
218
 
205
219
  ```bash
206
220
  systemctl --user cat foxclaw.service
207
221
  ```
208
222
 
223
+ On macOS, inspect `FOXCLAW_ENV` in the launchd plist:
224
+
225
+ ```bash
226
+ plutil -p ~/Library/LaunchAgents/app.foxden.foxclaw.plist | grep FOXCLAW_ENV -A1
227
+ ```
228
+
209
229
  `foxclaw init` detects proxy environment variables in the current shell and asks whether to save them into the FoxClaw `.env`. If you skipped that step, `foxclaw doctor` warns when it sees proxy variables in the shell but not in the FoxClaw env file.
210
230
 
211
231
  Make sure the file referenced by `Environment=FOXCLAW_ENV=...` contains your proxy variables, for example:
@@ -243,6 +263,8 @@ FoxClaw writes proxychains into the main service and removes stale FoxClaw `Exec
243
263
 
244
264
  The systemd installer records the absolute path of the Node process that is currently running FoxClaw. It does not rely on systemd loading `nvm.sh` or any other shell init script. Whether you use nvm, fnm, asdf, mise, Volta, Homebrew, or system Node, run `foxclaw start` from a Node 24+ shell and the service will keep using that Node 24+ path.
245
265
 
266
+ macOS launchd follows the same rule: the plist records the absolute Node path that ran `foxclaw start` and does not rely on login-shell initialization.
267
+
246
268
  If you installed the service from a shell using Node 22 or older, reinstall it from a Node 24+ shell. Example for nvm users:
247
269
 
248
270
  ```bash
@@ -251,7 +273,15 @@ foxclaw start
251
273
  systemctl --user status foxclaw.service
252
274
  ```
253
275
 
254
- The status output should show a Node 24+ path in `ExecStart`. `foxclaw doctor` also checks the installed service Node path and warns if it is missing or older than 24.
276
+ On macOS:
277
+
278
+ ```bash
279
+ nvm use 24
280
+ foxclaw start
281
+ launchctl print "gui/$(id -u)/app.foxden.foxclaw"
282
+ ```
283
+
284
+ The status output should show a Node 24+ path. `foxclaw doctor` also checks the installed systemd/launchd service Node path and warns if it is missing or older than 24.
255
285
 
256
286
  ## Does It Run After Reboot?
257
287
 
@@ -145,7 +145,14 @@ systemctl --user status foxclaw.service
145
145
  journalctl --user -u foxclaw.service -f
146
146
  ```
147
147
 
148
- On macOS, `foxclaw start` manages launchd. For foreground debugging, stop the background service and run:
148
+ On macOS, `foxclaw start` manages launchd. Check launchd state and startup logs with:
149
+
150
+ ```bash
151
+ launchctl print "gui/$(id -u)/app.foxden.foxclaw"
152
+ tail -f ~/.foxclaw/logs/launchd.err.log ~/.foxclaw/logs/service.log
153
+ ```
154
+
155
+ For foreground debugging, stop the background service and run:
149
156
 
150
157
  ```bash
151
158
  foxclaw stop
@@ -258,6 +265,10 @@ It controls:
258
265
 
259
266
  Telegram renders the HTML and buttons. This text block approximates the real panel:
260
267
 
268
+ For the Telegram Rich Message inventory and rollout plan, see [Telegram Rich Message Adaptation Check](./telegram-rich-messages.md). The default path still favors Telegram HTML for compatibility.
269
+
270
+ Send `/rich` to view a RichMessage demo in the current Telegram client. `/diff` also prefers RichMessage details plus a diff code block, with automatic Telegram HTML fallback.
271
+
261
272
  ```text
262
273
  Session preferences
263
274
  Current: gpt-5.5 · high · fast=off · default · Agent · Steer current turn
@@ -47,6 +47,7 @@ DEFAULT_CWD=<把绝对工作目录粘贴在这里>
47
47
  8. 让我在每个已配置的 Telegram bot 私聊里发送 /help、/status 和 /auth;确认 /auth 显示对应 bot runtime。
48
48
  9. 验证最终状态:
49
49
  - Linux 上 foxclaw.service 处于 active/enabled
50
+ - macOS 上 app.foxden.foxclaw launchd job 已加载,并且 ~/.foxclaw/logs/launchd.err.log 没有启动错误
50
51
  - foxclaw status 可以正常输出
51
52
  10. 汇报执行过的命令、最终状态和后续看日志的命令。请隐藏 TG_BOT_TOKENS,不要打印完整 token 或完整 .env。
52
53
  11. 如果启用了多个 bot,确认 foxclaw status 列出独立 app-server;群聊测试必须点名或回复目标 bot。
@@ -261,7 +261,12 @@ journalctl --user -u foxclaw.service -f
261
261
  sudo loginctl enable-linger "$USER"
262
262
  ```
263
263
 
264
- macOS 上 `foxclaw start` 管理 launchd,并在你登录后启动 FoxClaw
264
+ macOS 上 `foxclaw start` 管理 launchd,并在你登录后启动 FoxClaw。查看状态和启动日志:
265
+
266
+ ```bash
267
+ launchctl print "gui/$(id -u)/app.foxden.foxclaw"
268
+ tail -f ~/.foxclaw/logs/launchd.err.log ~/.foxclaw/logs/service.log
269
+ ```
265
270
 
266
271
  前台调试时,先停后台服务,再运行 `foxclaw serve`。
267
272
 
@@ -291,6 +296,12 @@ foxclaw stop
291
296
  foxclaw uninstall-systemd
292
297
  ```
293
298
 
299
+ 卸载 macOS launchd 服务:
300
+
301
+ ```bash
302
+ foxclaw uninstall-launchd
303
+ ```
304
+
294
305
  以后升级 FoxClaw:
295
306
 
296
307
  ```bash