@exreve/exk 1.0.26 → 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 +219 -25
- 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/agentSession.js
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from '
|
|
|
4
4
|
import { symlink as fsSymlink } from 'fs';
|
|
5
5
|
import { getSkillContent } from './skills/index.js';
|
|
6
6
|
import { isLocalModel, unwrapModelName, startOpenAIAdapter, getAdapterConfig } from './openaiAdapter.js';
|
|
7
|
-
import { createModuleMcpServer
|
|
7
|
+
import { createModuleMcpServer } from './moduleMcpServer.js';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import os from 'os';
|
|
10
10
|
import { createRequire } from 'module';
|
|
@@ -127,6 +127,43 @@ function lookupToolNameFromHistory(messages, toolUseId) {
|
|
|
127
127
|
// (Do not read ANTHROPIC_* / CLAUDE_MODEL from the host environment — only this file + code default model.)
|
|
128
128
|
const AI_CONFIG_PATH = path.join(os.homedir(), '.talk-to-code', 'ai-config.json');
|
|
129
129
|
const DEFAULT_AI_MODEL = 'glm-5.1';
|
|
130
|
+
const PROVIDERS = {
|
|
131
|
+
zai: {
|
|
132
|
+
apiKey: process.env.ZHIPU_API_KEY || '',
|
|
133
|
+
baseUrl: process.env.CLI_AI_BASE_URL || 'https://api.z.ai/api/anthropic',
|
|
134
|
+
models: ['glm-5.1', 'glm-4.7', 'glm-4.5-air'],
|
|
135
|
+
},
|
|
136
|
+
minimax: {
|
|
137
|
+
apiKey: process.env.MINIMAX_API_KEY || '',
|
|
138
|
+
baseUrl: 'https://api.minimax.io/anthropic',
|
|
139
|
+
models: ['MiniMax-M2.7', 'MiniMax-M2.7-highspeed'],
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
/** Resolve which provider to use based on model name or explicit provider ID.
|
|
143
|
+
* 1. If explicit providerId is given and has an API key configured, use that provider.
|
|
144
|
+
* 2. Else if model name matches one of a provider's model list, use that provider.
|
|
145
|
+
* 3. Else fall back to zai (default). */
|
|
146
|
+
function resolveProvider(model, providerId) {
|
|
147
|
+
// 1. Explicit provider selection
|
|
148
|
+
if (providerId && PROVIDERS[providerId]?.apiKey) {
|
|
149
|
+
const provider = PROVIDERS[providerId];
|
|
150
|
+
return { provider: providerId, apiKey: provider.apiKey, baseUrl: provider.baseUrl, model };
|
|
151
|
+
}
|
|
152
|
+
// 2. Match model name to a provider
|
|
153
|
+
for (const [id, config] of Object.entries(PROVIDERS)) {
|
|
154
|
+
if (config.models.includes(model) && config.apiKey) {
|
|
155
|
+
return { provider: id, apiKey: config.apiKey, baseUrl: config.baseUrl, model };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// 3. Fallback: use ai-config.json credentials (z.ai default)
|
|
159
|
+
const aiConfig = loadAiConfig();
|
|
160
|
+
return {
|
|
161
|
+
provider: 'zai',
|
|
162
|
+
apiKey: aiConfig.apiKey,
|
|
163
|
+
baseUrl: aiConfig.baseUrl || PROVIDERS.zai.baseUrl,
|
|
164
|
+
model,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
130
167
|
function loadAiConfig() {
|
|
131
168
|
try {
|
|
132
169
|
const data = readFileSync(AI_CONFIG_PATH, 'utf-8');
|
|
@@ -134,10 +171,11 @@ function loadAiConfig() {
|
|
|
134
171
|
const apiKey = typeof config.authToken === 'string' ? config.authToken.trim() : '';
|
|
135
172
|
const baseUrl = typeof config.baseUrl === 'string' ? config.baseUrl.trim() : '';
|
|
136
173
|
const model = typeof config.model === 'string' && config.model.trim() ? config.model.trim() : DEFAULT_AI_MODEL;
|
|
137
|
-
|
|
174
|
+
const proxy = typeof config.proxy === 'string' ? config.proxy.trim() : '';
|
|
175
|
+
return { apiKey, baseUrl, model, proxy };
|
|
138
176
|
}
|
|
139
177
|
catch {
|
|
140
|
-
return { apiKey: '', baseUrl: '', model: DEFAULT_AI_MODEL };
|
|
178
|
+
return { apiKey: '', baseUrl: '', model: DEFAULT_AI_MODEL, proxy: '' };
|
|
141
179
|
}
|
|
142
180
|
}
|
|
143
181
|
/** Create (or reuse) an empty directory to use as CLAUDE_CONFIG_DIR.
|
|
@@ -151,32 +189,66 @@ function getEmptyConfigDir() {
|
|
|
151
189
|
}
|
|
152
190
|
return configDir;
|
|
153
191
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
function
|
|
192
|
+
const PROXY_CONFIG_PATH = path.join(os.homedir(), '.talk-to-code', 'proxy.json');
|
|
193
|
+
/** Read proxy toggle state from disk (synchronous) */
|
|
194
|
+
function readProxyToggle() {
|
|
195
|
+
try {
|
|
196
|
+
const data = readFileSync(PROXY_CONFIG_PATH, 'utf-8');
|
|
197
|
+
return JSON.parse(data);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return { enabled: false };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Env for the Claude Code child: copy of host env with host ANTHROPIC_* stripped, then inject from provider or ai-config.
|
|
204
|
+
* If a local model is provided, override baseUrl to point to the anthropic-proxy adapter.
|
|
205
|
+
* If resolvedProvider is provided, use its credentials instead of ai-config defaults. */
|
|
206
|
+
function envForClaudeCodeChild(localModel, resolvedProvider) {
|
|
157
207
|
const env = { ...process.env };
|
|
158
208
|
// Strip any host ANTHROPIC_* vars to prevent leaking credentials or wrong URLs
|
|
159
209
|
delete env.ANTHROPIC_API_KEY;
|
|
160
210
|
delete env.ANTHROPIC_BASE_URL;
|
|
161
211
|
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
212
|
+
// Also strip model alias env vars to prevent stale overrides
|
|
213
|
+
delete env.ANTHROPIC_MODEL;
|
|
214
|
+
delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
215
|
+
delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
216
|
+
delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
217
|
+
// Determine credentials: use resolvedProvider if provided, else ai-config defaults
|
|
218
|
+
const { apiKey, baseUrl, proxy } = loadAiConfig();
|
|
219
|
+
const effectiveApiKey = resolvedProvider?.apiKey || apiKey;
|
|
220
|
+
const effectiveBaseUrl = resolvedProvider?.baseUrl || baseUrl;
|
|
221
|
+
if (effectiveApiKey)
|
|
222
|
+
env.ANTHROPIC_API_KEY = effectiveApiKey;
|
|
223
|
+
if (effectiveBaseUrl)
|
|
224
|
+
env.ANTHROPIC_BASE_URL = effectiveBaseUrl;
|
|
225
|
+
// For MiniMax specifically: override ALL model aliases so the SDK
|
|
226
|
+
// sends the correct model ID to the Anthropic-compatible endpoint
|
|
227
|
+
if (resolvedProvider?.provider === 'minimax') {
|
|
228
|
+
env.ANTHROPIC_MODEL = resolvedProvider.model;
|
|
229
|
+
env.ANTHROPIC_DEFAULT_SONNET_MODEL = resolvedProvider.model;
|
|
230
|
+
env.ANTHROPIC_DEFAULT_OPUS_MODEL = resolvedProvider.model;
|
|
231
|
+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = resolvedProvider.model;
|
|
232
|
+
env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1';
|
|
233
|
+
}
|
|
234
|
+
// Apply proxy if enabled
|
|
235
|
+
const proxyToggle = readProxyToggle();
|
|
236
|
+
if (proxyToggle.enabled && proxy) {
|
|
237
|
+
env.HTTPS_PROXY = proxy;
|
|
238
|
+
env.HTTP_PROXY = proxy;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
// Clear any inherited proxy env
|
|
242
|
+
delete env.HTTPS_PROXY;
|
|
243
|
+
delete env.HTTP_PROXY;
|
|
244
|
+
delete env.https_proxy;
|
|
245
|
+
delete env.http_proxy;
|
|
246
|
+
}
|
|
173
247
|
// Prevent ~/.claude/settings.json env section from overriding our base URL.
|
|
174
248
|
// This redirects the Claude config dir to an empty dir so that
|
|
175
249
|
// ~/.claude/settings.json (which may have ANTHROPIC_BASE_URL set to z.ai)
|
|
176
250
|
// is never read during the CLI process initialization.
|
|
177
251
|
env.CLAUDE_CONFIG_DIR = getEmptyConfigDir();
|
|
178
|
-
// Allow Claude to run as root (e.g. inside Docker containers)
|
|
179
|
-
env.IS_SANDBOX = '1';
|
|
180
252
|
return env;
|
|
181
253
|
}
|
|
182
254
|
/** Get env overrides for a local model (adapter proxy URL + dummy key). Returns null if not a local model. */
|
|
@@ -316,7 +388,8 @@ export class AgentSessionManager {
|
|
|
316
388
|
timestamp: Date.now(),
|
|
317
389
|
promptId: handler.promptId,
|
|
318
390
|
abortController: new AbortController(), // Pre-create for queued cancellation
|
|
319
|
-
model: handler.model || session.model // Use handler model or fall back to session model
|
|
391
|
+
model: handler.model || session.model, // Use handler model or fall back to session model
|
|
392
|
+
attachments: handler.attachments // Pass attachments through
|
|
320
393
|
});
|
|
321
394
|
// Start processing queue if not already processing
|
|
322
395
|
if (!session.isProcessingQueue) {
|
|
@@ -339,13 +412,30 @@ export class AgentSessionManager {
|
|
|
339
412
|
session.isProcessingQueue = true;
|
|
340
413
|
while (session.promptQueue.length > 0 && !this.emergencyStopInProgress.has(sessionId)) {
|
|
341
414
|
const queuedPrompt = session.promptQueue.shift();
|
|
342
|
-
const {
|
|
415
|
+
const { enhancers, handler, promptId: queuedPromptId, abortController: queuedAbortController } = queuedPrompt;
|
|
343
416
|
const { projectPath, promptId, onOutput, onError, onComplete, onStatusUpdate } = handler;
|
|
344
417
|
const promptStartTime = Date.now();
|
|
418
|
+
// Write attachments to temp dir and inject paths into prompt
|
|
419
|
+
let effectivePrompt = queuedPrompt.prompt;
|
|
420
|
+
let attachmentDir;
|
|
421
|
+
if (queuedPrompt.attachments && queuedPrompt.attachments.length > 0) {
|
|
422
|
+
attachmentDir = path.join(os.tmpdir(), 'talk-to-code', 'attachments', sessionId, String(promptId || Date.now()));
|
|
423
|
+
mkdirSync(attachmentDir, { recursive: true });
|
|
424
|
+
const attachmentLines = [];
|
|
425
|
+
for (const att of queuedPrompt.attachments) {
|
|
426
|
+
const safeName = att.filename.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
427
|
+
const filePath = path.join(attachmentDir, safeName);
|
|
428
|
+
const buf = Buffer.from(att.content, 'base64');
|
|
429
|
+
writeFileSync(filePath, buf);
|
|
430
|
+
attachmentLines.push(`- ${safeName} (${att.mimeType}): path="${filePath}"`);
|
|
431
|
+
}
|
|
432
|
+
effectivePrompt += `\n\n[Attachments]\nThe following files are attached and available on disk. Use the analyze_image tool to examine images.\n${attachmentLines.join('\n')}`;
|
|
433
|
+
console.log(`[agentSession] Wrote ${queuedPrompt.attachments.length} attachment(s) to ${attachmentDir}`);
|
|
434
|
+
}
|
|
345
435
|
try {
|
|
346
436
|
// Verify promptId is present in handler
|
|
347
437
|
if (!promptId) {
|
|
348
|
-
console.error(`[agentSession] Missing promptId in handler for prompt: ${prompt.substring(0, 50)}...`);
|
|
438
|
+
console.error(`[agentSession] Missing promptId in handler for prompt: ${queuedPrompt.prompt.substring(0, 50)}...`);
|
|
349
439
|
onError?.('Missing promptId in handler');
|
|
350
440
|
continue;
|
|
351
441
|
}
|
|
@@ -375,11 +465,11 @@ export class AgentSessionManager {
|
|
|
375
465
|
}
|
|
376
466
|
session.activeQueryStream = undefined;
|
|
377
467
|
// Build final prompt with enhancers
|
|
378
|
-
let finalPrompt =
|
|
468
|
+
let finalPrompt = effectivePrompt;
|
|
379
469
|
if (enhancers && enhancers.length > 0) {
|
|
380
470
|
const skillContent = getSkillContent(enhancers);
|
|
381
471
|
if (skillContent) {
|
|
382
|
-
finalPrompt = `${skillContent}\n\n${
|
|
472
|
+
finalPrompt = `${skillContent}\n\n${effectivePrompt}`;
|
|
383
473
|
}
|
|
384
474
|
}
|
|
385
475
|
// Add user message to history
|
|
@@ -418,25 +508,16 @@ export class AgentSessionManager {
|
|
|
418
508
|
apiKey: CLAUDE_CONFIG.apiKey,
|
|
419
509
|
model: CLAUDE_CONFIG.model,
|
|
420
510
|
tools: { type: 'preset', preset: 'claude_code' },
|
|
421
|
-
disallowedTools:
|
|
511
|
+
disallowedTools: attachmentDir
|
|
512
|
+
? ['AskUserQuestion', 'analyze_image'] // Disable built-in analyze_image when we have our own
|
|
513
|
+
: ['AskUserQuestion'],
|
|
422
514
|
settingSources: ['project'], // Enable CLAUDE.md loading
|
|
423
515
|
permissionMode: 'bypassPermissions',
|
|
424
516
|
allowDangerouslySkipPermissions: true,
|
|
425
517
|
// Create a fresh MCP server for each query call (SDK connects transport internally, cannot reuse)
|
|
426
518
|
...(() => {
|
|
427
|
-
const mcpServer = this.buildMcpServer(sessionId);
|
|
428
|
-
|
|
429
|
-
const toolHint = getModuleToolHint(session.enabledModules || []);
|
|
430
|
-
console.log(`[agentSession] MCP server created: name=${mcpServer.name}, hasHint=${!!toolHint}`);
|
|
431
|
-
console.log(`[agentSession] MCP server keys:`, Object.keys(mcpServer));
|
|
432
|
-
console.log(`[agentSession] MCP server type:`, mcpServer.type);
|
|
433
|
-
return {
|
|
434
|
-
mcpServers: { [mcpServer.name]: mcpServer },
|
|
435
|
-
...(toolHint ? { systemPrompt: { type: 'preset', preset: 'claude_code', append: toolHint } } : {})
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
console.log(`[agentSession] No MCP server created (enabledModules=${JSON.stringify(session.enabledModules)})`);
|
|
439
|
-
return {};
|
|
519
|
+
const mcpServer = this.buildMcpServer(sessionId, attachmentDir);
|
|
520
|
+
return mcpServer ? { mcpServers: [mcpServer] } : {};
|
|
440
521
|
})(),
|
|
441
522
|
...(pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable } : {}),
|
|
442
523
|
spawnClaudeCodeProcess: (spawnOptions) => {
|
|
@@ -582,29 +663,45 @@ export class AgentSessionManager {
|
|
|
582
663
|
console.log(`[agentSession] effectiveSettings for local model:`, JSON.stringify(effectiveSettings));
|
|
583
664
|
}
|
|
584
665
|
else {
|
|
585
|
-
//
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
666
|
+
// Resolve provider for multi-provider switching (Z.ai / MiniMax)
|
|
667
|
+
const resolved = resolveProvider(sessionModel);
|
|
668
|
+
console.log(`[agentSession] Resolved provider: ${resolved.provider} for model: ${sessionModel}`);
|
|
669
|
+
effectiveApiKey = resolved.apiKey;
|
|
670
|
+
effectiveEnv = envForClaudeCodeChild(undefined, resolved);
|
|
671
|
+
// Build settings env to prevent ~/.claude/settings.json from overriding our credentials
|
|
672
|
+
const settingsEnv = {
|
|
673
|
+
ANTHROPIC_API_KEY: resolved.apiKey,
|
|
674
|
+
ANTHROPIC_BASE_URL: resolved.baseUrl,
|
|
675
|
+
};
|
|
676
|
+
// For MiniMax: also override all model aliases in settings
|
|
677
|
+
if (resolved.provider === 'minimax') {
|
|
678
|
+
settingsEnv.ANTHROPIC_MODEL = resolved.model;
|
|
679
|
+
settingsEnv.ANTHROPIC_DEFAULT_SONNET_MODEL = resolved.model;
|
|
680
|
+
settingsEnv.ANTHROPIC_DEFAULT_OPUS_MODEL = resolved.model;
|
|
681
|
+
settingsEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL = resolved.model;
|
|
682
|
+
settingsEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1';
|
|
594
683
|
}
|
|
684
|
+
effectiveSettings = { env: settingsEnv };
|
|
685
|
+
console.log(`[agentSession] Provider: ${resolved.provider}, baseUrl: ${resolved.baseUrl}, model: ${resolved.model}`);
|
|
595
686
|
}
|
|
596
687
|
// Create query stream - resume session if we have a Claude session ID
|
|
597
688
|
// Always explicitly set model even when resuming to ensure we use the session's model
|
|
598
689
|
const queryStream = query({
|
|
599
|
-
prompt,
|
|
690
|
+
prompt: finalPrompt,
|
|
600
691
|
options: {
|
|
601
692
|
...queryOptions,
|
|
602
693
|
apiKey: effectiveApiKey,
|
|
603
694
|
model: effectiveModel,
|
|
604
695
|
env: effectiveEnv,
|
|
605
696
|
...(effectiveSettings ? { settings: effectiveSettings } : {}),
|
|
606
|
-
...(session.claudeSessionId && !localOverrides
|
|
697
|
+
...(session.claudeSessionId && !localOverrides && (() => {
|
|
698
|
+
// Don't resume if provider changed since last session (context format may differ)
|
|
699
|
+
const persisted = loadSessionState(sessionId);
|
|
700
|
+
const currentProvider = resolveProvider(sessionModel).provider;
|
|
701
|
+
return persisted?.provider === currentProvider;
|
|
702
|
+
})() ? { resume: session.claudeSessionId } : {})
|
|
607
703
|
// Note: don't resume session for local models - context format differs
|
|
704
|
+
// Note: also don't resume if provider differs from persisted session provider (context format may differ)
|
|
608
705
|
}
|
|
609
706
|
});
|
|
610
707
|
session.activeQueryStream = queryStream;
|
|
@@ -633,7 +730,7 @@ export class AgentSessionManager {
|
|
|
633
730
|
const systemMsg = message;
|
|
634
731
|
if (systemMsg.session_id && !session.claudeSessionId) {
|
|
635
732
|
session.claudeSessionId = systemMsg.session_id;
|
|
636
|
-
saveSessionState(sessionId, { claudeSessionId: systemMsg.session_id, model: session.model, updatedAt: Date.now() });
|
|
733
|
+
saveSessionState(sessionId, { claudeSessionId: systemMsg.session_id, model: session.model, provider: resolveProvider(session.model).provider, updatedAt: Date.now() });
|
|
637
734
|
}
|
|
638
735
|
}
|
|
639
736
|
if (message.type === 'assistant') {
|
|
@@ -641,7 +738,7 @@ export class AgentSessionManager {
|
|
|
641
738
|
// Capture Claude session ID from assistant message if not already set
|
|
642
739
|
if (msg.session_id && !session.claudeSessionId) {
|
|
643
740
|
session.claudeSessionId = msg.session_id;
|
|
644
|
-
saveSessionState(sessionId, { claudeSessionId: msg.session_id, model: session.model, updatedAt: Date.now() });
|
|
741
|
+
saveSessionState(sessionId, { claudeSessionId: msg.session_id, model: session.model, provider: resolveProvider(session.model).provider, updatedAt: Date.now() });
|
|
645
742
|
}
|
|
646
743
|
session.messages.push({
|
|
647
744
|
role: 'assistant',
|
|
@@ -1144,22 +1241,23 @@ export class AgentSessionManager {
|
|
|
1144
1241
|
* The SDK's query() connects the MCP server's internal transport, so we cannot
|
|
1145
1242
|
* reuse a single instance across multiple queries. This must be called fresh each time.
|
|
1146
1243
|
*/
|
|
1147
|
-
buildMcpServer(sessionId) {
|
|
1244
|
+
buildMcpServer(sessionId, attachmentDir) {
|
|
1148
1245
|
const session = this.sessions.get(sessionId);
|
|
1149
1246
|
if (!session) {
|
|
1150
1247
|
console.log(`[buildMcpServer] No session found for ${sessionId}`);
|
|
1151
1248
|
return undefined;
|
|
1152
1249
|
}
|
|
1153
1250
|
const enabledModules = session.enabledModules || [];
|
|
1154
|
-
console.log(`[buildMcpServer] Session ${sessionId}: enabledModules=${JSON.stringify(enabledModules)}`);
|
|
1155
|
-
if (enabledModules.length === 0) {
|
|
1156
|
-
console.log(`[buildMcpServer] No enabled modules, skipping MCP server creation`);
|
|
1251
|
+
console.log(`[buildMcpServer] Session ${sessionId}: enabledModules=${JSON.stringify(enabledModules)}, attachmentDir=${attachmentDir || 'none'}`);
|
|
1252
|
+
if (enabledModules.length === 0 && !attachmentDir) {
|
|
1253
|
+
console.log(`[buildMcpServer] No enabled modules and no attachments, skipping MCP server creation`);
|
|
1157
1254
|
return undefined;
|
|
1158
1255
|
}
|
|
1159
1256
|
const handler = this.sessionHandlers.get(sessionId);
|
|
1160
1257
|
return createModuleMcpServer({
|
|
1161
1258
|
enabledModules,
|
|
1162
1259
|
moduleSettings: session.moduleSettings || {},
|
|
1260
|
+
attachmentDir,
|
|
1163
1261
|
onChoiceRequest: handler?.onChoiceRequest
|
|
1164
1262
|
? async (request) => {
|
|
1165
1263
|
return new Promise((resolve) => {
|