@0xmaxma/claude-gateway 1.3.23 → 1.3.25

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.
Files changed (35) hide show
  1. package/README.md +125 -49
  2. package/dist/api/line-pending-senders.d.ts +7 -2
  3. package/dist/api/line-pending-senders.d.ts.map +1 -1
  4. package/dist/api/line-pending-senders.js +7 -2
  5. package/dist/api/line-pending-senders.js.map +1 -1
  6. package/dist/api/packages.d.ts.map +1 -1
  7. package/dist/api/packages.js +92 -16
  8. package/dist/api/packages.js.map +1 -1
  9. package/dist/api/router.d.ts.map +1 -1
  10. package/dist/api/router.js +563 -172
  11. package/dist/api/router.js.map +1 -1
  12. package/dist/api/wizard-state.d.ts +1 -4
  13. package/dist/api/wizard-state.d.ts.map +1 -1
  14. package/dist/api/wizard-state.js.map +1 -1
  15. package/dist/discord/receiver.d.ts +1 -0
  16. package/dist/discord/receiver.d.ts.map +1 -1
  17. package/dist/discord/receiver.js +24 -6
  18. package/dist/discord/receiver.js.map +1 -1
  19. package/dist/telegram/receiver.d.ts +1 -0
  20. package/dist/telegram/receiver.d.ts.map +1 -1
  21. package/dist/telegram/receiver.js +18 -6
  22. package/dist/telegram/receiver.js.map +1 -1
  23. package/mcp/tools/discord/access.ts +136 -20
  24. package/mcp/tools/discord/client.ts +3 -1
  25. package/mcp/tools/discord/module.ts +75 -4
  26. package/mcp/tools/discord/skills/access/SKILL.md +66 -8
  27. package/mcp/tools/discord/skills/configure/SKILL.md +11 -1
  28. package/mcp/tools/discord/types.ts +21 -2
  29. package/mcp/tools/telegram/dedup.ts +4 -1
  30. package/mcp/tools/telegram/module.ts +5 -14
  31. package/mcp/tools/telegram/pure.ts +169 -31
  32. package/mcp/tools/telegram/receiver-server.ts +108 -78
  33. package/mcp/tools/telegram/skills/access/SKILL.md +93 -23
  34. package/mcp/tools/telegram/skills/configure/SKILL.md +31 -26
  35. package/package.json +1 -1
@@ -39,6 +39,11 @@ const path = __importStar(require("path"));
39
39
  const logger_1 = require("../logger");
40
40
  const AUTO_RESTART_DELAY_MS = 5000;
41
41
  const MAX_RESTARTS = 3;
42
+ // After MAX_RESTARTS fast attempts, fall back to a slow indefinite retry
43
+ // instead of giving up permanently — self-heals if the admin fixes the
44
+ // underlying cause later (e.g. a Discord Developer Portal intent toggle)
45
+ // without needing to notice and manually reconnect.
46
+ const SLOW_RESTART_DELAY_MS = 5 * 60000;
42
47
  class DiscordReceiver {
43
48
  constructor(agentConfig, callbackPort, logDir) {
44
49
  this.agentConfig = agentConfig;
@@ -47,12 +52,14 @@ class DiscordReceiver {
47
52
  this.process = null;
48
53
  this.stopping = false;
49
54
  this.restartCount = 0;
55
+ this.slowPhaseAnnounced = false;
50
56
  this.restartTimer = null;
51
57
  this.logger = (0, logger_1.createLogger)(`${agentConfig.id}:discord-receiver`, logDir);
52
58
  }
53
59
  start() {
54
60
  this.stopping = false;
55
61
  this.restartCount = 0;
62
+ this.slowPhaseAnnounced = false;
56
63
  this.spawnProcess();
57
64
  }
58
65
  spawnProcess() {
@@ -63,6 +70,10 @@ class DiscordReceiver {
63
70
  ...process.env,
64
71
  DISCORD_BOT_TOKEN: this.agentConfig.discord?.botToken ?? '',
65
72
  DISCORD_STATE_DIR: stateDir,
73
+ // Legacy seed value: when no explicit dmPolicy is configured (the common
74
+ // token-only case), 'pairing' flows through access.ts:migrateAccess to
75
+ // { dmPolicy:'allowlist', pairing:true } — i.e. a new agent comes up with
76
+ // pairing ON so the owner can DM the bot and self-approve via a code.
66
77
  DISCORD_DM_POLICY: this.agentConfig.discord?.dmPolicy ?? 'pairing',
67
78
  DISCORD_DM_ALLOWLIST: (this.agentConfig.discord?.dmAllowlist ?? []).join(','),
68
79
  DISCORD_GUILD_ALLOWLIST: (this.agentConfig.discord?.guildAllowlist ?? []).join(','),
@@ -84,19 +95,26 @@ class DiscordReceiver {
84
95
  this.logger.info('DiscordReceiver started');
85
96
  }
86
97
  scheduleRestart() {
87
- if (this.restartCount >= MAX_RESTARTS) {
88
- this.logger.error('DiscordReceiver max restarts reached');
89
- return;
98
+ const slowPhase = this.restartCount >= MAX_RESTARTS;
99
+ const delay = slowPhase ? SLOW_RESTART_DELAY_MS : AUTO_RESTART_DELAY_MS;
100
+ if (!slowPhase)
101
+ this.restartCount++;
102
+ // Surface the transition into indefinite slow-retry once at error level so a
103
+ // permanently-broken config (bad token, missing intent) is visible instead
104
+ // of hiding behind a steady stream of warn-level retry lines.
105
+ if (slowPhase && !this.slowPhaseAnnounced) {
106
+ this.slowPhaseAnnounced = true;
107
+ this.logger.error(`DiscordReceiver failed ${MAX_RESTARTS} fast restarts; switching to slow retry every ${SLOW_RESTART_DELAY_MS}ms — check the bot token and Developer Portal intents`);
90
108
  }
91
- this.restartCount++;
92
- this.logger.warn(`Restarting DiscordReceiver in ${AUTO_RESTART_DELAY_MS}ms`, {
109
+ this.logger.warn(`Restarting DiscordReceiver in ${delay}ms`, {
93
110
  attempt: this.restartCount,
111
+ slowPhase,
94
112
  });
95
113
  this.restartTimer = setTimeout(() => {
96
114
  this.restartTimer = null;
97
115
  if (!this.stopping)
98
116
  this.spawnProcess();
99
- }, AUTO_RESTART_DELAY_MS);
117
+ }, delay);
100
118
  }
101
119
  stop() {
102
120
  this.stopping = true;
@@ -1 +1 @@
1
- {"version":3,"file":"receiver.js","sourceRoot":"","sources":["../../src/discord/receiver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"receiver.js","sourceRoot":"","sources":["../../src/discord/receiver.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,iDAAoD;AACpD,2CAA6B;AAE7B,sCAAyC;AAEzC,MAAM,qBAAqB,GAAG,IAAK,CAAC;AACpC,MAAM,YAAY,GAAG,CAAC,CAAC;AACvB,yEAAyE;AACzE,uEAAuE;AACvE,yEAAyE;AACzE,oDAAoD;AACpD,MAAM,qBAAqB,GAAG,CAAC,GAAG,KAAM,CAAC;AAEzC,MAAa,eAAe;IAQ1B,YACmB,WAAwB,EACxB,YAAoB,EACpB,MAAc;QAFd,gBAAW,GAAX,WAAW,CAAa;QACxB,iBAAY,GAAZ,YAAY,CAAQ;QACpB,WAAM,GAAN,MAAM,CAAQ;QAVzB,YAAO,GAAwB,IAAI,CAAC;QACpC,aAAQ,GAAG,KAAK,CAAC;QACjB,iBAAY,GAAG,CAAC,CAAC;QACjB,uBAAkB,GAAG,KAAK,CAAC;QAC3B,iBAAY,GAAyC,IAAI,CAAC;QAQhE,IAAI,CAAC,MAAM,GAAG,IAAA,qBAAY,EAAC,GAAG,WAAW,CAAC,EAAE,mBAAmB,EAAE,MAAM,CAAC,CAAC;IAC3E,CAAC;IAED,KAAK;QACH,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QACtB,IAAI,CAAC,kBAAkB,GAAG,KAAK,CAAC;QAChC,IAAI,CAAC,YAAY,EAAE,CAAC;IACtB,CAAC;IAEO,YAAY;QAClB,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,oBAAoB,CAAC,CAAC;QAC1G,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC;QAEzE,IAAI,CAAC,OAAO,GAAG,IAAA,qBAAK,EAAC,KAAK,EAAE,CAAC,YAAY,CAAC,EAAE;YAC1C,GAAG,EAAE;gBACH,GAAG,OAAO,CAAC,GAAG;gBACd,iBAAiB,EAAE,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,QAAQ,IAAI,EAAE;gBAC3D,iBAAiB,EAAE,QAAQ;gBAC3B,yEAAyE;gBACzE,uEAAuE;gBACvE,0EAA0E;gBAC1E,sEAAsE;gBACtE,iBAAiB,EAAE,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,QAAQ,IAAI,SAAS;gBAClE,oBAAoB,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;gBAC7E,uBAAuB,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,cAAc,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;gBACnF,yBAAyB,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,gBAAgB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;gBACvF,gBAAgB,EAAE,IAAI,CAAC,WAAW,CAAC,EAAE;gBACrC,uBAAuB,EAAE,oBAAoB,IAAI,CAAC,YAAY,UAAU;aACzE;YACD,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;SAClC,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,CAC5C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAC5E,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,CAC5C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CACpE,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;YACvC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,wBAAwB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YAC7D,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACpB,IAAI,CAAC,IAAI,CAAC,QAAQ;gBAAE,IAAI,CAAC,eAAe,EAAE,CAAC;QAC7C,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAC/B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,uBAAuB,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CACnE,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;IAC9C,CAAC;IAEO,eAAe;QACrB,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC;QACpD,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,qBAAqB,CAAC;QACxE,IAAI,CAAC,SAAS;YAAE,IAAI,CAAC,YAAY,EAAE,CAAC;QACpC,6EAA6E;QAC7E,2EAA2E;QAC3E,8DAA8D;QAC9D,IAAI,SAAS,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC1C,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;YAC/B,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,0BAA0B,YAAY,iDAAiD,qBAAqB,uDAAuD,CACpK,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,iCAAiC,KAAK,IAAI,EAAE;YAC3D,OAAO,EAAE,IAAI,CAAC,YAAY;YAC1B,SAAS;SACV,CAAC,CAAC;QACH,IAAI,CAAC,YAAY,GAAG,UAAU,CAAC,GAAG,EAAE;YAClC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;YACzB,IAAI,CAAC,IAAI,CAAC,QAAQ;gBAAE,IAAI,CAAC,YAAY,EAAE,CAAC;QAC1C,CAAC,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC;IAED,IAAI;QACF,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAChC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QAC3B,CAAC;QACD,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAChC,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,OAAO,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;IACvD,CAAC;CACF;AAlGD,0CAkGC"}
@@ -6,6 +6,7 @@ export declare class TelegramReceiver {
6
6
  private process;
7
7
  private stopping;
8
8
  private restartCount;
9
+ private slowPhaseAnnounced;
9
10
  private restartTimer;
10
11
  private readonly logger;
11
12
  constructor(agentConfig: AgentConfig, callbackPort: number, logDir: string);
@@ -1 +1 @@
1
- {"version":3,"file":"receiver.d.ts","sourceRoot":"","sources":["../../src/telegram/receiver.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAMvC,qBAAa,gBAAgB;IAQzB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM;IATzB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkC;gBAGtC,WAAW,EAAE,WAAW,EACxB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM;IAKjC,KAAK,IAAI,IAAI;IAMb,OAAO,CAAC,YAAY;IAgCpB,OAAO,CAAC,eAAe;IAevB,IAAI,IAAI,IAAI;IASZ,SAAS,IAAI,OAAO;CAGrB"}
1
+ {"version":3,"file":"receiver.d.ts","sourceRoot":"","sources":["../../src/telegram/receiver.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AASvC,qBAAa,gBAAgB;IASzB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM;IAVzB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkC;gBAGtC,WAAW,EAAE,WAAW,EACxB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM;IAKjC,KAAK,IAAI,IAAI;IAOb,OAAO,CAAC,YAAY;IAgCpB,OAAO,CAAC,eAAe;IAuBvB,IAAI,IAAI,IAAI;IASZ,SAAS,IAAI,OAAO;CAGrB"}
@@ -39,6 +39,9 @@ const path = __importStar(require("path"));
39
39
  const logger_1 = require("../logger");
40
40
  const AUTO_RESTART_DELAY_MS = 5000;
41
41
  const MAX_RESTARTS = 3;
42
+ // After MAX_RESTARTS fast attempts, fall back to a slow indefinite retry
43
+ // instead of giving up permanently — mirrors discord/receiver.ts.
44
+ const SLOW_RESTART_DELAY_MS = 5 * 60000;
42
45
  class TelegramReceiver {
43
46
  constructor(agentConfig, callbackPort, logDir) {
44
47
  this.agentConfig = agentConfig;
@@ -47,12 +50,14 @@ class TelegramReceiver {
47
50
  this.process = null;
48
51
  this.stopping = false;
49
52
  this.restartCount = 0;
53
+ this.slowPhaseAnnounced = false;
50
54
  this.restartTimer = null;
51
55
  this.logger = (0, logger_1.createLogger)(`${agentConfig.id}:receiver`, logDir);
52
56
  }
53
57
  start() {
54
58
  this.stopping = false;
55
59
  this.restartCount = 0;
60
+ this.slowPhaseAnnounced = false;
56
61
  this.spawnProcess();
57
62
  }
58
63
  spawnProcess() {
@@ -80,19 +85,26 @@ class TelegramReceiver {
80
85
  this.logger.info('TelegramReceiver started');
81
86
  }
82
87
  scheduleRestart() {
83
- if (this.restartCount >= MAX_RESTARTS) {
84
- this.logger.error('TelegramReceiver max restarts reached');
85
- return;
88
+ const slowPhase = this.restartCount >= MAX_RESTARTS;
89
+ const delay = slowPhase ? SLOW_RESTART_DELAY_MS : AUTO_RESTART_DELAY_MS;
90
+ if (!slowPhase)
91
+ this.restartCount++;
92
+ // Surface the transition into indefinite slow-retry once at error level so a
93
+ // permanently-broken config (bad/revoked token) is visible instead of hiding
94
+ // behind a steady stream of warn-level retry lines.
95
+ if (slowPhase && !this.slowPhaseAnnounced) {
96
+ this.slowPhaseAnnounced = true;
97
+ this.logger.error(`TelegramReceiver failed ${MAX_RESTARTS} fast restarts; switching to slow retry every ${SLOW_RESTART_DELAY_MS}ms — check the bot token`);
86
98
  }
87
- this.restartCount++;
88
- this.logger.warn(`Restarting TelegramReceiver in ${AUTO_RESTART_DELAY_MS}ms`, {
99
+ this.logger.warn(`Restarting TelegramReceiver in ${delay}ms`, {
89
100
  attempt: this.restartCount,
101
+ slowPhase,
90
102
  });
91
103
  this.restartTimer = setTimeout(() => {
92
104
  this.restartTimer = null;
93
105
  if (!this.stopping)
94
106
  this.spawnProcess();
95
- }, AUTO_RESTART_DELAY_MS);
107
+ }, delay);
96
108
  }
97
109
  stop() {
98
110
  this.stopping = true;
@@ -1 +1 @@
1
- {"version":3,"file":"receiver.js","sourceRoot":"","sources":["../../src/telegram/receiver.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,iDAAoD;AACpD,2CAA6B;AAE7B,sCAAyC;AAEzC,MAAM,qBAAqB,GAAG,IAAK,CAAC;AACpC,MAAM,YAAY,GAAG,CAAC,CAAC;AAEvB,MAAa,gBAAgB;IAO3B,YACmB,WAAwB,EACxB,YAAoB,EACpB,MAAc;QAFd,gBAAW,GAAX,WAAW,CAAa;QACxB,iBAAY,GAAZ,YAAY,CAAQ;QACpB,WAAM,GAAN,MAAM,CAAQ;QATzB,YAAO,GAAwB,IAAI,CAAC;QACpC,aAAQ,GAAG,KAAK,CAAC;QACjB,iBAAY,GAAG,CAAC,CAAC;QACjB,iBAAY,GAAyC,IAAI,CAAC;QAQhE,IAAI,CAAC,MAAM,GAAG,IAAA,qBAAY,EAAC,GAAG,WAAW,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;IACnE,CAAC;IAED,KAAK;QACH,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QACtB,IAAI,CAAC,YAAY,EAAE,CAAC;IACtB,CAAC;IAEO,YAAY;QAClB,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,oBAAoB,CAAC,CAAC;QAC3G,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAC;QAE1E,IAAI,CAAC,OAAO,GAAG,IAAA,qBAAK,EAAC,KAAK,EAAE,CAAC,YAAY,CAAC,EAAE;YAC1C,GAAG,EAAE;gBACH,GAAG,OAAO,CAAC,GAAG;gBACd,sBAAsB,EAAE,MAAM;gBAC9B,kBAAkB,EAAE,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,QAAQ,IAAI,EAAE;gBAC7D,kBAAkB,EAAE,QAAQ;gBAC5B,uBAAuB,EAAE,oBAAoB,IAAI,CAAC,YAAY,UAAU;aACzE;YACD,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;SAClC,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,CAC5C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CACpE,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,CAC5C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAC5D,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;YACvC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,yBAAyB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YAC9D,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACpB,IAAI,CAAC,IAAI,CAAC,QAAQ;gBAAE,IAAI,CAAC,eAAe,EAAE,CAAC;QAC7C,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAC/B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,wBAAwB,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CACpE,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IAC/C,CAAC;IAEO,eAAe;QACrB,IAAI,IAAI,CAAC,YAAY,IAAI,YAAY,EAAE,CAAC;YACtC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;YAC3D,OAAO;QACT,CAAC;QACD,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,kCAAkC,qBAAqB,IAAI,EAAE;YAC5E,OAAO,EAAE,IAAI,CAAC,YAAY;SAC3B,CAAC,CAAC;QACH,IAAI,CAAC,YAAY,GAAG,UAAU,CAAC,GAAG,EAAE;YAClC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;YACzB,IAAI,CAAC,IAAI,CAAC,QAAQ;gBAAE,IAAI,CAAC,YAAY,EAAE,CAAC;QAC1C,CAAC,EAAE,qBAAqB,CAAC,CAAC;IAC5B,CAAC;IAED,IAAI;QACF,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAChC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QAC3B,CAAC;QACD,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAChC,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,OAAO,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;IACvD,CAAC;CACF;AAhFD,4CAgFC"}
1
+ {"version":3,"file":"receiver.js","sourceRoot":"","sources":["../../src/telegram/receiver.ts"],"names":[],"mappings":"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"}
@@ -6,7 +6,7 @@
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import { randomBytes } from 'crypto';
9
- import type { DiscordAccessConfig, DiscordMessageContext, DiscordAccess, DiscordGateResult } from './types';
9
+ import type { DiscordAccessConfig, DiscordMessageContext, DiscordAccess, DiscordGateResult, DiscordPending } from './types';
10
10
  export type { DiscordAccessConfig, DiscordAccess } from './types';
11
11
 
12
12
  export type AccessResult = { allowed: boolean; reason?: string };
@@ -15,10 +15,17 @@ export type AccessResult = { allowed: boolean; reason?: string };
15
15
  // File-based access (new — pairing flow)
16
16
  // ---------------------------------------------------------------------------
17
17
 
18
+ // Default for a brand-new agent (no access.json yet): closed base + pairing on,
19
+ // so the owner can DM the bot and capture their own id via a code. This matches
20
+ // the pre-split behavior where a missing file defaulted to dmPolicy:'pairing'.
18
21
  export function defaultAccess(): DiscordAccess {
19
22
  return {
20
- dmPolicy: 'pairing',
23
+ dmPolicy: 'allowlist',
24
+ pairing: true,
21
25
  allowFrom: [],
26
+ // Secure new-agent default: guilds closed + answer only when @mentioned.
27
+ groupPolicy: 'allowlist',
28
+ requireMention: true,
22
29
  guildAllowlist: [],
23
30
  channelAllowlist: [],
24
31
  roleAllowlist: [],
@@ -26,24 +33,83 @@ export function defaultAccess(): DiscordAccess {
26
33
  };
27
34
  }
28
35
 
36
+ /**
37
+ * Normalize a parsed access.json into the current split shape, migrating the
38
+ * legacy fused dmPolicy ('pairing' folded pairing in) to the split model.
39
+ * Mirrors telegram/pure.ts:migrateAccess.
40
+ *
41
+ * SECURITY: a legacy `allowlist` file was deliberately locked down — it must
42
+ * migrate to `pairing:false`, NOT true, or it would start minting codes for
43
+ * strangers. Here an absent `pairing` on an existing file means a pre-split
44
+ * file → pairing off. Only the brand-new/ENOENT path (defaultAccess) is on.
45
+ */
46
+ export function migrateAccess(parsed: {
47
+ dmPolicy?: string;
48
+ pairing?: boolean;
49
+ allowFrom?: string[];
50
+ groupPolicy?: string;
51
+ requireMention?: boolean;
52
+ guildAllowlist?: string[];
53
+ channelAllowlist?: string[];
54
+ roleAllowlist?: string[];
55
+ pending?: Record<string, DiscordPending>;
56
+ }): DiscordAccess {
57
+ const legacy = parsed.dmPolicy;
58
+ let dmPolicy: DiscordAccess['dmPolicy'];
59
+ let pairing: boolean;
60
+ if (legacy === 'pairing') {
61
+ dmPolicy = 'allowlist';
62
+ pairing = true;
63
+ } else {
64
+ dmPolicy = (legacy as DiscordAccess['dmPolicy']) ?? 'allowlist';
65
+ pairing = parsed.pairing ?? false;
66
+ }
67
+ const guildAllowlist = parsed.guildAllowlist ?? [];
68
+ // Guild tier migration must be behavior-preserving on upgrade. Today an empty
69
+ // guildAllowlist means "deliver to all guilds" — so derive 'open' when empty,
70
+ // 'allowlist' when non-empty; and requireMention defaults to false for
71
+ // existing files (they had no mention gate). New agents get the secure
72
+ // 'allowlist'+requireMention:true from defaultAccess() instead.
73
+ const groupPolicy = (parsed.groupPolicy as DiscordAccess['groupPolicy'])
74
+ ?? (guildAllowlist.length > 0 ? 'allowlist' : 'open');
75
+ const requireMention = parsed.requireMention ?? false;
76
+ return {
77
+ dmPolicy,
78
+ pairing,
79
+ allowFrom: parsed.allowFrom ?? [],
80
+ groupPolicy,
81
+ requireMention,
82
+ guildAllowlist,
83
+ channelAllowlist: parsed.channelAllowlist ?? [],
84
+ roleAllowlist: parsed.roleAllowlist ?? [],
85
+ pending: parsed.pending ?? {},
86
+ };
87
+ }
88
+
29
89
  export function loadAccess(stateDir: string): DiscordAccess {
30
90
  const accessFile = path.join(stateDir, 'access.json');
31
91
  try {
32
92
  const raw = fs.readFileSync(accessFile, 'utf8');
33
- const parsed = JSON.parse(raw) as Partial<DiscordAccess>;
34
- return {
35
- dmPolicy: parsed.dmPolicy ?? 'pairing',
36
- allowFrom: parsed.allowFrom ?? [],
37
- guildAllowlist: parsed.guildAllowlist ?? [],
38
- channelAllowlist: parsed.channelAllowlist ?? [],
39
- roleAllowlist: parsed.roleAllowlist ?? [],
40
- pending: parsed.pending ?? {},
41
- };
93
+ const parsed = JSON.parse(raw) as Partial<DiscordAccess> & { dmPolicy?: string };
94
+ return migrateAccess(parsed);
42
95
  } catch (err) {
43
96
  if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
44
- // Seed from env vars for backward compat
45
- return {
46
- dmPolicy: (process.env.DISCORD_DM_POLICY as DiscordAccess['dmPolicy']) ?? 'pairing',
97
+ // Brand-new agent (no access.json yet). If the deployment configured
98
+ // access via DISCORD_* env vars, honour that (legacy backward-compat,
99
+ // normalized through migrate). Otherwise fall back to defaultAccess() so
100
+ // the owner can DM the bot and self-pair — migrateAccess would yield
101
+ // pairing:false here (its "locked existing file" rule), which wrongly
102
+ // drops the owner's first DM on a fresh agent.
103
+ const hasEnvConfig = Boolean(
104
+ process.env.DISCORD_DM_POLICY ||
105
+ process.env.DISCORD_DM_ALLOWLIST ||
106
+ process.env.DISCORD_GUILD_ALLOWLIST ||
107
+ process.env.DISCORD_CHANNEL_ALLOWLIST ||
108
+ process.env.DISCORD_ROLE_ALLOWLIST,
109
+ );
110
+ if (!hasEnvConfig) return defaultAccess();
111
+ return migrateAccess({
112
+ dmPolicy: process.env.DISCORD_DM_POLICY,
47
113
  allowFrom: process.env.DISCORD_DM_ALLOWLIST
48
114
  ? process.env.DISCORD_DM_ALLOWLIST.split(',').filter(Boolean)
49
115
  : [],
@@ -57,7 +123,7 @@ export function loadAccess(stateDir: string): DiscordAccess {
57
123
  ? process.env.DISCORD_ROLE_ALLOWLIST.split(',').filter(Boolean)
58
124
  : [],
59
125
  pending: {},
60
- };
126
+ });
61
127
  }
62
128
  try {
63
129
  fs.renameSync(accessFile, `${accessFile}.corrupt-${Date.now()}`);
@@ -100,36 +166,77 @@ export function gate(
100
166
  const { isDM, userId, guildId, channelId } = context;
101
167
 
102
168
  if (!isDM) {
103
- if (access.guildAllowlist.length > 0 && guildId) {
104
- if (!access.guildAllowlist.includes(guildId)) return { action: 'drop' };
169
+ if (access.groupPolicy === 'disabled') return { action: 'drop' };
170
+
171
+ if (access.groupPolicy === 'allowlist' && guildId && !access.guildAllowlist.includes(guildId)) {
172
+ // Unknown guild. Pairing off ⇒ silent drop. On ⇒ mint a code keyed on the
173
+ // guild id and post it here; a member relays it to the admin (mirrors LINE).
174
+ if (!access.pairing) return { action: 'drop' };
175
+ for (const [code, p] of Object.entries(access.pending)) {
176
+ if (p.kind === 'guild' && p.guildId === guildId) {
177
+ if (p.replies >= 2) return { action: 'drop' };
178
+ p.replies++;
179
+ saveAccessFn(access);
180
+ return { action: 'pair', code, isResend: true, isGuild: true };
181
+ }
182
+ }
183
+ if (countPending(access, 'guild') >= 5) return { action: 'drop' };
184
+ const code = generateCode();
185
+ access.pending[code] = {
186
+ senderId: userId,
187
+ channelId,
188
+ guildId,
189
+ kind: 'guild',
190
+ createdAt: ts,
191
+ expiresAt: ts + 60 * 60 * 1000,
192
+ replies: 1,
193
+ };
194
+ saveAccessFn(access);
195
+ return { action: 'pair', code, isResend: false, isGuild: true };
105
196
  }
197
+
198
+ // Allowlisted (or open policy) → keep the channel filter, then mention gate.
106
199
  if (access.channelAllowlist.length > 0) {
107
200
  if (!access.channelAllowlist.includes(channelId)) return { action: 'drop' };
108
201
  }
202
+ if (access.requireMention !== false && !context.mentionsBot) return { action: 'drop' };
109
203
  return { action: 'deliver' };
110
204
  }
111
205
 
112
206
  // DM message
113
207
  if (access.dmPolicy === 'disabled') return { action: 'drop' };
114
208
 
209
+ if (access.dmPolicy === 'open') {
210
+ // Open base: anyone may DM; capture their id so future messages are known.
211
+ if (!access.allowFrom.includes(userId)) {
212
+ access.allowFrom.push(userId);
213
+ saveAccessFn(access);
214
+ }
215
+ return { action: 'deliver' };
216
+ }
217
+
115
218
  if (access.allowFrom.includes(userId)) return { action: 'deliver' };
116
- if (access.dmPolicy === 'allowlist') return { action: 'drop' };
219
+ // Base policy is 'allowlist' ('disabled'/'open' handled above). Pairing is the
220
+ // orthogonal toggle: off ⇒ pure allowlist (drop strangers, no code).
221
+ if (!access.pairing) return { action: 'drop' };
117
222
 
118
223
  // pairing mode
119
224
  for (const [code, p] of Object.entries(access.pending)) {
120
- if (p.senderId === userId) {
225
+ if ((p.kind ?? 'dm') === 'dm' && p.senderId === userId) {
121
226
  if (p.replies >= 2) return { action: 'drop' };
122
227
  p.replies++;
123
228
  saveAccessFn(access);
124
229
  return { action: 'pair', code, isResend: true };
125
230
  }
126
231
  }
127
- if (Object.keys(access.pending).length >= 3) return { action: 'drop' };
232
+ // Cap pending per-kind so guild knocks and DM knocks don't starve each other.
233
+ if (countPending(access, 'dm') >= 5) return { action: 'drop' };
128
234
 
129
235
  const code = generateCode();
130
236
  access.pending[code] = {
131
237
  senderId: userId,
132
238
  channelId,
239
+ kind: 'dm',
133
240
  createdAt: ts,
134
241
  expiresAt: ts + 60 * 60 * 1000,
135
242
  replies: 1,
@@ -138,6 +245,15 @@ export function gate(
138
245
  return { action: 'pair', code, isResend: false };
139
246
  }
140
247
 
248
+ /** Count pending entries of a given kind (absent kind ⇒ 'dm'). */
249
+ function countPending(access: DiscordAccess, kind: 'dm' | 'guild'): number {
250
+ let n = 0;
251
+ for (const p of Object.values(access.pending)) {
252
+ if ((p.kind ?? 'dm') === kind) n++;
253
+ }
254
+ return n;
255
+ }
256
+
141
257
  // ---------------------------------------------------------------------------
142
258
  // Env-var based access (backward compat)
143
259
  // ---------------------------------------------------------------------------
@@ -22,7 +22,9 @@ export async function createDiscordClient(token: string): Promise<any> {
22
22
  });
23
23
 
24
24
  await new Promise<void>((resolve, reject) => {
25
- client.once('ready', () => resolve());
25
+ // 'clientReady' is the renamed 'ready' event (discord.js v14 emits both;
26
+ // 'ready' is deprecated and becomes clientReady-only in v15).
27
+ client.once('clientReady', () => resolve());
26
28
  client.once('error', reject);
27
29
  client.login(token).catch(reject);
28
30
  });
@@ -23,6 +23,12 @@ import { sendMessage, buildChoiceComponents } from './outbound';
23
23
  import { loadAccess, saveAccess, gate } from './access';
24
24
  import { maybeCreateThread } from './threading';
25
25
  import { createMessageHandler } from './inbound';
26
+ // Cross-process message dedup (shared with Telegram): when more than one
27
+ // receiver instance is connected on the same bot token — Discord's gateway
28
+ // delivers every event to all sessions, unlike Telegram's single-consumer
29
+ // getUpdates — the first instance to claim (channelId, messageId) processes it;
30
+ // the others see the O_EXCL marker and drop, so a stranger's DM is handled once.
31
+ import { initDedupDir, isDuplicate, pruneDedup } from '../telegram/dedup';
26
32
  import type { DiscordMessageContext } from './types';
27
33
 
28
34
  const MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024;
@@ -185,6 +191,14 @@ export class DiscordModule implements ChannelModule {
185
191
  const loadAccessFn = () => loadAccess(stateDir);
186
192
  const saveAccessFn = (a: ReturnType<typeof loadAccess>) => saveAccess(stateDir, a);
187
193
 
194
+ // Dedup dir shared by every receiver instance on this bot token (same
195
+ // DISCORD_STATE_DIR). Init once here, prune stale markers on a timer —
196
+ // mirrors the Telegram receiver's setup.
197
+ const dedupDir = path.join(stateDir, 'dedup');
198
+ initDedupDir(dedupDir);
199
+ pruneDedup(dedupDir);
200
+ setInterval(() => pruneDedup(dedupDir), 60_000).unref();
201
+
188
202
  // Permissive config — gate() already handled access, inbound.ts skips re-check
189
203
  const permissiveConfig = {
190
204
  botToken: this.getToken()!,
@@ -208,14 +222,24 @@ export class DiscordModule implements ChannelModule {
208
222
 
209
223
  const msgHandler = createMessageHandler(agentId, handler, permissiveConfig, permissiveAccessConfig);
210
224
 
211
- this.client.on('messageCreate', async (msg: any) => {
225
+ const handleDiscordMessage = async (msg: any): Promise<void> => {
212
226
  if (msg.author?.bot) return;
213
227
  if (msg.system) return;
214
228
 
229
+ // Drop if another receiver instance already claimed this message. Must run
230
+ // before gate() so a duplicate never mints a pairing code or replies twice.
231
+ if (isDuplicate(dedupDir, msg.channelId, msg.id)) return;
232
+
215
233
  this.lastMessageAt = Date.now();
216
234
 
217
235
  const isDM = !msg.guild;
218
236
  const isThread = msg.channel?.isThread?.() ?? false;
237
+ // Did this message @mention the bot (or reply to one of its messages)?
238
+ // Computed here so gate() stays discord.js-free. Works regardless of the
239
+ // MessageContent intent — mention entities are always delivered.
240
+ const botUser = this.client.user;
241
+ const mentionsBot = Boolean(botUser && msg.mentions?.has?.(botUser))
242
+ || (!!botUser && msg.mentions?.repliedUser?.id === botUser.id);
219
243
 
220
244
  const context: DiscordMessageContext = {
221
245
  guildId: msg.guildId ?? null,
@@ -226,6 +250,7 @@ export class DiscordModule implements ChannelModule {
226
250
  messageId: msg.id,
227
251
  isDM,
228
252
  isThread,
253
+ mentionsBot,
229
254
  };
230
255
 
231
256
  const access = loadAccessFn();
@@ -235,9 +260,18 @@ export class DiscordModule implements ChannelModule {
235
260
 
236
261
  if (result.action === 'pair') {
237
262
  try {
238
- await msg.channel.send(
239
- `Pairing required run in Claude Code:\n\n/discord:access pair ${result.code}`,
240
- );
263
+ if (result.isGuild) {
264
+ // LINE-style guild knock: post the code in the channel so a member
265
+ // can relay it to the admin, who approves it from the web UI. The
266
+ // user runs no command — a guild has no single owner to run one.
267
+ await msg.channel.send(
268
+ `This bot is private in this server.\n\nPairing code: ${result.code}\n\nShare this code with an admin to enable me here.`,
269
+ );
270
+ } else {
271
+ await msg.channel.send(
272
+ `Pairing required — run in Claude Code:\n\n/discord:access pair ${result.code}`,
273
+ );
274
+ }
241
275
  } catch {}
242
276
  return;
243
277
  }
@@ -259,6 +293,39 @@ export class DiscordModule implements ChannelModule {
259
293
  if (autoThread) {
260
294
  await maybeCreateThread(msg, autoThread, autoArchive).catch(() => {});
261
295
  }
296
+ };
297
+
298
+ this.client.on('messageCreate', handleDiscordMessage);
299
+
300
+ // discord.js's own MESSAGE_CREATE handling resolves msg.channel from its
301
+ // in-memory cache; DM channels are never included in the initial
302
+ // GUILD_CREATE/READY payload, so the *first* DM after every process
303
+ // restart hits an uncached channel. MessageCreateAction's partial-channel
304
+ // fallback constructs the stub without a `type` field (it only forwards
305
+ // {id, author, guild_id}), so ChannelManager#_add can't pick a Channel
306
+ // subclass, logs "Failed to find guild, or unknown type for channel ...
307
+ // undefined", and silently drops the event — messageCreate never fires,
308
+ // no error, no pairing code. Every subsequent DM in that channel fails
309
+ // the same way since the failed resolution never populates the cache.
310
+ // Work around it: on the raw MESSAGE_CREATE dispatch, if it's a DM whose
311
+ // channel isn't cached yet, fetch the channel (and the message) via REST
312
+ // — a real API response includes `type`, so this caches it properly —
313
+ // then hand the fully-formed Message object to the same handler used by
314
+ // the normal path. isDuplicate() above ensures no double-processing if
315
+ // discord.js's own path ever does resolve it first.
316
+ this.client.on('raw', async (packet: { t?: string; d?: any }) => {
317
+ if (packet.t !== 'MESSAGE_CREATE') return;
318
+ const d = packet.d;
319
+ if (!d || d.guild_id || d.author?.bot) return;
320
+ if (this.client.channels.cache.has(d.channel_id)) return;
321
+ try {
322
+ const channel = await this.client.channels.fetch(d.channel_id);
323
+ if (!channel) return;
324
+ const msg = await channel.messages.fetch(d.id);
325
+ await handleDiscordMessage(msg);
326
+ } catch (err) {
327
+ process.stderr.write(`discord: raw DM fallback failed: ${err}\n`);
328
+ }
262
329
  });
263
330
 
264
331
  // Interactive-menu button clicks: custom_id `choice:N`. A click is routed
@@ -295,6 +362,8 @@ export class DiscordModule implements ChannelModule {
295
362
  messageId: interaction.message?.id ?? '',
296
363
  isDM,
297
364
  isThread,
365
+ // A button click is an explicit interaction — never mention-gated.
366
+ mentionsBot: true,
298
367
  };
299
368
  const access = loadAccessFn();
300
369
  const result = gate(access, context, saveAccessFn, () => randomBytes(3).toString('hex'));
@@ -331,6 +400,8 @@ export class DiscordModule implements ChannelModule {
331
400
  messageId: interaction.message?.id ?? '',
332
401
  isDM,
333
402
  isThread,
403
+ // A button click is an explicit interaction — never mention-gated.
404
+ mentionsBot: true,
334
405
  };
335
406
  const access = loadAccessFn();
336
407
  const result = gate(access, context, saveAccessFn, () => randomBytes(3).toString('hex'));