@exreve/exk 1.0.36 → 1.0.38
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 +15 -0
- package/dist/transferService.js +281 -0
- package/dist/ttc-cli.tar.gz +0 -0
- package/package.json +1 -1
- package/dist/test-mcp-minimax.js +0 -252
- package/dist/test-mcp-spawn-repro.js +0 -187
- package/dist/test-mcp-tool.js +0 -50
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 (pack & upload)
|
|
1549
|
+
socket.on('transfer:pack', (data) => {
|
|
1550
|
+
if (foreground) {
|
|
1551
|
+
console.log(`[transfer] Pack request: ${data.sourcePath} (${data.selectedItems?.length || 0} items) → device ${data.destDeviceId.slice(0, 8)}`);
|
|
1552
|
+
}
|
|
1553
|
+
startSending(socket, data, foreground);
|
|
1554
|
+
});
|
|
1555
|
+
// Handle folder transfer — this device is the destination (download & extract)
|
|
1556
|
+
socket.on('transfer:pull', (data) => {
|
|
1557
|
+
if (foreground) {
|
|
1558
|
+
console.log(`[transfer] Pull request: device ${data.sourceDeviceId.slice(0, 8)} → ${data.destPath} (${(data.totalBytes / (1024 * 1024)).toFixed(1)} MB)`);
|
|
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,281 @@
|
|
|
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
|
+
import { Readable, Transform } from 'stream';
|
|
8
|
+
// ============ Transfer Service (CLI side) ============
|
|
9
|
+
// Two-phase transfer via HTTP:
|
|
10
|
+
// Phase 1 (source): tar selected items → HTTP POST to backend
|
|
11
|
+
// Phase 2 (dest): HTTP GET from backend → tar extract
|
|
12
|
+
// Socket.IO carries control events + progress/log.
|
|
13
|
+
// ---- Sender ----
|
|
14
|
+
export function startSending(socket, transfer, foreground) {
|
|
15
|
+
const { transferId, sourcePath, selectedItems } = transfer;
|
|
16
|
+
const startTime = Date.now();
|
|
17
|
+
const apiUrl = getApiUrl(socket);
|
|
18
|
+
const log = (message) => {
|
|
19
|
+
if (foreground)
|
|
20
|
+
console.log(`[transfer:send] ${message}`);
|
|
21
|
+
socket.emit('transfer:log', { transferId, side: 'source', level: 'info', message });
|
|
22
|
+
};
|
|
23
|
+
const sendProgress = (phase, bytesTransferred, totalBytes) => {
|
|
24
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
25
|
+
const speed = elapsed > 0 ? bytesTransferred / elapsed : 0;
|
|
26
|
+
const eta = speed > 0 ? (totalBytes - bytesTransferred) / speed : 0;
|
|
27
|
+
socket.emit('transfer:progress', {
|
|
28
|
+
transferId,
|
|
29
|
+
side: 'source',
|
|
30
|
+
phase,
|
|
31
|
+
bytesTransferred,
|
|
32
|
+
totalBytes,
|
|
33
|
+
speed: Math.round(speed),
|
|
34
|
+
eta: Math.round(eta),
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
const sendError = (message) => {
|
|
38
|
+
console.error(`[transfer:send] Error: ${message}`);
|
|
39
|
+
socket.emit('transfer:error', { transferId, side: 'source', message });
|
|
40
|
+
};
|
|
41
|
+
const items = selectedItems || ['.'];
|
|
42
|
+
log(`Packing ${items.length} item(s) from ${sourcePath}`);
|
|
43
|
+
// Estimate size first
|
|
44
|
+
let totalBytes = 0;
|
|
45
|
+
const sizeEstimate = spawn('du', ['-sb', ...items.map(i => path.join(sourcePath, i))], { cwd: sourcePath });
|
|
46
|
+
let duOutput = '';
|
|
47
|
+
sizeEstimate.stdout.on('data', (d) => { duOutput += d.toString(); });
|
|
48
|
+
sizeEstimate.on('close', (code) => {
|
|
49
|
+
if (code === 0) {
|
|
50
|
+
const lines = duOutput.trim().split('\n');
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const match = line.match(/^(\d+)/);
|
|
53
|
+
if (match)
|
|
54
|
+
totalBytes += parseInt(match[1], 10);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
log(`Estimated size: ${formatBytes(totalBytes)}`);
|
|
58
|
+
doPackAndUpload();
|
|
59
|
+
});
|
|
60
|
+
sizeEstimate.on('error', () => doPackAndUpload());
|
|
61
|
+
function doPackAndUpload() {
|
|
62
|
+
// Create tar with selected items
|
|
63
|
+
const tarArgs = ['cf', '-', '-C', sourcePath, ...items];
|
|
64
|
+
const tar = spawn('tar', tarArgs);
|
|
65
|
+
const hash = createHash('sha256');
|
|
66
|
+
let bytesTransferred = 0;
|
|
67
|
+
let lastProgressTime = Date.now();
|
|
68
|
+
let fileCount = 0;
|
|
69
|
+
// Count files as we go (approximate from tar output)
|
|
70
|
+
tar.stderr.on('data', (data) => {
|
|
71
|
+
const msg = data.toString().trim();
|
|
72
|
+
if (msg)
|
|
73
|
+
log(`tar: ${msg}`);
|
|
74
|
+
});
|
|
75
|
+
tar.on('error', (err) => {
|
|
76
|
+
sendError(`tar process error: ${err.message}`);
|
|
77
|
+
});
|
|
78
|
+
tar.on('close', (code) => {
|
|
79
|
+
if (code !== 0) {
|
|
80
|
+
sendError(`tar exited with code ${code}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
log(`Packing complete. Final size: ${formatBytes(bytesTransferred)}`);
|
|
84
|
+
const sha256 = hash.digest('hex');
|
|
85
|
+
const duration = Date.now() - startTime;
|
|
86
|
+
// Notify backend that upload is done
|
|
87
|
+
socket.emit('transfer:uploaded', {
|
|
88
|
+
transferId,
|
|
89
|
+
totalBytes: bytesTransferred,
|
|
90
|
+
sha256,
|
|
91
|
+
fileCount,
|
|
92
|
+
});
|
|
93
|
+
log(`Upload complete: ${formatBytes(bytesTransferred)} in ${(duration / 1000).toFixed(1)}s (sha256: ${sha256.slice(0, 12)}...)`);
|
|
94
|
+
});
|
|
95
|
+
// Stream tar output through hash, then HTTP POST
|
|
96
|
+
const uploadUrl = `${apiUrl}/transfer/${transferId}/upload`;
|
|
97
|
+
// Pipe tar stdout → hash transform → HTTP upload
|
|
98
|
+
const hashTransform = new Transform({
|
|
99
|
+
transform(chunk, _encoding, callback) {
|
|
100
|
+
hash.update(chunk);
|
|
101
|
+
bytesTransferred += chunk.length;
|
|
102
|
+
if (Date.now() - lastProgressTime > 500) {
|
|
103
|
+
sendProgress('uploading', bytesTransferred, totalBytes || bytesTransferred);
|
|
104
|
+
lastProgressTime = Date.now();
|
|
105
|
+
}
|
|
106
|
+
this.push(chunk);
|
|
107
|
+
callback();
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
// Use fetch with streaming body for the upload
|
|
111
|
+
const tarStream = tar.stdout.pipe(hashTransform);
|
|
112
|
+
// Node 18+ supports ReadableStream in fetch body
|
|
113
|
+
const nodeStream = Readable.toWeb(tarStream);
|
|
114
|
+
fetch(uploadUrl, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
body: nodeStream,
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': 'application/x-tar',
|
|
119
|
+
'Transfer-Encoding': 'chunked',
|
|
120
|
+
},
|
|
121
|
+
duplex: 'half',
|
|
122
|
+
}).then(async (res) => {
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
const text = await res.text();
|
|
125
|
+
sendError(`Upload failed: HTTP ${res.status} ${text}`);
|
|
126
|
+
}
|
|
127
|
+
}).catch((err) => {
|
|
128
|
+
sendError(`Upload network error: ${err.message}`);
|
|
129
|
+
});
|
|
130
|
+
// Handle cancellation
|
|
131
|
+
const cancelHandler = (data) => {
|
|
132
|
+
if (data.transferId === transferId) {
|
|
133
|
+
tar.kill('SIGKILL');
|
|
134
|
+
socket.off('transfer:cancel', cancelHandler);
|
|
135
|
+
log('Transfer cancelled');
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
socket.on('transfer:cancel', cancelHandler);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// ---- Receiver ----
|
|
142
|
+
export function startReceiving(socket, transfer, foreground) {
|
|
143
|
+
const { transferId, destPath } = transfer;
|
|
144
|
+
const startTime = Date.now();
|
|
145
|
+
const apiUrl = getApiUrl(socket);
|
|
146
|
+
const expectedBytes = transfer.totalBytes || 0;
|
|
147
|
+
const expectedSha256 = transfer.sha256 || '';
|
|
148
|
+
const log = (message) => {
|
|
149
|
+
if (foreground)
|
|
150
|
+
console.log(`[transfer:recv] ${message}`);
|
|
151
|
+
socket.emit('transfer:log', { transferId, side: 'dest', level: 'info', message });
|
|
152
|
+
};
|
|
153
|
+
const sendProgress = (phase, bytesTransferred, totalBytes) => {
|
|
154
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
155
|
+
const speed = elapsed > 0 ? bytesTransferred / elapsed : 0;
|
|
156
|
+
const eta = speed > 0 ? (totalBytes - bytesTransferred) / speed : 0;
|
|
157
|
+
socket.emit('transfer:progress', {
|
|
158
|
+
transferId,
|
|
159
|
+
side: 'dest',
|
|
160
|
+
phase,
|
|
161
|
+
bytesTransferred,
|
|
162
|
+
totalBytes,
|
|
163
|
+
speed: Math.round(speed),
|
|
164
|
+
eta: Math.round(eta),
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
const sendError = (message) => {
|
|
168
|
+
console.error(`[transfer:recv] Error: ${message}`);
|
|
169
|
+
socket.emit('transfer:error', { transferId, side: 'dest', message });
|
|
170
|
+
};
|
|
171
|
+
const tmpFile = path.join(os.tmpdir(), `ttc-transfer-${transferId}.tar`);
|
|
172
|
+
log(`Downloading from backend...`);
|
|
173
|
+
mkdir(destPath, { recursive: true }).then(async () => {
|
|
174
|
+
try {
|
|
175
|
+
const downloadUrl = `${apiUrl}/transfer/${transferId}/download`;
|
|
176
|
+
const res = await fetch(downloadUrl);
|
|
177
|
+
if (!res.ok) {
|
|
178
|
+
sendError(`Download failed: HTTP ${res.status}`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!res.body) {
|
|
182
|
+
sendError('Download failed: no response body');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// Stream download to temp file while hashing
|
|
186
|
+
const writeStream = fsSync.createWriteStream(tmpFile);
|
|
187
|
+
const hash = createHash('sha256');
|
|
188
|
+
let bytesReceived = 0;
|
|
189
|
+
let lastProgressTime = Date.now();
|
|
190
|
+
const reader = res.body.getReader();
|
|
191
|
+
// Read chunks from the response stream
|
|
192
|
+
while (true) {
|
|
193
|
+
const { done, value } = await reader.read();
|
|
194
|
+
if (done)
|
|
195
|
+
break;
|
|
196
|
+
writeStream.write(value);
|
|
197
|
+
hash.update(value);
|
|
198
|
+
bytesReceived += value.length;
|
|
199
|
+
if (Date.now() - lastProgressTime > 500) {
|
|
200
|
+
sendProgress('downloading', bytesReceived, expectedBytes || bytesReceived);
|
|
201
|
+
lastProgressTime = Date.now();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
writeStream.end();
|
|
205
|
+
log(`Downloaded ${formatBytes(bytesReceived)}. Verifying...`);
|
|
206
|
+
// Verify hash
|
|
207
|
+
const receivedHash = hash.digest('hex');
|
|
208
|
+
if (expectedSha256 && receivedHash !== expectedSha256) {
|
|
209
|
+
sendError(`Hash mismatch! Expected ${expectedSha256.slice(0, 16)}... got ${receivedHash.slice(0, 16)}...`);
|
|
210
|
+
try {
|
|
211
|
+
await unlink(tmpFile);
|
|
212
|
+
}
|
|
213
|
+
catch { }
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
log(`Hash verified ✓. Extracting to ${destPath}...`);
|
|
217
|
+
sendProgress('extracting', bytesReceived, bytesReceived);
|
|
218
|
+
// Extract tar
|
|
219
|
+
const extractProc = spawn('tar', ['xf', tmpFile, '-C', destPath]);
|
|
220
|
+
await new Promise((resolve, reject) => {
|
|
221
|
+
extractProc.on('close', (code) => {
|
|
222
|
+
if (code !== 0)
|
|
223
|
+
reject(new Error(`tar extraction failed with code ${code}`));
|
|
224
|
+
else
|
|
225
|
+
resolve();
|
|
226
|
+
});
|
|
227
|
+
extractProc.on('error', reject);
|
|
228
|
+
});
|
|
229
|
+
// Cleanup temp file
|
|
230
|
+
try {
|
|
231
|
+
await unlink(tmpFile);
|
|
232
|
+
}
|
|
233
|
+
catch { }
|
|
234
|
+
const duration = Date.now() - startTime;
|
|
235
|
+
socket.emit('transfer:complete', {
|
|
236
|
+
transferId,
|
|
237
|
+
totalBytes: bytesReceived,
|
|
238
|
+
sha256: receivedHash,
|
|
239
|
+
duration,
|
|
240
|
+
fileCount: 0,
|
|
241
|
+
});
|
|
242
|
+
log(`Complete! ${formatBytes(bytesReceived)} extracted to ${destPath} in ${(duration / 1000).toFixed(1)}s`);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
sendError(`Download/extract error: ${err.message}`);
|
|
246
|
+
try {
|
|
247
|
+
await unlink(tmpFile);
|
|
248
|
+
}
|
|
249
|
+
catch { }
|
|
250
|
+
}
|
|
251
|
+
}).catch((err) => {
|
|
252
|
+
sendError(`Failed to create destination directory: ${err.message}`);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
// ---- Helpers ----
|
|
256
|
+
function getApiUrl(socket) {
|
|
257
|
+
// Extract the base URL from the socket connection
|
|
258
|
+
const io = socket.io;
|
|
259
|
+
const opts = io.opts;
|
|
260
|
+
if (opts?.hostname) {
|
|
261
|
+
const protocol = opts.secure !== false ? 'https' : 'http';
|
|
262
|
+
const port = opts.port ? `:${opts.port}` : '';
|
|
263
|
+
return `${protocol}://${opts.hostname}${port}`;
|
|
264
|
+
}
|
|
265
|
+
// Fallback: read from config
|
|
266
|
+
try {
|
|
267
|
+
const configPath = path.join(os.homedir(), '.talk-to-code', 'config.json');
|
|
268
|
+
const config = JSON.parse(fsSync.readFileSync(configPath, 'utf-8'));
|
|
269
|
+
return config.apiUrl || 'https://api.talk-to-code.com';
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return 'https://api.talk-to-code.com';
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function formatBytes(bytes) {
|
|
276
|
+
if (bytes === 0)
|
|
277
|
+
return '0 B';
|
|
278
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
279
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
280
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
|
281
|
+
}
|
package/dist/ttc-cli.tar.gz
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/dist/test-mcp-minimax.js
DELETED
|
@@ -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); });
|
package/dist/test-mcp-tool.js
DELETED
|
@@ -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 ---');
|