@0xmaxma/claude-gateway 2.0.4 → 2.0.5

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 (228) hide show
  1. package/README.md +10 -1
  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 +3 -2
  5. package/dist/agent/builtin-commands.js.map +1 -1
  6. package/dist/agent/runner.d.ts +109 -9
  7. package/dist/agent/runner.d.ts.map +1 -1
  8. package/dist/agent/runner.js +148 -119
  9. package/dist/agent/runner.js.map +1 -1
  10. package/dist/agent/skill-learning/index.d.ts.map +1 -1
  11. package/dist/agent/skill-learning/index.js +27 -8
  12. package/dist/agent/skill-learning/index.js.map +1 -1
  13. package/dist/agent/skill-learning/reviewer.d.ts +3 -1
  14. package/dist/agent/skill-learning/reviewer.d.ts.map +1 -1
  15. package/dist/agent/skill-learning/reviewer.js +6 -2
  16. package/dist/agent/skill-learning/reviewer.js.map +1 -1
  17. package/dist/agent/turn-stream.d.ts +3 -1
  18. package/dist/agent/turn-stream.d.ts.map +1 -1
  19. package/dist/agent/turn-stream.js +8 -4
  20. package/dist/agent/turn-stream.js.map +1 -1
  21. package/dist/api/gateway-router.d.ts +2 -0
  22. package/dist/api/gateway-router.d.ts.map +1 -1
  23. package/dist/api/gateway-router.js +229 -12
  24. package/dist/api/gateway-router.js.map +1 -1
  25. package/dist/api/router.js +11 -11
  26. package/dist/api/router.js.map +1 -1
  27. package/dist/api/slack-webhook-router.js +1 -1
  28. package/dist/api/slack-webhook-router.js.map +1 -1
  29. package/dist/api/voice-router.d.ts +1 -0
  30. package/dist/api/voice-router.d.ts.map +1 -1
  31. package/dist/api/voice-router.js +129 -7
  32. package/dist/api/voice-router.js.map +1 -1
  33. package/dist/config/claude-settings.d.ts +14 -0
  34. package/dist/config/claude-settings.d.ts.map +1 -1
  35. package/dist/config/claude-settings.js +20 -0
  36. package/dist/config/claude-settings.js.map +1 -1
  37. package/dist/orchestration/agent-cli-session.d.ts +46 -0
  38. package/dist/orchestration/agent-cli-session.d.ts.map +1 -0
  39. package/dist/orchestration/agent-cli-session.js +121 -0
  40. package/dist/orchestration/agent-cli-session.js.map +1 -0
  41. package/dist/orchestration/agent-tool-schemas.d.ts +6 -0
  42. package/dist/orchestration/agent-tool-schemas.d.ts.map +1 -0
  43. package/dist/orchestration/agent-tool-schemas.js +23 -0
  44. package/dist/orchestration/agent-tool-schemas.js.map +1 -0
  45. package/dist/orchestration/bridge.d.ts +6 -3
  46. package/dist/orchestration/bridge.d.ts.map +1 -1
  47. package/dist/orchestration/bridge.js +31 -14
  48. package/dist/orchestration/bridge.js.map +1 -1
  49. package/dist/orchestration/browser-voice.d.ts +14 -0
  50. package/dist/orchestration/browser-voice.d.ts.map +1 -0
  51. package/dist/orchestration/browser-voice.js +30 -0
  52. package/dist/orchestration/browser-voice.js.map +1 -0
  53. package/dist/orchestration/context-footprint.d.ts +14 -0
  54. package/dist/orchestration/context-footprint.d.ts.map +1 -0
  55. package/dist/orchestration/context-footprint.js +57 -0
  56. package/dist/orchestration/context-footprint.js.map +1 -0
  57. package/dist/orchestration/dashboard-reader-worker.d.ts +2 -0
  58. package/dist/orchestration/dashboard-reader-worker.d.ts.map +1 -0
  59. package/dist/orchestration/dashboard-reader-worker.js +155 -0
  60. package/dist/orchestration/dashboard-reader-worker.js.map +1 -0
  61. package/dist/orchestration/dashboard-reader.d.ts +15 -0
  62. package/dist/orchestration/dashboard-reader.d.ts.map +1 -0
  63. package/dist/orchestration/dashboard-reader.js +87 -0
  64. package/dist/orchestration/dashboard-reader.js.map +1 -0
  65. package/dist/orchestration/decision-context.d.ts +15 -0
  66. package/dist/orchestration/decision-context.d.ts.map +1 -0
  67. package/dist/orchestration/decision-context.js +57 -0
  68. package/dist/orchestration/decision-context.js.map +1 -0
  69. package/dist/orchestration/display-stream.d.ts +21 -2
  70. package/dist/orchestration/display-stream.d.ts.map +1 -1
  71. package/dist/orchestration/display-stream.js +61 -8
  72. package/dist/orchestration/display-stream.js.map +1 -1
  73. package/dist/orchestration/inference-errors.d.ts.map +1 -1
  74. package/dist/orchestration/inference-errors.js +19 -21
  75. package/dist/orchestration/inference-errors.js.map +1 -1
  76. package/dist/orchestration/mutation-recovery.d.ts +12 -0
  77. package/dist/orchestration/mutation-recovery.d.ts.map +1 -0
  78. package/dist/orchestration/mutation-recovery.js +41 -0
  79. package/dist/orchestration/mutation-recovery.js.map +1 -0
  80. package/dist/orchestration/native-compact.d.ts +5 -0
  81. package/dist/orchestration/native-compact.d.ts.map +1 -0
  82. package/dist/orchestration/native-compact.js +29 -0
  83. package/dist/orchestration/native-compact.js.map +1 -0
  84. package/dist/orchestration/notification-mailbox.d.ts +3 -1
  85. package/dist/orchestration/notification-mailbox.d.ts.map +1 -1
  86. package/dist/orchestration/notification-mailbox.js +4 -2
  87. package/dist/orchestration/notification-mailbox.js.map +1 -1
  88. package/dist/orchestration/process-turn.d.ts +15 -2
  89. package/dist/orchestration/process-turn.d.ts.map +1 -1
  90. package/dist/orchestration/process-turn.js +78 -14
  91. package/dist/orchestration/process-turn.js.map +1 -1
  92. package/dist/orchestration/progress-review.d.ts +8 -18
  93. package/dist/orchestration/progress-review.d.ts.map +1 -1
  94. package/dist/orchestration/progress-review.js +12 -14
  95. package/dist/orchestration/progress-review.js.map +1 -1
  96. package/dist/orchestration/provider-message.d.ts +7 -0
  97. package/dist/orchestration/provider-message.d.ts.map +1 -0
  98. package/dist/orchestration/provider-message.js +63 -0
  99. package/dist/orchestration/provider-message.js.map +1 -0
  100. package/dist/orchestration/response-errors.d.ts +6 -0
  101. package/dist/orchestration/response-errors.d.ts.map +1 -0
  102. package/dist/orchestration/response-errors.js +88 -0
  103. package/dist/orchestration/response-errors.js.map +1 -0
  104. package/dist/orchestration/response-schema.d.ts +33 -0
  105. package/dist/orchestration/response-schema.d.ts.map +1 -0
  106. package/dist/orchestration/response-schema.js +61 -0
  107. package/dist/orchestration/response-schema.js.map +1 -0
  108. package/dist/orchestration/runtime.d.ts +67 -0
  109. package/dist/orchestration/runtime.d.ts.map +1 -1
  110. package/dist/orchestration/runtime.js +266 -62
  111. package/dist/orchestration/runtime.js.map +1 -1
  112. package/dist/orchestration/skills.d.ts.map +1 -1
  113. package/dist/orchestration/skills.js +3 -2
  114. package/dist/orchestration/skills.js.map +1 -1
  115. package/dist/orchestration/source-policy.d.ts +1 -1
  116. package/dist/orchestration/source-policy.d.ts.map +1 -1
  117. package/dist/orchestration/source-policy.js +1 -1
  118. package/dist/orchestration/speech.d.ts +15 -17
  119. package/dist/orchestration/speech.d.ts.map +1 -1
  120. package/dist/orchestration/speech.js +90 -30
  121. package/dist/orchestration/speech.js.map +1 -1
  122. package/dist/orchestration/store.d.ts +2 -0
  123. package/dist/orchestration/store.d.ts.map +1 -1
  124. package/dist/orchestration/store.js +16 -1
  125. package/dist/orchestration/store.js.map +1 -1
  126. package/dist/orchestration/tasks/driver.d.ts.map +1 -1
  127. package/dist/orchestration/tasks/driver.js +8 -3
  128. package/dist/orchestration/tasks/driver.js.map +1 -1
  129. package/dist/orchestration/tasks/pool.d.ts.map +1 -1
  130. package/dist/orchestration/tasks/pool.js +8 -4
  131. package/dist/orchestration/tasks/pool.js.map +1 -1
  132. package/dist/orchestration/tasks/service.d.ts +48 -1
  133. package/dist/orchestration/tasks/service.d.ts.map +1 -1
  134. package/dist/orchestration/tasks/service.js +41 -17
  135. package/dist/orchestration/tasks/service.js.map +1 -1
  136. package/dist/orchestration/token-ledger.d.ts +125 -0
  137. package/dist/orchestration/token-ledger.d.ts.map +1 -0
  138. package/dist/orchestration/token-ledger.js +232 -0
  139. package/dist/orchestration/token-ledger.js.map +1 -0
  140. package/dist/orchestration/token-usage.d.ts +46 -0
  141. package/dist/orchestration/token-usage.d.ts.map +1 -0
  142. package/dist/orchestration/token-usage.js +102 -0
  143. package/dist/orchestration/token-usage.js.map +1 -0
  144. package/dist/orchestration/tool-activity.d.ts.map +1 -1
  145. package/dist/orchestration/tool-activity.js +5 -3
  146. package/dist/orchestration/tool-activity.js.map +1 -1
  147. package/dist/orchestration/tool-name.d.ts +10 -0
  148. package/dist/orchestration/tool-name.d.ts.map +1 -0
  149. package/dist/orchestration/tool-name.js +14 -0
  150. package/dist/orchestration/tool-name.js.map +1 -0
  151. package/dist/orchestration/transcript-checkpoint.d.ts +14 -0
  152. package/dist/orchestration/transcript-checkpoint.d.ts.map +1 -0
  153. package/dist/orchestration/transcript-checkpoint.js +83 -0
  154. package/dist/orchestration/transcript-checkpoint.js.map +1 -0
  155. package/dist/orchestration/unanswered-inputs.d.ts +6 -0
  156. package/dist/orchestration/unanswered-inputs.d.ts.map +1 -0
  157. package/dist/orchestration/unanswered-inputs.js +18 -0
  158. package/dist/orchestration/unanswered-inputs.js.map +1 -0
  159. package/dist/session/process.d.ts +10 -0
  160. package/dist/session/process.d.ts.map +1 -1
  161. package/dist/session/process.js +91 -9
  162. package/dist/session/process.js.map +1 -1
  163. package/dist/session/request-tool-capture.d.ts +31 -0
  164. package/dist/session/request-tool-capture.d.ts.map +1 -0
  165. package/dist/session/request-tool-capture.js +194 -0
  166. package/dist/session/request-tool-capture.js.map +1 -0
  167. package/dist/session/runtime-profile.d.ts +7 -3
  168. package/dist/session/runtime-profile.d.ts.map +1 -1
  169. package/dist/session/runtime-profile.js +3 -1
  170. package/dist/session/runtime-profile.js.map +1 -1
  171. package/dist/session/store.d.ts +7 -0
  172. package/dist/session/store.d.ts.map +1 -1
  173. package/dist/session/store.js +27 -0
  174. package/dist/session/store.js.map +1 -1
  175. package/dist/shell/tailer.d.ts +7 -5
  176. package/dist/shell/tailer.d.ts.map +1 -1
  177. package/dist/shell/tailer.js +6 -17
  178. package/dist/shell/tailer.js.map +1 -1
  179. package/dist/ui/cli-viewer-ui.js +1 -1
  180. package/dist/ui/dashboard-client.d.ts +3 -0
  181. package/dist/ui/dashboard-client.d.ts.map +1 -0
  182. package/dist/ui/dashboard-client.js +188 -0
  183. package/dist/ui/dashboard-client.js.map +1 -0
  184. package/dist/ui/dashboard-presentation.d.ts +9 -0
  185. package/dist/ui/dashboard-presentation.d.ts.map +1 -0
  186. package/dist/ui/dashboard-presentation.js +56 -0
  187. package/dist/ui/dashboard-presentation.js.map +1 -0
  188. package/dist/ui/dashboard-range.d.ts +5 -0
  189. package/dist/ui/dashboard-range.d.ts.map +1 -0
  190. package/dist/ui/dashboard-range.js +20 -0
  191. package/dist/ui/dashboard-range.js.map +1 -0
  192. package/dist/ui/dashboard-theme.d.ts +4 -0
  193. package/dist/ui/dashboard-theme.d.ts.map +1 -0
  194. package/dist/ui/dashboard-theme.js +485 -0
  195. package/dist/ui/dashboard-theme.js.map +1 -0
  196. package/dist/ui/token-report-client.d.ts +3 -0
  197. package/dist/ui/token-report-client.d.ts.map +1 -0
  198. package/dist/ui/token-report-client.js +34 -0
  199. package/dist/ui/token-report-client.js.map +1 -0
  200. package/dist/ui/token-report.d.ts +89 -0
  201. package/dist/ui/token-report.d.ts.map +1 -0
  202. package/dist/ui/token-report.js +200 -0
  203. package/dist/ui/token-report.js.map +1 -0
  204. package/dist/ui/web-ui.d.ts.map +1 -1
  205. package/dist/ui/web-ui.js +124 -181
  206. package/dist/ui/web-ui.js.map +1 -1
  207. package/dist/voice/replay.d.ts +8 -0
  208. package/dist/voice/replay.d.ts.map +1 -0
  209. package/dist/voice/replay.js +48 -0
  210. package/dist/voice/replay.js.map +1 -0
  211. package/dist/voice/session.d.ts +9 -0
  212. package/dist/voice/session.d.ts.map +1 -1
  213. package/dist/voice/session.js +72 -14
  214. package/dist/voice/session.js.map +1 -1
  215. package/mcp/lazy-connector.test.ts +58 -0
  216. package/mcp/lazy-connector.ts +67 -0
  217. package/mcp/lazy-tools.test.ts +152 -0
  218. package/mcp/lazy-tools.ts +41 -0
  219. package/mcp/server.ts +53 -34
  220. package/mcp/tools/discord/commands.ts +2 -0
  221. package/mcp/tools/tasks/module.ts +1 -12
  222. package/mcp/tools/telegram/receiver-server.ts +14 -14
  223. package/package.json +2 -1
  224. package/resource/dashboard/OFL.txt +93 -0
  225. package/resource/dashboard/poppins-300.woff2 +0 -0
  226. package/resource/dashboard/poppins-400.woff2 +0 -0
  227. package/resource/dashboard/poppins-500.woff2 +0 -0
  228. package/resource/dashboard/poppins-600.woff2 +0 -0
@@ -0,0 +1,41 @@
1
+ import type { McpToolDefinition, McpToolResult } from './types';
2
+
3
+ export const LAZY_TOOL_DEFINITIONS: McpToolDefinition[] = [
4
+ { name: 'tool_search', description: 'Discover available gateway capabilities. Empty query lists tool names and descriptions; search by query or exact name to retrieve original argument schemas before calling tool_call. Results are paginated. This catalog contains only tools authorized for this worker.',
5
+ inputSchema: { type: 'object', properties: { query: { type: 'string' }, name: { type: 'string' }, offset: { type: 'integer', minimum: 0 }, limit: { type: 'integer', minimum: 1, maximum: 20 }, include_schema: { type: 'boolean' } }, additionalProperties: false } },
6
+ { name: 'tool_call', description: 'Execute a gateway capability discovered with tool_search. Use its exact name and original arguments. All original permissions, validation and cancellation apply. Task reporting and memory retrieval remain directly available.',
7
+ inputSchema: { type: 'object', properties: { name: { type: 'string' }, arguments: { type: 'object' } }, required: ['name', 'arguments'], additionalProperties: false } },
8
+ ];
9
+
10
+ const fail = (text: string): McpToolResult => ({ content: [{ type: 'text', text }], isError: true });
11
+ const result = (value: unknown): McpToolResult => ({ content: [{ type: 'text', text: JSON.stringify(value) }] });
12
+
13
+ /** Discovery is data-only. Invocation delegates to the SAME dispatcher as direct calls. */
14
+ export function createLazyToolCatalog(tools: readonly McpToolDefinition[], excludeReserved = true) {
15
+ const reserved = new Set(LAZY_TOOL_DEFINITIONS.map(tool => tool.name));
16
+ const catalog = [...tools].filter(tool => !excludeReserved || !reserved.has(tool.name))
17
+ .sort((a, b) => a.name.localeCompare(b.name));
18
+ const names = new Set(catalog.map(tool => tool.name));
19
+ return {
20
+ search(args: Record<string, unknown>): McpToolResult {
21
+ if ((args.query !== undefined && typeof args.query !== 'string') ||
22
+ (args.name !== undefined && typeof args.name !== 'string') ||
23
+ (args.include_schema !== undefined && typeof args.include_schema !== 'boolean')) return fail('Invalid search arguments');
24
+ const offset = args.offset ?? 0; const limit = args.limit ?? 5;
25
+ if (typeof offset !== 'number' || !Number.isSafeInteger(offset) || offset < 0 ||
26
+ typeof limit !== 'number' || !Number.isInteger(limit) || limit < 1 || limit > 20) return fail('Invalid search pagination');
27
+ const query = String(args.query ?? '').trim().toLowerCase();
28
+ const name = String(args.name ?? '').trim().toLowerCase();
29
+ const matches = catalog.filter(tool => name ? tool.name.toLowerCase() === name :
30
+ query.split(/\s+/).every(word => `${tool.name} ${tool.description}`.toLowerCase().includes(word)));
31
+ const schemas = args.include_schema ?? Boolean(query || name);
32
+ return result({ total: matches.length, offset, next_offset: offset + limit < matches.length ? offset + limit : null,
33
+ tools: matches.slice(offset, offset + limit).map(tool => schemas ? tool : { name: tool.name, description: tool.description }) });
34
+ },
35
+ async call(args: Record<string, unknown>, dispatch: (name: string, input: Record<string, unknown>) => Promise<McpToolResult>): Promise<McpToolResult> {
36
+ if (typeof args.name !== 'string' || !names.has(args.name)) return fail('Unknown or unauthorized gateway tool');
37
+ if (!args.arguments || typeof args.arguments !== 'object' || Array.isArray(args.arguments)) return fail('Tool arguments must be an object');
38
+ return dispatch(args.name, args.arguments as Record<string, unknown>);
39
+ },
40
+ };
41
+ }
package/mcp/server.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  } from '@modelcontextprotocol/sdk/types.js';
15
15
  import { gatewayModules } from './modules';
16
16
  import { AGENT_TASK_TOOLS, WORKER_REPORT_TOOLS, callTaskBridge } from './tools/tasks/module';
17
+ import { createLazyToolCatalog, LAZY_TOOL_DEFINITIONS } from './lazy-tools';
17
18
  import { buildChannelInstructions } from './instructions';
18
19
  import type { ChannelModule, ToolModule, McpToolDefinition } from './types';
19
20
 
@@ -52,9 +53,23 @@ for (const mod of modules) {
52
53
  }
53
54
  }
54
55
 
55
- const taskTools = ORCHESTRATION_ROLE === 'agent' ? AGENT_TASK_TOOLS.filter(tool => (tool.name !== 'conversation_intake' || process.env.GATEWAY_SEMANTIC_INTAKE === 'true') && (tool.name !== 'capabilities_list' || process.env.GATEWAY_CAPABILITY_CATALOG === 'true')) : ORCHESTRATION_ROLE === 'worker' ? WORKER_REPORT_TOOLS.filter(tool => tool.name !== 'task_memory_append' || process.env.GATEWAY_ORCHESTRATION_WRITE_MEMORY === 'true') : [];
56
+ // conversation_intake is declared on every agent turn, whatever the semantic-intake feature
57
+ // state. The advertised tool list is position 0 of Anthropic's cached [tools, system, messages]
58
+ // prefix, and the feature is decided per turn (runtime.ts computes it from the flag AND from
59
+ // whether this turn is a notification), so filtering the declaration here re-wrote the prefix
60
+ // mid-session and invalidated the whole cache — the same defect ee800a7 fixed for the response
61
+ // schema. The turn's INTAKE_OVERLAY prompt text, below the cache breakpoint, is what asks for
62
+ // the tool; the bridge refuses the call outright when the feature is not active.
63
+ const taskTools = ORCHESTRATION_ROLE === 'agent' ? AGENT_TASK_TOOLS.filter(tool => tool.name !== 'capabilities_list' || process.env.GATEWAY_CAPABILITY_CATALOG === 'true') : ORCHESTRATION_ROLE === 'worker' ? WORKER_REPORT_TOOLS.filter(tool => tool.name !== 'task_memory_append' || process.env.GATEWAY_ORCHESTRATION_WRITE_MEMORY === 'true') : [];
56
64
  if (ORCHESTRATION_ROLE && process.env.GATEWAY_ORCHESTRATION_TICKET_FILE) visibleTools.push(...taskTools);
57
65
 
66
+ // Preserve the full policy-filtered inventory internally; only schema exposure is lazy.
67
+ const lazyEnabled = ORCHESTRATION_ROLE === 'worker' && process.env.GATEWAY_LAZY_TOOLS === 'true';
68
+ const deferredTools = visibleTools.filter(tool => toolMap.has(tool.name) && !['memory_search','memory_get'].includes(tool.name));
69
+ const deferredNames = new Set(deferredTools.map(tool => tool.name));
70
+ const lazyCatalog = createLazyToolCatalog(deferredTools);
71
+ const advertisedTools = lazyEnabled ? [...visibleTools.filter(tool => !deferredNames.has(tool.name)), ...LAZY_TOOL_DEFINITIONS] : visibleTools;
72
+
58
73
  // Initialize channel modules so their bot API clients are ready for tool calls.
59
74
  // initBot() returns immediately after creating the client — no blocking.
60
75
  for (const mod of modules) {
@@ -79,47 +94,51 @@ const mcp = new Server(
79
94
  'claude/channel/permission': {},
80
95
  },
81
96
  },
82
- instructions: ORCHESTRATION_ROLE ? 'Use scoped capability discovery, task tools and memory retrieval. The agent writes user-facing responses; workers return task results and orchestration handles delivery.' : buildChannelInstructions(imageEnabled, videoEnabled),
97
+ instructions: ORCHESTRATION_ROLE ? 'Use scoped capability discovery, task tools and memory retrieval. The agent writes user-facing responses; workers return task results and orchestration handles delivery.' + (lazyEnabled ? ' Gateway execution capabilities are available through tool_search and tool_call: search for the capability to read its original arguments, then invoke it by exact name. An unlisted execution tool is not unavailable; inspect the catalog. Task reporting and memory retrieval remain direct tools.' : '') : buildChannelInstructions(imageEnabled, videoEnabled),
83
98
  },
84
99
  );
85
100
 
86
101
  mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
87
- tools: visibleTools,
102
+ tools: advertisedTools,
88
103
  }));
89
104
 
90
105
  mcp.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
91
- const toolName = req.params.name;
106
+ const dispatch = async (toolName: string, args: Record<string, unknown>) => {
107
+ if (ORCHESTRATION_ROLE && process.env.GATEWAY_ORCHESTRATION_TICKET_FILE && taskTools.some(tool => tool.name === toolName)) {
108
+ return callTaskBridge(toolName, args, String(extra.requestId), AbortSignal.any([extra.signal, shutdownController.signal]));
109
+ }
110
+
111
+ const mod = toolMap.get(toolName);
112
+ if (!mod) {
113
+ return {
114
+ content: [{ type: 'text', text: `unknown tool: ${toolName}` }],
115
+ isError: true,
116
+ };
117
+ }
118
+ if (ORCHESTRATION_ROLE === 'worker' && process.env.GATEWAY_ORCHESTRATION_MEDIA === 'true') {
119
+ const validation = await callTaskBridge('task_validate', {}, String(extra.requestId), AbortSignal.any([extra.signal, shutdownController.signal]));
120
+ if (validation.isError) return validation;
121
+ }
122
+
123
+ // extra.signal fires on notifications/cancelled for THIS call (the SDK matches
124
+ // it by requestId) — the CLI sends that when a user Stop/Ctrl-C interrupts an
125
+ // in-flight tool call. Threaded through so a long-running module (image
126
+ // generation's poll loop) can react instead of running to its full timeout.
127
+ // Also combine with shutdownController.signal: the CLI sending that
128
+ // notification is only a "SHOULD" in the MCP spec, not a "MUST", and in
129
+ // practice a Stop that lets the CLI process exit cleanly (rather than
130
+ // staying alive to keep chatting) closes this server's stdin without ever
131
+ // sending notifications/cancelled — extra.signal would then never fire, and
132
+ // an in-flight image generation would poll to its full timeout instead of
133
+ // cancelling. stdin closing is a reliable, protocol-independent signal that
134
+ // the turn is over either way, so it backstops the notification.
135
+ const combinedSignal = AbortSignal.any([extra.signal, shutdownController.signal]);
136
+ return mod.handleTool(toolName, args, combinedSignal);
137
+ };
92
138
  const args = (req.params.arguments ?? {}) as Record<string, unknown>;
93
- if (ORCHESTRATION_ROLE && process.env.GATEWAY_ORCHESTRATION_TICKET_FILE && taskTools.some(tool => tool.name === toolName)) {
94
- return callTaskBridge(toolName, args, String(extra.requestId), AbortSignal.any([extra.signal, shutdownController.signal]));
95
- }
96
-
97
- const mod = toolMap.get(toolName);
98
- if (!mod) {
99
- return {
100
- content: [{ type: 'text', text: `unknown tool: ${toolName}` }],
101
- isError: true,
102
- };
103
- }
104
- if (ORCHESTRATION_ROLE === 'worker' && process.env.GATEWAY_ORCHESTRATION_MEDIA === 'true') {
105
- const validation = await callTaskBridge('task_validate', {}, String(extra.requestId), AbortSignal.any([extra.signal, shutdownController.signal]));
106
- if (validation.isError) return validation;
107
- }
108
-
109
- // extra.signal fires on notifications/cancelled for THIS call (the SDK matches
110
- // it by requestId) — the CLI sends that when a user Stop/Ctrl-C interrupts an
111
- // in-flight tool call. Threaded through so a long-running module (image
112
- // generation's poll loop) can react instead of running to its full timeout.
113
- // Also combine with shutdownController.signal: the CLI sending that
114
- // notification is only a "SHOULD" in the MCP spec, not a "MUST", and in
115
- // practice a Stop that lets the CLI process exit cleanly (rather than
116
- // staying alive to keep chatting) closes this server's stdin without ever
117
- // sending notifications/cancelled — extra.signal would then never fire, and
118
- // an in-flight image generation would poll to its full timeout instead of
119
- // cancelling. stdin closing is a reliable, protocol-independent signal that
120
- // the turn is over either way, so it backstops the notification.
121
- const combinedSignal = AbortSignal.any([extra.signal, shutdownController.signal]);
122
- return mod.handleTool(toolName, args, combinedSignal);
139
+ if (lazyEnabled && req.params.name === 'tool_search') return lazyCatalog.search(args);
140
+ if (lazyEnabled && req.params.name === 'tool_call') return lazyCatalog.call(args, dispatch);
141
+ return dispatch(req.params.name, args);
123
142
  });
124
143
 
125
144
  // Connect MCP transport
@@ -11,6 +11,8 @@ export const SLASH_COMMANDS: SlashCommandDef[] = [
11
11
  description: 'Ask the agent a question',
12
12
  options: [{ name: 'question', description: 'Your question', required: true, type: 'STRING' }],
13
13
  },
14
+ { name: 'clear', description: 'Reset model context without deleting chat history' },
15
+ { name: 'compact', description: 'Compact Claude Code context without changing chat history' },
14
16
  { name: 'session', description: 'Show current session info' },
15
17
  {
16
18
  name: 'new',
@@ -5,18 +5,7 @@ import type { McpToolDefinition, McpToolResult } from '../../types';
5
5
 
6
6
  const schema = (properties: Record<string, unknown>, required: string[]) => ({ type: 'object', properties, required, additionalProperties: false });
7
7
  const text = { type: 'string' };
8
- export const AGENT_TASK_TOOLS: McpToolDefinition[] = [
9
- { name: 'capabilities_list', description: 'Discover all capabilities this agent can offer: installed/shared/native skills, gateway MCP tools and enabled connector/plugin MCP tools, including tools executed only by workers. Read metadata only; never executes a tool or grants permissions. Use query to find relevant tools before assigning work. Follow next_offset until null when the user asks for the complete list, passing catalog_version from the first page. On CAPABILITY_CATALOG_CHANGED restart from offset 0. Report unavailable discovery honestly; do not confuse your own restricted callable tools with the capabilities of the agent as a whole.', inputSchema: schema({query:text,catalog_version:text,offset:{type:'integer',minimum:0}},[]) },
10
- { name: 'conversation_intake', description: 'Prepare executable work, incomplete materials, or an amendment to an existing task. Do not call this for greetings, introductions, casual conversation or questions you can answer directly; answer those normally once. When semantic intake is enabled, acknowledge before work mutations. Incomplete material is inspected now; the gateway asks the supplied clarification after the configured silence interval, not on every item.',
11
- inputSchema: schema({mode:{type:'string',enum:['ready','wait','update']},acknowledgement:{type:'string',description:'A brief contextual message addressed directly to the user about the work you will do. Immediately displayed and spoken aloud. Never include private classification, reasoning, or instructions to yourself.'},preparation:text,clarification:text,task_id:text},['mode']) },
12
- { name: 'task_spawn', description: 'Durably queue a task for an independent worker. Returns after commit without waiting for worker startup or completion.',
13
- inputSchema: schema({ title: text, instructions: text, target_profile: text, spoken_acknowledgement: { type: 'string', description: 'Required for live voice: a short natural response in the user language explaining the specific work you are about to do. Describe the specific user request as your own task, in the voice of your existing persona; do not announce handing it to a worker or use a generic acknowledgement. Speak as the person doing the work; avoid Thai delegation phrases such as สั่งเช็คแล้ว, สั่งไปแล้ว or ส่งให้ตรวจแล้ว. Describe queued work as accepted/to do next, and only say it is running when that state is confirmed. Do not claim completion. The gateway manages playback and channel text-before-voice ordering.' }, skill_name: { type: 'string', description: 'Exact installed gateway or discovered Claude Code runtime skill name for skill-worker. Native skills are invoked directly by the CLI; shared/workspace skills are supplied with their resources. Never supply a file path.' }, skill_args: { type: 'string', description: 'Arguments for the installed skill; required with skill_name.' }, continue_task_id: { type: 'string', description: 'Prior task ID in this conversation for related follow-up work. Reuses its worker session when compatible; waits for success by default. Queue every authorized dependent step now, using each returned task ID for the next step. Omit for independent work.' }, continuation_policy: { type: 'string', enum: ['after_success', 'after_terminal'], description: 'With continue_task_id: after_success (default) starts only after the predecessor succeeds; otherwise fails without executing. Use after_terminal only for explicitly authorized recovery/inspection that must run after failure or cancellation.' }, context_refs: { type: 'array', items: text, description: 'Only orchestration input IDs, authorized attachment refs or task artifact IDs. Put project filenames in instructions instead.' } }, ['title', 'instructions', 'target_profile']) },
14
- { name: 'task_status', description: 'Read the task index in this conversation without waiting. Pass task_id to retrieve the complete stored result and evidence before discussing an earlier task. An index entry is not a partial worker report.', inputSchema: schema({ task_id: text }, []) },
15
- { name: 'task_cancel', description: 'Request cancellation, including needs_reconciliation cleanup. cancel_requested is not proof of termination. Existing files are retained. Set replaced_by_task_id only for a known replacement task, never infer from similar titles.', inputSchema: schema({ task_id: text, replaced_by_task_id: text }, ['task_id']) },
16
- { name: 'task_update', description: 'Accept a revised task instruction. applied_revision changes only when the worker accepts it.', inputSchema: schema({ task_id: text, expected_revision: { type: 'integer', minimum: 1 }, instruction: text, mode: { type: 'string', enum: ['when_ready', 'interrupt_and_resume'] } }, ['task_id', 'expected_revision', 'instruction', 'mode']) },
17
- { name: 'task_question', description: 'Manage pending questions without answering or resuming work. Ask stages one natural separate message after your reply; discuss marks ongoing consultation; defer/mute/resume persist the user reminder preference.', inputSchema: schema({ action: { type: 'string', enum: ['ask','discuss','defer','mute','resume'] }, question_ids: { type: 'array', items: text, minItems: 1, maxItems: 20 }, text, delay_ms: { type: 'integer', minimum: 60000, maximum: 2592000000 } }, ['action', 'question_ids']) },
18
- { name: 'task_answer', description: 'Answer the matching pending question on a task.', inputSchema: schema({ task_id: text, question_id: text, answer: text }, ['task_id', 'question_id', 'answer']) },
19
- ];
8
+ export { AGENT_TASK_TOOLS } from '../../../dist/orchestration/agent-tool-schemas.js';
20
9
  export const WORKER_REPORT_TOOLS: McpToolDefinition[] = [
21
10
  { name: 'task_memory_append', description: 'Append an explicitly requested memory note for a channel task. API tasks cannot write memory. Files are restricted to MEMORY.md, USER.md, or memory/<name>.md. Retains existing content and deduplicates retries.', inputSchema: schema({ note: text, path: text }, ['note']) },
22
11
  { name: 'task_stage_file', description: 'Stage a finished image or document for the user. The orchestration attaches it to the next completed agent response after this task succeeds, using the original destination. This tool does not send to a channel. Call once per output file. For an image returned by an MCP tool (such as a remote browser screenshot), omit path to use the latest captured image in this attempt, or pass source_tool_call_id to select its tool call. The gateway saves the actual image bytes; never invent a screenshot filename. For an existing local file supply path.', inputSchema: schema({ path: text, source_tool_call_id: text, caption: text }, []) },
@@ -877,8 +877,8 @@ const BOT_COMMANDS = [
877
877
  { command: 'sessions', description: 'Manage conversation sessions' },
878
878
  { command: 'new', description: 'Create a new session' },
879
879
  { command: 'rename', description: 'Rename current session' },
880
- { command: 'clear', description: 'Clear current session history' },
881
- { command: 'compact', description: 'Summarize and compress session history' },
880
+ { command: 'clear', description: 'Reset Claude Code context; keep chat history' },
881
+ { command: 'compact', description: 'Compact Claude Code context' },
882
882
  { command: 'stop', description: ORCHESTRATION_ENABLED ? 'Stop the agent reply and choose a task to cancel' : 'Interrupt the agent and stop current work' },
883
883
  { command: 'restart', description: 'Graceful restart session' },
884
884
  { command: 'model', description: 'Show current AI model' },
@@ -1169,8 +1169,8 @@ bot.command('help', async ctx => {
1169
1169
  `/sessions — list and switch between sessions\n` +
1170
1170
  `/new <name> — create a new session\n` +
1171
1171
  `/rename <name> — rename current session\n` +
1172
- `/clear — clear current session history\n` +
1173
- `/compact — summarise and compress session history\n` +
1172
+ `/clear — reset context; keep chat history\n` +
1173
+ `/compact — compact Claude Code context; keep chat history\n` +
1174
1174
  (ORCHESTRATION_ENABLED ? `/stop — stop the reply and choose a task to cancel\n` : `/stop — interrupt the running turn\n`) +
1175
1175
  `/restart — graceful restart session\n\n` +
1176
1176
  `*Agent*\n` +
@@ -1355,7 +1355,7 @@ bot.command('compact', async ctx => {
1355
1355
  .text('\u274c Cancel', 'compact:cancel')
1356
1356
 
1357
1357
  await ctx.reply(
1358
- '🧠 Compact session?\nThis will summarise old messages and keep only recent history.',
1358
+ '🧠 Compact session?\nThis compacts Claude Code context. Your chat history stays unchanged.',
1359
1359
  { reply_markup: keyboard },
1360
1360
  )
1361
1361
  })
@@ -1583,18 +1583,18 @@ bot.command('sessions', async ctx => {
1583
1583
  }
1584
1584
  })
1585
1585
 
1586
- // /clear — show Yes/No confirmation before clearing session history
1586
+ // /clear — confirm a fresh CLI context without deleting history
1587
1587
  bot.command('clear', async ctx => {
1588
1588
  if (ctx.chat?.type !== 'private') return
1589
1589
  const access = loadAccess()
1590
1590
  if (!access.allowFrom.includes(String(ctx.from!.id))) return
1591
1591
 
1592
1592
  await ctx.reply(
1593
- '🗑️ Clear session?\n\nThis will delete all message history. This cannot be undone.',
1593
+ 'Reset Claude Code context?\n\nChat history and attachments stay unchanged. Your next message starts a new context with the latest 50 messages.',
1594
1594
  {
1595
1595
  reply_markup: {
1596
1596
  inline_keyboard: [[
1597
- { text: '✅ Yes, clear it', callback_data: 'session_clear_confirm' },
1597
+ { text: '✅ Reset context', callback_data: 'session_clear_confirm' },
1598
1598
  { text: '❌ Cancel', callback_data: 'session_clear_cancel' },
1599
1599
  ]],
1600
1600
  },
@@ -1880,10 +1880,10 @@ bot.on('callback_query:data', async ctx => {
1880
1880
  payload: { model: newModel },
1881
1881
  }),
1882
1882
  })
1883
- const result = (await res.json()) as { success?: boolean; error?: string; restarted?: boolean }
1883
+ const result = (await res.json()) as { success?: boolean; error?: string; restarted?: boolean; appliesNextTurn?: boolean }
1884
1884
  if (result.success) {
1885
1885
  await ctx.answerCallbackQuery({ text: `✅ Model changed to ${newModel}` }).catch(() => {})
1886
- await ctx.editMessageText(`\u2705 Model changed to ${newModel}`).catch(() => {})
1886
+ await ctx.editMessageText(`\u2705 Model changed to ${newModel}${result.appliesNextTurn ? "\nApplies to the next response; current work continues." : ""}`).catch(() => {})
1887
1887
  } else {
1888
1888
  await ctx.answerCallbackQuery({ text: result.error ?? 'Failed' }).catch(() => {})
1889
1889
  }
@@ -1920,7 +1920,7 @@ bot.on('callback_query:data', async ctx => {
1920
1920
  chat_id: String(ctx.callbackQuery.message?.chat.id),
1921
1921
  }),
1922
1922
  })
1923
- const result = (await res.json()) as { success?: boolean; error?: string; restarted?: boolean }
1923
+ const result = (await res.json()) as { success?: boolean; error?: string; restarted?: boolean; appliesNextTurn?: boolean }
1924
1924
  if (result.success) {
1925
1925
  if (result.restarted === false) {
1926
1926
  // No active session — nothing to restart
@@ -2085,8 +2085,8 @@ bot.on('callback_query:data', async ctx => {
2085
2085
  }
2086
2086
  const chatId = String(ctx.callbackQuery.message?.chat.id)
2087
2087
  try {
2088
- await ctx.answerCallbackQuery({ text: 'Clearing...' }).catch(() => {})
2089
- await ctx.editMessageText('\u23f3 Clearing session...').catch(() => {})
2088
+ await ctx.answerCallbackQuery({ text: 'Resetting context...' }).catch(() => {})
2089
+ await ctx.editMessageText('\u23f3 Resetting context...').catch(() => {})
2090
2090
  const res = await fetch(CALLBACK_URL_BASE + '/command', {
2091
2091
  method: 'POST',
2092
2092
  headers: { 'Content-Type': 'application/json' },
@@ -2094,7 +2094,7 @@ bot.on('callback_query:data', async ctx => {
2094
2094
  })
2095
2095
  const result = (await res.json()) as { success?: boolean; error?: string }
2096
2096
  if (result.success) {
2097
- await ctx.reply('\uD83D\uDCA1 Session has been cleared').catch(() => {})
2097
+ await ctx.reply('\uD83D\uDCA1 Claude Code context reset. Chat history is unchanged. The next message loads the latest 50 messages.').catch(() => {})
2098
2098
  } else {
2099
2099
  await ctx.reply(`\u274C Clear failed: ${result.error ?? 'Unknown error'}`).catch(() => {})
2100
2100
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xmaxma/claude-gateway",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "description": "Multi-agent gateway for Claude",
5
5
  "repository": {
6
6
  "type": "git",
@@ -52,6 +52,7 @@
52
52
  "chokidar": "^3.5.0",
53
53
  "cron-parser": "^5.5.0",
54
54
  "express": "^4.18.0",
55
+ "js-tiktoken": "^1.0.21",
55
56
  "js-yaml": "^4.1.0",
56
57
  "lru-cache": "^10.0.0",
57
58
  "node-cron": "^3.0.0",
@@ -0,0 +1,93 @@
1
+ Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.