@0xmaxma/claude-gateway 1.8.12 → 1.8.14

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 (37) hide show
  1. package/README.md +8 -0
  2. package/dist/agent/create-agent-prompts.d.ts +6 -0
  3. package/dist/agent/create-agent-prompts.d.ts.map +1 -1
  4. package/dist/agent/create-agent-prompts.js +30 -0
  5. package/dist/agent/create-agent-prompts.js.map +1 -1
  6. package/dist/agent/runner.d.ts +17 -0
  7. package/dist/agent/runner.d.ts.map +1 -1
  8. package/dist/agent/runner.js +187 -14
  9. package/dist/agent/runner.js.map +1 -1
  10. package/dist/api/router.d.ts.map +1 -1
  11. package/dist/api/router.js +309 -1
  12. package/dist/api/router.js.map +1 -1
  13. package/dist/api/wechat-access.d.ts +45 -0
  14. package/dist/api/wechat-access.d.ts.map +1 -0
  15. package/dist/api/wechat-access.js +48 -0
  16. package/dist/api/wechat-access.js.map +1 -0
  17. package/dist/history/types.d.ts +10 -1
  18. package/dist/history/types.d.ts.map +1 -1
  19. package/dist/history/types.js +10 -1
  20. package/dist/history/types.js.map +1 -1
  21. package/dist/session/process.d.ts.map +1 -1
  22. package/dist/session/process.js +1 -0
  23. package/dist/session/process.js.map +1 -1
  24. package/dist/types.d.ts +40 -0
  25. package/dist/types.d.ts.map +1 -1
  26. package/dist/wechat/ilink-client.d.ts +194 -0
  27. package/dist/wechat/ilink-client.d.ts.map +1 -0
  28. package/dist/wechat/ilink-client.js +641 -0
  29. package/dist/wechat/ilink-client.js.map +1 -0
  30. package/dist/wechat/manager.d.ts +93 -0
  31. package/dist/wechat/manager.d.ts.map +1 -0
  32. package/dist/wechat/manager.js +409 -0
  33. package/dist/wechat/manager.js.map +1 -0
  34. package/mcp/server.ts +2 -0
  35. package/mcp/tools/wechat/client.ts +49 -0
  36. package/mcp/tools/wechat/module.ts +88 -0
  37. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAW,QAAQ,EAAE,MAAM,SAAS,CAAC;AAOpD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,EAAe,WAAW,EAAe,MAAM,UAAU,CAAC;AAoFtF,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,MAAM,CAExD;AAMD,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,MAAM,CAEtD;AAuED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CA4CjD;AAmTD,wBAAgB,eAAe,CAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EACtC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EACtC,OAAO,EAAE,MAAM,EAAE,EACjB,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,WAAW,EAAE,GACrB,MAAM,CAq+HR"}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAW,QAAQ,EAAE,MAAM,SAAS,CAAC;AAOpD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,EAAe,WAAW,EAAe,MAAM,UAAU,CAAC;AAoFtF,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,MAAM,CAExD;AAMD,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,MAAM,CAEtD;AAuED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CA4CjD;AAmUD,wBAAgB,eAAe,CAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EACtC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EACtC,OAAO,EAAE,MAAM,EAAE,EACjB,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,WAAW,EAAE,GACrB,MAAM,CAytIR"}
@@ -449,6 +449,20 @@ const TELEGRAM_API_BASE = process.env.TELEGRAM_API_BASE ?? 'https://api.telegram
449
449
  /** Max simultaneous wizard/start Claude subprocesses to prevent resource exhaustion. */
450
450
  let wizardStartsInFlight = 0;
451
451
  const WIZARD_MAX_CONCURRENT = 2;
452
+ /**
453
+ * Max simultaneous describe/rewrite Claude subprocesses. Kept separate from
454
+ * wizardStartsInFlight so a burst of rewrite calls can't starve wizard-start
455
+ * slots (or vice versa) — the two features don't share a concurrency budget.
456
+ */
457
+ let rewritesInFlight = 0;
458
+ const REWRITE_MAX_CONCURRENT = 2;
459
+ /**
460
+ * Max input length for a describe/rewrite. A real "describe your agent" draft is
461
+ * far shorter than this; the cap keeps an oversized payload from pinning a Claude
462
+ * subprocess for the full 120s timeout (two such requests would occupy both
463
+ * REWRITE_MAX_CONCURRENT slots and block everyone else).
464
+ */
465
+ const REWRITE_MAX_TEXT_LENGTH = 8000;
452
466
  /** Call Claude --print with stdin prompt; resolves with stdout on exit 0. */
453
467
  function runClaude(prompt, timeoutMs = 120000) {
454
468
  return new Promise((resolve, reject) => {
@@ -994,6 +1008,13 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
994
1008
  whatsapp_cloud_dm_allowlist: cfg.whatsapp_cloud?.appSecret ? (cfg.whatsapp_cloud?.dmAllowlist ?? []) : null,
995
1009
  whatsapp_cloud_pairing: cfg.whatsapp_cloud?.appSecret ? (cfg.whatsapp_cloud?.pairing ?? true) : null,
996
1010
  whatsapp_cloud_templates_enabled: cfg.whatsapp_cloud?.appSecret ? (cfg.whatsapp_cloud?.templatesEnabled ?? false) : null,
1011
+ // WeChat — "connected" is LIVE manager state (a linked QR session),
1012
+ // never config-derived (there is no credential field to check — see
1013
+ // AgentConfig.wechat's doc comment). DM-only: no group_* fields exist.
1014
+ wechat_connected: agentRunners.get(id)?.getWeChatStatus?.()?.status === 'linked',
1015
+ wechat_dm_policy: cfg.wechat?.dmPolicy ?? null,
1016
+ wechat_dm_allowlist: cfg.wechat?.dmAllowlist ?? [],
1017
+ wechat_pairing: cfg.wechat?.pairing ?? true,
997
1018
  }));
998
1019
  res.json({ agents });
999
1020
  });
@@ -1362,6 +1383,64 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
1362
1383
  expiresAt: new Date(state.expiresAt).toISOString(),
1363
1384
  });
1364
1385
  });
1386
+ /**
1387
+ * POST /api/v1/agents/describe/rewrite
1388
+ * Re-write a rough "describe your agent" draft into cleaner, moderately
1389
+ * clarified text. Stateless: no wizardId/agentId involved, single string
1390
+ * in, single string out.
1391
+ */
1392
+ router.post('/v1/agents/describe/rewrite', auth, async (req, res) => {
1393
+ const apiKey = req.apiKey;
1394
+ if (!(0, auth_1.isAdmin)(apiKey)) {
1395
+ res.status(403).json({ error: 'Admin key required' });
1396
+ return;
1397
+ }
1398
+ const body = req.body;
1399
+ const { text } = body;
1400
+ if (!text || typeof text !== 'string' || !text.trim()) {
1401
+ res.status(400).json({ error: 'text is required' });
1402
+ return;
1403
+ }
1404
+ if (text.length > REWRITE_MAX_TEXT_LENGTH) {
1405
+ res.status(400).json({ error: `text too long (max ${REWRITE_MAX_TEXT_LENGTH} characters)` });
1406
+ return;
1407
+ }
1408
+ if (rewritesInFlight >= REWRITE_MAX_CONCURRENT) {
1409
+ res.status(429).json({ error: 'Too many re-write requests in progress, please retry later' });
1410
+ return;
1411
+ }
1412
+ let rawOutput;
1413
+ rewritesInFlight++;
1414
+ try {
1415
+ rawOutput = await runClaude((0, create_agent_prompts_1.buildRewriteDescriptionPrompt)(text.trim()));
1416
+ }
1417
+ catch (err) {
1418
+ console.error(`[api] describe/rewrite failed: ${err.message}`);
1419
+ res.status(500).json({ error: 'Failed to re-write description' });
1420
+ return;
1421
+ }
1422
+ finally {
1423
+ rewritesInFlight--;
1424
+ }
1425
+ let rewritten = rawOutput.trim();
1426
+ // Strip a wrapping code fence if the model added one, regardless of the
1427
+ // language hint (```markdown, ```md, ```text, ```plaintext, ```html, …) or
1428
+ // none at all. Requires the opening fence to sit on its own line so inline
1429
+ // backticks in a single-line answer are never mistaken for a wrapper; the
1430
+ // closing newline is optional and CRLF is tolerated.
1431
+ const fenceMatch = rewritten.match(/^```[^\r\n`]*\r?\n([\s\S]*?)\r?\n?```\s*$/);
1432
+ if (fenceMatch)
1433
+ rewritten = (fenceMatch[1] ?? '').trim();
1434
+ // Claude occasionally returns an empty/whitespace-only completion. Never
1435
+ // answer 200 with an empty string: the frontend would overwrite the user's
1436
+ // draft with nothing and silently destroy their input. Surface an error so
1437
+ // the client can keep the original text and let the user retry.
1438
+ if (!rewritten) {
1439
+ res.status(502).json({ error: 'Re-write produced no output, please try again' });
1440
+ return;
1441
+ }
1442
+ res.status(200).json({ text: rewritten });
1443
+ });
1365
1444
  /**
1366
1445
  * PUT /api/v1/agents/wizard/:wizardId/avatar
1367
1446
  * Upload avatar into wizard state (in-memory until confirm).
@@ -1688,7 +1767,7 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
1688
1767
  return;
1689
1768
  }
1690
1769
  const body = req.body;
1691
- const { name, description, model, allow_tools, telegram_bot_token, discord_bot_token, line_channel_access_token, line_channel_secret, line_dm_policy, line_dm_allowlist, line_group_policy, line_group_allowlist, line_require_mention, line_pairing, slack_bot_token, slack_signing_secret, slack_dm_policy, slack_dm_allowlist, slack_group_policy, slack_group_allowlist, slack_require_mention, slack_pairing, connectors, whatsapp_account_id, whatsapp_dm_policy, whatsapp_dm_allowlist, whatsapp_group_policy, whatsapp_group_allowlist, whatsapp_require_mention, whatsapp_pairing, whatsapp_cloud_access_token, whatsapp_cloud_phone_number_id, whatsapp_cloud_app_secret, whatsapp_cloud_verify_token, whatsapp_cloud_dm_policy, whatsapp_cloud_dm_allowlist, whatsapp_cloud_pairing, whatsapp_cloud_templates_enabled } = body;
1770
+ const { name, description, model, allow_tools, telegram_bot_token, discord_bot_token, line_channel_access_token, line_channel_secret, line_dm_policy, line_dm_allowlist, line_group_policy, line_group_allowlist, line_require_mention, line_pairing, slack_bot_token, slack_signing_secret, slack_dm_policy, slack_dm_allowlist, slack_group_policy, slack_group_allowlist, slack_require_mention, slack_pairing, connectors, whatsapp_account_id, whatsapp_dm_policy, whatsapp_dm_allowlist, whatsapp_group_policy, whatsapp_group_allowlist, whatsapp_require_mention, whatsapp_pairing, whatsapp_cloud_access_token, whatsapp_cloud_phone_number_id, whatsapp_cloud_app_secret, whatsapp_cloud_verify_token, whatsapp_cloud_dm_policy, whatsapp_cloud_dm_allowlist, whatsapp_cloud_pairing, whatsapp_cloud_templates_enabled, wechat_dm_policy, wechat_dm_allowlist, wechat_pairing } = body;
1692
1771
  if (name !== undefined && name !== null && typeof name !== 'string') {
1693
1772
  res.status(400).json({ error: 'name must be a string or null' });
1694
1773
  return;
@@ -1840,6 +1919,29 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
1840
1919
  const slackAccessTouched = slack_dm_policy !== undefined || slack_dm_allowlist !== undefined ||
1841
1920
  slack_group_policy !== undefined || slack_group_allowlist !== undefined || slack_require_mention !== undefined ||
1842
1921
  slack_pairing !== undefined;
1922
+ // WeChat — DM-only access control (no credential, no group tier: see
1923
+ // AgentConfig.wechat's doc comment for why). Unlike LINE/Slack's access
1924
+ // fields, this is NOT gated on an existing wechat block already being
1925
+ // present — linking happens out-of-band (QR scan, via the /wechat/link
1926
+ // route below) and never touches config.json, so policy must be settable
1927
+ // independent of link status.
1928
+ if (wechat_dm_policy !== undefined && wechat_dm_policy !== null &&
1929
+ !(typeof wechat_dm_policy === 'string' && ['open', 'allowlist', 'disabled'].includes(wechat_dm_policy))) {
1930
+ res.status(400).json({ error: "wechat_dm_policy must be 'open', 'allowlist', 'disabled', or null" });
1931
+ return;
1932
+ }
1933
+ if (wechat_dm_allowlist !== undefined && wechat_dm_allowlist !== null &&
1934
+ !(Array.isArray(wechat_dm_allowlist) && wechat_dm_allowlist.every((u) => typeof u === 'string'))) {
1935
+ res.status(400).json({ error: 'wechat_dm_allowlist must be an array of strings or null' });
1936
+ return;
1937
+ }
1938
+ if (wechat_pairing !== undefined && wechat_pairing !== null &&
1939
+ typeof wechat_pairing !== 'boolean') {
1940
+ res.status(400).json({ error: 'wechat_pairing must be a boolean or null' });
1941
+ return;
1942
+ }
1943
+ const wechatAccessTouched = wechat_dm_policy !== undefined || wechat_dm_allowlist !== undefined ||
1944
+ wechat_pairing !== undefined;
1843
1945
  // connectors: a partial map of { [connectorId]: { enabled: boolean } } to merge.
1844
1946
  const connectorPatch = {};
1845
1947
  if (connectors !== undefined) {
@@ -2159,6 +2261,31 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
2159
2261
  }
2160
2262
  }
2161
2263
  }
2264
+ // WeChat access fields — get-or-create `agent.wechat` (not gated on it
2265
+ // already existing, unlike LINE/Slack: see the validation block above
2266
+ // for why).
2267
+ if (wechatAccessTouched) {
2268
+ const existing = agent.wechat ?? {};
2269
+ if (wechat_dm_policy !== undefined) {
2270
+ if (wechat_dm_policy === null)
2271
+ delete existing.dmPolicy;
2272
+ else
2273
+ existing.dmPolicy = wechat_dm_policy;
2274
+ }
2275
+ if (wechat_dm_allowlist !== undefined) {
2276
+ if (wechat_dm_allowlist === null)
2277
+ delete existing.dmAllowlist;
2278
+ else
2279
+ existing.dmAllowlist = wechat_dm_allowlist;
2280
+ }
2281
+ if (wechat_pairing !== undefined) {
2282
+ if (wechat_pairing === null)
2283
+ delete existing.pairing;
2284
+ else
2285
+ existing.pairing = wechat_pairing;
2286
+ }
2287
+ agent.wechat = existing;
2288
+ }
2162
2289
  if (connectors !== undefined) {
2163
2290
  const existing = agent.connectors ?? {};
2164
2291
  agent.connectors = { ...existing, ...connectorPatch };
@@ -2420,6 +2547,38 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
2420
2547
  (0, pending_senders_1.clearPendingSender)('slack', agentId, id);
2421
2548
  }
2422
2549
  }
2550
+ if (wechatAccessTouched) {
2551
+ const existing = cfg.wechat ?? {};
2552
+ if (wechat_dm_policy !== undefined) {
2553
+ if (wechat_dm_policy === null)
2554
+ delete existing.dmPolicy;
2555
+ else
2556
+ existing.dmPolicy = wechat_dm_policy;
2557
+ }
2558
+ if (wechat_dm_allowlist !== undefined) {
2559
+ if (wechat_dm_allowlist === null)
2560
+ delete existing.dmAllowlist;
2561
+ else
2562
+ existing.dmAllowlist = wechat_dm_allowlist;
2563
+ }
2564
+ if (wechat_pairing !== undefined) {
2565
+ if (wechat_pairing === null)
2566
+ delete existing.pairing;
2567
+ else
2568
+ existing.pairing = wechat_pairing;
2569
+ }
2570
+ cfg.wechat = existing;
2571
+ // WeChat has no receiver/webhook to start/stop — WeChatManager reads
2572
+ // access control live off the agent config on every inbound message
2573
+ // (see AgentRunner.handleWeChatInboundMessage); just keep it in sync.
2574
+ agentRunners.get(agentId)?.updateAgentConfig(cfg);
2575
+ // Anyone just added to an allowlist is now allowed — drop them from the
2576
+ // in-memory knock list so the discovery UI stops surfacing them.
2577
+ if (Array.isArray(wechat_dm_allowlist)) {
2578
+ for (const userId of wechat_dm_allowlist)
2579
+ (0, pending_senders_1.clearPendingSender)('wechat', agentId, userId);
2580
+ }
2581
+ }
2423
2582
  if (connectors !== undefined) {
2424
2583
  const before = cfg.connectors ?? {};
2425
2584
  const merged = { ...before, ...connectorPatch };
@@ -2592,6 +2751,10 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
2592
2751
  whatsapp_cloud_dm_allowlist: cfg.whatsapp_cloud?.appSecret ? (cfg.whatsapp_cloud?.dmAllowlist ?? []) : null,
2593
2752
  whatsapp_cloud_pairing: cfg.whatsapp_cloud?.appSecret ? (cfg.whatsapp_cloud?.pairing ?? true) : null,
2594
2753
  whatsapp_cloud_templates_enabled: cfg.whatsapp_cloud?.appSecret ? (cfg.whatsapp_cloud?.templatesEnabled ?? false) : null,
2754
+ wechat_connected: agentRunners.get(agentId)?.getWeChatStatus?.()?.status === 'linked',
2755
+ wechat_dm_policy: cfg.wechat?.dmPolicy ?? null,
2756
+ wechat_dm_allowlist: cfg.wechat?.dmAllowlist ?? [],
2757
+ wechat_pairing: cfg.wechat?.pairing ?? true,
2595
2758
  },
2596
2759
  });
2597
2760
  });
@@ -2868,6 +3031,151 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
2868
3031
  (0, pending_senders_1.clearPendingSender)('slack', agentId, senderId);
2869
3032
  res.json({ ok: true });
2870
3033
  });
3034
+ /**
3035
+ * GET /api/v1/agents/:agentId/wechat/status
3036
+ * Live link status — status/qr straight from the in-process WeChatManager,
3037
+ * never config-derived (there is no credential field to derive it from —
3038
+ * see AgentConfig.wechat's doc comment). Polled by the web UI during
3039
+ * linking and while showing the connected card. Requires write access
3040
+ * (same as every other channel's connect surface), not just read. Mirrors
3041
+ * WhatsApp's `/whatsapp/status` route shape, once that channel lands.
3042
+ */
3043
+ router.get('/v1/agents/:agentId/wechat/status', auth, (req, res) => {
3044
+ const { agentId } = req.params;
3045
+ const apiKey = req.apiKey;
3046
+ if (!(0, auth_1.canWriteAgent)(apiKey, agentId)) {
3047
+ res.status(403).json({ error: 'Write permission required' });
3048
+ return;
3049
+ }
3050
+ const runner = agentRunners.get(agentId);
3051
+ if (!runner) {
3052
+ res.status(404).json({ error: `Agent '${agentId}' not found` });
3053
+ return;
3054
+ }
3055
+ res.json(runner.getWeChatStatus());
3056
+ });
3057
+ /**
3058
+ * POST /api/v1/agents/:agentId/wechat/link
3059
+ * Start (or restart) a QR-code linking flow. Returns once linked or once
3060
+ * the attempt window elapses — unlike WhatsApp's fire-and-forget
3061
+ * `/whatsapp/link`, there is no separate pairing-code path to race against,
3062
+ * so the client can simply await this and then re-check `/status` once.
3063
+ */
3064
+ router.post('/v1/agents/:agentId/wechat/link', auth, async (req, res) => {
3065
+ const { agentId } = req.params;
3066
+ const apiKey = req.apiKey;
3067
+ if (!(0, auth_1.canWriteAgent)(apiKey, agentId)) {
3068
+ res.status(403).json({ error: 'Write permission required' });
3069
+ return;
3070
+ }
3071
+ const runner = agentRunners.get(agentId);
3072
+ if (!runner) {
3073
+ res.status(404).json({ error: `Agent '${agentId}' not found` });
3074
+ return;
3075
+ }
3076
+ try {
3077
+ await runner.startWeChatLinking();
3078
+ res.json({ ok: true, status: runner.getWeChatStatus() });
3079
+ }
3080
+ catch (err) {
3081
+ res.status(500).json({ error: err.message });
3082
+ }
3083
+ });
3084
+ /**
3085
+ * POST /api/v1/agents/:agentId/wechat/unlink
3086
+ * Logout and wipe the linked session. The user must scan fresh afterward.
3087
+ */
3088
+ router.post('/v1/agents/:agentId/wechat/unlink', auth, async (req, res) => {
3089
+ const { agentId } = req.params;
3090
+ const apiKey = req.apiKey;
3091
+ if (!(0, auth_1.canWriteAgent)(apiKey, agentId)) {
3092
+ res.status(403).json({ error: 'Write permission required' });
3093
+ return;
3094
+ }
3095
+ const runner = agentRunners.get(agentId);
3096
+ if (!runner) {
3097
+ res.status(404).json({ error: `Agent '${agentId}' not found` });
3098
+ return;
3099
+ }
3100
+ await runner.unlinkWeChat();
3101
+ res.json({ ok: true });
3102
+ });
3103
+ /**
3104
+ * POST /api/v1/agents/:agentId/wechat/send
3105
+ * INTERNAL — called by the `wechat_reply` MCP tool (via GATEWAY_API_URL/
3106
+ * GATEWAY_API_KEY, same as every other MCP subprocess reaches the
3107
+ * gateway), never by the web UI. Exists because the manager's live iLink
3108
+ * session/credentials only exist inside this process — same reasoning
3109
+ * WhatsApp's `/whatsapp/send` route documents for its Baileys socket.
3110
+ */
3111
+ router.post('/v1/agents/:agentId/wechat/send', auth, async (req, res) => {
3112
+ const { agentId } = req.params;
3113
+ const apiKey = req.apiKey;
3114
+ if (!(0, auth_1.canWriteAgent)(apiKey, agentId)) {
3115
+ res.status(403).json({ error: 'Write permission required' });
3116
+ return;
3117
+ }
3118
+ const runner = agentRunners.get(agentId);
3119
+ if (!runner) {
3120
+ res.status(404).json({ error: `Agent '${agentId}' not found` });
3121
+ return;
3122
+ }
3123
+ const { to_id, text } = req.body;
3124
+ if (typeof to_id !== 'string' || !to_id) {
3125
+ res.status(400).json({ error: 'to_id is required' });
3126
+ return;
3127
+ }
3128
+ if (typeof text !== 'string' || !text) {
3129
+ res.status(400).json({ error: 'text is required' });
3130
+ return;
3131
+ }
3132
+ try {
3133
+ await runner.sendWeChatMessage(to_id, text);
3134
+ res.json({ ok: true });
3135
+ }
3136
+ catch (err) {
3137
+ res.status(500).json({ error: err.message });
3138
+ }
3139
+ });
3140
+ /**
3141
+ * GET /api/v1/agents/:agentId/wechat/pending
3142
+ * Recently denied WeChat senders (Tier 1 allowlist discovery aid). Admin
3143
+ * only. Mirrors GET .../line/pending exactly, keyed under the 'wechat'
3144
+ * channel namespace in the shared pending-senders store.
3145
+ */
3146
+ router.get('/v1/agents/:agentId/wechat/pending', auth, (req, res) => {
3147
+ const { agentId } = req.params;
3148
+ const apiKey = req.apiKey;
3149
+ if (!(0, auth_1.isAdmin)(apiKey)) {
3150
+ res.status(403).json({ error: 'Admin key required' });
3151
+ return;
3152
+ }
3153
+ if (!agentConfigs.has(agentId)) {
3154
+ res.status(404).json({ error: `Agent '${agentId}' not found` });
3155
+ return;
3156
+ }
3157
+ res.json({ senders: (0, pending_senders_1.getPendingSenders)('wechat', agentId) });
3158
+ });
3159
+ /**
3160
+ * DELETE /api/v1/agents/:agentId/wechat/pending/:senderId
3161
+ * Dismiss one knock from the in-memory pending list (admin only). Mirrors
3162
+ * DELETE .../line/pending/:senderId exactly. The id is an iLink sender id,
3163
+ * so no format validation.
3164
+ */
3165
+ router.delete('/v1/agents/:agentId/wechat/pending/:senderId', auth, (req, res) => {
3166
+ const apiKey = req.apiKey;
3167
+ if (!(0, auth_1.isAdmin)(apiKey)) {
3168
+ res.status(403).json({ error: 'Admin key required' });
3169
+ return;
3170
+ }
3171
+ const { agentId, senderId } = req.params;
3172
+ if (!agentConfigs.has(agentId)) {
3173
+ res.status(404).json({ error: `Agent '${agentId}' not found` });
3174
+ return;
3175
+ }
3176
+ (0, pending_senders_1.clearPendingSender)('wechat', agentId, senderId);
3177
+ res.json({ ok: true });
3178
+ });
2871
3179
  /**
2872
3180
  * GET /api/v1/agents/:agentId/whatsapp/pending?account_id=...
2873
3181
  * Recently denied WhatsApp senders/groups (Tier 1/3 discovery aid) for ONE