@0xmaxma/claude-gateway 1.3.16 → 1.3.18

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 (62) hide show
  1. package/config.template.json +8 -4
  2. package/dist/agent/builtin-commands.d.ts +1 -1
  3. package/dist/agent/builtin-commands.d.ts.map +1 -1
  4. package/dist/agent/builtin-commands.js +5 -0
  5. package/dist/agent/builtin-commands.js.map +1 -1
  6. package/dist/agent/line-pure.d.ts +31 -0
  7. package/dist/agent/line-pure.d.ts.map +1 -0
  8. package/dist/agent/line-pure.js +99 -0
  9. package/dist/agent/line-pure.js.map +1 -0
  10. package/dist/agent/line-reply-manager.d.ts +68 -0
  11. package/dist/agent/line-reply-manager.d.ts.map +1 -0
  12. package/dist/agent/line-reply-manager.js +383 -0
  13. package/dist/agent/line-reply-manager.js.map +1 -0
  14. package/dist/agent/runner.d.ts +11 -2
  15. package/dist/agent/runner.d.ts.map +1 -1
  16. package/dist/agent/runner.js +106 -8
  17. package/dist/agent/runner.js.map +1 -1
  18. package/dist/api/gateway-router.d.ts.map +1 -1
  19. package/dist/api/gateway-router.js +4 -0
  20. package/dist/api/gateway-router.js.map +1 -1
  21. package/dist/api/line-access.d.ts +60 -0
  22. package/dist/api/line-access.d.ts.map +1 -0
  23. package/dist/api/line-access.js +69 -0
  24. package/dist/api/line-access.js.map +1 -0
  25. package/dist/api/line-mention.d.ts +35 -0
  26. package/dist/api/line-mention.d.ts.map +1 -0
  27. package/dist/api/line-mention.js +33 -0
  28. package/dist/api/line-mention.js.map +1 -0
  29. package/dist/api/line-pending-senders.d.ts +40 -0
  30. package/dist/api/line-pending-senders.d.ts.map +1 -0
  31. package/dist/api/line-pending-senders.js +107 -0
  32. package/dist/api/line-pending-senders.js.map +1 -0
  33. package/dist/api/line-webhook-router.d.ts +51 -0
  34. package/dist/api/line-webhook-router.d.ts.map +1 -0
  35. package/dist/api/line-webhook-router.js +416 -0
  36. package/dist/api/line-webhook-router.js.map +1 -0
  37. package/dist/api/router.d.ts.map +1 -1
  38. package/dist/api/router.js +244 -7
  39. package/dist/api/router.js.map +1 -1
  40. package/dist/history/types.d.ts +1 -1
  41. package/dist/history/types.d.ts.map +1 -1
  42. package/dist/session/compactor.d.ts +1 -1
  43. package/dist/session/compactor.d.ts.map +1 -1
  44. package/dist/session/compactor.js.map +1 -1
  45. package/dist/session/process.d.ts +2 -2
  46. package/dist/session/process.d.ts.map +1 -1
  47. package/dist/session/process.js +14 -3
  48. package/dist/session/process.js.map +1 -1
  49. package/dist/session/store.d.ts +13 -13
  50. package/dist/session/store.d.ts.map +1 -1
  51. package/dist/session/store.js.map +1 -1
  52. package/dist/shell/claude-pty-shell.js +12 -0
  53. package/dist/shell/claude-pty-shell.js.map +1 -1
  54. package/dist/types.d.ts +60 -0
  55. package/dist/types.d.ts.map +1 -1
  56. package/mcp/bun.lock +9 -2
  57. package/mcp/package.json +1 -0
  58. package/mcp/server.ts +2 -0
  59. package/mcp/tools/line/module.ts +127 -0
  60. package/mcp/tools/line/pure.ts +62 -0
  61. package/mcp/tools/telegram/receiver-server.ts +8 -5
  62. package/package.json +2 -1
package/mcp/server.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  } from '@modelcontextprotocol/sdk/types.js';
15
15
  import { TelegramModule } from './tools/telegram/module';
16
16
  import { DiscordModule } from './tools/discord/module';
17
+ import { LineModule } from './tools/line/module';
17
18
  import { CronModule } from './tools/cron/module';
18
19
  import { SkillsModule } from './tools/skills/module';
19
20
  import { AgentModule } from './tools/agent/module';
@@ -33,6 +34,7 @@ function isChannelModule(mod: AnyModule): mod is ChannelModule {
33
34
  const modules: AnyModule[] = [
34
35
  new TelegramModule(),
35
36
  new DiscordModule(),
37
+ new LineModule(),
36
38
  new CronModule(),
37
39
  new SkillsModule(),
38
40
  new AgentModule(),
@@ -0,0 +1,127 @@
1
+ /**
2
+ * LINE outbound tool module — exposes `line_reply` to the Claude session.
3
+ *
4
+ * LINE is a ToolModule (reply-only, like ApiModule): inbound arrives via the
5
+ * gateway's Express webhook route (src/api/line-webhook-router.ts), not here.
6
+ *
7
+ * Delivery is reply-token-first → push fallback (matches openclaw/hermes-agent):
8
+ * LINE counts push/broadcast against the OA's monthly quota but reply-token
9
+ * messages are FREE, so we send via the reply API when a (still-valid) reply_token
10
+ * is passed, and fall back to push if it's absent or expired/used (the single-use
11
+ * token has no guaranteed lifetime — ~1 min — so push remains the safety net).
12
+ */
13
+ import type { ToolModule, McpToolDefinition, McpToolResult, ToolVisibility } from '../../types';
14
+ import { messagingApi } from '@line/bot-sdk';
15
+ import { chunkText, planLineSend, LINE_TEXT_LIMIT } from './pure';
16
+
17
+ export class LineModule implements ToolModule {
18
+ id = 'line';
19
+ toolVisibility: ToolVisibility = 'current-channel';
20
+
21
+ isEnabled(): boolean {
22
+ return process.env.GATEWAY_ORIGIN_CHANNEL === 'line';
23
+ }
24
+
25
+ getTools(): McpToolDefinition[] {
26
+ return [
27
+ {
28
+ name: 'line_reply',
29
+ description:
30
+ 'Send a reply to the current LINE user. ' +
31
+ 'Pass chat_id (the LINE userId shown in the <channel> tag) and text. ' +
32
+ 'Also pass reply_token from the <channel> tag when present — it sends the ' +
33
+ 'reply for FREE (push messages count against the LINE quota); the tool ' +
34
+ 'automatically falls back to push if the token is expired/used. ' +
35
+ 'Long text is split into multiple messages automatically.',
36
+ inputSchema: {
37
+ type: 'object',
38
+ properties: {
39
+ chat_id: {
40
+ type: 'string',
41
+ description: 'LINE userId to send to (the chat_id from the channel turn).',
42
+ },
43
+ text: {
44
+ type: 'string',
45
+ description: 'Message text. Automatically split into <=5000-char messages.',
46
+ },
47
+ reply_token: {
48
+ type: 'string',
49
+ description:
50
+ 'Optional single-use reply token from the <channel> tag. When valid, the ' +
51
+ 'reply is sent free via the reply API; falls back to push automatically.',
52
+ },
53
+ },
54
+ required: ['chat_id', 'text'],
55
+ },
56
+ },
57
+ ];
58
+ }
59
+
60
+ async handleTool(name: string, args: Record<string, unknown>): Promise<McpToolResult> {
61
+ if (name === 'line_reply') return this.handleReply(args);
62
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
63
+ }
64
+
65
+ private async handleReply(args: Record<string, unknown>): Promise<McpToolResult> {
66
+ const chatId = typeof args.chat_id === 'string' ? args.chat_id : '';
67
+ const text = typeof args.text === 'string' ? args.text : '';
68
+ const replyToken = typeof args.reply_token === 'string' ? args.reply_token : '';
69
+ const token = process.env.LINE_CHANNEL_ACCESS_TOKEN ?? '';
70
+
71
+ if (!chatId) {
72
+ return { content: [{ type: 'text', text: 'line_reply: missing chat_id' }], isError: true };
73
+ }
74
+ if (!text) {
75
+ return { content: [{ type: 'text', text: 'line_reply: text cannot be empty' }], isError: true };
76
+ }
77
+
78
+ // Refresh mode: the gateway (LineReplyManager) is the sole LINE sender — it
79
+ // observes this tool call in the session output stream and delivers the
80
+ // reply (free reply, or cache + postback button when slow). Sending here too
81
+ // would double-send and contend for the single-use reply token, so skip it.
82
+ if (process.env.LINE_REPLY_REFRESH === '1') {
83
+ return { content: [{ type: 'text', text: 'Reply handed to gateway for delivery.' }] };
84
+ }
85
+ if (!token) {
86
+ return {
87
+ content: [{ type: 'text', text: 'line_reply: missing LINE_CHANNEL_ACCESS_TOKEN' }],
88
+ isError: true,
89
+ };
90
+ }
91
+
92
+ const client = new messagingApi.MessagingApiClient({
93
+ channelAccessToken: token,
94
+ });
95
+
96
+ const toMessages = (group: string[]) =>
97
+ group.map((t) => ({ type: 'text' as const, text: t }));
98
+ const push = (group: string[]) => client.pushMessage({ to: chatId, messages: toMessages(group) });
99
+
100
+ const chunks = chunkText(text, LINE_TEXT_LIMIT);
101
+ const plan = planLineSend(chunks, replyToken.length > 0);
102
+ let via = 'push';
103
+ try {
104
+ // Reply-token-first: the first batch goes via the FREE reply API when a token
105
+ // is present, falling back to push if the token is expired/used. Remaining
106
+ // batches always push (a reply token is single-use).
107
+ if (plan.replyBatch) {
108
+ try {
109
+ await client.replyMessage({ replyToken, messages: toMessages(plan.replyBatch) });
110
+ via = plan.pushBatches.length > 0 ? 'reply + push' : 'reply';
111
+ } catch {
112
+ await push(plan.replyBatch); // token expired/used → push fallback
113
+ via = 'push (reply fallback)';
114
+ }
115
+ }
116
+ for (const group of plan.pushBatches) await push(group);
117
+ return {
118
+ content: [{ type: 'text', text: `Sent ${chunks.length} message(s) to LINE (${via}).` }],
119
+ };
120
+ } catch (err) {
121
+ return {
122
+ content: [{ type: 'text', text: `line_reply failed: ${(err as Error).message}` }],
123
+ isError: true,
124
+ };
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Pure, dependency-free helpers for the LINE channel — unit-tested in isolation
3
+ * (mirrors mcp/tools/telegram/pure.ts and mcp/tools/discord/outbound.ts chunkText).
4
+ */
5
+
6
+ /** LINE hard limit: max characters in a single text message object. */
7
+ export const LINE_TEXT_LIMIT = 5000;
8
+ /** LINE hard limit: max message objects per push/reply request. */
9
+ export const LINE_MAX_MESSAGES_PER_REQUEST = 5;
10
+
11
+ /**
12
+ * Split text into chunks each <= limit chars. Prefers to break on a newline
13
+ * within the limit; falls back to a hard cut. Always returns at least one chunk
14
+ * (an empty string in → [''], so callers always have something to send).
15
+ */
16
+ export function chunkText(text: string, limit = LINE_TEXT_LIMIT): string[] {
17
+ if (limit <= 0) return [text];
18
+ if (text.length === 0) return [''];
19
+ const out: string[] = [];
20
+ let rest = text;
21
+ while (rest.length > limit) {
22
+ let cut = rest.lastIndexOf('\n', limit);
23
+ if (cut < 0) cut = limit; // not found — hard cut
24
+ else if (cut === 0) { rest = rest.slice(1); continue; } // leading newline — skip, retry
25
+ out.push(rest.slice(0, cut));
26
+ rest = rest.slice(cut).replace(/^\n/, '');
27
+ }
28
+ if (rest.length > 0) out.push(rest);
29
+ return out.length > 0 ? out : [''];
30
+ }
31
+
32
+ /** Group items into batches of <= size (default = LINE per-request message cap). */
33
+ export function batch<T>(items: T[], size = LINE_MAX_MESSAGES_PER_REQUEST): T[][] {
34
+ const out: T[][] = [];
35
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
36
+ return out;
37
+ }
38
+
39
+ export interface LineSendPlan {
40
+ /** First batch (<=5 objects) to send via the FREE reply API, or null when no usable reply token. */
41
+ replyBatch: string[] | null;
42
+ /** Remaining batches to send via the metered push API. */
43
+ pushBatches: string[][];
44
+ }
45
+
46
+ /**
47
+ * Decide how to deliver chunked text given whether a usable reply token exists.
48
+ *
49
+ * Reply-token-first to save push quota: LINE counts push/broadcast against the OA
50
+ * monthly message quota but **reply-token messages are free**. The single-use token
51
+ * can carry one reply call of up to LINE_MAX_MESSAGES_PER_REQUEST objects, so it
52
+ * takes the FIRST batch; anything beyond one batch must push. With no token,
53
+ * everything pushes. The runtime falls back to push if the reply call fails (token
54
+ * expired/invalid/already used) — that fallback lives in module.ts, not here.
55
+ */
56
+ export function planLineSend(chunks: string[], hasReplyToken: boolean): LineSendPlan {
57
+ const batches = batch(chunks);
58
+ if (!hasReplyToken || batches.length === 0) {
59
+ return { replyBatch: null, pushBatches: batches };
60
+ }
61
+ return { replyBatch: batches[0], pushBatches: batches.slice(1) };
62
+ }
@@ -710,11 +710,14 @@ const BOT_COMMANDS = [
710
710
 
711
711
  // Available AI models for /models command
712
712
  const AVAILABLE_MODELS = [
713
- { id: 'claude-opus-4-8', label: 'Opus 4.8', alias: 'opus' },
714
- { id: 'claude-opus-4-7', label: 'Opus 4.7', alias: 'opus47' },
715
- { id: 'claude-opus-4-6', label: 'Opus 4.6', alias: 'opus46' },
716
- { id: 'claude-sonnet-4-6', label: 'Sonnet 4.6', alias: 'sonnet' },
717
- { id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5', alias: 'haiku' },
713
+ { id: 'claude-fable-5', label: 'Fable 5', alias: 'fable' },
714
+ { id: 'claude-opus-4-8[1m]', label: 'Opus 4.8 (1M)', alias: 'opus[1m]' },
715
+ { id: 'claude-sonnet-5[1m]', label: 'Sonnet 5 (1M)', alias: 'sonnet[1m]' },
716
+ { id: 'claude-opus-4-8', label: 'Opus 4.8', alias: 'opus' },
717
+ { id: 'claude-opus-4-6', label: 'Opus 4.6', alias: 'opus46' },
718
+ { id: 'claude-sonnet-5', label: 'Sonnet 5', alias: 'sonnet' },
719
+ { id: 'claude-sonnet-4-6', label: 'Sonnet 4.6', alias: 'sonnet46' },
720
+ { id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5', alias: 'haiku' },
718
721
  ]
719
722
 
720
723
  // Base URL for command API calls to the AgentRunner callback server.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xmaxma/claude-gateway",
3
- "version": "1.3.16",
3
+ "version": "1.3.18",
4
4
  "description": "Multi-agent gateway for Claude",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,6 +39,7 @@
39
39
  "typecheck": "tsc --noEmit"
40
40
  },
41
41
  "dependencies": {
42
+ "@line/bot-sdk": "^11.0.2",
42
43
  "@xterm/headless": "^6.0.0",
43
44
  "chokidar": "^3.5.0",
44
45
  "cron-parser": "^5.5.0",