@exreve/exk 1.0.16 → 1.0.18
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 +222 -145
- package/dist/index.js +93 -0
- package/dist/openaiAdapter.js +192 -0
- package/dist/ttc-cli.tar.gz +0 -0
- package/package.json +3 -2
package/dist/agentSession.js
CHANGED
|
@@ -1,14 +1,57 @@
|
|
|
1
1
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import { execSync, spawn } from 'child_process';
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
4
4
|
import { symlink as fsSymlink } from 'fs';
|
|
5
|
-
// Memory system removed for performance
|
|
6
5
|
import { getSkillContent } from './skills/index.js';
|
|
6
|
+
import { isLocalModel, unwrapModelName, startOpenAIAdapter, getAdapterConfig } from './openaiAdapter.js';
|
|
7
7
|
import { createModuleMcpServer } from './moduleMcpServer.js';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import os from 'os';
|
|
10
10
|
import { createRequire } from 'module';
|
|
11
11
|
import { promisify } from 'util';
|
|
12
|
+
// ============ Session State Persistence ============
|
|
13
|
+
// Persists claudeSessionId to disk so context survives CLI restarts.
|
|
14
|
+
// Files are stored in ~/.talk-to-code/session-state/<sessionId>.json
|
|
15
|
+
const SESSION_STATE_DIR = path.join(os.homedir(), '.talk-to-code', 'session-state');
|
|
16
|
+
function sessionStatePath(sessionId) {
|
|
17
|
+
return path.join(SESSION_STATE_DIR, `${sessionId}.json`);
|
|
18
|
+
}
|
|
19
|
+
function saveSessionState(sessionId, state) {
|
|
20
|
+
try {
|
|
21
|
+
if (!existsSync(SESSION_STATE_DIR)) {
|
|
22
|
+
mkdirSync(SESSION_STATE_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
writeFileSync(sessionStatePath(sessionId), JSON.stringify(state, null, 2));
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
console.error(`[AgentSessionManager] Failed to persist session state for ${sessionId}:`, err);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function loadSessionState(sessionId) {
|
|
31
|
+
try {
|
|
32
|
+
const filePath = sessionStatePath(sessionId);
|
|
33
|
+
if (!existsSync(filePath))
|
|
34
|
+
return null;
|
|
35
|
+
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
36
|
+
if (data && typeof data.claudeSessionId === 'string' && data.claudeSessionId) {
|
|
37
|
+
return data;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function deleteSessionState(sessionId) {
|
|
46
|
+
try {
|
|
47
|
+
const filePath = sessionStatePath(sessionId);
|
|
48
|
+
if (existsSync(filePath))
|
|
49
|
+
unlinkSync(filePath);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Ignore cleanup errors
|
|
53
|
+
}
|
|
54
|
+
}
|
|
12
55
|
/**
|
|
13
56
|
* Resolve path to the SDK's bundled cli.js.
|
|
14
57
|
* We resolve this ourselves so it works reliably on Windows when running from
|
|
@@ -91,25 +134,83 @@ function loadAiConfig() {
|
|
|
91
134
|
const apiKey = typeof config.authToken === 'string' ? config.authToken.trim() : '';
|
|
92
135
|
const baseUrl = typeof config.baseUrl === 'string' ? config.baseUrl.trim() : '';
|
|
93
136
|
const model = typeof config.model === 'string' && config.model.trim() ? config.model.trim() : DEFAULT_AI_MODEL;
|
|
94
|
-
|
|
137
|
+
const proxy = typeof config.proxy === 'string' ? config.proxy.trim() : '';
|
|
138
|
+
return { apiKey, baseUrl, model, proxy };
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return { apiKey: '', baseUrl: '', model: DEFAULT_AI_MODEL, proxy: '' };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Create (or reuse) an empty directory to use as CLAUDE_CONFIG_DIR.
|
|
145
|
+
* Setting this prevents the spawned Claude CLI from reading ~/.claude/settings.json,
|
|
146
|
+
* which may contain env.ANTHROPIC_BASE_URL pointing to z.ai and would override our
|
|
147
|
+
* carefully configured base URL. */
|
|
148
|
+
function getEmptyConfigDir() {
|
|
149
|
+
const configDir = path.join(os.homedir(), '.talk-to-code', 'empty-config-dir');
|
|
150
|
+
if (!existsSync(configDir)) {
|
|
151
|
+
mkdirSync(configDir, { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
return configDir;
|
|
154
|
+
}
|
|
155
|
+
const PROXY_CONFIG_PATH = path.join(os.homedir(), '.talk-to-code', 'proxy.json');
|
|
156
|
+
/** Read proxy toggle state from disk (synchronous) */
|
|
157
|
+
function readProxyToggle() {
|
|
158
|
+
try {
|
|
159
|
+
const data = readFileSync(PROXY_CONFIG_PATH, 'utf-8');
|
|
160
|
+
return JSON.parse(data);
|
|
95
161
|
}
|
|
96
162
|
catch {
|
|
97
|
-
return {
|
|
163
|
+
return { enabled: false };
|
|
98
164
|
}
|
|
99
165
|
}
|
|
100
|
-
/** Env for the Claude Code child: copy of host env with host ANTHROPIC_* stripped, then inject from ai-config only.
|
|
101
|
-
|
|
166
|
+
/** Env for the Claude Code child: copy of host env with host ANTHROPIC_* stripped, then inject from ai-config only.
|
|
167
|
+
* If a local model is provided, override baseUrl to point to the anthropic-proxy adapter. */
|
|
168
|
+
function envForClaudeCodeChild(localModel) {
|
|
102
169
|
const env = { ...process.env };
|
|
170
|
+
// Strip any host ANTHROPIC_* vars to prevent leaking credentials or wrong URLs
|
|
103
171
|
delete env.ANTHROPIC_API_KEY;
|
|
104
172
|
delete env.ANTHROPIC_BASE_URL;
|
|
105
173
|
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
106
|
-
|
|
174
|
+
// Inject from our ai-config.json only
|
|
175
|
+
const { apiKey, baseUrl, proxy } = loadAiConfig();
|
|
107
176
|
if (apiKey)
|
|
108
177
|
env.ANTHROPIC_API_KEY = apiKey;
|
|
109
178
|
if (baseUrl)
|
|
110
179
|
env.ANTHROPIC_BASE_URL = baseUrl;
|
|
180
|
+
// Apply proxy if enabled
|
|
181
|
+
const proxyToggle = readProxyToggle();
|
|
182
|
+
if (proxyToggle.enabled && proxy) {
|
|
183
|
+
env.HTTPS_PROXY = proxy;
|
|
184
|
+
env.HTTP_PROXY = proxy;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// Clear any inherited proxy env
|
|
188
|
+
delete env.HTTPS_PROXY;
|
|
189
|
+
delete env.HTTP_PROXY;
|
|
190
|
+
delete env.https_proxy;
|
|
191
|
+
delete env.http_proxy;
|
|
192
|
+
}
|
|
193
|
+
// Prevent ~/.claude/settings.json env section from overriding our base URL.
|
|
194
|
+
// This redirects the Claude config dir to an empty dir so that
|
|
195
|
+
// ~/.claude/settings.json (which may have ANTHROPIC_BASE_URL set to z.ai)
|
|
196
|
+
// is never read during the CLI process initialization.
|
|
197
|
+
env.CLAUDE_CONFIG_DIR = getEmptyConfigDir();
|
|
111
198
|
return env;
|
|
112
199
|
}
|
|
200
|
+
/** Get env overrides for a local model (adapter proxy URL + dummy key). Returns null if not a local model. */
|
|
201
|
+
async function getLocalModelEnvOverrides(model) {
|
|
202
|
+
if (!isLocalModel(model))
|
|
203
|
+
return null;
|
|
204
|
+
const adapterConfig = getAdapterConfig(model);
|
|
205
|
+
if (!adapterConfig)
|
|
206
|
+
return null;
|
|
207
|
+
const proxyUrl = await startOpenAIAdapter({
|
|
208
|
+
targetBaseUrl: adapterConfig.targetBaseUrl,
|
|
209
|
+
model: unwrapModelName(model),
|
|
210
|
+
apiKey: adapterConfig.apiKey,
|
|
211
|
+
});
|
|
212
|
+
return { baseUrl: proxyUrl, apiKey: 'local-no-key-needed' };
|
|
213
|
+
}
|
|
113
214
|
// Lazy config getter - reloads from file each time (so daemon picks up changes without restart)
|
|
114
215
|
const CLAUDE_CONFIG = {
|
|
115
216
|
get apiKey() { return loadAiConfig().apiKey; },
|
|
@@ -146,60 +247,39 @@ export class AgentSessionManager {
|
|
|
146
247
|
}
|
|
147
248
|
}
|
|
148
249
|
}
|
|
149
|
-
// If session already exists, update
|
|
250
|
+
// If session already exists, update mutable fields
|
|
150
251
|
if (this.sessions.has(sessionId)) {
|
|
151
252
|
const existingSession = this.sessions.get(sessionId);
|
|
152
253
|
// Update model if a new one is provided
|
|
153
254
|
if (model && model !== existingSession.model) {
|
|
154
255
|
existingSession.model = model;
|
|
155
|
-
// DISABLED: File logging removed for performance
|
|
156
|
-
// if (existingSession.logger) {
|
|
157
|
-
// await existingSession.logger.info(`Model updated to: ${model}`)
|
|
158
|
-
// }
|
|
159
256
|
}
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
257
|
+
// Update enabled modules and settings if provided
|
|
258
|
+
const newModules = handler.enabledModules;
|
|
259
|
+
if (newModules) {
|
|
260
|
+
existingSession.enabledModules = newModules;
|
|
261
|
+
}
|
|
262
|
+
const newModuleSettings = handler.moduleSettings;
|
|
263
|
+
if (newModuleSettings) {
|
|
264
|
+
existingSession.moduleSettings = newModuleSettings;
|
|
265
|
+
}
|
|
266
|
+
existingSession.userChoiceEnabled = handler.userChoiceEnabled || false;
|
|
267
|
+
// Ensure abort controller is fresh for new queries
|
|
165
268
|
existingSession.abortController = new AbortController();
|
|
269
|
+
// Update handler to keep onChoiceRequest callback fresh
|
|
270
|
+
this.sessionHandlers.set(sessionId, handler);
|
|
166
271
|
return;
|
|
167
272
|
}
|
|
168
|
-
// DISABLED: File logging removed for performance
|
|
169
|
-
// const logger = new AgentLogger(sessionId)
|
|
170
|
-
// await logger.logSessionCreated(projectPath)
|
|
171
|
-
// await logger.logModelConfig(sessionModel, CLAUDE_CONFIG.baseUrl)
|
|
172
|
-
const logger = undefined;
|
|
173
273
|
// Store the handler for this session
|
|
174
274
|
this.sessionHandlers.set(sessionId, handler);
|
|
175
275
|
const abortController = new AbortController();
|
|
176
|
-
// Create MCP server for modules if enabled modules are provided
|
|
177
|
-
let mcpServer;
|
|
178
276
|
const enabledModules = handler.enabledModules || [];
|
|
179
277
|
const moduleSettings = handler.moduleSettings || {};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
? async (request) => {
|
|
186
|
-
return new Promise((resolve) => {
|
|
187
|
-
const session = this.sessions.get(sessionId);
|
|
188
|
-
if (session) {
|
|
189
|
-
session.pendingChoice = { request, resolve };
|
|
190
|
-
handler.onChoiceRequest(request);
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
resolve({ choiceId: request.choiceId, selectedValue: null });
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
: undefined
|
|
198
|
-
});
|
|
199
|
-
// DISABLED: File logging removed for performance
|
|
200
|
-
// if (logger) {
|
|
201
|
-
// await logger.info(`MCP server created with modules: ${enabledModules.join(', ')}`)
|
|
202
|
-
// }
|
|
278
|
+
// Restore claudeSessionId from disk (survives CLI restart)
|
|
279
|
+
const persistedState = loadSessionState(sessionId);
|
|
280
|
+
const restoredClaudeSessionId = persistedState?.claudeSessionId;
|
|
281
|
+
if (restoredClaudeSessionId) {
|
|
282
|
+
console.log(`[AgentSessionManager] Restored claudeSessionId for session ${sessionId}: ${restoredClaudeSessionId}`);
|
|
203
283
|
}
|
|
204
284
|
this.sessions.set(sessionId, {
|
|
205
285
|
abortController,
|
|
@@ -209,35 +289,26 @@ export class AgentSessionManager {
|
|
|
209
289
|
totalCostUsd: 0,
|
|
210
290
|
promptQueue: [],
|
|
211
291
|
isProcessingQueue: false,
|
|
212
|
-
claudeSessionId:
|
|
213
|
-
// logger, // DISABLED: File logging removed for performance
|
|
214
|
-
// Memory system removed for performance
|
|
292
|
+
claudeSessionId: restoredClaudeSessionId, // Restored from disk or undefined
|
|
215
293
|
childProcesses: new Set(),
|
|
216
294
|
claudeProcessGroupId: undefined,
|
|
217
295
|
currentPromptId: undefined,
|
|
218
296
|
model: sessionModel,
|
|
219
297
|
userChoiceEnabled: handler.userChoiceEnabled || false,
|
|
220
298
|
enabledModules,
|
|
221
|
-
moduleSettings
|
|
222
|
-
mcpServer
|
|
299
|
+
moduleSettings
|
|
223
300
|
});
|
|
224
301
|
// Auto-regenerate CLAUDE.md for fresh project context
|
|
225
|
-
|
|
226
|
-
await this.regenerateClaudeMd(projectPath, undefined);
|
|
302
|
+
await this.regenerateClaudeMd(projectPath);
|
|
227
303
|
}
|
|
228
304
|
/**
|
|
229
305
|
* Regenerate CLAUDE.md for fresh project context
|
|
230
306
|
*/
|
|
231
|
-
async regenerateClaudeMd(projectPath
|
|
307
|
+
async regenerateClaudeMd(projectPath) {
|
|
232
308
|
try {
|
|
233
309
|
const scriptPath = path.join(projectPath, 'scripts', 'generate-claude-md.js');
|
|
234
310
|
if (existsSync(scriptPath)) {
|
|
235
|
-
const startTime = Date.now();
|
|
236
311
|
execSync(`node "${scriptPath}"`, { cwd: projectPath, stdio: 'ignore' });
|
|
237
|
-
const duration = Date.now() - startTime;
|
|
238
|
-
if (logger) {
|
|
239
|
-
await logger.info(`CLAUDE.md regenerated in ${duration}ms`);
|
|
240
|
-
}
|
|
241
312
|
}
|
|
242
313
|
}
|
|
243
314
|
catch (error) {
|
|
@@ -245,7 +316,6 @@ export class AgentSessionManager {
|
|
|
245
316
|
console.error('[AgentSessionManager] Failed to regenerate CLAUDE.md:', error);
|
|
246
317
|
}
|
|
247
318
|
}
|
|
248
|
-
// Memory system removed for performance - no loadMemoryContext or storeConversationInMemory
|
|
249
319
|
async sendPrompt(sessionId, prompt, enhancers = [], handler) {
|
|
250
320
|
// Ensure session exists
|
|
251
321
|
if (!this.sessions.has(sessionId)) {
|
|
@@ -255,10 +325,6 @@ export class AgentSessionManager {
|
|
|
255
325
|
// Update session model if provided in handler
|
|
256
326
|
if (handler.model && handler.model !== session.model) {
|
|
257
327
|
session.model = handler.model;
|
|
258
|
-
// DISABLED: File logging removed for performance
|
|
259
|
-
// if (session.logger) {
|
|
260
|
-
// await session.logger.info(`Model updated to: ${handler.model}`)
|
|
261
|
-
// }
|
|
262
328
|
}
|
|
263
329
|
// Add prompt to queue - store promptId for cancellation
|
|
264
330
|
session.promptQueue.push({
|
|
@@ -317,19 +383,12 @@ export class AgentSessionManager {
|
|
|
317
383
|
// Emit status update: CLI is starting to process the prompt (IMMEDIATELY)
|
|
318
384
|
// This ensures real-time status updates before any async operations
|
|
319
385
|
onStatusUpdate?.('running');
|
|
320
|
-
// DISABLED: File logging removed for performance
|
|
321
|
-
// // Log prompt start
|
|
322
|
-
// if (session.logger) {
|
|
323
|
-
// await session.logger.logPromptStart(prompt, projectPath)
|
|
324
|
-
// }
|
|
325
386
|
// Wait for current query to finish before starting next prompt
|
|
326
|
-
// REMOVED: 200ms artificial delay - stream draining is sufficient
|
|
327
387
|
if (session.activeQueryStream !== undefined) {
|
|
328
388
|
try {
|
|
329
389
|
for await (const _ of session.activeQueryStream) { }
|
|
330
390
|
}
|
|
331
391
|
catch { }
|
|
332
|
-
// REMOVED: await new Promise(resolve => setTimeout(resolve, 200))
|
|
333
392
|
session.activeQueryStream = undefined;
|
|
334
393
|
}
|
|
335
394
|
session.activeQueryStream = undefined;
|
|
@@ -339,11 +398,6 @@ export class AgentSessionManager {
|
|
|
339
398
|
const skillContent = getSkillContent(enhancers);
|
|
340
399
|
if (skillContent) {
|
|
341
400
|
finalPrompt = `${skillContent}\n\n${prompt}`;
|
|
342
|
-
// DISABLED: File logging removed for performance
|
|
343
|
-
// // Log that enhancers are being used
|
|
344
|
-
// if (session.logger) {
|
|
345
|
-
// await session.logger.info(`Using enhancers: ${enhancers.join(', ')}`)
|
|
346
|
-
// }
|
|
347
401
|
}
|
|
348
402
|
}
|
|
349
403
|
// Add user message to history
|
|
@@ -375,14 +429,6 @@ export class AgentSessionManager {
|
|
|
375
429
|
});
|
|
376
430
|
// Use cached Claude executable path (resolved at module load time for performance)
|
|
377
431
|
const pathToClaudeCodeExecutable = CACHED_CLAUDE_PATH;
|
|
378
|
-
// DISABLED: File logging removed for performance
|
|
379
|
-
// if (session.logger) {
|
|
380
|
-
// if (pathToClaudeCodeExecutable) {
|
|
381
|
-
// session.logger.info(`Using Claude (cli.js): ${pathToClaudeCodeExecutable}`).catch(() => {})
|
|
382
|
-
// } else {
|
|
383
|
-
// session.logger.info('Using SDK built-in Claude (cli.js)').catch(() => {})
|
|
384
|
-
// }
|
|
385
|
-
// }
|
|
386
432
|
// Build query options - include abort signal for cancellation
|
|
387
433
|
const queryOptions = {
|
|
388
434
|
signal: abortController.signal, // Pass abort signal to SDK for interruption
|
|
@@ -394,11 +440,18 @@ export class AgentSessionManager {
|
|
|
394
440
|
settingSources: ['project'], // Enable CLAUDE.md loading
|
|
395
441
|
permissionMode: 'bypassPermissions',
|
|
396
442
|
allowDangerouslySkipPermissions: true,
|
|
397
|
-
//
|
|
398
|
-
...(
|
|
443
|
+
// Create a fresh MCP server for each query call (SDK connects transport internally, cannot reuse)
|
|
444
|
+
...(() => {
|
|
445
|
+
const mcpServer = this.buildMcpServer(sessionId);
|
|
446
|
+
return mcpServer ? { mcpServers: [mcpServer] } : {};
|
|
447
|
+
})(),
|
|
399
448
|
...(pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable } : {}),
|
|
400
449
|
spawnClaudeCodeProcess: (spawnOptions) => {
|
|
401
450
|
const { command, args, cwd: cwd2, env, signal } = spawnOptions;
|
|
451
|
+
// Debug: log what env/args are being passed to Claude process
|
|
452
|
+
console.log(`[agentSession] Spawn ANTHROPIC_BASE_URL:`, env?.ANTHROPIC_BASE_URL || '(not set)');
|
|
453
|
+
console.log(`[agentSession] Spawn ANTHROPIC_API_KEY:`, env?.ANTHROPIC_API_KEY ? '(set)' : '(not set)');
|
|
454
|
+
console.log(`[agentSession] Spawn args:`, args?.join(' '));
|
|
402
455
|
// Only check file existence when command is a path (not a bare name like "claude" from PATH)
|
|
403
456
|
const hasPathSep = command.includes(path.sep) || command.includes('/') || command.includes('\\');
|
|
404
457
|
if (hasPathSep && !existsSync(command)) {
|
|
@@ -514,18 +567,51 @@ export class AgentSessionManager {
|
|
|
514
567
|
};
|
|
515
568
|
// Log model being used for debugging
|
|
516
569
|
const sessionModel = session.model || CLAUDE_CONFIG.model;
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
570
|
+
// Resolve local model adapter overrides (if using OpenAI-compatible endpoint)
|
|
571
|
+
let effectiveModel = sessionModel;
|
|
572
|
+
let effectiveApiKey = queryOptions.apiKey;
|
|
573
|
+
let effectiveEnv = queryOptions.env;
|
|
574
|
+
let effectiveSettings;
|
|
575
|
+
const localOverrides = await getLocalModelEnvOverrides(sessionModel);
|
|
576
|
+
if (localOverrides) {
|
|
577
|
+
effectiveModel = unwrapModelName(sessionModel);
|
|
578
|
+
effectiveApiKey = localOverrides.apiKey;
|
|
579
|
+
effectiveEnv = {
|
|
580
|
+
...effectiveEnv,
|
|
581
|
+
ANTHROPIC_API_KEY: localOverrides.apiKey,
|
|
582
|
+
ANTHROPIC_BASE_URL: localOverrides.baseUrl,
|
|
583
|
+
};
|
|
584
|
+
// Override settings to prevent ~/.claude/settings.json env from overriding our proxy URL.
|
|
585
|
+
// Claude Code CLI reads settings.json → env section and applies those on top of spawn env,
|
|
586
|
+
// which would replace our ANTHROPIC_BASE_URL with the z.ai URL.
|
|
587
|
+
effectiveSettings = { env: { ANTHROPIC_API_KEY: localOverrides.apiKey, ANTHROPIC_BASE_URL: localOverrides.baseUrl } };
|
|
588
|
+
console.log(`[agentSession] Using local model adapter: ${sessionModel} -> ${localOverrides.baseUrl}`);
|
|
589
|
+
console.log(`[agentSession] effectiveSettings for local model:`, JSON.stringify(effectiveSettings));
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
// Even for non-local models, ensure ~/.claude/settings.json env doesn't override our baseUrl
|
|
593
|
+
const aiConfig = loadAiConfig();
|
|
594
|
+
if (aiConfig.baseUrl || aiConfig.apiKey) {
|
|
595
|
+
const settingsEnv = {};
|
|
596
|
+
if (aiConfig.apiKey)
|
|
597
|
+
settingsEnv.ANTHROPIC_API_KEY = aiConfig.apiKey;
|
|
598
|
+
if (aiConfig.baseUrl)
|
|
599
|
+
settingsEnv.ANTHROPIC_BASE_URL = aiConfig.baseUrl;
|
|
600
|
+
effectiveSettings = { env: settingsEnv };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
521
603
|
// Create query stream - resume session if we have a Claude session ID
|
|
522
604
|
// Always explicitly set model even when resuming to ensure we use the session's model
|
|
523
605
|
const queryStream = query({
|
|
524
606
|
prompt,
|
|
525
607
|
options: {
|
|
526
608
|
...queryOptions,
|
|
527
|
-
|
|
528
|
-
|
|
609
|
+
apiKey: effectiveApiKey,
|
|
610
|
+
model: effectiveModel,
|
|
611
|
+
env: effectiveEnv,
|
|
612
|
+
...(effectiveSettings ? { settings: effectiveSettings } : {}),
|
|
613
|
+
...(session.claudeSessionId && !localOverrides ? { resume: session.claudeSessionId } : {})
|
|
614
|
+
// Note: don't resume session for local models - context format differs
|
|
529
615
|
}
|
|
530
616
|
});
|
|
531
617
|
session.activeQueryStream = queryStream;
|
|
@@ -554,10 +640,7 @@ export class AgentSessionManager {
|
|
|
554
640
|
const systemMsg = message;
|
|
555
641
|
if (systemMsg.session_id && !session.claudeSessionId) {
|
|
556
642
|
session.claudeSessionId = systemMsg.session_id;
|
|
557
|
-
|
|
558
|
-
// if (session.logger) {
|
|
559
|
-
// await session.logger.logClaudeSessionId(systemMsg.session_id)
|
|
560
|
-
// }
|
|
643
|
+
saveSessionState(sessionId, { claudeSessionId: systemMsg.session_id, model: session.model, updatedAt: Date.now() });
|
|
561
644
|
}
|
|
562
645
|
}
|
|
563
646
|
if (message.type === 'assistant') {
|
|
@@ -565,27 +648,13 @@ export class AgentSessionManager {
|
|
|
565
648
|
// Capture Claude session ID from assistant message if not already set
|
|
566
649
|
if (msg.session_id && !session.claudeSessionId) {
|
|
567
650
|
session.claudeSessionId = msg.session_id;
|
|
568
|
-
|
|
569
|
-
// if (session.logger) {
|
|
570
|
-
// await session.logger.logClaudeSessionId(msg.session_id)
|
|
571
|
-
// }
|
|
651
|
+
saveSessionState(sessionId, { claudeSessionId: msg.session_id, model: session.model, updatedAt: Date.now() });
|
|
572
652
|
}
|
|
573
653
|
session.messages.push({
|
|
574
654
|
role: 'assistant',
|
|
575
655
|
content: msg.message,
|
|
576
656
|
timestamp: Date.now()
|
|
577
657
|
});
|
|
578
|
-
// DISABLED: File logging removed for performance
|
|
579
|
-
// // Log agent response
|
|
580
|
-
// const responseText = typeof msg.message === 'string' ? msg.message : JSON.stringify(msg.message)
|
|
581
|
-
// if (session.logger) {
|
|
582
|
-
// await session.logger.logAgentResponse(responseText, {
|
|
583
|
-
// uuid: msg.uuid,
|
|
584
|
-
// sessionId: msg.session_id,
|
|
585
|
-
// error: msg.error,
|
|
586
|
-
// contextSize: session.messages.length
|
|
587
|
-
// })
|
|
588
|
-
// }
|
|
589
658
|
onOutput({
|
|
590
659
|
type: 'assistant',
|
|
591
660
|
data: msg.message,
|
|
@@ -614,31 +683,7 @@ export class AgentSessionManager {
|
|
|
614
683
|
outputTokens,
|
|
615
684
|
totalTokens: inputTokens + outputTokens
|
|
616
685
|
};
|
|
617
|
-
// DISABLED: File logging removed for performance
|
|
618
|
-
// // Log token usage
|
|
619
|
-
// if (session.logger) {
|
|
620
|
-
// await session.logger.logTokenUsage({
|
|
621
|
-
// inputTokens: session.totalInputTokens,
|
|
622
|
-
// outputTokens: session.totalOutputTokens,
|
|
623
|
-
// totalTokens: session.totalInputTokens + session.totalOutputTokens,
|
|
624
|
-
// costUsd: session.totalCostUsd
|
|
625
|
-
// })
|
|
626
|
-
// }
|
|
627
686
|
}
|
|
628
|
-
const promptDuration = Date.now() - promptStartTime;
|
|
629
|
-
// DISABLED: File logging removed for performance
|
|
630
|
-
// // Log prompt completion
|
|
631
|
-
// if (session.logger) {
|
|
632
|
-
// await session.logger.logPromptEnd(
|
|
633
|
-
// msg.is_error ? 1 : 0,
|
|
634
|
-
// promptDuration,
|
|
635
|
-
// session.lastUsage ? {
|
|
636
|
-
// inputTokens: session.lastUsage.inputTokens,
|
|
637
|
-
// outputTokens: session.lastUsage.outputTokens,
|
|
638
|
-
// costUsd: msg.total_cost_usd || 0
|
|
639
|
-
// } : undefined
|
|
640
|
-
// )
|
|
641
|
-
// }
|
|
642
687
|
onOutput({
|
|
643
688
|
type: 'result',
|
|
644
689
|
data: msg,
|
|
@@ -672,7 +717,6 @@ export class AgentSessionManager {
|
|
|
672
717
|
onStatusUpdate?.(exitCode === 0 ? 'completed' : 'error');
|
|
673
718
|
onComplete(exitCode);
|
|
674
719
|
session.activeQueryStream = undefined;
|
|
675
|
-
// Memory system removed for performance
|
|
676
720
|
break; // Prompt complete, continue to next in queue
|
|
677
721
|
}
|
|
678
722
|
else if (message.type === 'user') {
|
|
@@ -872,14 +916,6 @@ export class AgentSessionManager {
|
|
|
872
916
|
const currentSession = this.sessions.get(sessionId);
|
|
873
917
|
if (currentSession) {
|
|
874
918
|
currentSession.activeQueryStream = undefined;
|
|
875
|
-
// DISABLED: File logging removed for performance
|
|
876
|
-
// // Log error
|
|
877
|
-
// if (currentSession.logger) {
|
|
878
|
-
// await currentSession.logger.logSessionError(error, {
|
|
879
|
-
// prompt: prompt.substring(0, 200),
|
|
880
|
-
// projectPath
|
|
881
|
-
// })
|
|
882
|
-
// }
|
|
883
919
|
}
|
|
884
920
|
// Clean up abort controller (promptId is guaranteed to exist here due to check at line 204)
|
|
885
921
|
if (promptId) {
|
|
@@ -920,6 +956,8 @@ export class AgentSessionManager {
|
|
|
920
956
|
// Clear Claude session ID to start fresh
|
|
921
957
|
session.claudeSessionId = undefined;
|
|
922
958
|
session.lastUsage = undefined;
|
|
959
|
+
// Also clear persisted state
|
|
960
|
+
deleteSessionState(sessionId);
|
|
923
961
|
}
|
|
924
962
|
}
|
|
925
963
|
async deleteSession(sessionId) {
|
|
@@ -929,6 +967,8 @@ export class AgentSessionManager {
|
|
|
929
967
|
session.activeQueryStream = undefined;
|
|
930
968
|
this.sessions.delete(sessionId);
|
|
931
969
|
}
|
|
970
|
+
// Clean up persisted state
|
|
971
|
+
deleteSessionState(sessionId);
|
|
932
972
|
}
|
|
933
973
|
/**
|
|
934
974
|
* Cancel a running or queued prompt by promptId
|
|
@@ -1106,6 +1146,43 @@ export class AgentSessionManager {
|
|
|
1106
1146
|
return { success: false, message: error.message || 'Emergency stop failed' };
|
|
1107
1147
|
}
|
|
1108
1148
|
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Build a fresh MCP server for a query call.
|
|
1151
|
+
* The SDK's query() connects the MCP server's internal transport, so we cannot
|
|
1152
|
+
* reuse a single instance across multiple queries. This must be called fresh each time.
|
|
1153
|
+
*/
|
|
1154
|
+
buildMcpServer(sessionId) {
|
|
1155
|
+
const session = this.sessions.get(sessionId);
|
|
1156
|
+
if (!session) {
|
|
1157
|
+
console.log(`[buildMcpServer] No session found for ${sessionId}`);
|
|
1158
|
+
return undefined;
|
|
1159
|
+
}
|
|
1160
|
+
const enabledModules = session.enabledModules || [];
|
|
1161
|
+
console.log(`[buildMcpServer] Session ${sessionId}: enabledModules=${JSON.stringify(enabledModules)}`);
|
|
1162
|
+
if (enabledModules.length === 0) {
|
|
1163
|
+
console.log(`[buildMcpServer] No enabled modules, skipping MCP server creation`);
|
|
1164
|
+
return undefined;
|
|
1165
|
+
}
|
|
1166
|
+
const handler = this.sessionHandlers.get(sessionId);
|
|
1167
|
+
return createModuleMcpServer({
|
|
1168
|
+
enabledModules,
|
|
1169
|
+
moduleSettings: session.moduleSettings || {},
|
|
1170
|
+
onChoiceRequest: handler?.onChoiceRequest
|
|
1171
|
+
? async (request) => {
|
|
1172
|
+
return new Promise((resolve) => {
|
|
1173
|
+
const sess = this.sessions.get(sessionId);
|
|
1174
|
+
if (sess) {
|
|
1175
|
+
sess.pendingChoice = { request, resolve };
|
|
1176
|
+
handler.onChoiceRequest(request);
|
|
1177
|
+
}
|
|
1178
|
+
else {
|
|
1179
|
+
resolve({ choiceId: request.choiceId, selectedValue: null });
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
: undefined
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1109
1186
|
/**
|
|
1110
1187
|
* Handle user choice response from frontend
|
|
1111
1188
|
*/
|
package/dist/index.js
CHANGED
|
@@ -66,6 +66,33 @@ async function fetchAiConfig(authToken) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
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
|
+
}
|
|
69
96
|
/** True if ai-config.json has a model API key (not read from host ANTHROPIC_* env). */
|
|
70
97
|
function hasAiCredentials() {
|
|
71
98
|
try {
|
|
@@ -1796,6 +1823,39 @@ async function runDaemon(foreground = false, email) {
|
|
|
1796
1823
|
});
|
|
1797
1824
|
});
|
|
1798
1825
|
// Cloudflared handlers
|
|
1826
|
+
// Proxy toggle handler
|
|
1827
|
+
socket.on('proxy:toggle:request', async (data, callback) => {
|
|
1828
|
+
try {
|
|
1829
|
+
const { enable } = data;
|
|
1830
|
+
const proxyUrl = getProxyUrl();
|
|
1831
|
+
if (enable && !proxyUrl) {
|
|
1832
|
+
callback?.({ success: false, enabled: false });
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
await writeProxyConfig({ enabled: enable });
|
|
1836
|
+
if (foreground) {
|
|
1837
|
+
console.log(`[CLI] Proxy ${enable ? 'enabled' : 'disabled'}${proxyUrl ? `: ${proxyUrl}` : ''}`);
|
|
1838
|
+
}
|
|
1839
|
+
else {
|
|
1840
|
+
console.log(`Proxy ${enable ? 'enabled' : 'disabled'}`);
|
|
1841
|
+
}
|
|
1842
|
+
callback?.({ success: true, enabled: enable, proxyUrl: enable ? proxyUrl : undefined });
|
|
1843
|
+
}
|
|
1844
|
+
catch (error) {
|
|
1845
|
+
callback?.({ success: false, enabled: false });
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
// Proxy status handler
|
|
1849
|
+
socket.on('proxy:status:request', async (_data, callback) => {
|
|
1850
|
+
try {
|
|
1851
|
+
const proxyConfig = await readProxyConfig();
|
|
1852
|
+
const proxyUrl = getProxyUrl();
|
|
1853
|
+
callback?.({ enabled: proxyConfig.enabled, proxyUrl: proxyConfig.enabled ? proxyUrl : undefined });
|
|
1854
|
+
}
|
|
1855
|
+
catch {
|
|
1856
|
+
callback?.({ enabled: false });
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1799
1859
|
socket.on('cloudflared:check:request', async () => {
|
|
1800
1860
|
try {
|
|
1801
1861
|
let installed = false;
|
|
@@ -2779,4 +2839,37 @@ program
|
|
|
2779
2839
|
process.exit(1);
|
|
2780
2840
|
});
|
|
2781
2841
|
});
|
|
2842
|
+
// Enable proxy command
|
|
2843
|
+
program
|
|
2844
|
+
.command('enable')
|
|
2845
|
+
.description('Enable a feature (e.g. proxy)')
|
|
2846
|
+
.argument('<feature>', 'Feature to enable (proxy)')
|
|
2847
|
+
.action(async (feature) => {
|
|
2848
|
+
if (feature !== 'proxy') {
|
|
2849
|
+
console.error(`Unknown feature: ${feature}. Available: proxy`);
|
|
2850
|
+
process.exit(1);
|
|
2851
|
+
}
|
|
2852
|
+
const proxyUrl = getProxyUrl();
|
|
2853
|
+
if (!proxyUrl) {
|
|
2854
|
+
console.log('No proxy URL configured. Run "exk daemon" first to sync config from server.');
|
|
2855
|
+
process.exit(1);
|
|
2856
|
+
}
|
|
2857
|
+
await writeProxyConfig({ enabled: true });
|
|
2858
|
+
console.log(`Proxy enabled: ${proxyUrl}`);
|
|
2859
|
+
process.exit(0);
|
|
2860
|
+
});
|
|
2861
|
+
// Disable proxy command
|
|
2862
|
+
program
|
|
2863
|
+
.command('disable')
|
|
2864
|
+
.description('Disable a feature (e.g. proxy)')
|
|
2865
|
+
.argument('<feature>', 'Feature to disable (proxy)')
|
|
2866
|
+
.action(async (feature) => {
|
|
2867
|
+
if (feature !== 'proxy') {
|
|
2868
|
+
console.error(`Unknown feature: ${feature}. Available: proxy`);
|
|
2869
|
+
process.exit(1);
|
|
2870
|
+
}
|
|
2871
|
+
await writeProxyConfig({ enabled: false });
|
|
2872
|
+
console.log('Proxy disabled');
|
|
2873
|
+
process.exit(0);
|
|
2874
|
+
});
|
|
2782
2875
|
program.parse();
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Adapter - Spawns an anthropic-proxy that translates Anthropic Messages API
|
|
3
|
+
* to OpenAI Chat Completions API. This allows Claude Agent SDK to talk to any
|
|
4
|
+
* OpenAI-compatible endpoint (Ollama, vLLM, local LLMs, etc.)
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { startOpenAIAdapter, isLocalModel } from './openaiAdapter.js'
|
|
8
|
+
*
|
|
9
|
+
* if (isLocalModel(sessionModel)) {
|
|
10
|
+
* const proxyUrl = await startOpenAIAdapter({
|
|
11
|
+
* targetBaseUrl: 'http://192.168.1.101:3000',
|
|
12
|
+
* model: 'qwen3.5-27b',
|
|
13
|
+
* })
|
|
14
|
+
* // point ANTHROPIC_BASE_URL to proxyUrl
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
import { spawn } from 'child_process';
|
|
18
|
+
import { createRequire } from 'module';
|
|
19
|
+
const VALID_LOCAL_PREFIXES = ['ollama:', 'openai:', 'local:'];
|
|
20
|
+
/** Check if a model ID refers to a local/OpenAI model that needs the adapter */
|
|
21
|
+
export function isLocalModel(model) {
|
|
22
|
+
return VALID_LOCAL_PREFIXES.some(p => model.startsWith(p));
|
|
23
|
+
}
|
|
24
|
+
/** Extract the actual model name by stripping the prefix (e.g. "ollama:qwen3:4b" -> "qwen3:4b") */
|
|
25
|
+
export function unwrapModelName(model) {
|
|
26
|
+
for (const prefix of VALID_LOCAL_PREFIXES) {
|
|
27
|
+
if (model.startsWith(prefix)) {
|
|
28
|
+
return model.slice(prefix.length);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return model;
|
|
32
|
+
}
|
|
33
|
+
// Track running proxies by target+model to reuse them
|
|
34
|
+
const runningProxies = new Map();
|
|
35
|
+
function getProxyKey(config) {
|
|
36
|
+
return `${config.targetBaseUrl}::${config.model}`;
|
|
37
|
+
}
|
|
38
|
+
/** Find a free port in the given range */
|
|
39
|
+
async function findFreePort(start, end) {
|
|
40
|
+
const net = await import('net');
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
function tryPort(port) {
|
|
43
|
+
if (port > end) {
|
|
44
|
+
reject(new Error(`No free port found in range ${start}-${end}`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const server = net.createServer();
|
|
48
|
+
server.listen(port, '127.0.0.1', () => {
|
|
49
|
+
const addr = server.address();
|
|
50
|
+
server.close(() => resolve(typeof addr === 'object' && addr ? addr.port : port));
|
|
51
|
+
});
|
|
52
|
+
server.on('error', () => tryPort(port + 1));
|
|
53
|
+
}
|
|
54
|
+
tryPort(start);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Start an anthropic-proxy instance for the given config.
|
|
59
|
+
* Returns the local proxy URL (e.g. http://127.0.0.1:8321) that speaks Anthropic Messages API.
|
|
60
|
+
*
|
|
61
|
+
* Proxies are reused - calling this twice with the same target+model returns the same URL.
|
|
62
|
+
*/
|
|
63
|
+
export async function startOpenAIAdapter(config) {
|
|
64
|
+
const key = getProxyKey(config);
|
|
65
|
+
// Reuse existing proxy if running
|
|
66
|
+
const existing = runningProxies.get(key);
|
|
67
|
+
if (existing && !existing.process.killed) {
|
|
68
|
+
return existing.url;
|
|
69
|
+
}
|
|
70
|
+
const port = config.port || await findFreePort(8321, 8340);
|
|
71
|
+
// Resolve anthropic-proxy entry point
|
|
72
|
+
const req = typeof globalThis.require === 'function'
|
|
73
|
+
? globalThis.require
|
|
74
|
+
: createRequire(import.meta.url);
|
|
75
|
+
let proxyPath;
|
|
76
|
+
try {
|
|
77
|
+
const pkgPath = req.resolve('anthropic-proxy/package.json');
|
|
78
|
+
proxyPath = pkgPath.replace(/package\.json$/, 'index.js');
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new Error('anthropic-proxy package not found. Run: npm install anthropic-proxy');
|
|
82
|
+
}
|
|
83
|
+
const env = {
|
|
84
|
+
...process.env,
|
|
85
|
+
ANTHROPIC_PROXY_BASE_URL: config.targetBaseUrl,
|
|
86
|
+
PORT: String(port),
|
|
87
|
+
REASONING_MODEL: config.model,
|
|
88
|
+
COMPLETION_MODEL: config.model,
|
|
89
|
+
// Suppress fastify logger noise
|
|
90
|
+
FASTIFY_LOG_LEVEL: 'error',
|
|
91
|
+
};
|
|
92
|
+
if (config.apiKey) {
|
|
93
|
+
env.OPENAI_API_KEY = config.apiKey;
|
|
94
|
+
}
|
|
95
|
+
const child = spawn('node', [proxyPath], {
|
|
96
|
+
env,
|
|
97
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
98
|
+
windowsHide: true,
|
|
99
|
+
});
|
|
100
|
+
// Suppress stdout/stderr but log errors
|
|
101
|
+
let stderrBuf = '';
|
|
102
|
+
child.stderr?.on('data', (data) => {
|
|
103
|
+
stderrBuf += data.toString();
|
|
104
|
+
// Only log actual errors, not fastify startup messages
|
|
105
|
+
if (stderrBuf.toLowerCase().includes('error') && !stderrBuf.includes('fastify')) {
|
|
106
|
+
console.error(`[openaiAdapter] Proxy stderr: ${stderrBuf}`);
|
|
107
|
+
}
|
|
108
|
+
stderrBuf = '';
|
|
109
|
+
});
|
|
110
|
+
child.on('exit', (code) => {
|
|
111
|
+
runningProxies.delete(key);
|
|
112
|
+
if (code && code !== 0) {
|
|
113
|
+
console.error(`[openaiAdapter] Proxy exited with code ${code}`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
const url = `http://127.0.0.1:${port}`;
|
|
117
|
+
// Wait for proxy to be ready (poll /v1/messages with a simple request)
|
|
118
|
+
const maxAttempts = 20;
|
|
119
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
120
|
+
await new Promise(r => setTimeout(r, 100));
|
|
121
|
+
try {
|
|
122
|
+
const resp = await fetch(`${url}/v1/messages`, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Content-Type': 'application/json' },
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
model: config.model,
|
|
127
|
+
max_tokens: 1,
|
|
128
|
+
messages: [{ role: 'user', content: 'ping' }],
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
if (resp.ok || resp.status === 400 || resp.status === 422) {
|
|
132
|
+
// 400/422 means the proxy is up but the request was invalid - that's fine for a health check
|
|
133
|
+
runningProxies.set(key, { process: child, port, url });
|
|
134
|
+
console.log(`[openaiAdapter] Proxy ready at ${url} -> ${config.targetBaseUrl} (model: ${config.model})`);
|
|
135
|
+
return url;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Not ready yet, retry
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// If health check failed but process is still running, assume it's ready
|
|
143
|
+
if (!child.killed) {
|
|
144
|
+
runningProxies.set(key, { process: child, port, url });
|
|
145
|
+
console.log(`[openaiAdapter] Proxy started at ${url} (health check timed out, assuming ready)`);
|
|
146
|
+
return url;
|
|
147
|
+
}
|
|
148
|
+
throw new Error(`Failed to start anthropic-proxy for ${config.targetBaseUrl}`);
|
|
149
|
+
}
|
|
150
|
+
/** Stop all running proxy instances */
|
|
151
|
+
export function stopAllAdapters() {
|
|
152
|
+
for (const [key, { process }] of runningProxies) {
|
|
153
|
+
try {
|
|
154
|
+
if (!process.killed)
|
|
155
|
+
process.kill('SIGTERM');
|
|
156
|
+
}
|
|
157
|
+
catch { }
|
|
158
|
+
runningProxies.delete(key);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get the adapter configuration for a local model.
|
|
163
|
+
* Reads from ~/.talk-to-code/openai-adapters.json or falls back to defaults.
|
|
164
|
+
*/
|
|
165
|
+
export function getAdapterConfig(model) {
|
|
166
|
+
// Strip prefix to get the raw model name
|
|
167
|
+
const rawModel = unwrapModelName(model);
|
|
168
|
+
// Determine provider from prefix
|
|
169
|
+
if (model.startsWith('ollama:')) {
|
|
170
|
+
return { targetBaseUrl: 'http://127.0.0.1:11434' };
|
|
171
|
+
}
|
|
172
|
+
// For 'openai:' and 'local:' prefix, read from config file
|
|
173
|
+
try {
|
|
174
|
+
const req = typeof globalThis.require === 'function'
|
|
175
|
+
? globalThis.require
|
|
176
|
+
: createRequire(import.meta.url);
|
|
177
|
+
const fs = req('fs');
|
|
178
|
+
const path = req('path');
|
|
179
|
+
const os = req('os');
|
|
180
|
+
const configPath = path.join(os.homedir(), '.talk-to-code', 'openai-adapters.json');
|
|
181
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
182
|
+
// Config format: { "default": { "baseUrl": "...", "apiKey": "..." }, ... }
|
|
183
|
+
const entry = config.default || config[rawModel];
|
|
184
|
+
if (entry?.baseUrl) {
|
|
185
|
+
return { targetBaseUrl: entry.baseUrl, apiKey: entry.apiKey };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
console.error('[openaiAdapter] Failed to read adapter config:', e);
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
package/dist/ttc-cli.tar.gz
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exreve/exk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
4
4
|
"description": "exk - Control Claude CLI with voice and programmable interfaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"@anthropic-ai/sdk": "^0.80.0",
|
|
38
38
|
"@fastify/static": "^9.0.0",
|
|
39
39
|
"@xenova/transformers": "^2.17.2",
|
|
40
|
+
"anthropic-proxy": "^1.3.0",
|
|
40
41
|
"chokidar": "^3.6.0",
|
|
41
42
|
"commander": "^13.1.0",
|
|
42
43
|
"express": "^4.18.2",
|
|
@@ -47,8 +48,8 @@
|
|
|
47
48
|
"uuid": "^11.0.3"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
|
-
"@types/node": "^22.10.2",
|
|
51
51
|
"@types/chokidar": "^2.1.3",
|
|
52
|
+
"@types/node": "^22.10.2",
|
|
52
53
|
"@types/uuid": "^10.0.0",
|
|
53
54
|
"@vercel/ncc": "^0.38.1",
|
|
54
55
|
"esbuild": "^0.27.2",
|