@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.
- package/config.template.json +8 -4
- package/dist/agent/builtin-commands.d.ts +1 -1
- package/dist/agent/builtin-commands.d.ts.map +1 -1
- package/dist/agent/builtin-commands.js +5 -0
- package/dist/agent/builtin-commands.js.map +1 -1
- package/dist/agent/line-pure.d.ts +31 -0
- package/dist/agent/line-pure.d.ts.map +1 -0
- package/dist/agent/line-pure.js +99 -0
- package/dist/agent/line-pure.js.map +1 -0
- package/dist/agent/line-reply-manager.d.ts +68 -0
- package/dist/agent/line-reply-manager.d.ts.map +1 -0
- package/dist/agent/line-reply-manager.js +383 -0
- package/dist/agent/line-reply-manager.js.map +1 -0
- package/dist/agent/runner.d.ts +11 -2
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +106 -8
- package/dist/agent/runner.js.map +1 -1
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +4 -0
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/line-access.d.ts +60 -0
- package/dist/api/line-access.d.ts.map +1 -0
- package/dist/api/line-access.js +69 -0
- package/dist/api/line-access.js.map +1 -0
- package/dist/api/line-mention.d.ts +35 -0
- package/dist/api/line-mention.d.ts.map +1 -0
- package/dist/api/line-mention.js +33 -0
- package/dist/api/line-mention.js.map +1 -0
- package/dist/api/line-pending-senders.d.ts +40 -0
- package/dist/api/line-pending-senders.d.ts.map +1 -0
- package/dist/api/line-pending-senders.js +107 -0
- package/dist/api/line-pending-senders.js.map +1 -0
- package/dist/api/line-webhook-router.d.ts +51 -0
- package/dist/api/line-webhook-router.d.ts.map +1 -0
- package/dist/api/line-webhook-router.js +416 -0
- package/dist/api/line-webhook-router.js.map +1 -0
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +244 -7
- package/dist/api/router.js.map +1 -1
- package/dist/history/types.d.ts +1 -1
- package/dist/history/types.d.ts.map +1 -1
- package/dist/session/compactor.d.ts +1 -1
- package/dist/session/compactor.d.ts.map +1 -1
- package/dist/session/compactor.js.map +1 -1
- package/dist/session/process.d.ts +2 -2
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +14 -3
- package/dist/session/process.js.map +1 -1
- package/dist/session/store.d.ts +13 -13
- package/dist/session/store.d.ts.map +1 -1
- package/dist/session/store.js.map +1 -1
- package/dist/shell/claude-pty-shell.js +12 -0
- package/dist/shell/claude-pty-shell.js.map +1 -1
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -1
- package/mcp/bun.lock +9 -2
- package/mcp/package.json +1 -0
- package/mcp/server.ts +2 -0
- package/mcp/tools/line/module.ts +127 -0
- package/mcp/tools/line/pure.ts +62 -0
- package/mcp/tools/telegram/receiver-server.ts +8 -5
- 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-
|
|
714
|
-
{ id: 'claude-opus-4-
|
|
715
|
-
{ id: 'claude-
|
|
716
|
-
{ id: 'claude-
|
|
717
|
-
{ id: 'claude-
|
|
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.
|
|
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",
|