dr-claw 1.1.2 → 1.1.4
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/README.md +286 -177
- package/README.zh-CN.md +286 -174
- package/dist/assets/{_basePickBy-C0n1uo4u.js → _basePickBy-7CEpiVqc.js} +1 -1
- package/dist/assets/{_baseUniq-BRNmhFkc.js → _baseUniq-yh8GoKah.js} +1 -1
- package/dist/assets/{arc-DOMBmC_A.js → arc-xxYot-So.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-CtMWglbD.js → architectureDiagram-VXUJARFQ-Bb1NqOFB.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-DK_gTkJy.js → blockDiagram-VD42YOAC-D8xMHk8C.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-DrDAvsn7.js → c4Diagram-YG6GDRKO-69UiBMVa.js} +1 -1
- package/dist/assets/channel-BJdHHkhR.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-CGbhdZcN.js → chunk-4BX2VUAB-DXrPhE6K.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-sGeFSxen.js → chunk-55IACEB6-B3sw4AGX.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-RxgHAU_P.js → chunk-B4BG7PRW-Dd4n17kR.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-CvHUXZrx.js → chunk-DI55MBZ5-DuTb9gb8.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Sn7cXVNU.js → chunk-FMBD7UC4-CGErQ-ud.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-bDKOHppN.js → chunk-QN33PNHL-Bk5fuYbR.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-B9kN5LGm.js → chunk-QZHKN3VN-2UdZTbge.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-CCBe6len.js → chunk-TZMSLE5B-Bl-UYKEK.js} +1 -1
- package/dist/assets/{classDiagram-2ON5EDUG-DQoMNYlY.js → classDiagram-2ON5EDUG-EaoBDWqJ.js} +1 -1
- package/dist/assets/{classDiagram-v2-WZHVMYZB-DQoMNYlY.js → classDiagram-v2-WZHVMYZB-EaoBDWqJ.js} +1 -1
- package/dist/assets/clone-CwXTbwxl.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-r6sLiHBW.js → cose-bilkent-S5V4N54A-qXNKLX2R.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-AbsaR1Oa.js → dagre-6UL2VRFP-DmHvDWep.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-DjCvdUyy.js → diagram-PSM6KHXK-C_Rj4DX3.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-CqiocXkH.js → diagram-QEK2KX5R-BVr3mPXW.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-D5ALeoII.js → diagram-S2PKOQOG-DVx6i0Qe.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-C1AeOsiz.js → erDiagram-Q2GNP2WA-BJQyv9_t.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-C98ve_DO.js → flowDiagram-NV44I4VS-DCR8sPAE.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-3zdLvcxD.js → ganttDiagram-JELNMOA3-BcXFAthK.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-nDuDMsHh.js → gitGraphDiagram-V2S2FVAM-CnS56w1x.js} +1 -1
- package/dist/assets/{graph-Blm-emkR.js → graph-DGtZXuDY.js} +1 -1
- package/dist/assets/index-C-sL-ag0.css +1 -0
- package/dist/assets/index-VdqxIFPL.js +670 -0
- package/dist/assets/{infoDiagram-HS3SLOUP-CmzE3q0R.js → infoDiagram-HS3SLOUP-DtI272ZD.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-CYaW7LrP.js → journeyDiagram-XKPGCS4Q-DHp_Kica.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-mkmW7p0j.js → kanban-definition-3W4ZIXB7-CjgHMNku.js} +1 -1
- package/dist/assets/{layout-Ct2kFIId.js → layout-Cnc-DCSb.js} +1 -1
- package/dist/assets/{linear-CKG9EwIJ.js → linear-BtRgmV19.js} +1 -1
- package/dist/assets/{mermaid.core-CaKcUnnd.js → mermaid.core-B20uzv6N.js} +5 -5
- package/dist/assets/{mindmap-definition-VGOIOE7T-Bct-pYin.js → mindmap-definition-VGOIOE7T-bbcLXkWp.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-C8wFD4Jr.js → pieDiagram-ADFJNKIX-B4fEzWKO.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-CuiE_f46.js → quadrantDiagram-AYHSOK5B-B5G0SMbK.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-C4v1CqU8.js → requirementDiagram-UZGBJVZJ-DsqtOFgj.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-keAqqs-Y.js → sankeyDiagram-TZEHDZUN-CiGemFRE.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-ClNbvKTB.js → sequenceDiagram-WL72ISMW-hEMvYbe9.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-D8We-2vq.js → stateDiagram-FKZM4ZOC-Cn8VB7b1.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-DaH0PZOq.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-DsvEMT5T.js → timeline-definition-IT6M3QCI-DChT1GUr.js} +1 -1
- package/dist/assets/{treemap-GDKQZRPO-TJrzN7Bj.js → treemap-GDKQZRPO-Dflv6a8z.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-BdATmY5F.js → xychartDiagram-PRI3JC2R-Dr4urXR8.js} +1 -1
- package/dist/icons/nanoclaude.png +0 -0
- package/dist/index.html +2 -2
- package/dist/screenshots/screenshot_v1_2.png +0 -0
- package/package.json +61 -4
- package/public/icons/nanoclaude.png +0 -0
- package/public/screenshots/screenshot_v1_2.png +0 -0
- package/scripts/build-electron-icons.mjs +40 -0
- package/scripts/check-deps.js +28 -0
- package/scripts/native-runtime.mjs +128 -0
- package/server/__tests__/gemini-api.test.mjs +699 -0
- package/server/__tests__/gemini-session-index.test.mjs +95 -0
- package/server/__tests__/project-sync-dedup.test.mjs +173 -0
- package/server/__tests__/session-delete.test.mjs +152 -0
- package/server/claude-sdk.js +175 -14
- package/server/cli-chat.js +28 -17
- package/server/cli.js +48 -11
- package/server/data/news-log-arxiv.json +1 -0
- package/server/data/news-log-huggingface.json +1 -0
- package/server/data/news-results-arxiv.json +460 -0
- package/server/data/news-results-huggingface.json +817 -0
- package/server/data/research_interests_arxiv.json +50 -0
- package/server/data/research_interests_huggingface.json +4 -0
- package/server/database/db.js +101 -5
- package/server/database/init.sql +17 -1
- package/server/gemini-api.js +1838 -0
- package/server/gemini-cli.js +13 -2
- package/server/index.js +94 -16
- package/server/load-env.js +13 -1
- package/server/local-gpu.js +166 -24
- package/server/mcp-compute.js +223 -0
- package/server/middleware/auth.js +25 -6
- package/server/nano-claude-code.js +340 -0
- package/server/nanoSessionPaths.js +41 -0
- package/server/openai-codex.js +6 -2
- package/server/openrouter.js +37 -22
- package/server/projects.js +516 -110
- package/server/routes/agent.js +32 -7
- package/server/routes/auto-research.js +1 -1
- package/server/routes/claude-btw.js +55 -0
- package/server/routes/cli-auth.js +422 -58
- package/server/routes/commands.js +25 -1
- package/server/routes/community-tools.js +398 -0
- package/server/routes/compute.js +155 -0
- package/server/routes/git.js +9 -11
- package/server/routes/memory.js +143 -0
- package/server/routes/news.js +3 -0
- package/server/routes/skills.js +35 -0
- package/server/routes/user.js +3 -5
- package/server/utils/__tests__/safePath.test.js +93 -0
- package/server/utils/cliResolution.js +13 -1
- package/server/utils/codexSessionMessages.js +285 -0
- package/server/utils/geminiOAuth.js +333 -0
- package/server/utils/memoryPrompt.js +78 -0
- package/server/utils/safePath.js +51 -0
- package/server/utils/sessionIndex.js +2 -0
- package/server/utils/skillExpander.js +142 -0
- package/server/utils/spawnAsync.js +78 -0
- package/shared/modelConstants.js +19 -1
- package/dist/assets/channel-BFOE7OyC.js +0 -1
- package/dist/assets/clone-BulQSblH.js +0 -1
- package/dist/assets/index-B8Yutsnm.css +0 -1
- package/dist/assets/index-Cf6gyhc3.js +0 -654
- package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +0 -1
- package/scripts/install-python-tools.js +0 -64
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const originalHome = process.env.HOME;
|
|
7
|
+
const originalUserProfile = process.env.USERPROFILE;
|
|
8
|
+
const originalDatabasePath = process.env.DATABASE_PATH;
|
|
9
|
+
|
|
10
|
+
let tempRoot = null;
|
|
11
|
+
|
|
12
|
+
async function loadTestModules() {
|
|
13
|
+
vi.resetModules();
|
|
14
|
+
const projects = await import('../projects.js');
|
|
15
|
+
const database = await import('../database/db.js');
|
|
16
|
+
await database.initializeDatabase();
|
|
17
|
+
return { projects, database };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('Gemini API session indexing', () => {
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
tempRoot = await mkdtemp(path.join(os.tmpdir(), 'dr-claw-gemini-index-'));
|
|
23
|
+
process.env.HOME = tempRoot;
|
|
24
|
+
process.env.USERPROFILE = tempRoot;
|
|
25
|
+
process.env.DATABASE_PATH = path.join(tempRoot, 'db', 'auth.db');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
vi.resetModules();
|
|
30
|
+
|
|
31
|
+
if (originalHome === undefined) delete process.env.HOME;
|
|
32
|
+
else process.env.HOME = originalHome;
|
|
33
|
+
|
|
34
|
+
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
|
|
35
|
+
else process.env.USERPROFILE = originalUserProfile;
|
|
36
|
+
|
|
37
|
+
if (originalDatabasePath === undefined) delete process.env.DATABASE_PATH;
|
|
38
|
+
else process.env.DATABASE_PATH = originalDatabasePath;
|
|
39
|
+
|
|
40
|
+
if (tempRoot) {
|
|
41
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
42
|
+
tempRoot = null;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('discovers API-written Gemini sessions from ~/.gemini/sessions', async () => {
|
|
47
|
+
const { projects } = await loadTestModules();
|
|
48
|
+
const projectPath = path.join(tempRoot, 'workspace', 'gemini-project');
|
|
49
|
+
const sessionId = 'gemini-api-session-1';
|
|
50
|
+
const sessionsDir = path.join(tempRoot, '.gemini', 'sessions');
|
|
51
|
+
const sessionFile = path.join(sessionsDir, `${sessionId}.jsonl`);
|
|
52
|
+
|
|
53
|
+
await mkdir(projectPath, { recursive: true });
|
|
54
|
+
await mkdir(sessionsDir, { recursive: true });
|
|
55
|
+
await writeFile(sessionFile, [
|
|
56
|
+
JSON.stringify({
|
|
57
|
+
type: 'session_meta',
|
|
58
|
+
payload: {
|
|
59
|
+
id: sessionId,
|
|
60
|
+
cwd: projectPath,
|
|
61
|
+
timestamp: '2026-04-05T12:00:00.000Z',
|
|
62
|
+
sessionMode: 'research',
|
|
63
|
+
title: 'Explore Gemini API flow',
|
|
64
|
+
},
|
|
65
|
+
cwd: projectPath,
|
|
66
|
+
timestamp: '2026-04-05T12:00:00.000Z',
|
|
67
|
+
title: 'Explore Gemini API flow',
|
|
68
|
+
}),
|
|
69
|
+
JSON.stringify({
|
|
70
|
+
type: 'message',
|
|
71
|
+
role: 'user',
|
|
72
|
+
content: 'Explore Gemini API flow',
|
|
73
|
+
timestamp: '2026-04-05T12:00:01.000Z',
|
|
74
|
+
}),
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
type: 'message',
|
|
77
|
+
role: 'assistant',
|
|
78
|
+
content: 'I inspected the repository.',
|
|
79
|
+
timestamp: '2026-04-05T12:00:02.000Z',
|
|
80
|
+
}),
|
|
81
|
+
'',
|
|
82
|
+
].join('\n'), 'utf-8');
|
|
83
|
+
|
|
84
|
+
const sessions = await projects.getGeminiSessions(projectPath, { limit: 5 });
|
|
85
|
+
|
|
86
|
+
expect(sessions).toHaveLength(1);
|
|
87
|
+
expect(sessions[0]).toEqual(expect.objectContaining({
|
|
88
|
+
id: sessionId,
|
|
89
|
+
name: 'Explore Gemini API flow',
|
|
90
|
+
mode: 'research',
|
|
91
|
+
projectPath,
|
|
92
|
+
messageCount: 2,
|
|
93
|
+
}));
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { mkdtemp, mkdir, rm } from 'fs/promises';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const originalHome = process.env.HOME;
|
|
7
|
+
const originalUserProfile = process.env.USERPROFILE;
|
|
8
|
+
const originalDatabasePath = process.env.DATABASE_PATH;
|
|
9
|
+
|
|
10
|
+
let tempRoot = null;
|
|
11
|
+
|
|
12
|
+
async function loadTestModules() {
|
|
13
|
+
vi.resetModules();
|
|
14
|
+
const database = await import('../database/db.js');
|
|
15
|
+
await database.initializeDatabase();
|
|
16
|
+
const projects = await import('../projects.js');
|
|
17
|
+
return { projects, database };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Create a test user and return its numeric id */
|
|
21
|
+
function createTestUser(database, username = 'testuser') {
|
|
22
|
+
const user = database.userDb.createUser(username, 'hash');
|
|
23
|
+
return user.id;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('project sync and dedup (PR #89)', () => {
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
tempRoot = await mkdtemp(path.join(os.tmpdir(), 'dr-claw-project-dedup-'));
|
|
29
|
+
process.env.HOME = tempRoot;
|
|
30
|
+
process.env.USERPROFILE = tempRoot;
|
|
31
|
+
process.env.DATABASE_PATH = path.join(tempRoot, 'db', 'auth.db');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
vi.resetModules();
|
|
36
|
+
|
|
37
|
+
if (originalHome === undefined) delete process.env.HOME;
|
|
38
|
+
else process.env.HOME = originalHome;
|
|
39
|
+
|
|
40
|
+
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
|
|
41
|
+
else process.env.USERPROFILE = originalUserProfile;
|
|
42
|
+
|
|
43
|
+
if (originalDatabasePath === undefined) delete process.env.DATABASE_PATH;
|
|
44
|
+
else process.env.DATABASE_PATH = originalDatabasePath;
|
|
45
|
+
|
|
46
|
+
if (tempRoot) {
|
|
47
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
48
|
+
tempRoot = null;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('getProjectByPath', () => {
|
|
53
|
+
it('returns unowned project when queried with a userId', async () => {
|
|
54
|
+
const { database } = await loadTestModules();
|
|
55
|
+
const testPath = '/tmp/test-project-alpha';
|
|
56
|
+
|
|
57
|
+
database.projectDb.upsertProject('proj-alpha', null, 'Alpha', testPath, 0, null, null);
|
|
58
|
+
|
|
59
|
+
const result = database.projectDb.getProjectByPath(testPath, 1);
|
|
60
|
+
expect(result).not.toBeNull();
|
|
61
|
+
expect(result.id).toBe('proj-alpha');
|
|
62
|
+
expect(result.user_id).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('prefers same-user record over unowned when both exist', async () => {
|
|
66
|
+
const { database } = await loadTestModules();
|
|
67
|
+
const userId = createTestUser(database);
|
|
68
|
+
const testPath = '/tmp/test-project-beta';
|
|
69
|
+
|
|
70
|
+
// Insert unowned record first
|
|
71
|
+
database.projectDb.upsertProject('proj-beta-unowned', null, 'Beta Unowned', testPath, 0, null, null);
|
|
72
|
+
// Insert user-owned record (different ID to allow both rows)
|
|
73
|
+
database.projectDb.upsertProject('proj-beta-user1', userId, 'Beta User1', testPath, 0, null, null);
|
|
74
|
+
|
|
75
|
+
const result = database.projectDb.getProjectByPath(testPath, userId);
|
|
76
|
+
expect(result).not.toBeNull();
|
|
77
|
+
expect(result.id).toBe('proj-beta-user1');
|
|
78
|
+
expect(result.user_id).toBe(userId);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns user-owned project when queried with userId=null', async () => {
|
|
82
|
+
const { database } = await loadTestModules();
|
|
83
|
+
const userId = createTestUser(database);
|
|
84
|
+
const testPath = '/tmp/test-project-gamma';
|
|
85
|
+
|
|
86
|
+
database.projectDb.upsertProject('proj-gamma', userId, 'Gamma', testPath, 0, null, null);
|
|
87
|
+
|
|
88
|
+
const result = database.projectDb.getProjectByPath(testPath, null);
|
|
89
|
+
expect(result).not.toBeNull();
|
|
90
|
+
expect(result.id).toBe('proj-gamma');
|
|
91
|
+
expect(result.user_id).toBe(userId);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('addProjectManually — preserves existing values', () => {
|
|
96
|
+
it('preserves is_starred and metadata when project already exists', async () => {
|
|
97
|
+
const { database, projects } = await loadTestModules();
|
|
98
|
+
|
|
99
|
+
// Create a real directory for addProjectManually's fs.access check
|
|
100
|
+
const projectDir = path.join(tempRoot, 'my-project');
|
|
101
|
+
await mkdir(projectDir, { recursive: true });
|
|
102
|
+
|
|
103
|
+
const encodedId = projects.encodeProjectPath(projectDir);
|
|
104
|
+
const customMetadata = { customKey: 'customValue', provider: 'claude' };
|
|
105
|
+
|
|
106
|
+
// Seed the DB with a starred project that has metadata
|
|
107
|
+
database.projectDb.upsertProject(
|
|
108
|
+
encodedId, null, 'My Project', projectDir,
|
|
109
|
+
1, '2026-01-01T00:00:00.000Z', customMetadata,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Now call addProjectManually — should NOT wipe is_starred or metadata
|
|
113
|
+
const result = await projects.addProjectManually(projectDir, null, null);
|
|
114
|
+
|
|
115
|
+
expect(result.alreadyExists).toBe(true);
|
|
116
|
+
|
|
117
|
+
// Verify DB state
|
|
118
|
+
const dbRow = database.projectDb.getProjectByPath(projectDir, null);
|
|
119
|
+
expect(dbRow.is_starred).toBe(1);
|
|
120
|
+
expect(dbRow.metadata.customKey).toBe('customValue');
|
|
121
|
+
expect(dbRow.metadata.provider).toBe('claude');
|
|
122
|
+
expect(dbRow.metadata.manuallyAdded).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('does not create duplicate when same path exists with different ID', async () => {
|
|
126
|
+
const { database, projects } = await loadTestModules();
|
|
127
|
+
|
|
128
|
+
const projectDir = path.join(tempRoot, 'dup-project');
|
|
129
|
+
await mkdir(projectDir, { recursive: true });
|
|
130
|
+
|
|
131
|
+
// Insert with a "legacy" ID that differs from encodeProjectPath output
|
|
132
|
+
const legacyId = 'legacy-encoded-dup-project';
|
|
133
|
+
database.projectDb.upsertProject(
|
|
134
|
+
legacyId, null, 'Dup Project', projectDir,
|
|
135
|
+
0, null, null,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// addProjectManually will compute a different ID via encodeProjectPath
|
|
139
|
+
const result = await projects.addProjectManually(projectDir, null, null);
|
|
140
|
+
expect(result.alreadyExists).toBe(true);
|
|
141
|
+
|
|
142
|
+
// Should still be only one record for this path
|
|
143
|
+
const all = database.projectDb.getAllProjects(null);
|
|
144
|
+
const matching = all.filter(p => p.path === projectDir);
|
|
145
|
+
expect(matching.length).toBe(1);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('bootstrap throttle', () => {
|
|
150
|
+
it('does not re-scan legacy sources within 60 seconds', async () => {
|
|
151
|
+
const { projects, database } = await loadTestModules();
|
|
152
|
+
|
|
153
|
+
// Set up minimal directory structure so getProjects doesn't error
|
|
154
|
+
await mkdir(path.join(tempRoot, 'dr-claw'), { recursive: true });
|
|
155
|
+
await mkdir(path.join(tempRoot, '.claude', 'projects'), { recursive: true });
|
|
156
|
+
|
|
157
|
+
// Spy on bootstrapProjectsIndexFromLegacySources via upsertProject call count
|
|
158
|
+
const upsertSpy = vi.spyOn(database.projectDb, 'upsertProject');
|
|
159
|
+
|
|
160
|
+
await projects.getProjects(null);
|
|
161
|
+
const callsAfterFirst = upsertSpy.mock.calls.length;
|
|
162
|
+
|
|
163
|
+
// Second call within 60s — bootstrap should be skipped
|
|
164
|
+
await projects.getProjects(null);
|
|
165
|
+
const callsAfterSecond = upsertSpy.mock.calls.length;
|
|
166
|
+
|
|
167
|
+
// No additional upsertProject calls from bootstrap on second run
|
|
168
|
+
expect(callsAfterSecond).toBe(callsAfterFirst);
|
|
169
|
+
|
|
170
|
+
upsertSpy.mockRestore();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -163,4 +163,156 @@ describe('session deletion fallbacks', () => {
|
|
|
163
163
|
expect(sessions[0].provider).toBe('codex');
|
|
164
164
|
expect(sessions[0].summary).toContain('Inspect the project state');
|
|
165
165
|
});
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
it('parses Codex history into session context inputs that survive reload', async () => {
|
|
169
|
+
const sessionId = 'codex-session-context';
|
|
170
|
+
const projectRoot = path.join(tempRoot, 'workspace', 'demo');
|
|
171
|
+
const sessionDir = path.join(tempRoot, '.codex', 'sessions', '2026', '03', '30');
|
|
172
|
+
const sessionFile = path.join(sessionDir, `rollout-2026-03-30T05-00-00-${sessionId}.jsonl`);
|
|
173
|
+
|
|
174
|
+
await mkdir(sessionDir, { recursive: true });
|
|
175
|
+
await writeFile(sessionFile, [
|
|
176
|
+
JSON.stringify({
|
|
177
|
+
timestamp: '2026-03-30T05:18:20.000Z',
|
|
178
|
+
type: 'response_item',
|
|
179
|
+
payload: {
|
|
180
|
+
type: 'function_call',
|
|
181
|
+
name: 'exec_command',
|
|
182
|
+
arguments: JSON.stringify({
|
|
183
|
+
cmd: "sed -n '1,200p' src/components/chat/utils/sessionContextSummary.ts",
|
|
184
|
+
workdir: projectRoot,
|
|
185
|
+
}),
|
|
186
|
+
call_id: 'call-read',
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
JSON.stringify({
|
|
190
|
+
timestamp: '2026-03-30T05:18:21.000Z',
|
|
191
|
+
type: 'response_item',
|
|
192
|
+
payload: {
|
|
193
|
+
type: 'function_call_output',
|
|
194
|
+
call_id: 'call-read',
|
|
195
|
+
output: 'export const example = true;',
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
JSON.stringify({
|
|
199
|
+
timestamp: '2026-03-30T05:18:22.000Z',
|
|
200
|
+
type: 'event_msg',
|
|
201
|
+
payload: {
|
|
202
|
+
type: 'exec_command_end',
|
|
203
|
+
call_id: 'call-read',
|
|
204
|
+
cwd: projectRoot,
|
|
205
|
+
parsed_cmd: [
|
|
206
|
+
{
|
|
207
|
+
type: 'read',
|
|
208
|
+
cmd: "sed -n '1,200p' src/components/chat/utils/sessionContextSummary.ts",
|
|
209
|
+
path: 'src/components/chat/utils/sessionContextSummary.ts',
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
aggregated_output: 'export const example = true;',
|
|
213
|
+
exit_code: 0,
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
JSON.stringify({
|
|
217
|
+
timestamp: '2026-03-30T05:19:00.000Z',
|
|
218
|
+
type: 'response_item',
|
|
219
|
+
payload: {
|
|
220
|
+
type: 'function_call',
|
|
221
|
+
name: 'update_plan',
|
|
222
|
+
arguments: JSON.stringify({
|
|
223
|
+
plan: [
|
|
224
|
+
{ step: 'Normalize Codex history', status: 'in_progress' },
|
|
225
|
+
{ step: 'Expand session summary', status: 'pending' },
|
|
226
|
+
],
|
|
227
|
+
}),
|
|
228
|
+
call_id: 'call-plan',
|
|
229
|
+
},
|
|
230
|
+
}),
|
|
231
|
+
JSON.stringify({
|
|
232
|
+
timestamp: '2026-03-30T05:20:00.000Z',
|
|
233
|
+
type: 'response_item',
|
|
234
|
+
payload: {
|
|
235
|
+
type: 'custom_tool_call',
|
|
236
|
+
name: 'apply_patch',
|
|
237
|
+
input: [
|
|
238
|
+
'*** Begin Patch',
|
|
239
|
+
'*** Update File: src/components/chat/utils/sessionContextSummary.ts',
|
|
240
|
+
'*** Add File: docs/plan.md',
|
|
241
|
+
'*** End Patch',
|
|
242
|
+
].join('\n'),
|
|
243
|
+
call_id: 'call-patch',
|
|
244
|
+
},
|
|
245
|
+
}),
|
|
246
|
+
JSON.stringify({
|
|
247
|
+
timestamp: '2026-03-30T05:20:01.000Z',
|
|
248
|
+
type: 'event_msg',
|
|
249
|
+
payload: {
|
|
250
|
+
type: 'patch_apply_end',
|
|
251
|
+
call_id: 'call-patch',
|
|
252
|
+
stdout: 'Success. Updated the following files:\nM src/components/chat/utils/sessionContextSummary.ts\nA docs/plan.md\n',
|
|
253
|
+
stderr: '',
|
|
254
|
+
success: true,
|
|
255
|
+
status: 'completed',
|
|
256
|
+
changes: {
|
|
257
|
+
[path.join(projectRoot, 'src/components/chat/utils/sessionContextSummary.ts')]: { type: 'update' },
|
|
258
|
+
[path.join(projectRoot, 'docs/plan.md')]: { type: 'add' },
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
}),
|
|
262
|
+
JSON.stringify({
|
|
263
|
+
timestamp: '2026-03-30T05:21:00.000Z',
|
|
264
|
+
type: 'response_item',
|
|
265
|
+
payload: {
|
|
266
|
+
type: 'web_search_call',
|
|
267
|
+
status: 'completed',
|
|
268
|
+
action: {
|
|
269
|
+
type: 'search',
|
|
270
|
+
query: 'Codex session context panel',
|
|
271
|
+
queries: ['Codex session context panel'],
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
JSON.stringify({
|
|
276
|
+
timestamp: '2026-03-30T05:21:30.000Z',
|
|
277
|
+
type: 'response_item',
|
|
278
|
+
payload: {
|
|
279
|
+
type: 'web_search_call',
|
|
280
|
+
status: 'completed',
|
|
281
|
+
action: {
|
|
282
|
+
type: 'open_page',
|
|
283
|
+
url: 'https://developers.openai.com/api/docs',
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
}),
|
|
287
|
+
JSON.stringify({
|
|
288
|
+
timestamp: '2026-03-30T05:22:00.000Z',
|
|
289
|
+
type: 'response_item',
|
|
290
|
+
payload: {
|
|
291
|
+
type: 'web_search_call',
|
|
292
|
+
status: 'completed',
|
|
293
|
+
action: {
|
|
294
|
+
type: 'find_in_page',
|
|
295
|
+
url: 'https://developers.openai.com/api/docs',
|
|
296
|
+
pattern: 'session',
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
}),
|
|
300
|
+
].join('\n'));
|
|
301
|
+
|
|
302
|
+
const { projects } = await loadTestModules();
|
|
303
|
+
const { convertSessionMessages } = await import('../../src/components/chat/utils/messageTransforms.ts');
|
|
304
|
+
const { deriveSessionContextSummary } = await import('../../src/components/chat/utils/sessionContextSummary.ts');
|
|
305
|
+
const result = await projects.getCodexSessionMessages(sessionId, null, 0);
|
|
306
|
+
const summary = deriveSessionContextSummary(convertSessionMessages(result.messages), projectRoot);
|
|
307
|
+
|
|
308
|
+
expect(summary.contextFiles.some((item) => item.relativePath === 'src/components/chat/utils/sessionContextSummary.ts')).toBe(true);
|
|
309
|
+
expect(summary.outputFiles.map((item) => item.relativePath).sort()).toEqual([
|
|
310
|
+
'docs/plan.md',
|
|
311
|
+
'src/components/chat/utils/sessionContextSummary.ts',
|
|
312
|
+
]);
|
|
313
|
+
expect(summary.tasks.some((item) => item.label === 'Normalize Codex history')).toBe(true);
|
|
314
|
+
expect(summary.tasks.some((item) => item.label === 'Codex session context panel')).toBe(true);
|
|
315
|
+
expect(summary.tasks.some((item) => item.label === 'https://developers.openai.com/api/docs')).toBe(true);
|
|
316
|
+
expect(summary.tasks.some((item) => item.label === 'session')).toBe(true);
|
|
317
|
+
});
|
|
166
318
|
});
|
package/server/claude-sdk.js
CHANGED
|
@@ -25,6 +25,7 @@ import { applyStageTagsToSession, recordIndexedSession } from './utils/sessionIn
|
|
|
25
25
|
import { buildTempAttachmentFilename } from './utils/imageAttachmentFiles.js';
|
|
26
26
|
|
|
27
27
|
import { createRequestId, waitForToolApproval, resolveToolApproval as resolvePermApproval, matchesToolPermission } from './utils/permissions.js';
|
|
28
|
+
import { buildMemoryBlock } from './utils/memoryPrompt.js';
|
|
28
29
|
|
|
29
30
|
const activeSessions = new Map();
|
|
30
31
|
const pendingClaudeSessionIndexReconciles = new Map();
|
|
@@ -143,10 +144,12 @@ function mapCliOptionsToSDK(options = {}) {
|
|
|
143
144
|
sdkOptions.model = options.model || CLAUDE_MODELS.DEFAULT;
|
|
144
145
|
console.log(`Using model: ${sdkOptions.model}`);
|
|
145
146
|
|
|
146
|
-
// Map system prompt configuration
|
|
147
|
+
// Map system prompt configuration with optional user memory injection
|
|
148
|
+
const memoryBlock = options.userId ? buildMemoryBlock(options.userId) : '';
|
|
147
149
|
sdkOptions.systemPrompt = {
|
|
148
150
|
type: 'preset',
|
|
149
|
-
preset: 'claude_code' // Required to use CLAUDE.md
|
|
151
|
+
preset: 'claude_code', // Required to use CLAUDE.md
|
|
152
|
+
...(memoryBlock ? { append: memoryBlock } : {}),
|
|
150
153
|
};
|
|
151
154
|
|
|
152
155
|
// Map setting sources for CLAUDE.md loading
|
|
@@ -220,28 +223,78 @@ function transformMessage(sdkMessage) {
|
|
|
220
223
|
}
|
|
221
224
|
|
|
222
225
|
/**
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
+
* Returns the context window size for a given model name.
|
|
227
|
+
* Supports both SDK format ('sonnet', 'opus') and API format ('claude-opus-4-6').
|
|
228
|
+
* Falls back to CONTEXT_WINDOW env var, then 200000.
|
|
229
|
+
* @param {string|null} modelName
|
|
230
|
+
* @returns {number}
|
|
231
|
+
*/
|
|
232
|
+
function getContextWindowForModel(modelName) {
|
|
233
|
+
const MODEL_CONTEXT_WINDOWS = {
|
|
234
|
+
// SDK format names
|
|
235
|
+
'sonnet': 200000,
|
|
236
|
+
'opus': 200000,
|
|
237
|
+
'haiku': 200000,
|
|
238
|
+
'opusplan': 200000,
|
|
239
|
+
'sonnet[1m]': 1000000,
|
|
240
|
+
// API format names
|
|
241
|
+
'claude-opus-4-6': 200000,
|
|
242
|
+
'claude-opus-4-20250918': 200000,
|
|
243
|
+
'claude-sonnet-4-6': 200000,
|
|
244
|
+
'claude-sonnet-4-20250514': 200000,
|
|
245
|
+
'claude-haiku-4-5': 200000,
|
|
246
|
+
'claude-haiku-4-5-20251001': 200000,
|
|
247
|
+
'claude-3-5-sonnet': 200000,
|
|
248
|
+
'claude-3-5-sonnet-20241022': 200000,
|
|
249
|
+
'claude-3-5-haiku': 200000,
|
|
250
|
+
'claude-3-5-haiku-20241022': 200000,
|
|
251
|
+
'claude-3-opus': 200000,
|
|
252
|
+
'claude-3-opus-20240229': 200000,
|
|
253
|
+
'claude-3-sonnet': 200000,
|
|
254
|
+
'claude-3-haiku': 200000,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
if (modelName) {
|
|
258
|
+
// Exact match first
|
|
259
|
+
const exact = MODEL_CONTEXT_WINDOWS[modelName];
|
|
260
|
+
if (exact) return exact;
|
|
261
|
+
// Prefix match (e.g. 'claude-opus-4-6-20260301' matches 'claude-opus-4-6')
|
|
262
|
+
const prefix = Object.keys(MODEL_CONTEXT_WINDOWS).find(k => modelName.startsWith(k));
|
|
263
|
+
if (prefix) return MODEL_CONTEXT_WINDOWS[prefix];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Fallback: env var override, then default
|
|
267
|
+
const envVal = parseInt(process.env.CONTEXT_WINDOW, 10);
|
|
268
|
+
return Number.isFinite(envVal) ? envVal : 200000;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Extracts token budget from the latest assistant message's usage data.
|
|
273
|
+
* Returns a snapshot of context window usage for a single API call,
|
|
274
|
+
* including both input tokens (with cache) and output tokens.
|
|
275
|
+
* Not cumulative across agentic turns.
|
|
226
276
|
* @param {Object|null} usage - usage object from assistant message (message.usage)
|
|
227
|
-
* @
|
|
277
|
+
* @param {string|null} modelName - model name for context window lookup
|
|
278
|
+
* @returns {Object|null} Token budget object { used, total } or null
|
|
228
279
|
*/
|
|
229
|
-
function extractTokenBudgetFromUsage(usage) {
|
|
280
|
+
function extractTokenBudgetFromUsage(usage, modelName) {
|
|
230
281
|
if (!usage) {
|
|
231
282
|
return null;
|
|
232
283
|
}
|
|
233
284
|
|
|
234
285
|
// In Claude API: input_tokens is the non-cached portion.
|
|
235
|
-
// Total context = input_tokens + cache_read_input_tokens + cache_creation_input_tokens
|
|
286
|
+
// Total context = input_tokens + cache_read_input_tokens + cache_creation_input_tokens + output_tokens
|
|
287
|
+
// This is a per-call snapshot — output_tokens here are for THIS response only,
|
|
288
|
+
// not double-counted with future turns' input_tokens.
|
|
236
289
|
const inputTokens = usage.input_tokens || 0;
|
|
237
290
|
const cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
238
291
|
const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
|
|
239
|
-
const
|
|
292
|
+
const outputTokens = usage.output_tokens || 0;
|
|
293
|
+
const totalUsed = inputTokens + cacheReadTokens + cacheCreationTokens + outputTokens;
|
|
240
294
|
|
|
241
|
-
|
|
242
|
-
const contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 200000;
|
|
295
|
+
const contextWindow = getContextWindowForModel(modelName);
|
|
243
296
|
|
|
244
|
-
console.log(`Token calculation: input=${inputTokens}, cacheRead=${cacheReadTokens}, cacheCreation=${cacheCreationTokens}, total=${totalUsed}/${contextWindow}`);
|
|
297
|
+
console.log(`Token calculation: model=${modelName}, input=${inputTokens}, cacheRead=${cacheReadTokens}, cacheCreation=${cacheCreationTokens}, output=${outputTokens}, total=${totalUsed}/${contextWindow}`);
|
|
245
298
|
|
|
246
299
|
return {
|
|
247
300
|
used: totalUsed,
|
|
@@ -469,6 +522,37 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
469
522
|
sdkOptions.mcpServers = mcpServers;
|
|
470
523
|
}
|
|
471
524
|
|
|
525
|
+
// Inject compute-tools MCP server if an active compute node is configured
|
|
526
|
+
try {
|
|
527
|
+
const { getActiveNode } = await import('./compute-node.js');
|
|
528
|
+
const activeNode = await getActiveNode();
|
|
529
|
+
if (activeNode) {
|
|
530
|
+
const computeMcpPath = new URL('./mcp-compute.js', import.meta.url).pathname;
|
|
531
|
+
sdkOptions.mcpServers = {
|
|
532
|
+
...(sdkOptions.mcpServers || {}),
|
|
533
|
+
'compute-tools': {
|
|
534
|
+
command: 'node',
|
|
535
|
+
args: [computeMcpPath],
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
// Append compute node context to system prompt
|
|
539
|
+
sdkOptions.appendSystemPrompt = [
|
|
540
|
+
sdkOptions.appendSystemPrompt || '',
|
|
541
|
+
'\n\n# Active Compute Node',
|
|
542
|
+
`A remote compute node "${activeNode.name}" (${activeNode.user}@${activeNode.host}) is configured and available.`,
|
|
543
|
+
`Type: ${activeNode.type === 'slurm' ? 'Slurm HPC cluster' : 'Direct GPU server'}.`,
|
|
544
|
+
`Work directory: ${activeNode.workDir || '~'}.`,
|
|
545
|
+
'When the user asks to run training, GPU tasks, or heavy computation, use the compute_* tools (compute_run, compute_sync, compute_slurm_submit, etc.) instead of local Bash.',
|
|
546
|
+
'Use compute_sync direction="up" to upload project code before running, and compute_run to execute commands remotely.',
|
|
547
|
+
'Use compute_info first if you need to check what GPU resources are available.',
|
|
548
|
+
].join('\n');
|
|
549
|
+
console.log(`[claude-sdk] Compute MCP server injected for node "${activeNode.name}" (${activeNode.host})`);
|
|
550
|
+
}
|
|
551
|
+
} catch (err) {
|
|
552
|
+
// Compute node not configured or error loading — skip silently
|
|
553
|
+
console.log('[claude-sdk] No active compute node:', err.message);
|
|
554
|
+
}
|
|
555
|
+
|
|
472
556
|
// Handle images - save to temp files and modify prompt
|
|
473
557
|
const imageResult = await handleImages(command, options.images, options.cwd);
|
|
474
558
|
const finalCommand = imageResult.modifiedCommand;
|
|
@@ -645,7 +729,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
645
729
|
|
|
646
730
|
// Send token budget update when the turn completes
|
|
647
731
|
if (message.type === 'result') {
|
|
648
|
-
const tokenBudget = extractTokenBudgetFromUsage(lastAssistantUsage);
|
|
732
|
+
const tokenBudget = extractTokenBudgetFromUsage(lastAssistantUsage, options.model);
|
|
649
733
|
if (tokenBudget) {
|
|
650
734
|
console.log('Token budget from last assistant usage:', tokenBudget);
|
|
651
735
|
ws.send({
|
|
@@ -788,6 +872,81 @@ function getActiveClaudeSDKSessions() {
|
|
|
788
872
|
return getAllSessions();
|
|
789
873
|
}
|
|
790
874
|
|
|
875
|
+
const BTW_SYSTEM_PROMPT = `You answer a short side question for someone in the middle of a coding session.
|
|
876
|
+
|
|
877
|
+
Rules:
|
|
878
|
+
- You have NO tools. Do not claim to read files, run commands, or fetch URLs unless that information already appears in the conversation context below.
|
|
879
|
+
- Use the "Conversation context" section plus general programming knowledge. If something is not in the context, say you do not see it there.
|
|
880
|
+
- Be concise.`;
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* One-shot, tool-free side question (Claude Code /btw-style). Does not resume the main SDK session.
|
|
884
|
+
*/
|
|
885
|
+
async function runClaudeBtw({ question, transcript, cwd, model, signal }) {
|
|
886
|
+
const safeTranscript =
|
|
887
|
+
typeof transcript === 'string' && transcript.trim()
|
|
888
|
+
? transcript
|
|
889
|
+
: '(No prior conversation in this session.)';
|
|
890
|
+
const userBlock = `## Conversation context\n\n${safeTranscript}\n\n---\n\n## Side question\n\n${question}`;
|
|
891
|
+
|
|
892
|
+
const sdkOptions = {
|
|
893
|
+
cwd: cwd || process.cwd(),
|
|
894
|
+
model: model || CLAUDE_MODELS.DEFAULT,
|
|
895
|
+
tools: [],
|
|
896
|
+
allowedTools: [],
|
|
897
|
+
settingSources: [],
|
|
898
|
+
systemPrompt: BTW_SYSTEM_PROMPT,
|
|
899
|
+
permissionMode: 'default',
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
|
|
903
|
+
process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '120000';
|
|
904
|
+
|
|
905
|
+
let answer = '';
|
|
906
|
+
let lastError = null;
|
|
907
|
+
|
|
908
|
+
try {
|
|
909
|
+
const queryInstance = query({
|
|
910
|
+
prompt: userBlock,
|
|
911
|
+
options: sdkOptions,
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
for await (const message of queryInstance) {
|
|
915
|
+
if (signal?.aborted) {
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
if (message.type === 'result') {
|
|
919
|
+
if (message.subtype === 'success') {
|
|
920
|
+
answer = message.result || answer;
|
|
921
|
+
} else {
|
|
922
|
+
lastError = Array.isArray(message.errors) ? message.errors.join('\n') : 'Side question failed';
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
} catch (err) {
|
|
927
|
+
if (signal?.aborted) {
|
|
928
|
+
return { answer: '' };
|
|
929
|
+
}
|
|
930
|
+
throw new Error(err?.message || String(err));
|
|
931
|
+
} finally {
|
|
932
|
+
if (prevStreamTimeout !== undefined) {
|
|
933
|
+
process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = prevStreamTimeout;
|
|
934
|
+
} else {
|
|
935
|
+
delete process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (signal?.aborted) {
|
|
940
|
+
return { answer: '' };
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (lastError) {
|
|
944
|
+
throw new Error(lastError);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return { answer: answer || '' };
|
|
948
|
+
}
|
|
949
|
+
|
|
791
950
|
// Export public API
|
|
792
951
|
export {
|
|
793
952
|
queryClaudeSDK,
|
|
@@ -795,5 +954,7 @@ export {
|
|
|
795
954
|
isClaudeSDKSessionActive,
|
|
796
955
|
getClaudeSDKSessionStartTime,
|
|
797
956
|
getActiveClaudeSDKSessions,
|
|
798
|
-
resolveToolApproval
|
|
957
|
+
resolveToolApproval,
|
|
958
|
+
getContextWindowForModel,
|
|
959
|
+
runClaudeBtw,
|
|
799
960
|
};
|