@exreve/exk 1.0.15 → 1.0.17
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 +194 -142
- 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
|
|
@@ -97,19 +140,52 @@ function loadAiConfig() {
|
|
|
97
140
|
return { apiKey: '', baseUrl: '', model: DEFAULT_AI_MODEL };
|
|
98
141
|
}
|
|
99
142
|
}
|
|
100
|
-
/**
|
|
101
|
-
|
|
143
|
+
/** Create (or reuse) an empty directory to use as CLAUDE_CONFIG_DIR.
|
|
144
|
+
* Setting this prevents the spawned Claude CLI from reading ~/.claude/settings.json,
|
|
145
|
+
* which may contain env.ANTHROPIC_BASE_URL pointing to z.ai and would override our
|
|
146
|
+
* carefully configured base URL. */
|
|
147
|
+
function getEmptyConfigDir() {
|
|
148
|
+
const configDir = path.join(os.homedir(), '.talk-to-code', 'empty-config-dir');
|
|
149
|
+
if (!existsSync(configDir)) {
|
|
150
|
+
mkdirSync(configDir, { recursive: true });
|
|
151
|
+
}
|
|
152
|
+
return configDir;
|
|
153
|
+
}
|
|
154
|
+
/** Env for the Claude Code child: copy of host env with host ANTHROPIC_* stripped, then inject from ai-config only.
|
|
155
|
+
* If a local model is provided, override baseUrl to point to the anthropic-proxy adapter. */
|
|
156
|
+
function envForClaudeCodeChild(localModel) {
|
|
102
157
|
const env = { ...process.env };
|
|
158
|
+
// Strip any host ANTHROPIC_* vars to prevent leaking credentials or wrong URLs
|
|
103
159
|
delete env.ANTHROPIC_API_KEY;
|
|
104
160
|
delete env.ANTHROPIC_BASE_URL;
|
|
105
161
|
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
162
|
+
// Inject from our ai-config.json only
|
|
106
163
|
const { apiKey, baseUrl } = loadAiConfig();
|
|
107
164
|
if (apiKey)
|
|
108
165
|
env.ANTHROPIC_API_KEY = apiKey;
|
|
109
166
|
if (baseUrl)
|
|
110
167
|
env.ANTHROPIC_BASE_URL = baseUrl;
|
|
168
|
+
// Prevent ~/.claude/settings.json env section from overriding our base URL.
|
|
169
|
+
// This redirects the Claude config dir to an empty dir so that
|
|
170
|
+
// ~/.claude/settings.json (which may have ANTHROPIC_BASE_URL set to z.ai)
|
|
171
|
+
// is never read during the CLI process initialization.
|
|
172
|
+
env.CLAUDE_CONFIG_DIR = getEmptyConfigDir();
|
|
111
173
|
return env;
|
|
112
174
|
}
|
|
175
|
+
/** Get env overrides for a local model (adapter proxy URL + dummy key). Returns null if not a local model. */
|
|
176
|
+
async function getLocalModelEnvOverrides(model) {
|
|
177
|
+
if (!isLocalModel(model))
|
|
178
|
+
return null;
|
|
179
|
+
const adapterConfig = getAdapterConfig(model);
|
|
180
|
+
if (!adapterConfig)
|
|
181
|
+
return null;
|
|
182
|
+
const proxyUrl = await startOpenAIAdapter({
|
|
183
|
+
targetBaseUrl: adapterConfig.targetBaseUrl,
|
|
184
|
+
model: unwrapModelName(model),
|
|
185
|
+
apiKey: adapterConfig.apiKey,
|
|
186
|
+
});
|
|
187
|
+
return { baseUrl: proxyUrl, apiKey: 'local-no-key-needed' };
|
|
188
|
+
}
|
|
113
189
|
// Lazy config getter - reloads from file each time (so daemon picks up changes without restart)
|
|
114
190
|
const CLAUDE_CONFIG = {
|
|
115
191
|
get apiKey() { return loadAiConfig().apiKey; },
|
|
@@ -146,60 +222,39 @@ export class AgentSessionManager {
|
|
|
146
222
|
}
|
|
147
223
|
}
|
|
148
224
|
}
|
|
149
|
-
// If session already exists, update
|
|
225
|
+
// If session already exists, update mutable fields
|
|
150
226
|
if (this.sessions.has(sessionId)) {
|
|
151
227
|
const existingSession = this.sessions.get(sessionId);
|
|
152
228
|
// Update model if a new one is provided
|
|
153
229
|
if (model && model !== existingSession.model) {
|
|
154
230
|
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
231
|
}
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
232
|
+
// Update enabled modules and settings if provided
|
|
233
|
+
const newModules = handler.enabledModules;
|
|
234
|
+
if (newModules) {
|
|
235
|
+
existingSession.enabledModules = newModules;
|
|
236
|
+
}
|
|
237
|
+
const newModuleSettings = handler.moduleSettings;
|
|
238
|
+
if (newModuleSettings) {
|
|
239
|
+
existingSession.moduleSettings = newModuleSettings;
|
|
240
|
+
}
|
|
241
|
+
existingSession.userChoiceEnabled = handler.userChoiceEnabled || false;
|
|
242
|
+
// Ensure abort controller is fresh for new queries
|
|
165
243
|
existingSession.abortController = new AbortController();
|
|
244
|
+
// Update handler to keep onChoiceRequest callback fresh
|
|
245
|
+
this.sessionHandlers.set(sessionId, handler);
|
|
166
246
|
return;
|
|
167
247
|
}
|
|
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
248
|
// Store the handler for this session
|
|
174
249
|
this.sessionHandlers.set(sessionId, handler);
|
|
175
250
|
const abortController = new AbortController();
|
|
176
|
-
// Create MCP server for modules if enabled modules are provided
|
|
177
|
-
let mcpServer;
|
|
178
251
|
const enabledModules = handler.enabledModules || [];
|
|
179
252
|
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
|
-
// }
|
|
253
|
+
// Restore claudeSessionId from disk (survives CLI restart)
|
|
254
|
+
const persistedState = loadSessionState(sessionId);
|
|
255
|
+
const restoredClaudeSessionId = persistedState?.claudeSessionId;
|
|
256
|
+
if (restoredClaudeSessionId) {
|
|
257
|
+
console.log(`[AgentSessionManager] Restored claudeSessionId for session ${sessionId}: ${restoredClaudeSessionId}`);
|
|
203
258
|
}
|
|
204
259
|
this.sessions.set(sessionId, {
|
|
205
260
|
abortController,
|
|
@@ -209,35 +264,26 @@ export class AgentSessionManager {
|
|
|
209
264
|
totalCostUsd: 0,
|
|
210
265
|
promptQueue: [],
|
|
211
266
|
isProcessingQueue: false,
|
|
212
|
-
claudeSessionId:
|
|
213
|
-
// logger, // DISABLED: File logging removed for performance
|
|
214
|
-
// Memory system removed for performance
|
|
267
|
+
claudeSessionId: restoredClaudeSessionId, // Restored from disk or undefined
|
|
215
268
|
childProcesses: new Set(),
|
|
216
269
|
claudeProcessGroupId: undefined,
|
|
217
270
|
currentPromptId: undefined,
|
|
218
271
|
model: sessionModel,
|
|
219
272
|
userChoiceEnabled: handler.userChoiceEnabled || false,
|
|
220
273
|
enabledModules,
|
|
221
|
-
moduleSettings
|
|
222
|
-
mcpServer
|
|
274
|
+
moduleSettings
|
|
223
275
|
});
|
|
224
276
|
// Auto-regenerate CLAUDE.md for fresh project context
|
|
225
|
-
|
|
226
|
-
await this.regenerateClaudeMd(projectPath, undefined);
|
|
277
|
+
await this.regenerateClaudeMd(projectPath);
|
|
227
278
|
}
|
|
228
279
|
/**
|
|
229
280
|
* Regenerate CLAUDE.md for fresh project context
|
|
230
281
|
*/
|
|
231
|
-
async regenerateClaudeMd(projectPath
|
|
282
|
+
async regenerateClaudeMd(projectPath) {
|
|
232
283
|
try {
|
|
233
284
|
const scriptPath = path.join(projectPath, 'scripts', 'generate-claude-md.js');
|
|
234
285
|
if (existsSync(scriptPath)) {
|
|
235
|
-
const startTime = Date.now();
|
|
236
286
|
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
287
|
}
|
|
242
288
|
}
|
|
243
289
|
catch (error) {
|
|
@@ -245,7 +291,6 @@ export class AgentSessionManager {
|
|
|
245
291
|
console.error('[AgentSessionManager] Failed to regenerate CLAUDE.md:', error);
|
|
246
292
|
}
|
|
247
293
|
}
|
|
248
|
-
// Memory system removed for performance - no loadMemoryContext or storeConversationInMemory
|
|
249
294
|
async sendPrompt(sessionId, prompt, enhancers = [], handler) {
|
|
250
295
|
// Ensure session exists
|
|
251
296
|
if (!this.sessions.has(sessionId)) {
|
|
@@ -255,10 +300,6 @@ export class AgentSessionManager {
|
|
|
255
300
|
// Update session model if provided in handler
|
|
256
301
|
if (handler.model && handler.model !== session.model) {
|
|
257
302
|
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
303
|
}
|
|
263
304
|
// Add prompt to queue - store promptId for cancellation
|
|
264
305
|
session.promptQueue.push({
|
|
@@ -317,19 +358,12 @@ export class AgentSessionManager {
|
|
|
317
358
|
// Emit status update: CLI is starting to process the prompt (IMMEDIATELY)
|
|
318
359
|
// This ensures real-time status updates before any async operations
|
|
319
360
|
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
361
|
// Wait for current query to finish before starting next prompt
|
|
326
|
-
// REMOVED: 200ms artificial delay - stream draining is sufficient
|
|
327
362
|
if (session.activeQueryStream !== undefined) {
|
|
328
363
|
try {
|
|
329
364
|
for await (const _ of session.activeQueryStream) { }
|
|
330
365
|
}
|
|
331
366
|
catch { }
|
|
332
|
-
// REMOVED: await new Promise(resolve => setTimeout(resolve, 200))
|
|
333
367
|
session.activeQueryStream = undefined;
|
|
334
368
|
}
|
|
335
369
|
session.activeQueryStream = undefined;
|
|
@@ -339,11 +373,6 @@ export class AgentSessionManager {
|
|
|
339
373
|
const skillContent = getSkillContent(enhancers);
|
|
340
374
|
if (skillContent) {
|
|
341
375
|
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
376
|
}
|
|
348
377
|
}
|
|
349
378
|
// Add user message to history
|
|
@@ -375,14 +404,6 @@ export class AgentSessionManager {
|
|
|
375
404
|
});
|
|
376
405
|
// Use cached Claude executable path (resolved at module load time for performance)
|
|
377
406
|
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
407
|
// Build query options - include abort signal for cancellation
|
|
387
408
|
const queryOptions = {
|
|
388
409
|
signal: abortController.signal, // Pass abort signal to SDK for interruption
|
|
@@ -394,11 +415,18 @@ export class AgentSessionManager {
|
|
|
394
415
|
settingSources: ['project'], // Enable CLAUDE.md loading
|
|
395
416
|
permissionMode: 'bypassPermissions',
|
|
396
417
|
allowDangerouslySkipPermissions: true,
|
|
397
|
-
//
|
|
398
|
-
...(
|
|
418
|
+
// Create a fresh MCP server for each query call (SDK connects transport internally, cannot reuse)
|
|
419
|
+
...(() => {
|
|
420
|
+
const mcpServer = this.buildMcpServer(sessionId);
|
|
421
|
+
return mcpServer ? { mcpServers: [mcpServer] } : {};
|
|
422
|
+
})(),
|
|
399
423
|
...(pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable } : {}),
|
|
400
424
|
spawnClaudeCodeProcess: (spawnOptions) => {
|
|
401
425
|
const { command, args, cwd: cwd2, env, signal } = spawnOptions;
|
|
426
|
+
// Debug: log what env/args are being passed to Claude process
|
|
427
|
+
console.log(`[agentSession] Spawn ANTHROPIC_BASE_URL:`, env?.ANTHROPIC_BASE_URL || '(not set)');
|
|
428
|
+
console.log(`[agentSession] Spawn ANTHROPIC_API_KEY:`, env?.ANTHROPIC_API_KEY ? '(set)' : '(not set)');
|
|
429
|
+
console.log(`[agentSession] Spawn args:`, args?.join(' '));
|
|
402
430
|
// Only check file existence when command is a path (not a bare name like "claude" from PATH)
|
|
403
431
|
const hasPathSep = command.includes(path.sep) || command.includes('/') || command.includes('\\');
|
|
404
432
|
if (hasPathSep && !existsSync(command)) {
|
|
@@ -514,18 +542,51 @@ export class AgentSessionManager {
|
|
|
514
542
|
};
|
|
515
543
|
// Log model being used for debugging
|
|
516
544
|
const sessionModel = session.model || CLAUDE_CONFIG.model;
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
545
|
+
// Resolve local model adapter overrides (if using OpenAI-compatible endpoint)
|
|
546
|
+
let effectiveModel = sessionModel;
|
|
547
|
+
let effectiveApiKey = queryOptions.apiKey;
|
|
548
|
+
let effectiveEnv = queryOptions.env;
|
|
549
|
+
let effectiveSettings;
|
|
550
|
+
const localOverrides = await getLocalModelEnvOverrides(sessionModel);
|
|
551
|
+
if (localOverrides) {
|
|
552
|
+
effectiveModel = unwrapModelName(sessionModel);
|
|
553
|
+
effectiveApiKey = localOverrides.apiKey;
|
|
554
|
+
effectiveEnv = {
|
|
555
|
+
...effectiveEnv,
|
|
556
|
+
ANTHROPIC_API_KEY: localOverrides.apiKey,
|
|
557
|
+
ANTHROPIC_BASE_URL: localOverrides.baseUrl,
|
|
558
|
+
};
|
|
559
|
+
// Override settings to prevent ~/.claude/settings.json env from overriding our proxy URL.
|
|
560
|
+
// Claude Code CLI reads settings.json → env section and applies those on top of spawn env,
|
|
561
|
+
// which would replace our ANTHROPIC_BASE_URL with the z.ai URL.
|
|
562
|
+
effectiveSettings = { env: { ANTHROPIC_API_KEY: localOverrides.apiKey, ANTHROPIC_BASE_URL: localOverrides.baseUrl } };
|
|
563
|
+
console.log(`[agentSession] Using local model adapter: ${sessionModel} -> ${localOverrides.baseUrl}`);
|
|
564
|
+
console.log(`[agentSession] effectiveSettings for local model:`, JSON.stringify(effectiveSettings));
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
// Even for non-local models, ensure ~/.claude/settings.json env doesn't override our baseUrl
|
|
568
|
+
const aiConfig = loadAiConfig();
|
|
569
|
+
if (aiConfig.baseUrl || aiConfig.apiKey) {
|
|
570
|
+
const settingsEnv = {};
|
|
571
|
+
if (aiConfig.apiKey)
|
|
572
|
+
settingsEnv.ANTHROPIC_API_KEY = aiConfig.apiKey;
|
|
573
|
+
if (aiConfig.baseUrl)
|
|
574
|
+
settingsEnv.ANTHROPIC_BASE_URL = aiConfig.baseUrl;
|
|
575
|
+
effectiveSettings = { env: settingsEnv };
|
|
576
|
+
}
|
|
577
|
+
}
|
|
521
578
|
// Create query stream - resume session if we have a Claude session ID
|
|
522
579
|
// Always explicitly set model even when resuming to ensure we use the session's model
|
|
523
580
|
const queryStream = query({
|
|
524
581
|
prompt,
|
|
525
582
|
options: {
|
|
526
583
|
...queryOptions,
|
|
527
|
-
|
|
528
|
-
|
|
584
|
+
apiKey: effectiveApiKey,
|
|
585
|
+
model: effectiveModel,
|
|
586
|
+
env: effectiveEnv,
|
|
587
|
+
...(effectiveSettings ? { settings: effectiveSettings } : {}),
|
|
588
|
+
...(session.claudeSessionId && !localOverrides ? { resume: session.claudeSessionId } : {})
|
|
589
|
+
// Note: don't resume session for local models - context format differs
|
|
529
590
|
}
|
|
530
591
|
});
|
|
531
592
|
session.activeQueryStream = queryStream;
|
|
@@ -554,10 +615,7 @@ export class AgentSessionManager {
|
|
|
554
615
|
const systemMsg = message;
|
|
555
616
|
if (systemMsg.session_id && !session.claudeSessionId) {
|
|
556
617
|
session.claudeSessionId = systemMsg.session_id;
|
|
557
|
-
|
|
558
|
-
// if (session.logger) {
|
|
559
|
-
// await session.logger.logClaudeSessionId(systemMsg.session_id)
|
|
560
|
-
// }
|
|
618
|
+
saveSessionState(sessionId, { claudeSessionId: systemMsg.session_id, model: session.model, updatedAt: Date.now() });
|
|
561
619
|
}
|
|
562
620
|
}
|
|
563
621
|
if (message.type === 'assistant') {
|
|
@@ -565,27 +623,13 @@ export class AgentSessionManager {
|
|
|
565
623
|
// Capture Claude session ID from assistant message if not already set
|
|
566
624
|
if (msg.session_id && !session.claudeSessionId) {
|
|
567
625
|
session.claudeSessionId = msg.session_id;
|
|
568
|
-
|
|
569
|
-
// if (session.logger) {
|
|
570
|
-
// await session.logger.logClaudeSessionId(msg.session_id)
|
|
571
|
-
// }
|
|
626
|
+
saveSessionState(sessionId, { claudeSessionId: msg.session_id, model: session.model, updatedAt: Date.now() });
|
|
572
627
|
}
|
|
573
628
|
session.messages.push({
|
|
574
629
|
role: 'assistant',
|
|
575
630
|
content: msg.message,
|
|
576
631
|
timestamp: Date.now()
|
|
577
632
|
});
|
|
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
633
|
onOutput({
|
|
590
634
|
type: 'assistant',
|
|
591
635
|
data: msg.message,
|
|
@@ -614,31 +658,7 @@ export class AgentSessionManager {
|
|
|
614
658
|
outputTokens,
|
|
615
659
|
totalTokens: inputTokens + outputTokens
|
|
616
660
|
};
|
|
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
661
|
}
|
|
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
662
|
onOutput({
|
|
643
663
|
type: 'result',
|
|
644
664
|
data: msg,
|
|
@@ -672,7 +692,6 @@ export class AgentSessionManager {
|
|
|
672
692
|
onStatusUpdate?.(exitCode === 0 ? 'completed' : 'error');
|
|
673
693
|
onComplete(exitCode);
|
|
674
694
|
session.activeQueryStream = undefined;
|
|
675
|
-
// Memory system removed for performance
|
|
676
695
|
break; // Prompt complete, continue to next in queue
|
|
677
696
|
}
|
|
678
697
|
else if (message.type === 'user') {
|
|
@@ -872,14 +891,6 @@ export class AgentSessionManager {
|
|
|
872
891
|
const currentSession = this.sessions.get(sessionId);
|
|
873
892
|
if (currentSession) {
|
|
874
893
|
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
894
|
}
|
|
884
895
|
// Clean up abort controller (promptId is guaranteed to exist here due to check at line 204)
|
|
885
896
|
if (promptId) {
|
|
@@ -920,6 +931,8 @@ export class AgentSessionManager {
|
|
|
920
931
|
// Clear Claude session ID to start fresh
|
|
921
932
|
session.claudeSessionId = undefined;
|
|
922
933
|
session.lastUsage = undefined;
|
|
934
|
+
// Also clear persisted state
|
|
935
|
+
deleteSessionState(sessionId);
|
|
923
936
|
}
|
|
924
937
|
}
|
|
925
938
|
async deleteSession(sessionId) {
|
|
@@ -929,6 +942,8 @@ export class AgentSessionManager {
|
|
|
929
942
|
session.activeQueryStream = undefined;
|
|
930
943
|
this.sessions.delete(sessionId);
|
|
931
944
|
}
|
|
945
|
+
// Clean up persisted state
|
|
946
|
+
deleteSessionState(sessionId);
|
|
932
947
|
}
|
|
933
948
|
/**
|
|
934
949
|
* Cancel a running or queued prompt by promptId
|
|
@@ -1106,6 +1121,43 @@ export class AgentSessionManager {
|
|
|
1106
1121
|
return { success: false, message: error.message || 'Emergency stop failed' };
|
|
1107
1122
|
}
|
|
1108
1123
|
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Build a fresh MCP server for a query call.
|
|
1126
|
+
* The SDK's query() connects the MCP server's internal transport, so we cannot
|
|
1127
|
+
* reuse a single instance across multiple queries. This must be called fresh each time.
|
|
1128
|
+
*/
|
|
1129
|
+
buildMcpServer(sessionId) {
|
|
1130
|
+
const session = this.sessions.get(sessionId);
|
|
1131
|
+
if (!session) {
|
|
1132
|
+
console.log(`[buildMcpServer] No session found for ${sessionId}`);
|
|
1133
|
+
return undefined;
|
|
1134
|
+
}
|
|
1135
|
+
const enabledModules = session.enabledModules || [];
|
|
1136
|
+
console.log(`[buildMcpServer] Session ${sessionId}: enabledModules=${JSON.stringify(enabledModules)}`);
|
|
1137
|
+
if (enabledModules.length === 0) {
|
|
1138
|
+
console.log(`[buildMcpServer] No enabled modules, skipping MCP server creation`);
|
|
1139
|
+
return undefined;
|
|
1140
|
+
}
|
|
1141
|
+
const handler = this.sessionHandlers.get(sessionId);
|
|
1142
|
+
return createModuleMcpServer({
|
|
1143
|
+
enabledModules,
|
|
1144
|
+
moduleSettings: session.moduleSettings || {},
|
|
1145
|
+
onChoiceRequest: handler?.onChoiceRequest
|
|
1146
|
+
? async (request) => {
|
|
1147
|
+
return new Promise((resolve) => {
|
|
1148
|
+
const sess = this.sessions.get(sessionId);
|
|
1149
|
+
if (sess) {
|
|
1150
|
+
sess.pendingChoice = { request, resolve };
|
|
1151
|
+
handler.onChoiceRequest(request);
|
|
1152
|
+
}
|
|
1153
|
+
else {
|
|
1154
|
+
resolve({ choiceId: request.choiceId, selectedValue: null });
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
: undefined
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1109
1161
|
/**
|
|
1110
1162
|
* Handle user choice response from frontend
|
|
1111
1163
|
*/
|
|
@@ -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.17",
|
|
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",
|