@yeaft/webchat-agent 0.1.857 → 0.1.860
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/conversation.js +114 -32
- package/history.js +22 -59
- package/package.json +1 -1
- package/providers/base.js +58 -0
- package/providers/claude-code.js +84 -0
- package/providers/copilot.js +484 -0
- package/providers/index.js +19 -0
- package/yeaft/conversation/persist.js +94 -53
- package/yeaft/conversation/search.js +24 -6
- package/yeaft/init.js +6 -11
package/conversation.js
CHANGED
|
@@ -6,6 +6,7 @@ import { query } from './sdk/index.js';
|
|
|
6
6
|
import { loadSessionHistory } from './history.js';
|
|
7
7
|
import { startClaudeQuery } from './claude.js';
|
|
8
8
|
import { crewSessions, loadCrewIndex } from './crew.js';
|
|
9
|
+
import { getProvider, DEFAULT_PROVIDER, isValidProvider } from './providers/index.js';
|
|
9
10
|
|
|
10
11
|
// 不支持的斜杠命令(真正需要交互式 CLI 的命令)
|
|
11
12
|
const UNSUPPORTED_SLASH_COMMANDS = ['/help', '/bug', '/login', '/logout', '/terminal-setup', '/vim', '/config'];
|
|
@@ -293,7 +294,8 @@ export async function sendConversationList() {
|
|
|
293
294
|
createdAt: state.createdAt,
|
|
294
295
|
processing: !!state.turnActive,
|
|
295
296
|
userId: state.userId,
|
|
296
|
-
username: state.username
|
|
297
|
+
username: state.username,
|
|
298
|
+
provider: state.providerName || DEFAULT_PROVIDER
|
|
297
299
|
};
|
|
298
300
|
list.push(entry);
|
|
299
301
|
}
|
|
@@ -356,33 +358,42 @@ export function sendError(conversationId, message) {
|
|
|
356
358
|
export async function createConversation(msg) {
|
|
357
359
|
const { conversationId, workDir, userId, username, disallowedTools } = msg;
|
|
358
360
|
const effectiveWorkDir = workDir || ctx.CONFIG.workDir;
|
|
361
|
+
const provider = isValidProvider(msg.provider) ? msg.provider : DEFAULT_PROVIDER;
|
|
359
362
|
|
|
360
|
-
console.log(`Creating conversation: ${conversationId} in ${effectiveWorkDir} (lazy start)`);
|
|
363
|
+
console.log(`Creating conversation: ${conversationId} in ${effectiveWorkDir} (lazy start, provider=${provider})`);
|
|
361
364
|
if (username) console.log(` User: ${username} (${userId})`);
|
|
362
365
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
inputTokens: 0,
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
366
|
+
if (provider === 'claude-code') {
|
|
367
|
+
// Claude path: lazy-init state, real CLI boots on first message.
|
|
368
|
+
ctx.conversations.set(conversationId, {
|
|
369
|
+
query: null,
|
|
370
|
+
inputStream: null,
|
|
371
|
+
workDir: effectiveWorkDir,
|
|
372
|
+
claudeSessionId: null,
|
|
373
|
+
createdAt: Date.now(),
|
|
374
|
+
abortController: null,
|
|
375
|
+
tools: [],
|
|
376
|
+
slashCommands: [],
|
|
377
|
+
model: null,
|
|
378
|
+
userId,
|
|
379
|
+
username,
|
|
380
|
+
providerName: provider,
|
|
381
|
+
disallowedTools: disallowedTools || null,
|
|
382
|
+
usage: { inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheCreation: 0, totalCostUsd: 0 }
|
|
383
|
+
});
|
|
384
|
+
} else {
|
|
385
|
+
// Non-Claude providers own their own state construction.
|
|
386
|
+
const driver = getProvider(provider);
|
|
387
|
+
const state = await driver.start({
|
|
388
|
+
conversationId,
|
|
389
|
+
workDir: effectiveWorkDir,
|
|
390
|
+
resumeSessionId: null,
|
|
391
|
+
userId,
|
|
392
|
+
username,
|
|
393
|
+
providerOptions: msg.providerOptions || {},
|
|
394
|
+
});
|
|
395
|
+
state.disallowedTools = disallowedTools || null;
|
|
396
|
+
}
|
|
386
397
|
|
|
387
398
|
ctx.sendToServer({
|
|
388
399
|
type: 'conversation_created',
|
|
@@ -390,6 +401,7 @@ export async function createConversation(msg) {
|
|
|
390
401
|
workDir: effectiveWorkDir,
|
|
391
402
|
userId,
|
|
392
403
|
username,
|
|
404
|
+
provider,
|
|
393
405
|
disallowedTools: disallowedTools || null
|
|
394
406
|
});
|
|
395
407
|
|
|
@@ -414,22 +426,27 @@ export async function createConversation(msg) {
|
|
|
414
426
|
|
|
415
427
|
// ★ Prestart Claude CLI in background to eagerly fetch skills/tools/model
|
|
416
428
|
// Fire-and-forget: failure just degrades to lazy-start behavior
|
|
417
|
-
|
|
429
|
+
if (provider === 'claude-code') {
|
|
430
|
+
prestartClaude(conversationId, effectiveWorkDir, null);
|
|
431
|
+
}
|
|
418
432
|
}
|
|
419
433
|
|
|
420
434
|
// Resume 历史 conversation (延迟启动 Claude,等待用户发送第一条消息)
|
|
421
435
|
export async function resumeConversation(msg) {
|
|
422
436
|
const { conversationId, claudeSessionId, workDir, userId, username, disallowedTools } = msg;
|
|
423
437
|
const effectiveWorkDir = workDir || ctx.CONFIG.workDir;
|
|
438
|
+
const provider = isValidProvider(msg.provider) ? msg.provider : DEFAULT_PROVIDER;
|
|
424
439
|
|
|
425
440
|
console.log(`[Resume] conversationId: ${conversationId}`);
|
|
426
441
|
console.log(`[Resume] claudeSessionId: ${claudeSessionId}`);
|
|
427
442
|
console.log(`[Resume] workDir: ${effectiveWorkDir} (lazy start)`);
|
|
428
443
|
|
|
429
444
|
// 清理旧条目:同 conversationId 或同 claudeSessionId 的条目(避免重复恢复同一个 session 累积)
|
|
445
|
+
let priorProviderOptions = null;
|
|
430
446
|
for (const [id, conv] of ctx.conversations) {
|
|
431
447
|
if (id === conversationId || (claudeSessionId && conv.claudeSessionId === claudeSessionId)) {
|
|
432
448
|
console.log(`[Resume] Cleaning up old conversation: ${id} (claudeSessionId: ${conv.claudeSessionId})`);
|
|
449
|
+
if (conv.providerOptions && !priorProviderOptions) priorProviderOptions = conv.providerOptions;
|
|
433
450
|
if (conv.abortController) {
|
|
434
451
|
conv.abortController.abort();
|
|
435
452
|
}
|
|
@@ -440,7 +457,15 @@ export async function resumeConversation(msg) {
|
|
|
440
457
|
}
|
|
441
458
|
}
|
|
442
459
|
|
|
443
|
-
|
|
460
|
+
let driverForHistory;
|
|
461
|
+
try { driverForHistory = getProvider(provider); }
|
|
462
|
+
catch (err) {
|
|
463
|
+
console.warn(`[Resume] unknown provider "${provider}", falling back to claude-code history loader:`, err?.message || err);
|
|
464
|
+
driverForHistory = {};
|
|
465
|
+
}
|
|
466
|
+
const historyMessages = typeof driverForHistory.loadHistory === 'function'
|
|
467
|
+
? await driverForHistory.loadHistory(effectiveWorkDir, claudeSessionId)
|
|
468
|
+
: loadSessionHistory(effectiveWorkDir, claudeSessionId);
|
|
444
469
|
if (username) console.log(`[Resume] User: ${username} (${userId})`);
|
|
445
470
|
console.log(`Loaded ${historyMessages.length} history messages`);
|
|
446
471
|
|
|
@@ -458,6 +483,7 @@ export async function resumeConversation(msg) {
|
|
|
458
483
|
model: null,
|
|
459
484
|
userId,
|
|
460
485
|
username,
|
|
486
|
+
providerName: provider,
|
|
461
487
|
disallowedTools: disallowedTools || null, // null = 使用全局默认
|
|
462
488
|
usage: {
|
|
463
489
|
inputTokens: 0,
|
|
@@ -468,6 +494,20 @@ export async function resumeConversation(msg) {
|
|
|
468
494
|
}
|
|
469
495
|
});
|
|
470
496
|
|
|
497
|
+
// Non-Claude providers: re-init state via driver so sessionId/providerName are set correctly.
|
|
498
|
+
if (provider !== 'claude-code') {
|
|
499
|
+
const driver = getProvider(provider);
|
|
500
|
+
const state = await driver.start({
|
|
501
|
+
conversationId,
|
|
502
|
+
workDir: effectiveWorkDir,
|
|
503
|
+
resumeSessionId: claudeSessionId || null,
|
|
504
|
+
userId,
|
|
505
|
+
username,
|
|
506
|
+
providerOptions: msg.providerOptions || priorProviderOptions || {},
|
|
507
|
+
});
|
|
508
|
+
state.disallowedTools = disallowedTools || null;
|
|
509
|
+
}
|
|
510
|
+
|
|
471
511
|
ctx.sendToServer({
|
|
472
512
|
type: 'conversation_resumed',
|
|
473
513
|
conversationId,
|
|
@@ -475,7 +515,8 @@ export async function resumeConversation(msg) {
|
|
|
475
515
|
workDir: effectiveWorkDir,
|
|
476
516
|
historyMessages,
|
|
477
517
|
userId,
|
|
478
|
-
username
|
|
518
|
+
username,
|
|
519
|
+
provider
|
|
479
520
|
});
|
|
480
521
|
|
|
481
522
|
// 立即发送 agent 级别的 MCP servers 列表
|
|
@@ -498,7 +539,7 @@ export async function resumeConversation(msg) {
|
|
|
498
539
|
// ★ Prestart Claude CLI in background to eagerly fetch skills/tools/model
|
|
499
540
|
// Skip if conversation already has an active query (shouldn't happen, but safety check)
|
|
500
541
|
const resumeState = ctx.conversations.get(conversationId);
|
|
501
|
-
if (!resumeState?.query) {
|
|
542
|
+
if (provider === 'claude-code' && !resumeState?.query) {
|
|
502
543
|
prestartClaude(conversationId, effectiveWorkDir, claudeSessionId);
|
|
503
544
|
}
|
|
504
545
|
}
|
|
@@ -627,9 +668,15 @@ export async function handleCancelExecution(msg) {
|
|
|
627
668
|
// 标记为取消状态,防止 processClaudeOutput 的 finally 发送 conversation_closed
|
|
628
669
|
state.cancelled = true;
|
|
629
670
|
|
|
630
|
-
//
|
|
631
|
-
|
|
632
|
-
state.
|
|
671
|
+
// 通过 driver 中止当前查询(Claude 走 abortController;Copilot 走 SIGTERM)
|
|
672
|
+
try {
|
|
673
|
+
const driver = getProvider(state.providerName || DEFAULT_PROVIDER);
|
|
674
|
+
if (typeof driver.abort === 'function') driver.abort(state);
|
|
675
|
+
} catch (err) {
|
|
676
|
+
console.warn(`[${conversationId}] driver.abort failed:`, err?.message || err);
|
|
677
|
+
if (state.abortController) {
|
|
678
|
+
try { state.abortController.abort(); } catch { /* noop */ }
|
|
679
|
+
}
|
|
633
680
|
}
|
|
634
681
|
|
|
635
682
|
// 关闭输入流
|
|
@@ -704,6 +751,41 @@ export async function handleUserInput(msg) {
|
|
|
704
751
|
|
|
705
752
|
let state = ctx.conversations.get(conversationId);
|
|
706
753
|
|
|
754
|
+
// ★ Non-Claude providers: dispatch to driver and return
|
|
755
|
+
const providerName = state?.providerName || DEFAULT_PROVIDER;
|
|
756
|
+
if (providerName !== 'claude-code') {
|
|
757
|
+
const driver = getProvider(providerName);
|
|
758
|
+
const effectiveWorkDir = workDir || state?.workDir || ctx.CONFIG.workDir;
|
|
759
|
+
if (!state) {
|
|
760
|
+
state = await driver.start({
|
|
761
|
+
conversationId,
|
|
762
|
+
workDir: effectiveWorkDir,
|
|
763
|
+
resumeSessionId: claudeSessionId || null,
|
|
764
|
+
userId: msg.userId,
|
|
765
|
+
username: msg.username,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
if (workDir) state.workDir = workDir;
|
|
769
|
+
sendOutput(conversationId, { type: 'user', message: { role: 'user', content: prompt } });
|
|
770
|
+
state.turnActive = true;
|
|
771
|
+
sendConversationList();
|
|
772
|
+
try {
|
|
773
|
+
await driver.sendInput(state, prompt, { conversationId, raw: msg });
|
|
774
|
+
} catch (err) {
|
|
775
|
+
sendOutput(conversationId, {
|
|
776
|
+
type: 'result',
|
|
777
|
+
subtype: 'error',
|
|
778
|
+
session_id: state.sessionId || null,
|
|
779
|
+
is_error: true,
|
|
780
|
+
error: `${providerName} error: ${err?.message || err}`,
|
|
781
|
+
});
|
|
782
|
+
} finally {
|
|
783
|
+
state.turnActive = false;
|
|
784
|
+
sendConversationList();
|
|
785
|
+
}
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
707
789
|
// 如果没有活跃的查询,启动新的
|
|
708
790
|
if (!state || !state.query || !state.inputStream) {
|
|
709
791
|
const resumeSessionId = claudeSessionId || state?.claudeSessionId || null;
|
package/history.js
CHANGED
|
@@ -2,6 +2,7 @@ import { homedir } from 'os';
|
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import ctx from './context.js';
|
|
5
|
+
import { getProvider, DEFAULT_PROVIDER } from './providers/index.js';
|
|
5
6
|
|
|
6
7
|
// Claude 项目目录
|
|
7
8
|
export function getClaudeProjectsDir() {
|
|
@@ -184,18 +185,23 @@ export function loadSessionHistory(workDir, claudeSessionId, limit = 500) {
|
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
export async function handleListHistorySessions(msg) {
|
|
187
|
-
const { workDir, requestId, _requestClientId } = msg;
|
|
188
|
+
const { workDir, requestId, _requestClientId, provider } = msg;
|
|
188
189
|
const effectiveWorkDir = workDir || ctx.CONFIG.workDir;
|
|
190
|
+
const providerName = provider || DEFAULT_PROVIDER;
|
|
189
191
|
|
|
190
|
-
console.log(`Listing history sessions for: ${effectiveWorkDir}`);
|
|
192
|
+
console.log(`Listing history sessions for: ${effectiveWorkDir} (provider=${providerName})`);
|
|
191
193
|
|
|
192
194
|
try {
|
|
193
|
-
const
|
|
195
|
+
const driver = getProvider(providerName);
|
|
196
|
+
const sessions = typeof driver.listSessions === 'function'
|
|
197
|
+
? await driver.listSessions(effectiveWorkDir)
|
|
198
|
+
: await getHistorySessions(effectiveWorkDir);
|
|
194
199
|
ctx.sendToServer({
|
|
195
200
|
type: 'history_sessions_list',
|
|
196
201
|
requestId,
|
|
197
202
|
_requestClientId,
|
|
198
203
|
workDir: effectiveWorkDir,
|
|
204
|
+
provider: providerName,
|
|
199
205
|
sessions
|
|
200
206
|
});
|
|
201
207
|
} catch (e) {
|
|
@@ -205,85 +211,42 @@ export async function handleListHistorySessions(msg) {
|
|
|
205
211
|
requestId,
|
|
206
212
|
_requestClientId,
|
|
207
213
|
workDir: effectiveWorkDir,
|
|
214
|
+
provider: providerName,
|
|
208
215
|
sessions: [],
|
|
209
216
|
error: e.message
|
|
210
217
|
});
|
|
211
218
|
}
|
|
212
219
|
}
|
|
213
220
|
|
|
214
|
-
//
|
|
221
|
+
// 列出指定 provider 下所有 folder (工作目录)
|
|
215
222
|
export async function handleListFolders(msg) {
|
|
216
|
-
const { requestId, _requestClientId } = msg;
|
|
217
|
-
const
|
|
223
|
+
const { requestId, _requestClientId, provider } = msg;
|
|
224
|
+
const providerName = provider || DEFAULT_PROVIDER;
|
|
218
225
|
|
|
219
|
-
console.log(`Listing folders
|
|
226
|
+
console.log(`Listing folders for provider=${providerName}`);
|
|
220
227
|
|
|
221
228
|
try {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
for (const entry of entries) {
|
|
228
|
-
const entryPath = join(projectsDir, entry);
|
|
229
|
-
const stats = statSync(entryPath);
|
|
230
|
-
|
|
231
|
-
if (stats.isDirectory()) {
|
|
232
|
-
// 过滤掉 crew 角色的 session 文件夹
|
|
233
|
-
// crew 角色的 cwd 在 .crew/roles/{roleName} 下,对应的文件夹名包含 --crew-roles-
|
|
234
|
-
if (entry.includes('--crew-roles-')) {
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// 从 session 文件读取真实的工作目录路径
|
|
239
|
-
const originalPath = getWorkDirFromProjectFolder(entryPath, entry);
|
|
240
|
-
|
|
241
|
-
// 快速计数:只数 .jsonl 文件数量,不读取文件内容
|
|
242
|
-
let sessionCount = 0;
|
|
243
|
-
let lastModified = stats.mtime.getTime();
|
|
244
|
-
|
|
245
|
-
try {
|
|
246
|
-
const files = readdirSync(entryPath);
|
|
247
|
-
|
|
248
|
-
for (const file of files) {
|
|
249
|
-
if (file.endsWith('.jsonl')) {
|
|
250
|
-
sessionCount++;
|
|
251
|
-
try {
|
|
252
|
-
const fileStats = statSync(join(entryPath, file));
|
|
253
|
-
if (fileStats.mtime.getTime() > lastModified) {
|
|
254
|
-
lastModified = fileStats.mtime.getTime();
|
|
255
|
-
}
|
|
256
|
-
} catch {}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
} catch {}
|
|
260
|
-
|
|
261
|
-
folders.push({
|
|
262
|
-
name: entry,
|
|
263
|
-
path: originalPath,
|
|
264
|
-
sessionCount,
|
|
265
|
-
lastModified
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
229
|
+
const driver = getProvider(providerName);
|
|
230
|
+
let folders = [];
|
|
231
|
+
if (typeof driver.listFolders === 'function') {
|
|
232
|
+
folders = await driver.listFolders();
|
|
269
233
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
console.log(`Found ${folders.length} folders, sending response...`);
|
|
234
|
+
folders.sort((a, b) => (b.lastModified || 0) - (a.lastModified || 0));
|
|
235
|
+
console.log(`Found ${folders.length} folders (provider=${providerName}), sending response...`);
|
|
274
236
|
ctx.sendToServer({
|
|
275
237
|
type: 'folders_list',
|
|
276
238
|
requestId,
|
|
277
239
|
_requestClientId,
|
|
240
|
+
provider: providerName,
|
|
278
241
|
folders
|
|
279
242
|
});
|
|
280
|
-
console.log(`folders_list sent with ${folders.length} folders`);
|
|
281
243
|
} catch (e) {
|
|
282
244
|
console.error('Error listing folders:', e);
|
|
283
245
|
ctx.sendToServer({
|
|
284
246
|
type: 'folders_list',
|
|
285
247
|
requestId,
|
|
286
248
|
_requestClientId,
|
|
249
|
+
provider: providerName,
|
|
287
250
|
folders: [],
|
|
288
251
|
error: e.message
|
|
289
252
|
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat provider abstraction.
|
|
3
|
+
*
|
|
4
|
+
* Wire-protocol note: `claude_output` is now a PROTOCOL name, not a
|
|
5
|
+
* vendor name. Every driver MUST emit events on
|
|
6
|
+
* `ctx.sendToServer({ type: 'claude_output', conversationId, data })`,
|
|
7
|
+
* where `data` follows the Claude stream-json envelope:
|
|
8
|
+
* - { type: 'assistant', message: { role, content: [...] } }
|
|
9
|
+
* - { type: 'user', message: { role, content: [...] } }
|
|
10
|
+
* - { type: 'result', subtype, session_id, is_error, ... }
|
|
11
|
+
* - { type: 'system', subtype, ... }
|
|
12
|
+
*
|
|
13
|
+
* Non-Claude drivers (e.g. Copilot) MUST translate their native event
|
|
14
|
+
* streams into this envelope so the existing renderer needs no changes.
|
|
15
|
+
*
|
|
16
|
+
* @typedef {Object} ChatProvider
|
|
17
|
+
* @property {string} name
|
|
18
|
+
* @property {(opts: StartOpts) => Promise<Object>} start
|
|
19
|
+
* @property {(state: Object, prompt: string, opts?: Object) => Promise<void>} sendInput
|
|
20
|
+
* @property {(state: Object) => void} abort
|
|
21
|
+
* @property {() => Promise<FolderInfo[]>} listFolders
|
|
22
|
+
* Return the list of work-directories that this provider has sessions for.
|
|
23
|
+
* @property {(workDir: string) => Promise<SessionInfo[]>} listSessions
|
|
24
|
+
* Return resumable sessions for a given work-directory.
|
|
25
|
+
* @property {(workDir: string, sessionId: string, limit?: number) => Promise<HistoryMessage[]>} loadHistory
|
|
26
|
+
* Return the resumable transcript as an array of `claude_output`-compatible
|
|
27
|
+
* envelopes (the same shape the live stream would have produced).
|
|
28
|
+
*
|
|
29
|
+
* @typedef {Object} StartOpts
|
|
30
|
+
* @property {string} conversationId
|
|
31
|
+
* @property {string} workDir
|
|
32
|
+
* @property {string|null} [resumeSessionId]
|
|
33
|
+
* @property {string} [userId]
|
|
34
|
+
* @property {string} [username]
|
|
35
|
+
* @property {Object} [providerOptions] per-provider knobs (model, allowAllTools, ...)
|
|
36
|
+
*
|
|
37
|
+
* @typedef {Object} FolderInfo
|
|
38
|
+
* @property {string} name opaque folder identifier (provider-specific)
|
|
39
|
+
* @property {string} path original cwd path
|
|
40
|
+
* @property {number} sessionCount
|
|
41
|
+
* @property {number} lastModified epoch ms
|
|
42
|
+
*
|
|
43
|
+
* @typedef {Object} SessionInfo
|
|
44
|
+
* @property {string} sessionId
|
|
45
|
+
* @property {string} workDir
|
|
46
|
+
* @property {string} title
|
|
47
|
+
* @property {string} [preview]
|
|
48
|
+
* @property {number} lastModified
|
|
49
|
+
* @property {number} [size]
|
|
50
|
+
*
|
|
51
|
+
* @typedef {Object} HistoryMessage a single claude_output `data` envelope
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
export const PROVIDER_NAMES = Object.freeze(['claude-code', 'copilot']);
|
|
55
|
+
export const DEFAULT_PROVIDER = 'claude-code';
|
|
56
|
+
export function isValidProvider(name) {
|
|
57
|
+
return typeof name === 'string' && PROVIDER_NAMES.includes(name);
|
|
58
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { startClaudeQuery } from '../claude.js';
|
|
4
|
+
import {
|
|
5
|
+
getClaudeProjectsDir,
|
|
6
|
+
getHistorySessions,
|
|
7
|
+
loadSessionHistory,
|
|
8
|
+
getWorkDirFromProjectFolder,
|
|
9
|
+
} from '../history.js';
|
|
10
|
+
|
|
11
|
+
export const name = 'claude-code';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Start (or resume) a Claude Code CLI session.
|
|
15
|
+
* Returns the same state object that startClaudeQuery stores in ctx.conversations.
|
|
16
|
+
*/
|
|
17
|
+
export async function start(opts) {
|
|
18
|
+
const state = await startClaudeQuery(
|
|
19
|
+
opts.conversationId,
|
|
20
|
+
opts.workDir,
|
|
21
|
+
opts.resumeSessionId || null
|
|
22
|
+
);
|
|
23
|
+
state.providerName = name;
|
|
24
|
+
return state;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Claude CLI handles input via the persistent stdin Stream that
|
|
29
|
+
* conversation.js manages directly, so this driver's sendInput is a no-op.
|
|
30
|
+
* conversation.js's existing branch keeps owning the Claude path.
|
|
31
|
+
*/
|
|
32
|
+
export async function sendInput(_state, _prompt, _opts) {
|
|
33
|
+
/* handled inline by conversation.js for the Claude branch */
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function abort(state) {
|
|
37
|
+
if (state?.abortController) {
|
|
38
|
+
try { state.abortController.abort(); } catch { /* noop */ }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------- history surface ----------
|
|
43
|
+
|
|
44
|
+
export async function listFolders() {
|
|
45
|
+
const projectsDir = getClaudeProjectsDir();
|
|
46
|
+
const folders = [];
|
|
47
|
+
if (!existsSync(projectsDir)) return folders;
|
|
48
|
+
|
|
49
|
+
for (const entry of readdirSync(projectsDir)) {
|
|
50
|
+
const entryPath = join(projectsDir, entry);
|
|
51
|
+
let stats;
|
|
52
|
+
try { stats = statSync(entryPath); } catch { continue; }
|
|
53
|
+
if (!stats.isDirectory()) continue;
|
|
54
|
+
if (entry.includes('--crew-roles-')) continue;
|
|
55
|
+
|
|
56
|
+
const originalPath = getWorkDirFromProjectFolder(entryPath, entry);
|
|
57
|
+
let sessionCount = 0;
|
|
58
|
+
let lastModified = stats.mtime.getTime();
|
|
59
|
+
try {
|
|
60
|
+
for (const file of readdirSync(entryPath)) {
|
|
61
|
+
if (!file.endsWith('.jsonl')) continue;
|
|
62
|
+
sessionCount++;
|
|
63
|
+
try {
|
|
64
|
+
const fs = statSync(join(entryPath, file));
|
|
65
|
+
if (fs.mtime.getTime() > lastModified) lastModified = fs.mtime.getTime();
|
|
66
|
+
} catch { /* noop */ }
|
|
67
|
+
}
|
|
68
|
+
} catch { /* noop */ }
|
|
69
|
+
|
|
70
|
+
folders.push({ name: entry, path: originalPath, sessionCount, lastModified });
|
|
71
|
+
}
|
|
72
|
+
folders.sort((a, b) => b.lastModified - a.lastModified);
|
|
73
|
+
return folders;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function listSessions(workDir) {
|
|
77
|
+
return await getHistorySessions(workDir);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function loadHistory(workDir, sessionId, limit = 500) {
|
|
81
|
+
return loadSessionHistory(workDir, sessionId, limit);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default { name, start, sendInput, abort, listFolders, listSessions, loadHistory };
|