@exreve/exk 1.0.25 → 1.0.27
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/agentSession.js +153 -55
- package/dist/app-child.js +2590 -0
- package/dist/index.js +211 -23
- package/dist/moduleMcpServer.js +77 -23
- package/dist/ttc-cli.tar.gz +0 -0
- package/dist/updater.js +425 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { startApp, stopApp, restartApp, getAppStatuses, getAppLogs } from './app
|
|
|
16
16
|
import { spawn, execSync } from 'child_process';
|
|
17
17
|
import readline from 'readline';
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
|
+
import { createHash } from 'crypto';
|
|
19
20
|
// ============ Constants ============
|
|
20
21
|
const CONFIG_DIR = path.join(os.homedir(), '.talk-to-code');
|
|
21
22
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
@@ -65,6 +66,33 @@ async function fetchAiConfig(authToken) {
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
const AI_CONFIG_FILE = path.join(CONFIG_DIR, 'ai-config.json');
|
|
69
|
+
const PROXY_CONFIG_FILE = path.join(CONFIG_DIR, 'proxy.json');
|
|
70
|
+
/** Read proxy toggle state from disk */
|
|
71
|
+
async function readProxyConfig() {
|
|
72
|
+
try {
|
|
73
|
+
const raw = await fs.readFile(PROXY_CONFIG_FILE, 'utf-8');
|
|
74
|
+
return JSON.parse(raw);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return { enabled: false };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Write proxy toggle state to disk */
|
|
81
|
+
async function writeProxyConfig(cfg) {
|
|
82
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
83
|
+
await fs.writeFile(PROXY_CONFIG_FILE, JSON.stringify(cfg, null, 2));
|
|
84
|
+
}
|
|
85
|
+
/** Get the proxy URL from ai-config.json (saved from backend) */
|
|
86
|
+
function getProxyUrl() {
|
|
87
|
+
try {
|
|
88
|
+
const raw = fsSync.readFileSync(AI_CONFIG_FILE, 'utf-8');
|
|
89
|
+
const j = JSON.parse(raw);
|
|
90
|
+
return typeof j.proxy === 'string' ? j.proxy.trim() : '';
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
68
96
|
/** True if ai-config.json has a model API key (not read from host ANTHROPIC_* env). */
|
|
69
97
|
function hasAiCredentials() {
|
|
70
98
|
try {
|
|
@@ -199,24 +227,97 @@ function getCliVersion() {
|
|
|
199
227
|
return '0.0.0';
|
|
200
228
|
}
|
|
201
229
|
}
|
|
202
|
-
|
|
203
|
-
const __dirname = path.dirname(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
230
|
+
const CURRENT_FILE = fileURLToPath(import.meta.url);
|
|
231
|
+
const __dirname = path.dirname(CURRENT_FILE);
|
|
232
|
+
function getCliHash() {
|
|
233
|
+
return createHash('sha256').update(fsSync.readFileSync(CURRENT_FILE)).digest('hex');
|
|
234
|
+
}
|
|
235
|
+
async function checkForUpdate() {
|
|
207
236
|
try {
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
237
|
+
const config = await readConfig();
|
|
238
|
+
const res = await fetch(`${config.apiUrl}/update/check`, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: { 'Content-Type': 'application/json' },
|
|
241
|
+
body: JSON.stringify({ hash: getCliHash(), platform: os.platform() })
|
|
242
|
+
});
|
|
243
|
+
if (!res.ok)
|
|
244
|
+
return null;
|
|
245
|
+
return await res.json();
|
|
215
246
|
}
|
|
216
247
|
catch {
|
|
217
248
|
return null;
|
|
218
249
|
}
|
|
219
250
|
}
|
|
251
|
+
async function replaceSelf(tarballBuffer) {
|
|
252
|
+
const extractDir = path.join(os.tmpdir(), `ttc-update-${Date.now()}`);
|
|
253
|
+
await fs.mkdir(extractDir, { recursive: true });
|
|
254
|
+
const tarPath = path.join(extractDir, 'ttc-cli.tar.gz');
|
|
255
|
+
await fs.writeFile(tarPath, tarballBuffer);
|
|
256
|
+
execSync(`tar -xzf "${tarPath}" -C "${extractDir}"`);
|
|
257
|
+
await fs.unlink(tarPath);
|
|
258
|
+
// Preserve user config/token files (never overwrite on update)
|
|
259
|
+
const preserveFiles = ['device-config.json', 'device-id.json', 'config.json'];
|
|
260
|
+
const preserved = {};
|
|
261
|
+
for (const f of preserveFiles) {
|
|
262
|
+
try {
|
|
263
|
+
preserved[f] = await fs.readFile(path.join(CONFIG_DIR, f));
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
/* file may not exist */
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Replace cli/ and shared/ in CONFIG_DIR
|
|
270
|
+
const cliDest = path.join(CONFIG_DIR, 'cli');
|
|
271
|
+
const sharedDest = path.join(CONFIG_DIR, 'shared');
|
|
272
|
+
await fs.rm(cliDest, { recursive: true, force: true });
|
|
273
|
+
await fs.rm(sharedDest, { recursive: true, force: true });
|
|
274
|
+
await fs.cp(path.join(extractDir, 'cli'), cliDest, { recursive: true });
|
|
275
|
+
await fs.cp(path.join(extractDir, 'shared'), sharedDest, { recursive: true });
|
|
276
|
+
// Update package.json and npm install
|
|
277
|
+
await fs.copyFile(path.join(extractDir, 'package.json'), path.join(CONFIG_DIR, 'package.json'));
|
|
278
|
+
await fs.rm(extractDir, { recursive: true, force: true });
|
|
279
|
+
// Restore preserved config
|
|
280
|
+
for (const f of preserveFiles) {
|
|
281
|
+
if (preserved[f])
|
|
282
|
+
await fs.writeFile(path.join(CONFIG_DIR, f), preserved[f]);
|
|
283
|
+
}
|
|
284
|
+
console.log('✓ CLI updated');
|
|
285
|
+
const npmPaths = [path.join(os.homedir(), '.nvm/versions/node/v22/bin/npm'), path.join(os.homedir(), '.nvm/versions/node/v20/bin/npm')];
|
|
286
|
+
const npmBin = npmPaths.find(p => fsSync.existsSync(p)) || 'npm';
|
|
287
|
+
try {
|
|
288
|
+
execSync(`"${npmBin}" install --omit=dev`, { cwd: CONFIG_DIR, stdio: 'inherit' });
|
|
289
|
+
console.log('✓ Dependencies updated');
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
console.warn('⚠ npm install failed');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function selfUpdate(force = false) {
|
|
296
|
+
const info = await checkForUpdate();
|
|
297
|
+
if (!info || !info.updateAvailable) {
|
|
298
|
+
console.log('✓ Already up to date');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
console.log('📦 Update available!');
|
|
302
|
+
if (!force && process.stdin.isTTY) {
|
|
303
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
304
|
+
const answer = await new Promise(r => rl.question('Update now? [Y/n] ', a => { rl.close(); r(a.trim()); }));
|
|
305
|
+
if (answer.toLowerCase().startsWith('n'))
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (!info.downloadUrl || !info.hash)
|
|
309
|
+
return;
|
|
310
|
+
console.log('Downloading...');
|
|
311
|
+
const res = await fetch(info.downloadUrl);
|
|
312
|
+
if (!res.ok)
|
|
313
|
+
throw new Error('Download failed');
|
|
314
|
+
const bundle = Buffer.from(await res.arrayBuffer());
|
|
315
|
+
if (createHash('sha256').update(bundle).digest('hex') !== info.hash) {
|
|
316
|
+
throw new Error('Hash mismatch');
|
|
317
|
+
}
|
|
318
|
+
await replaceSelf(bundle);
|
|
319
|
+
process.exit(0);
|
|
320
|
+
}
|
|
220
321
|
// ============ Commands ============
|
|
221
322
|
async function registerDevice(name, email) {
|
|
222
323
|
try {
|
|
@@ -516,9 +617,9 @@ async function runDaemon(foreground = false, email) {
|
|
|
516
617
|
const ipAddress = getLocalIpAddress();
|
|
517
618
|
const hostname = getHostname();
|
|
518
619
|
// Silent update check on startup
|
|
519
|
-
|
|
620
|
+
checkForUpdate().then(info => {
|
|
520
621
|
if (info?.updateAvailable)
|
|
521
|
-
console.log(
|
|
622
|
+
console.log('📦 Update available: run "ttc update"');
|
|
522
623
|
}).catch(() => { });
|
|
523
624
|
if (foreground)
|
|
524
625
|
console.log('=== TalkToCode CLI (Foreground) ===');
|
|
@@ -1263,6 +1364,7 @@ async function runDaemon(foreground = false, email) {
|
|
|
1263
1364
|
projectPath,
|
|
1264
1365
|
promptId: capturedPromptId,
|
|
1265
1366
|
model: model, // Pass the model from the session
|
|
1367
|
+
attachments: data.attachments, // Pass attachments from frontend
|
|
1266
1368
|
onStatusUpdate: (status) => {
|
|
1267
1369
|
// Emit status update from CLI (CLI is source of truth)
|
|
1268
1370
|
// Use captured promptId to ensure correct prompt is updated
|
|
@@ -1642,13 +1744,10 @@ async function runDaemon(foreground = false, email) {
|
|
|
1642
1744
|
// ========== Version & Update Handlers ==========
|
|
1643
1745
|
// Respond with CLI version info
|
|
1644
1746
|
socket.on('version:info', (_data, callback) => {
|
|
1645
|
-
if (isUpdating) {
|
|
1646
|
-
callback({ success: false, error: 'Update in progress' });
|
|
1647
|
-
return;
|
|
1648
|
-
}
|
|
1649
1747
|
callback({
|
|
1650
1748
|
success: true,
|
|
1651
1749
|
version: getCliVersion(),
|
|
1750
|
+
hash: getCliHash(),
|
|
1652
1751
|
date: new Date().toISOString(),
|
|
1653
1752
|
nodeVersion: process.version,
|
|
1654
1753
|
platform: os.platform(),
|
|
@@ -1657,10 +1756,6 @@ async function runDaemon(foreground = false, email) {
|
|
|
1657
1756
|
});
|
|
1658
1757
|
// Force update: npm update -g @exreve/exk then restart PM2
|
|
1659
1758
|
socket.on('force-update', (_data, callback) => {
|
|
1660
|
-
if (isUpdating) {
|
|
1661
|
-
callback?.({ success: false, error: 'Update already in progress' });
|
|
1662
|
-
return;
|
|
1663
|
-
}
|
|
1664
1759
|
if (foreground) {
|
|
1665
1760
|
console.log('[force-update] Received force update command from server');
|
|
1666
1761
|
}
|
|
@@ -1701,7 +1796,67 @@ async function runDaemon(foreground = false, email) {
|
|
|
1701
1796
|
}
|
|
1702
1797
|
});
|
|
1703
1798
|
});
|
|
1799
|
+
// ========== update:start handler (legacy compatibility) ==========
|
|
1800
|
+
socket.on('update:start', (_data, callback) => {
|
|
1801
|
+
// Use npm-based self-update
|
|
1802
|
+
if (foreground) {
|
|
1803
|
+
console.log('[update:start] Starting npm self-update...');
|
|
1804
|
+
}
|
|
1805
|
+
callback?.({ success: true, message: 'Update started via npm' });
|
|
1806
|
+
const npmPaths = [
|
|
1807
|
+
path.join(os.homedir(), '.nvm/versions/node/v22/bin/npm'),
|
|
1808
|
+
path.join(os.homedir(), '.nvm/versions/node/v20/bin/npm')
|
|
1809
|
+
];
|
|
1810
|
+
const npmBin = npmPaths.find(p => fsSync.existsSync(p)) || 'npm';
|
|
1811
|
+
const updateProcess = spawn(npmBin, ['update', '-g', '@exreve/exk'], {
|
|
1812
|
+
stdio: 'pipe',
|
|
1813
|
+
detached: true
|
|
1814
|
+
});
|
|
1815
|
+
updateProcess.on('close', () => {
|
|
1816
|
+
setTimeout(() => {
|
|
1817
|
+
try {
|
|
1818
|
+
execSync('pm2 restart cli', { stdio: 'inherit' });
|
|
1819
|
+
}
|
|
1820
|
+
catch {
|
|
1821
|
+
process.exit(0);
|
|
1822
|
+
}
|
|
1823
|
+
}, 2000);
|
|
1824
|
+
});
|
|
1825
|
+
});
|
|
1704
1826
|
// Cloudflared handlers
|
|
1827
|
+
// Proxy toggle handler
|
|
1828
|
+
socket.on('proxy:toggle:request', async (data, callback) => {
|
|
1829
|
+
try {
|
|
1830
|
+
const { enable } = data;
|
|
1831
|
+
const proxyUrl = getProxyUrl();
|
|
1832
|
+
if (enable && !proxyUrl) {
|
|
1833
|
+
callback?.({ success: false, enabled: false });
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
await writeProxyConfig({ enabled: enable });
|
|
1837
|
+
if (foreground) {
|
|
1838
|
+
console.log(`[CLI] Proxy ${enable ? 'enabled' : 'disabled'}${proxyUrl ? `: ${proxyUrl}` : ''}`);
|
|
1839
|
+
}
|
|
1840
|
+
else {
|
|
1841
|
+
console.log(`Proxy ${enable ? 'enabled' : 'disabled'}`);
|
|
1842
|
+
}
|
|
1843
|
+
callback?.({ success: true, enabled: enable, proxyUrl: enable ? proxyUrl : undefined });
|
|
1844
|
+
}
|
|
1845
|
+
catch (error) {
|
|
1846
|
+
callback?.({ success: false, enabled: false });
|
|
1847
|
+
}
|
|
1848
|
+
});
|
|
1849
|
+
// Proxy status handler
|
|
1850
|
+
socket.on('proxy:status:request', async (_data, callback) => {
|
|
1851
|
+
try {
|
|
1852
|
+
const proxyConfig = await readProxyConfig();
|
|
1853
|
+
const proxyUrl = getProxyUrl();
|
|
1854
|
+
callback?.({ enabled: proxyConfig.enabled, proxyUrl: proxyConfig.enabled ? proxyUrl : undefined });
|
|
1855
|
+
}
|
|
1856
|
+
catch {
|
|
1857
|
+
callback?.({ enabled: false });
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1705
1860
|
socket.on('cloudflared:check:request', async () => {
|
|
1706
1861
|
try {
|
|
1707
1862
|
let installed = false;
|
|
@@ -2211,7 +2366,7 @@ async function runDaemon(foreground = false, email) {
|
|
|
2211
2366
|
const { name, image, ports = [], env = {}, runAsRoot = false } = data;
|
|
2212
2367
|
// Get CLI directory path (where this script is running from)
|
|
2213
2368
|
// Use the same pattern as elsewhere in the file
|
|
2214
|
-
const cliDir =
|
|
2369
|
+
const cliDir = path.dirname(CURRENT_FILE);
|
|
2215
2370
|
// Build docker run command
|
|
2216
2371
|
let cmd = `${runtime} run -d --name ${name}`;
|
|
2217
2372
|
// Security: Running as root INSIDE container is safe - it's still isolated from host
|
|
@@ -2685,4 +2840,37 @@ program
|
|
|
2685
2840
|
process.exit(1);
|
|
2686
2841
|
});
|
|
2687
2842
|
});
|
|
2843
|
+
// Enable proxy command
|
|
2844
|
+
program
|
|
2845
|
+
.command('enable')
|
|
2846
|
+
.description('Enable a feature (e.g. proxy)')
|
|
2847
|
+
.argument('<feature>', 'Feature to enable (proxy)')
|
|
2848
|
+
.action(async (feature) => {
|
|
2849
|
+
if (feature !== 'proxy') {
|
|
2850
|
+
console.error(`Unknown feature: ${feature}. Available: proxy`);
|
|
2851
|
+
process.exit(1);
|
|
2852
|
+
}
|
|
2853
|
+
const proxyUrl = getProxyUrl();
|
|
2854
|
+
if (!proxyUrl) {
|
|
2855
|
+
console.log('No proxy URL configured. Run "exk daemon" first to sync config from server.');
|
|
2856
|
+
process.exit(1);
|
|
2857
|
+
}
|
|
2858
|
+
await writeProxyConfig({ enabled: true });
|
|
2859
|
+
console.log(`Proxy enabled: ${proxyUrl}`);
|
|
2860
|
+
process.exit(0);
|
|
2861
|
+
});
|
|
2862
|
+
// Disable proxy command
|
|
2863
|
+
program
|
|
2864
|
+
.command('disable')
|
|
2865
|
+
.description('Disable a feature (e.g. proxy)')
|
|
2866
|
+
.argument('<feature>', 'Feature to disable (proxy)')
|
|
2867
|
+
.action(async (feature) => {
|
|
2868
|
+
if (feature !== 'proxy') {
|
|
2869
|
+
console.error(`Unknown feature: ${feature}. Available: proxy`);
|
|
2870
|
+
process.exit(1);
|
|
2871
|
+
}
|
|
2872
|
+
await writeProxyConfig({ enabled: false });
|
|
2873
|
+
console.log('Proxy disabled');
|
|
2874
|
+
process.exit(0);
|
|
2875
|
+
});
|
|
2688
2876
|
program.parse();
|
package/dist/moduleMcpServer.js
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Exposes enabled modules as MCP tools to the agent.
|
|
5
5
|
* This allows the agent to interact with modules like user-choice through standard tool calls.
|
|
6
|
+
* Also provides built-in tools like analyze_image for vision capabilities.
|
|
6
7
|
*/
|
|
7
8
|
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
|
|
8
9
|
import { z } from 'zod';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import * as os from 'os';
|
|
9
13
|
/**
|
|
10
14
|
* Create a tool for the user-choice module
|
|
11
15
|
*/
|
|
@@ -15,7 +19,7 @@ function createUserChoiceTool(onChoiceRequest) {
|
|
|
15
19
|
options: z.array(z.object({ label: z.string(), value: z.string() })),
|
|
16
20
|
timeout: z.number().optional()
|
|
17
21
|
};
|
|
18
|
-
return tool('user_choice_request', '
|
|
22
|
+
return tool('user_choice_request', 'Request user input when making decisions. Use this when you need the user to choose between options or provide input on a decision. This tool will present a modal to the user with your question and wait for their response.', schema, async (args, _extra) => {
|
|
19
23
|
if (!onChoiceRequest) {
|
|
20
24
|
return {
|
|
21
25
|
content: [
|
|
@@ -67,6 +71,74 @@ function createUserChoiceTool(onChoiceRequest) {
|
|
|
67
71
|
}
|
|
68
72
|
});
|
|
69
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Convert a file to a data URI for vision API consumption
|
|
76
|
+
*/
|
|
77
|
+
function fileToDataUri(filePath) {
|
|
78
|
+
try {
|
|
79
|
+
const buf = fs.readFileSync(filePath);
|
|
80
|
+
const ext = path.extname(filePath).toLowerCase().replace('.', '');
|
|
81
|
+
const mimeMap = {
|
|
82
|
+
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
|
|
83
|
+
gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp',
|
|
84
|
+
svg: 'image/svg+xml',
|
|
85
|
+
};
|
|
86
|
+
const mime = mimeMap[ext] || 'application/octet-stream';
|
|
87
|
+
return `data:${mime};base64,${buf.toString('base64')}`;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Create the analyze_image tool for vision capabilities via OpenRouter
|
|
95
|
+
*/
|
|
96
|
+
function createAnalyzeImageTool(attachmentDir) {
|
|
97
|
+
const workDir = attachmentDir || os.tmpdir();
|
|
98
|
+
return tool('analyze_image', 'Analyze one or more image files using a vision model. Pass the path to an image file and a question. Returns a detailed text answer about the image content.', {
|
|
99
|
+
image_path: z.string().describe('Path to the image file to analyze (can be relative to working directory, e.g. "attachments/photo.jpg")'),
|
|
100
|
+
question: z.string().describe('Question or instruction about the image. Be specific about what you want to know.'),
|
|
101
|
+
}, async (args) => {
|
|
102
|
+
const apiKey = process.env.OPENROUTER_API_KEY || '';
|
|
103
|
+
if (!apiKey) {
|
|
104
|
+
return { content: [{ type: 'text', text: 'Error: OPENROUTER_API_KEY not configured.' }], isError: true };
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
// Resolve relative paths against the attachment dir
|
|
108
|
+
const imagePath = path.resolve(workDir, args.image_path);
|
|
109
|
+
if (!fs.existsSync(imagePath)) {
|
|
110
|
+
return { content: [{ type: 'text', text: `Error: Image file not found: ${args.image_path}` }], isError: true };
|
|
111
|
+
}
|
|
112
|
+
const dataUri = fileToDataUri(imagePath);
|
|
113
|
+
if (!dataUri) {
|
|
114
|
+
return { content: [{ type: 'text', text: `Error: Could not read image file: ${args.image_path}` }], isError: true };
|
|
115
|
+
}
|
|
116
|
+
const OPENROUTER_ENDPOINT = 'https://openrouter.ai/api/v1/chat/completions';
|
|
117
|
+
const OPENROUTER_MODEL = 'qwen/qwen3.5-27b';
|
|
118
|
+
const res = await fetch(OPENROUTER_ENDPOINT, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
121
|
+
body: JSON.stringify({
|
|
122
|
+
model: OPENROUTER_MODEL,
|
|
123
|
+
messages: [{ role: 'user', content: [
|
|
124
|
+
{ type: 'text', text: args.question },
|
|
125
|
+
{ type: 'image_url', image_url: { url: dataUri } },
|
|
126
|
+
] }],
|
|
127
|
+
}),
|
|
128
|
+
signal: AbortSignal.timeout(60_000),
|
|
129
|
+
});
|
|
130
|
+
const raw = await res.text();
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
return { content: [{ type: 'text', text: `Error from vision API (${res.status}): ${raw.slice(0, 500)}` }], isError: true };
|
|
133
|
+
}
|
|
134
|
+
const parsed = JSON.parse(raw);
|
|
135
|
+
return { content: [{ type: 'text', text: parsed.choices?.[0]?.message?.content || raw }] };
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
return { content: [{ type: 'text', text: `Error analyzing image: ${error.message}` }], isError: true };
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
70
142
|
/**
|
|
71
143
|
* Get available tools based on enabled modules
|
|
72
144
|
*/
|
|
@@ -76,6 +148,10 @@ function getModuleTools(config) {
|
|
|
76
148
|
if (config.enabledModules.includes('user-choice')) {
|
|
77
149
|
tools.push(createUserChoiceTool(config.onChoiceRequest));
|
|
78
150
|
}
|
|
151
|
+
// Always add analyze_image tool if attachmentDir is provided (i.e. there are attachments)
|
|
152
|
+
if (config.attachmentDir) {
|
|
153
|
+
tools.push(createAnalyzeImageTool(config.attachmentDir));
|
|
154
|
+
}
|
|
79
155
|
// Add more module tools here as they are implemented
|
|
80
156
|
return tools;
|
|
81
157
|
}
|
|
@@ -91,25 +167,3 @@ export function createModuleMcpServer(config) {
|
|
|
91
167
|
});
|
|
92
168
|
return server;
|
|
93
169
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Get a system prompt hint describing available module tools.
|
|
96
|
-
* This ensures the model knows about custom MCP tools upfront without
|
|
97
|
-
* having to discover them through the MCP tool-listing mechanism.
|
|
98
|
-
*/
|
|
99
|
-
export function getModuleToolHint(enabledModules) {
|
|
100
|
-
if (enabledModules.length === 0)
|
|
101
|
-
return null;
|
|
102
|
-
const toolDescriptions = [];
|
|
103
|
-
if (enabledModules.includes('user-choice')) {
|
|
104
|
-
toolDescriptions.push(`- user_choice_request(question: str, options: [{label: str, value: str}], timeout?: int): Present a modal to the remote user with a question and clickable options. Returns the user's selection.
|
|
105
|
-
IMPORTANT: The built-in AskUserQuestion tool is DISABLED. Whenever you need user input, decisions, or confirmations, you MUST use user_choice_request instead.
|
|
106
|
-
Always call this tool when:
|
|
107
|
-
- There are multiple valid approaches and you need the user to pick one
|
|
108
|
-
- You need explicit confirmation before a destructive or significant action
|
|
109
|
-
- The user's preference matters for how to proceed`);
|
|
110
|
-
}
|
|
111
|
-
// Add more module tool descriptions here as they are implemented
|
|
112
|
-
if (toolDescriptions.length === 0)
|
|
113
|
-
return null;
|
|
114
|
-
return `You have access to the following custom MCP tools:\n${toolDescriptions.join('\n')}\n\nThese tools are your ONLY way to interact with the user (AskUserQuestion is disabled). Call them directly — do NOT ask the user to type responses in chat.`;
|
|
115
|
-
}
|
|
Binary file
|