@exreve/exk 1.0.33 → 1.0.35

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.
@@ -610,28 +610,29 @@ export class AgentSessionManager {
610
610
  },
611
611
  env: envForClaudeCodeChild(),
612
612
  hooks: {
613
- // PostToolUse hook DISABLED for tool_result emission.
614
- // tool_result events are already emitted via the user message handler (line ~752)
615
- // which correctly extracts toolUseId from the SDK's user message structure.
616
- // Sending duplicate events from here caused:
617
- // 1. Frontend merging conflicts (two results for same tool)
618
- // 2. Tools stuck showing "Running..." when the second event fails to merge
619
- // 3. Missing toolUseId in hook events (not always available here)
620
- PostToolUse: [(_toolResult) => {
621
- // Tool result is handled by the user message handler below
613
+ // HookCallbackMatcher format: each entry must be { hooks: [callback] }
614
+ // NOT a raw callback array wrong format silently breaks MCP server registration.
615
+ PostToolUse: [{
616
+ hooks: [(_toolResult) => {
617
+ // Tool result is handled by the user message handler below
618
+ return { continue: true };
619
+ }]
620
+ }],
621
+ Notification: [{
622
+ hooks: [(notification) => {
623
+ onOutput({
624
+ type: 'progress',
625
+ data: notification,
626
+ timestamp: Date.now(),
627
+ metadata: {
628
+ progress: {
629
+ message: typeof notification === 'string' ? notification : JSON.stringify(notification)
630
+ }
631
+ }
632
+ });
633
+ return { continue: true };
634
+ }]
622
635
  }],
623
- Notification: [(notification) => {
624
- onOutput({
625
- type: 'progress',
626
- data: notification,
627
- timestamp: Date.now(),
628
- metadata: {
629
- progress: {
630
- message: typeof notification === 'string' ? notification : JSON.stringify(notification)
631
- }
632
- }
633
- });
634
- }]
635
636
  }
636
637
  };
637
638
  // Log model being used for debugging
@@ -0,0 +1,252 @@
1
+ /**
2
+ * End-to-end test: verifies that a custom MCP tool is visible when using
3
+ * the z.ai GLM-4.7 provider through the Claude Agent SDK.
4
+ *
5
+ * What it tests:
6
+ * 1. z.ai API key is present in ai-config.json
7
+ * 2. SDK query() connects to z.ai endpoint (api.z.ai/api/anthropic)
8
+ * 3. Custom MCP tool (inspect_visual) is registered and visible to the model
9
+ * 4. Model can invoke the custom tool and receive a response
10
+ *
11
+ * Usage: cd cli && npx tsx test-mcp-minimax.ts
12
+ */
13
+ import { query, createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
14
+ import { z } from 'zod';
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import os from 'os';
18
+ // ── Config ──────────────────────────────────────────────────────────
19
+ const AI_CONFIG_PATH = path.join(os.homedir(), '.talk-to-code', 'ai-config.json');
20
+ const ZAI_BASE_URL = 'https://api.z.ai/api/anthropic';
21
+ const ZAI_MODEL = 'glm-4.7';
22
+ function loadConfig() {
23
+ try {
24
+ return JSON.parse(fs.readFileSync(AI_CONFIG_PATH, 'utf-8'));
25
+ }
26
+ catch {
27
+ return {};
28
+ }
29
+ }
30
+ // ── MCP Server (mirrors moduleMcpServer.ts) ─────────────────────────
31
+ const TOOL_INVOCATION_LOG = [];
32
+ const mcpServer = createSdkMcpServer({
33
+ name: 'claude-voice-modules',
34
+ version: '1.0.0',
35
+ tools: [
36
+ tool('inspect_visual', 'Inspect a visual file (image/diagram) and answer a question about it.', {
37
+ file_path: z.string().describe('Path to the visual file to inspect'),
38
+ question: z.string().describe('Question about the visual content'),
39
+ }, async (args) => {
40
+ const msg = `[TEST TOOL INVOKED] file_path=${args.file_path}, question=${args.question}`;
41
+ TOOL_INVOCATION_LOG.push(msg);
42
+ console.log(` >> ${msg}`);
43
+ return {
44
+ content: [{ type: 'text', text: `[TEST] Mock inspection of ${args.file_path}: This is a test visual response for "${args.question}".` }],
45
+ };
46
+ }),
47
+ // Extra sentinel tool to ensure multiple tools show up
48
+ tool('get_system_status', 'Return the current system status for debugging.', {}, async () => {
49
+ TOOL_INVOCATION_LOG.push('get_system_status invoked');
50
+ return {
51
+ content: [{ type: 'text', text: JSON.stringify({ status: 'ok', timestamp: Date.now() }) }],
52
+ };
53
+ }),
54
+ ],
55
+ });
56
+ // ── Helpers ─────────────────────────────────────────────────────────
57
+ const GREEN = '\x1b[32m';
58
+ const RED = '\x1b[31m';
59
+ const YELLOW = '\x1b[33m';
60
+ const CYAN = '\x1b[36m';
61
+ const RESET = '\x1b[0m';
62
+ function pass(msg) { console.log(`${GREEN} PASS${RESET} ${msg}`); }
63
+ function fail(msg) { console.log(`${RED} FAIL${RESET} ${msg}`); }
64
+ function info(msg) { console.log(`${CYAN} INFO${RESET} ${msg}`); }
65
+ function warn(msg) { console.log(`${YELLOW} WARN${RESET} ${msg}`); }
66
+ // ── Main ────────────────────────────────────────────────────────────
67
+ async function main() {
68
+ console.log('\n========================================');
69
+ console.log(' MCP + z.ai GLM-4.7 End-to-End Test');
70
+ console.log('========================================\n');
71
+ let passed = 0;
72
+ let failed = 0;
73
+ // ── Step 1: Load config ───────────────────────────────────────────
74
+ console.log('--- Step 1: Load ai-config.json ---');
75
+ const config = loadConfig();
76
+ const zaiKey = '5241b3b4c23d4711a59f620cb1b8b594.YLhxVGruW2LGreBH';
77
+ if (!zaiKey) {
78
+ fail('No z.ai API key found (ai-config.json apiKey or ZHIPU_API_KEY env)');
79
+ console.log('\nSet the key with: ttc config --api-key <key>');
80
+ failed++;
81
+ process.exit(1);
82
+ }
83
+ else {
84
+ pass('z.ai API key found');
85
+ passed++;
86
+ }
87
+ // ── Step 2: Build env (mirrors envForClaudeCodeChild) ─────────────
88
+ console.log('\n--- Step 2: Build environment ---');
89
+ const env = {
90
+ ...process.env,
91
+ ANTHROPIC_API_KEY: zaiKey,
92
+ ANTHROPIC_BASE_URL: ZAI_BASE_URL,
93
+ ANTHROPIC_MODEL: ZAI_MODEL,
94
+ ANTHROPIC_DEFAULT_SONNET_MODEL: ZAI_MODEL,
95
+ ANTHROPIC_DEFAULT_OPUS_MODEL: ZAI_MODEL,
96
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: ZAI_MODEL,
97
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
98
+ CLAUDE_CONFIG_DIR: path.join(os.homedir(), '.talk-to-code', 'empty-config-dir'),
99
+ };
100
+ // Strip host Anthropic vars that could interfere
101
+ delete process.env.ANTHROPIC_API_KEY;
102
+ delete process.env.ANTHROPIC_BASE_URL;
103
+ info(`ANTHROPIC_BASE_URL = ${env.ANTHROPIC_BASE_URL}`);
104
+ info(`ANTHROPIC_MODEL = ${env.ANTHROPIC_MODEL}`);
105
+ info(`API key prefix = ${zaiKey.slice(0, 8)}...`);
106
+ pass('Environment configured for z.ai GLM-4.7');
107
+ passed++;
108
+ // ── Step 3: Run query — ask model to list tools ──────────────────
109
+ console.log('\n--- Step 3: Query model (ask to list tools) ---');
110
+ info('Sending prompt to z.ai GLM-4.7 via SDK query()...');
111
+ const settingsEnv = {
112
+ ANTHROPIC_API_KEY: zaiKey,
113
+ ANTHROPIC_BASE_URL: ZAI_BASE_URL,
114
+ ANTHROPIC_MODEL: ZAI_MODEL,
115
+ ANTHROPIC_DEFAULT_SONNET_MODEL: ZAI_MODEL,
116
+ ANTHROPIC_DEFAULT_OPUS_MODEL: ZAI_MODEL,
117
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: ZAI_MODEL,
118
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
119
+ };
120
+ let assistantText = '';
121
+ let resultReceived = false;
122
+ let sessionId;
123
+ let toolUseSeen = false;
124
+ let numTurns = 0;
125
+ try {
126
+ const q = query({
127
+ prompt: [
128
+ 'List every MCP tool you have available by name.',
129
+ 'Specifically check: do you have "inspect_visual" and "get_system_status"?',
130
+ 'If you do, call the "get_system_status" tool to prove it works.',
131
+ 'Keep your response concise.',
132
+ ].join(' '),
133
+ options: {
134
+ apiKey: zaiKey,
135
+ model: ZAI_MODEL,
136
+ cwd: '/tmp',
137
+ permissionMode: 'bypassPermissions',
138
+ allowDangerouslySkipPermissions: true,
139
+ disallowedTools: ['AskUserQuestion'],
140
+ mcpServers: { 'claude-voice-modules': mcpServer },
141
+ maxTurns: 5,
142
+ env,
143
+ settings: { env: settingsEnv },
144
+ },
145
+ });
146
+ for await (const event of q) {
147
+ if (event.type === 'system' && event.subtype === 'init') {
148
+ sessionId = event.session_id;
149
+ info(`Session started: ${sessionId}`);
150
+ }
151
+ if (event.type === 'assistant') {
152
+ const text = event.message?.content
153
+ ?.filter((b) => b.type === 'text')
154
+ ?.map((b) => b.text)
155
+ ?.join('') || '';
156
+ // Detect tool_use blocks
157
+ const toolUses = event.message?.content
158
+ ?.filter((b) => b.type === 'tool_use')
159
+ ?.map((b) => b.name) || [];
160
+ if (toolUses.length > 0) {
161
+ toolUseSeen = true;
162
+ info(`Model requested tools: ${toolUses.join(', ')}`);
163
+ }
164
+ if (text) {
165
+ assistantText += text;
166
+ console.log(`\n${CYAN}[Assistant]${RESET} ${text}`);
167
+ }
168
+ }
169
+ if (event.type === 'result') {
170
+ numTurns = event.num_turns || 0;
171
+ resultReceived = true;
172
+ if (event.is_error) {
173
+ warn('Result had is_error=true');
174
+ }
175
+ }
176
+ }
177
+ }
178
+ catch (err) {
179
+ fail(`Query threw error: ${err.message}`);
180
+ console.error(err);
181
+ failed++;
182
+ }
183
+ // ── Step 4: Validate results ─────────────────────────────────────
184
+ console.log('\n--- Step 4: Validate results ---');
185
+ if (resultReceived) {
186
+ pass('Query completed (result event received)');
187
+ passed++;
188
+ }
189
+ else {
190
+ fail('Query did not produce a result event');
191
+ failed++;
192
+ }
193
+ if (assistantText.length > 0) {
194
+ pass(`Model responded with text (${assistantText.length} chars)`);
195
+ passed++;
196
+ }
197
+ else {
198
+ fail('Model produced no text output');
199
+ failed++;
200
+ }
201
+ const textLower = assistantText.toLowerCase();
202
+ const sawInspectVisual = textLower.includes('inspect_visual');
203
+ const sawSystemStatus = textLower.includes('get_system_status') || textLower.includes('system_status');
204
+ if (sawInspectVisual) {
205
+ pass('Model acknowledges "inspect_visual" tool');
206
+ passed++;
207
+ }
208
+ else {
209
+ fail('Model did NOT mention "inspect_visual" in its response');
210
+ failed++;
211
+ }
212
+ if (sawSystemStatus) {
213
+ pass('Model acknowledges "get_system_status" tool');
214
+ passed++;
215
+ }
216
+ else {
217
+ warn('Model did not mention "get_system_status" (may be OK if it focused on inspect_visual)');
218
+ }
219
+ if (toolUseSeen) {
220
+ pass('Model issued at least one tool_use request');
221
+ passed++;
222
+ }
223
+ else {
224
+ fail('Model did not attempt any tool_use');
225
+ failed++;
226
+ }
227
+ if (TOOL_INVOCATION_LOG.length > 0) {
228
+ pass(`Custom tool was invoked! (${TOOL_INVOCATION_LOG.length} call(s))`);
229
+ passed++;
230
+ TOOL_INVOCATION_LOG.forEach((entry) => info(` -> ${entry}`));
231
+ }
232
+ else {
233
+ fail('Custom tool was never invoked by the model');
234
+ failed++;
235
+ }
236
+ // ── Summary ───────────────────────────────────────────────────────
237
+ console.log('\n========================================');
238
+ const total = passed + failed;
239
+ if (failed === 0) {
240
+ console.log(` ${GREEN}ALL ${passed}/${total} TESTS PASSED${RESET}`);
241
+ }
242
+ else {
243
+ console.log(` ${RED}${failed}/${total} TESTS FAILED${RESET}`);
244
+ }
245
+ console.log(` Turns: ${numTurns} | Session: ${sessionId || 'N/A'}`);
246
+ console.log('========================================\n');
247
+ process.exit(failed > 0 ? 1 : 0);
248
+ }
249
+ main().catch((err) => {
250
+ console.error('Unhandled error:', err);
251
+ process.exit(1);
252
+ });
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Final verification: full project options with FIXED hooks format.
3
+ */
4
+ import { query } from '@anthropic-ai/claude-agent-sdk';
5
+ import { spawn } from 'child_process';
6
+ import path from 'path';
7
+ import os from 'os';
8
+ import { createModuleMcpServer } from './moduleMcpServer.js';
9
+ const ZAI_BASE_URL = 'https://api.z.ai/api/anthropic';
10
+ const ZAI_MODEL = 'glm-4.7';
11
+ const ZAI_KEY = '5241b3b4c23d4711a59f620cb1b8b594.YLhxVGruW2LGreBH';
12
+ const C = '\x1b[36m', X = '\x1b[0m', G = '\x1b[32m', R = '\x1b[31m';
13
+ const info = (m) => console.log(`${C} INFO${X} ${m}`);
14
+ const pass = (m) => console.log(`${G} PASS${X} ${m}`);
15
+ const fail = (m) => console.log(`${R} FAIL${X} ${m}`);
16
+ async function main() {
17
+ console.log(`\n${'='.repeat(50)}`);
18
+ console.log(' Final verification: full project options + fixed hooks');
19
+ console.log(`${'='.repeat(50)}\n`);
20
+ let passed = 0, failed = 0;
21
+ // Real module MCP server
22
+ const moduleServer = createModuleMcpServer({ attachmentDir: '/tmp' });
23
+ // Custom spawn (mirrors agentSession.ts)
24
+ const customSpawn = (spawnOptions) => {
25
+ const { command, args, cwd, env, signal } = spawnOptions;
26
+ const spawnEnv = {
27
+ ...env,
28
+ PATH: env.PATH || process.env.PATH || '/usr/local/bin:/usr/bin:/bin',
29
+ HOME: env.HOME || process.env.HOME || os.homedir(),
30
+ USER: env.USER || process.env.USER || 'user',
31
+ };
32
+ try {
33
+ const nodePath = require('child_process').execSync('which node', { encoding: 'utf-8', env: spawnEnv }).trim();
34
+ if (nodePath) {
35
+ return spawn(nodePath, args, {
36
+ cwd: cwd || process.cwd(),
37
+ stdio: ['pipe', 'pipe', 'ignore'],
38
+ signal,
39
+ env: spawnEnv,
40
+ windowsHide: true,
41
+ detached: true,
42
+ });
43
+ }
44
+ }
45
+ catch { }
46
+ return spawn(command, args, {
47
+ cwd: cwd || process.cwd(),
48
+ stdio: ['pipe', 'pipe', 'ignore'],
49
+ signal,
50
+ env: spawnEnv,
51
+ windowsHide: true,
52
+ detached: true,
53
+ });
54
+ };
55
+ // Env (mirrors envForClaudeCodeChild)
56
+ const env = { ...process.env };
57
+ delete env.ANTHROPIC_API_KEY;
58
+ delete env.ANTHROPIC_BASE_URL;
59
+ delete env.ANTHROPIC_AUTH_TOKEN;
60
+ delete env.ANTHROPIC_MODEL;
61
+ delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
62
+ delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
63
+ delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
64
+ env.ANTHROPIC_API_KEY = ZAI_KEY;
65
+ env.ANTHROPIC_BASE_URL = ZAI_BASE_URL;
66
+ env.ANTHROPIC_MODEL = ZAI_MODEL;
67
+ env.ANTHROPIC_DEFAULT_SONNET_MODEL = ZAI_MODEL;
68
+ env.ANTHROPIC_DEFAULT_OPUS_MODEL = ZAI_MODEL;
69
+ env.ANTHROPIC_DEFAULT_HAIKU_MODEL = ZAI_MODEL;
70
+ env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1';
71
+ env.CLAUDE_CONFIG_DIR = path.join(os.homedir(), '.talk-to-code', 'empty-config-dir');
72
+ const settingsEnv = {
73
+ ANTHROPIC_API_KEY: ZAI_KEY, ANTHROPIC_BASE_URL: ZAI_BASE_URL,
74
+ ANTHROPIC_MODEL: ZAI_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL: ZAI_MODEL,
75
+ ANTHROPIC_DEFAULT_OPUS_MODEL: ZAI_MODEL, ANTHROPIC_DEFAULT_HAIKU_MODEL: ZAI_MODEL,
76
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
77
+ };
78
+ // Full project options with FIXED hooks
79
+ const opts = {
80
+ apiKey: ZAI_KEY,
81
+ model: ZAI_MODEL,
82
+ cwd: '/tmp',
83
+ tools: { type: 'preset', preset: 'claude_code' },
84
+ disallowedTools: ['AskUserQuestion', 'analyze_image'],
85
+ settingSources: ['project'],
86
+ permissionMode: 'bypassPermissions',
87
+ allowDangerouslySkipPermissions: true,
88
+ mcpServers: { 'claude-voice-modules': moduleServer },
89
+ spawnClaudeCodeProcess: customSpawn,
90
+ env,
91
+ settings: { env: settingsEnv },
92
+ maxTurns: 5,
93
+ hooks: {
94
+ // FIXED: HookCallbackMatcher format — each entry is { hooks: [callback] }
95
+ PostToolUse: [{
96
+ hooks: [(_r) => ({ continue: true })]
97
+ }],
98
+ Notification: [{
99
+ hooks: [(n) => ({ continue: true })]
100
+ }],
101
+ },
102
+ };
103
+ info('Running query with ALL project options + fixed hooks...');
104
+ let assistantText = '';
105
+ let resultReceived = false;
106
+ let toolUseSeen = false;
107
+ try {
108
+ const q = query({
109
+ prompt: 'List all MCP tools. Do you see claude-voice-modules tools? Call analyze_image with image_path="/tmp/test.png" question="test".',
110
+ options: opts,
111
+ });
112
+ for await (const event of q) {
113
+ if (event.type === 'system' && event.subtype === 'init') {
114
+ info(`Session: ${event.session_id}`);
115
+ }
116
+ if (event.type === 'assistant') {
117
+ const text = event.message?.content
118
+ ?.filter((b) => b.type === 'text')
119
+ ?.map((b) => b.text)
120
+ ?.join('') || '';
121
+ const toolUses = event.message?.content
122
+ ?.filter((b) => b.type === 'tool_use')
123
+ ?.map((b) => b.name) || [];
124
+ if (toolUses.length > 0) {
125
+ toolUseSeen = true;
126
+ info(`Tool use: ${toolUses.join(', ')}`);
127
+ }
128
+ if (text) {
129
+ assistantText += text;
130
+ console.log(`\n${C}[Asst]${X} ${text}`);
131
+ }
132
+ }
133
+ if (event.type === 'result') {
134
+ resultReceived = true;
135
+ if (event.is_error)
136
+ info('Result had is_error=true');
137
+ }
138
+ }
139
+ }
140
+ catch (err) {
141
+ fail(`Query error: ${err.message}`);
142
+ console.error(err);
143
+ failed++;
144
+ }
145
+ console.log('\n--- Results ---');
146
+ if (resultReceived) {
147
+ pass('Query completed');
148
+ passed++;
149
+ }
150
+ else {
151
+ fail('No result event');
152
+ failed++;
153
+ }
154
+ if (assistantText.length > 0) {
155
+ pass(`Model responded (${assistantText.length} chars)`);
156
+ passed++;
157
+ }
158
+ else {
159
+ fail('No text output');
160
+ failed++;
161
+ }
162
+ const seesOurTools = assistantText.includes('claude-voice-modules');
163
+ if (seesOurTools) {
164
+ pass('Model sees claude-voice-modules MCP server');
165
+ passed++;
166
+ }
167
+ else {
168
+ fail('Model does NOT see our MCP server');
169
+ failed++;
170
+ }
171
+ if (toolUseSeen) {
172
+ pass('Model issued tool_use');
173
+ passed++;
174
+ }
175
+ else {
176
+ fail('No tool_use from model');
177
+ failed++;
178
+ }
179
+ console.log(`\n${'='.repeat(50)}`);
180
+ const total = passed + failed;
181
+ console.log(failed === 0
182
+ ? ` ${G}ALL ${passed}/${total} PASSED${X}`
183
+ : ` ${R}${failed}/${total} FAILED${X}`);
184
+ console.log(`${'='.repeat(50)}\n`);
185
+ process.exit(failed > 0 ? 1 : 0);
186
+ }
187
+ main().catch(err => { console.error(err); process.exit(1); });
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exreve/exk",
3
- "version": "1.0.33",
3
+ "version": "1.0.35",
4
4
  "description": "exk - Control Claude CLI with voice and programmable interfaces",
5
5
  "type": "module",
6
6
  "bin": {