@exreve/exk 1.0.36 → 1.0.37

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/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { agentSessionManager } from './agentSession.js';
13
13
  import { getProjectConfig, analyzeProjectWithClaude, saveProjectConfig } from './projectAnalyzer.js';
14
14
  import { generateRunnerCode } from './runnerGenerator.js';
15
15
  import { startApp, stopApp, restartApp, getAppStatuses, getAppLogs } from './appManager.js';
16
+ import { startSending, startReceiving } from './transferService.js';
16
17
  import { spawn, execSync } from 'child_process';
17
18
  import readline from 'readline';
18
19
  import { fileURLToPath } from 'url';
@@ -1544,6 +1545,20 @@ async function runDaemon(foreground = false, email) {
1544
1545
  console.log(`[CLI] Emitted error app:control:response`);
1545
1546
  }
1546
1547
  });
1548
+ // Handle folder transfer — this device is the source (sender)
1549
+ socket.on('transfer:send', (data) => {
1550
+ if (foreground) {
1551
+ console.log(`[transfer] Send request: ${data.sourcePath} → device ${data.destDeviceId.slice(0, 8)}`);
1552
+ }
1553
+ startSending(socket, data, foreground);
1554
+ });
1555
+ // Handle folder transfer — this device is the destination (receiver)
1556
+ socket.on('transfer:receive', (data) => {
1557
+ if (foreground) {
1558
+ console.log(`[transfer] Receive request: device ${data.sourceDeviceId.slice(0, 8)} → ${data.destPath}`);
1559
+ }
1560
+ startReceiving(socket, data, foreground);
1561
+ });
1547
1562
  socket.on('connect', () => {
1548
1563
  if (foreground) {
1549
1564
  console.log(`✓ Connected to backend at ${config.apiUrl}`);
@@ -0,0 +1,250 @@
1
+ import { spawn } from 'child_process';
2
+ import { mkdir, unlink } from 'fs/promises';
3
+ import fsSync from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import { createHash } from 'crypto';
7
+ // ============ Transfer Service (CLI side) ============
8
+ // Handles both sending and receiving folders via tar streaming.
9
+ // Binary chunks flow over Socket.IO — no HTTP endpoints needed.
10
+ const CHUNK_SIZE = 1024 * 1024; // 1MB chunks
11
+ // ---- Sender ----
12
+ export function startSending(socket, transfer, foreground) {
13
+ const { transferId, sourcePath } = transfer;
14
+ const startTime = Date.now();
15
+ const log = (message) => {
16
+ if (foreground)
17
+ console.log(`[transfer:send] ${message}`);
18
+ socket.emit('transfer:log', { transferId, side: 'source', level: 'info', message });
19
+ };
20
+ const sendProgress = (bytesTransferred, totalBytes) => {
21
+ const elapsed = (Date.now() - startTime) / 1000;
22
+ const speed = elapsed > 0 ? bytesTransferred / elapsed : 0;
23
+ const eta = speed > 0 ? (totalBytes - bytesTransferred) / speed : 0;
24
+ socket.emit('transfer:progress', {
25
+ transferId,
26
+ side: 'source',
27
+ bytesTransferred,
28
+ totalBytes,
29
+ speed: Math.round(speed),
30
+ eta: Math.round(eta),
31
+ });
32
+ };
33
+ const sendError = (message) => {
34
+ console.error(`[transfer:send] Error: ${message}`);
35
+ socket.emit('transfer:error', { transferId, side: 'source', message });
36
+ };
37
+ log(`Starting to tar and send: ${sourcePath}`);
38
+ // Estimate folder size first with a quick du
39
+ let totalBytes = 0;
40
+ try {
41
+ const duResult = spawn('du', ['-sb', sourcePath], { timeout: 30000 });
42
+ let duOutput = '';
43
+ duResult.stdout.on('data', (d) => { duOutput += d.toString(); });
44
+ duResult.on('close', (code) => {
45
+ if (code === 0) {
46
+ const match = duOutput.match(/^(\d+)/);
47
+ if (match)
48
+ totalBytes = parseInt(match[1], 10);
49
+ }
50
+ log(`Estimated size: ${formatBytes(totalBytes)}`);
51
+ doSend();
52
+ });
53
+ duResult.on('error', () => doSend());
54
+ }
55
+ catch {
56
+ doSend();
57
+ }
58
+ function doSend() {
59
+ // Spawn tar to stream the folder
60
+ const tar = spawn('tar', ['cf', '-', '-C', sourcePath, '.']);
61
+ const hash = createHash('sha256');
62
+ let bytesTransferred = 0;
63
+ let seq = 0;
64
+ let lastProgressTime = Date.now();
65
+ tar.stdout.on('data', (chunk) => {
66
+ hash.update(chunk);
67
+ bytesTransferred += chunk.length;
68
+ // Emit binary chunk
69
+ socket.emit('transfer:chunk', {
70
+ transferId,
71
+ seq,
72
+ data: chunk,
73
+ size: chunk.length,
74
+ });
75
+ seq++;
76
+ // Throttle progress updates to every 500ms
77
+ if (Date.now() - lastProgressTime > 500) {
78
+ sendProgress(bytesTransferred, totalBytes || bytesTransferred);
79
+ lastProgressTime = Date.now();
80
+ }
81
+ });
82
+ tar.stderr.on('data', (data) => {
83
+ const msg = data.toString().trim();
84
+ if (msg)
85
+ log(`tar: ${msg}`);
86
+ });
87
+ tar.on('error', (err) => {
88
+ sendError(`tar process error: ${err.message}`);
89
+ });
90
+ tar.on('close', (code) => {
91
+ if (code !== 0) {
92
+ sendError(`tar exited with code ${code}`);
93
+ return;
94
+ }
95
+ const duration = Date.now() - startTime;
96
+ const sha256 = hash.digest('hex');
97
+ // Final progress
98
+ sendProgress(bytesTransferred, bytesTransferred);
99
+ // We don't send transfer:complete here — the dest does that after verification
100
+ // Instead, we send a source:done so the dest knows the stream is over
101
+ socket.emit('transfer:source:done', {
102
+ transferId,
103
+ totalBytes: bytesTransferred,
104
+ sha256,
105
+ duration,
106
+ });
107
+ log(`Sent ${formatBytes(bytesTransferred)} in ${(duration / 1000).toFixed(1)}s (${seq} chunks)`);
108
+ });
109
+ // Handle cancellation
110
+ const cancelHandler = (data) => {
111
+ if (data.transferId === transferId) {
112
+ tar.kill('SIGKILL');
113
+ socket.off('transfer:cancel', cancelHandler);
114
+ log('Transfer cancelled');
115
+ }
116
+ };
117
+ socket.on('transfer:cancel', cancelHandler);
118
+ }
119
+ }
120
+ // ---- Receiver ----
121
+ export function startReceiving(socket, transfer, foreground) {
122
+ const { transferId, destPath } = transfer;
123
+ const startTime = Date.now();
124
+ const log = (message) => {
125
+ if (foreground)
126
+ console.log(`[transfer:recv] ${message}`);
127
+ socket.emit('transfer:log', { transferId, side: 'dest', level: 'info', message });
128
+ };
129
+ const sendProgress = (bytesTransferred, totalBytes) => {
130
+ const elapsed = (Date.now() - startTime) / 1000;
131
+ const speed = elapsed > 0 ? bytesTransferred / elapsed : 0;
132
+ const eta = speed > 0 ? (totalBytes - bytesTransferred) / speed : 0;
133
+ socket.emit('transfer:progress', {
134
+ transferId,
135
+ side: 'dest',
136
+ bytesTransferred,
137
+ totalBytes,
138
+ speed: Math.round(speed),
139
+ eta: Math.round(eta),
140
+ });
141
+ };
142
+ const sendError = (message) => {
143
+ console.error(`[transfer:recv] Error: ${message}`);
144
+ socket.emit('transfer:error', { transferId, side: 'dest', message });
145
+ };
146
+ const tmpFile = path.join(os.tmpdir(), `ttc-transfer-${transferId}.tar`);
147
+ log(`Ready to receive into: ${destPath}`);
148
+ // Ensure dest directory exists
149
+ mkdir(destPath, { recursive: true }).then(() => {
150
+ const writeStream = fsSync.createWriteStream(tmpFile);
151
+ const hash = createHash('sha256');
152
+ let bytesReceived = 0;
153
+ let totalExpected = 0;
154
+ let lastProgressTime = Date.now();
155
+ let sourceSha256 = '';
156
+ // Handle incoming chunks
157
+ const chunkHandler = (data) => {
158
+ if (data.transferId !== transferId)
159
+ return;
160
+ const chunk = Buffer.isBuffer(data.data) ? data.data : Buffer.from(data.data);
161
+ writeStream.write(chunk);
162
+ hash.update(chunk);
163
+ bytesReceived += chunk.length;
164
+ if (Date.now() - lastProgressTime > 500) {
165
+ sendProgress(bytesReceived, totalExpected || bytesReceived);
166
+ lastProgressTime = Date.now();
167
+ }
168
+ };
169
+ socket.on('transfer:chunk', chunkHandler);
170
+ // Handle source:done — source finished sending
171
+ const sourceDoneHandler = (data) => {
172
+ if (data.transferId !== transferId)
173
+ return;
174
+ totalExpected = data.totalBytes;
175
+ sourceSha256 = data.sha256;
176
+ log(`Source finished. Received ${formatBytes(bytesReceived)} / ${formatBytes(totalExpected)}`);
177
+ // Finish writing and verify
178
+ writeStream.end(async () => {
179
+ try {
180
+ const receivedHash = hash.digest('hex');
181
+ // Verify hash
182
+ if (receivedHash !== sourceSha256) {
183
+ sendError(`Hash mismatch! Expected ${sourceSha256.slice(0, 16)}... got ${receivedHash.slice(0, 16)}...`);
184
+ try {
185
+ await unlink(tmpFile);
186
+ }
187
+ catch { }
188
+ return;
189
+ }
190
+ log(`Hash verified ✓. Extracting to ${destPath}...`);
191
+ // Extract tar
192
+ const extractProc = spawn('tar', ['xf', tmpFile, '-C', destPath]);
193
+ extractProc.on('close', async (code) => {
194
+ // Cleanup temp file
195
+ try {
196
+ await unlink(tmpFile);
197
+ }
198
+ catch { }
199
+ if (code !== 0) {
200
+ sendError(`tar extraction failed with code ${code}`);
201
+ return;
202
+ }
203
+ const duration = Date.now() - startTime;
204
+ // Notify completion
205
+ socket.emit('transfer:complete', {
206
+ transferId,
207
+ totalBytes: bytesReceived,
208
+ sha256: receivedHash,
209
+ duration,
210
+ });
211
+ log(`Complete! ${formatBytes(bytesReceived)} extracted to ${destPath} in ${(duration / 1000).toFixed(1)}s`);
212
+ // Cleanup listeners
213
+ socket.off('transfer:chunk', chunkHandler);
214
+ socket.off('transfer:source:done', sourceDoneHandler);
215
+ });
216
+ extractProc.on('error', (err) => {
217
+ sendError(`tar extraction error: ${err.message}`);
218
+ });
219
+ }
220
+ catch (err) {
221
+ sendError(`Post-processing error: ${err.message}`);
222
+ }
223
+ });
224
+ };
225
+ socket.on('transfer:source:done', sourceDoneHandler);
226
+ // Handle cancellation
227
+ const cancelHandler = (data) => {
228
+ if (data.transferId === transferId) {
229
+ writeStream.destroy();
230
+ socket.off('transfer:chunk', chunkHandler);
231
+ socket.off('transfer:source:done', sourceDoneHandler);
232
+ socket.off('transfer:cancel', cancelHandler);
233
+ log('Transfer cancelled');
234
+ // Cleanup temp file
235
+ unlink(tmpFile).catch(() => { });
236
+ }
237
+ };
238
+ socket.on('transfer:cancel', cancelHandler);
239
+ }).catch((err) => {
240
+ sendError(`Failed to create destination directory: ${err.message}`);
241
+ });
242
+ }
243
+ // ---- Helpers ----
244
+ function formatBytes(bytes) {
245
+ if (bytes === 0)
246
+ return '0 B';
247
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
248
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
249
+ return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
250
+ }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exreve/exk",
3
- "version": "1.0.36",
3
+ "version": "1.0.37",
4
4
  "description": "exk - Control Claude CLI with voice and programmable interfaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,252 +0,0 @@
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
- });
@@ -1,187 +0,0 @@
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); });
@@ -1,50 +0,0 @@
1
- /**
2
- * Test: verify that a custom MCP tool is visible to the Claude Agent SDK.
3
- * Usage: npx tsx test-mcp-tool.ts
4
- */
5
- import { query, createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
6
- import { z } from 'zod';
7
- import fs from 'fs';
8
- import path from 'path';
9
- import os from 'os';
10
- const AI_CONFIG = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.talk-to-code', 'ai-config.json'), 'utf-8'));
11
- // Create a trivial test tool
12
- const mcpServer = createSdkMcpServer({
13
- name: 'test-tools',
14
- version: '1.0.0',
15
- tools: [
16
- tool('analyze_image', 'Analyze an image file. Returns a text description.', {
17
- image_path: z.string().describe('Path to image'),
18
- question: z.string().describe('What to answer about the image'),
19
- }, async (args) => ({
20
- content: [{ type: 'text', text: `[TEST] Would analyze ${args.image_path} with question: ${args.question}` }],
21
- })),
22
- ],
23
- });
24
- console.log('=== MCP server created ===');
25
- // Now run a query with the CORRECT format (Record<string, config>)
26
- console.log('\n--- Running query (mcpServers as Record) ---');
27
- const q = query({
28
- prompt: 'List every tool you have available by name. Specifically, do you have an "analyze_image" tool? Just answer with the list.',
29
- options: {
30
- apiKey: AI_CONFIG.authToken,
31
- model: AI_CONFIG.model,
32
- cwd: '/tmp',
33
- permissionMode: 'bypassPermissions',
34
- allowDangerouslySkipPermissions: true,
35
- disallowedTools: ['AskUserQuestion', 'analyze_image'],
36
- mcpServers: { 'claude-voice-modules': mcpServer },
37
- maxTurns: 2,
38
- },
39
- });
40
- for await (const event of q) {
41
- if (event.type === 'assistant') {
42
- const text = event.message?.content
43
- ?.filter((b) => b.type === 'text')
44
- ?.map((b) => b.text)
45
- ?.join('') || '';
46
- if (text)
47
- console.log('Agent:', text);
48
- }
49
- }
50
- console.log('\n--- Done ---');