@sumicom/quicksave 0.6.1 → 0.6.3
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/ai/cardBuilder.d.ts +14 -3
- package/dist/ai/cardBuilder.d.ts.map +1 -1
- package/dist/ai/cardBuilder.js +217 -66
- package/dist/ai/cardBuilder.js.map +1 -1
- package/dist/ai/claudeCliProvider.d.ts +2 -0
- package/dist/ai/claudeCliProvider.d.ts.map +1 -1
- package/dist/ai/claudeCliProvider.js +133 -23
- package/dist/ai/claudeCliProvider.js.map +1 -1
- package/dist/ai/claudeCodeProvider.d.ts +18 -0
- package/dist/ai/claudeCodeProvider.d.ts.map +1 -0
- package/dist/ai/claudeCodeProvider.js +30 -0
- package/dist/ai/claudeCodeProvider.js.map +1 -0
- package/dist/ai/claudeSdkProvider.d.ts +2 -0
- package/dist/ai/claudeSdkProvider.d.ts.map +1 -1
- package/dist/ai/claudeSdkProvider.js +13 -3
- package/dist/ai/claudeSdkProvider.js.map +1 -1
- package/dist/ai/codexMcpProvider.d.ts +15 -0
- package/dist/ai/codexMcpProvider.d.ts.map +1 -0
- package/dist/ai/codexMcpProvider.js +442 -0
- package/dist/ai/codexMcpProvider.js.map +1 -0
- package/dist/ai/codexSdkProvider.d.ts +35 -0
- package/dist/ai/codexSdkProvider.d.ts.map +1 -0
- package/dist/ai/codexSdkProvider.js +422 -0
- package/dist/ai/codexSdkProvider.js.map +1 -0
- package/dist/ai/debugLogger.d.ts +24 -0
- package/dist/ai/debugLogger.d.ts.map +1 -0
- package/dist/ai/debugLogger.js +54 -0
- package/dist/ai/debugLogger.js.map +1 -0
- package/dist/ai/provider.d.ts +4 -1
- package/dist/ai/provider.d.ts.map +1 -1
- package/dist/ai/sessionManager.d.ts +13 -8
- package/dist/ai/sessionManager.d.ts.map +1 -1
- package/dist/ai/sessionManager.js +150 -28
- package/dist/ai/sessionManager.js.map +1 -1
- package/dist/ai/sessionRegistry.d.ts +2 -0
- package/dist/ai/sessionRegistry.d.ts.map +1 -1
- package/dist/ai/sessionRegistry.js +4 -0
- package/dist/ai/sessionRegistry.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +20 -10
- package/dist/config.js.map +1 -1
- package/dist/connection/connection.d.ts +1 -0
- package/dist/connection/connection.d.ts.map +1 -1
- package/dist/connection/connection.js +19 -1
- package/dist/connection/connection.js.map +1 -1
- package/dist/git/operations.d.ts +20 -0
- package/dist/git/operations.d.ts.map +1 -1
- package/dist/git/operations.js +52 -0
- package/dist/git/operations.js.map +1 -1
- package/dist/handlers/messageHandler.d.ts +18 -0
- package/dist/handlers/messageHandler.d.ts.map +1 -1
- package/dist/handlers/messageHandler.js +420 -20
- package/dist/handlers/messageHandler.js.map +1 -1
- package/dist/index.js +22 -2
- package/dist/index.js.map +1 -1
- package/dist/service/debugHttpServer.d.ts +27 -0
- package/dist/service/debugHttpServer.d.ts.map +1 -0
- package/dist/service/debugHttpServer.js +189 -0
- package/dist/service/debugHttpServer.js.map +1 -0
- package/dist/service/ipcClient.js +1 -1
- package/dist/service/run.d.ts.map +1 -1
- package/dist/service/run.js +21 -1
- package/dist/service/run.js.map +1 -1
- package/dist/service/singleton.d.ts +18 -1
- package/dist/service/singleton.d.ts.map +1 -1
- package/dist/service/singleton.js +43 -18
- package/dist/service/singleton.js.map +1 -1
- package/dist/service/types.js +1 -1
- package/package.json +5 -3
- package/dist/ai/asyncQueue.test.d.ts +0 -2
- package/dist/ai/asyncQueue.test.d.ts.map +0 -1
- package/dist/ai/asyncQueue.test.js +0 -56
- package/dist/ai/asyncQueue.test.js.map +0 -1
- package/dist/ai/sessionStore.test.d.ts +0 -2
- package/dist/ai/sessionStore.test.d.ts.map +0 -1
- package/dist/ai/sessionStore.test.js +0 -99
- package/dist/ai/sessionStore.test.js.map +0 -1
- package/dist/connection/pubsub.test.d.ts +0 -2
- package/dist/connection/pubsub.test.d.ts.map +0 -1
- package/dist/connection/pubsub.test.js +0 -96
- package/dist/connection/pubsub.test.js.map +0 -1
- package/dist/git/operations.test.d.ts +0 -2
- package/dist/git/operations.test.d.ts.map +0 -1
- package/dist/git/operations.test.js +0 -300
- package/dist/git/operations.test.js.map +0 -1
- package/dist/handlers/messageHandler.test.d.ts +0 -2
- package/dist/handlers/messageHandler.test.d.ts.map +0 -1
- package/dist/handlers/messageHandler.test.js +0 -361
- package/dist/handlers/messageHandler.test.js.map +0 -1
- package/dist/service/ipc.test.d.ts +0 -2
- package/dist/service/ipc.test.d.ts.map +0 -1
- package/dist/service/ipc.test.js +0 -140
- package/dist/service/ipc.test.js.map +0 -1
- package/dist/service/singleton.test.d.ts +0 -2
- package/dist/service/singleton.test.d.ts.map +0 -1
- package/dist/service/singleton.test.js +0 -39
- package/dist/service/singleton.test.js.map +0 -1
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
import { createMessage, generateMessageId, } from '@sumicom/quicksave-shared';
|
|
2
2
|
import { GitOperations } from '../git/operations.js';
|
|
3
|
-
import { getAnthropicApiKey, setAnthropicApiKey, hasAnthropicApiKey, addManagedRepo, addManagedCodingPath } from '../config.js';
|
|
3
|
+
import { getAnthropicApiKey, setAnthropicApiKey, hasAnthropicApiKey, addManagedRepo, removeManagedRepo, addManagedCodingPath, removeManagedCodingPath } from '../config.js';
|
|
4
4
|
import { CommitSummaryService } from '../ai/commitSummary.js';
|
|
5
5
|
import { SessionManager } from '../ai/sessionManager.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { ClaudeCodeProvider } from '../ai/claudeCodeProvider.js';
|
|
7
|
+
import { CodexSdkProvider } from '../ai/codexSdkProvider.js';
|
|
8
8
|
import { getSessionRegistry } from '../ai/sessionRegistry.js';
|
|
9
|
-
import { readdir, stat } from 'fs/promises';
|
|
9
|
+
import { readdir, stat, readFile } from 'fs/promises';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
10
11
|
import { join, dirname, basename } from 'path';
|
|
11
12
|
import { homedir } from 'os';
|
|
12
13
|
const VERSION_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000; // 12 hours
|
|
14
|
+
const CODEX_MODELS_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000; // 12 hours
|
|
13
15
|
export class MessageHandler {
|
|
14
16
|
repos;
|
|
15
|
-
agentVersion = '0.6.
|
|
17
|
+
agentVersion = '0.6.3';
|
|
16
18
|
defaultRepoPath;
|
|
17
19
|
clientRepos = new Map(); // peerAddress -> repoPath
|
|
18
20
|
repoLocks = new Map(); // repoPath -> peerAddress holding lock
|
|
19
21
|
availableRepos;
|
|
20
22
|
codingPaths = new Map(); // path -> CodingPath
|
|
21
23
|
aiService = null;
|
|
22
|
-
claudeService = new SessionManager(
|
|
24
|
+
claudeService = new SessionManager([
|
|
25
|
+
new ClaudeCodeProvider(),
|
|
26
|
+
new CodexSdkProvider(),
|
|
27
|
+
]);
|
|
23
28
|
latestVersionCache = null;
|
|
24
29
|
versionCheckInFlight = null;
|
|
30
|
+
codexModelsCache = null;
|
|
31
|
+
codexModelsCheckInFlight = null;
|
|
25
32
|
onPeerSubscribed;
|
|
26
33
|
onPeerUnsubscribed;
|
|
27
34
|
onHistoryUpdated;
|
|
@@ -77,6 +84,59 @@ export class MessageHandler {
|
|
|
77
84
|
})();
|
|
78
85
|
return this.versionCheckInFlight;
|
|
79
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Fetch available Codex models from OpenAI /v1/models. Reads API key from
|
|
89
|
+
* ~/.codex/auth.json (ChatGPT OAuth token or OPENAI_API_KEY). Cached for 12h
|
|
90
|
+
* with concurrent-request dedup — same pattern as checkLatestVersion.
|
|
91
|
+
*/
|
|
92
|
+
async fetchCodexModels(force = false) {
|
|
93
|
+
if (!force && this.codexModelsCache &&
|
|
94
|
+
Date.now() - this.codexModelsCache.checkedAt < CODEX_MODELS_CHECK_INTERVAL_MS) {
|
|
95
|
+
return this.codexModelsCache.models;
|
|
96
|
+
}
|
|
97
|
+
if (this.codexModelsCheckInFlight)
|
|
98
|
+
return this.codexModelsCheckInFlight;
|
|
99
|
+
this.codexModelsCheckInFlight = (async () => {
|
|
100
|
+
try {
|
|
101
|
+
// Resolve API key: env var first, then ~/.codex/auth.json
|
|
102
|
+
let apiKey = process.env.OPENAI_API_KEY;
|
|
103
|
+
if (!apiKey) {
|
|
104
|
+
try {
|
|
105
|
+
const authPath = join(homedir(), '.codex', 'auth.json');
|
|
106
|
+
const raw = await readFile(authPath, 'utf-8');
|
|
107
|
+
const auth = JSON.parse(raw);
|
|
108
|
+
apiKey = auth.OPENAI_API_KEY || auth.tokens?.access_token;
|
|
109
|
+
}
|
|
110
|
+
catch { /* no auth file */ }
|
|
111
|
+
}
|
|
112
|
+
if (!apiKey)
|
|
113
|
+
return null;
|
|
114
|
+
const res = await fetch('https://api.openai.com/v1/models', {
|
|
115
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
116
|
+
signal: AbortSignal.timeout(10_000),
|
|
117
|
+
});
|
|
118
|
+
if (!res.ok)
|
|
119
|
+
return null;
|
|
120
|
+
const data = await res.json();
|
|
121
|
+
if (!Array.isArray(data.data))
|
|
122
|
+
return null;
|
|
123
|
+
// Filter to chat-capable models (gpt-*, o*) and sort descending
|
|
124
|
+
const models = data.data
|
|
125
|
+
.filter((m) => /^(gpt-|o\d)/.test(m.id))
|
|
126
|
+
.map((m) => ({ id: m.id, name: m.id }))
|
|
127
|
+
.sort((a, b) => b.id.localeCompare(a.id));
|
|
128
|
+
this.codexModelsCache = { models, checkedAt: Date.now() };
|
|
129
|
+
return models;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
this.codexModelsCheckInFlight = null;
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
138
|
+
return this.codexModelsCheckInFlight;
|
|
139
|
+
}
|
|
80
140
|
addRepo(repo) {
|
|
81
141
|
if (this.repos.has(repo.path))
|
|
82
142
|
return;
|
|
@@ -181,6 +241,12 @@ export class MessageHandler {
|
|
|
181
241
|
return this.handleDiscard(message, peerAddress);
|
|
182
242
|
case 'git:untrack':
|
|
183
243
|
return this.handleUntrack(message, peerAddress);
|
|
244
|
+
case 'git:submodules':
|
|
245
|
+
return this.handleSubmodules(message, peerAddress);
|
|
246
|
+
case 'git:config-get':
|
|
247
|
+
return this.handleGitConfigGet(message, peerAddress);
|
|
248
|
+
case 'git:config-set':
|
|
249
|
+
return this.handleGitConfigSet(message, peerAddress);
|
|
184
250
|
case 'git:gitignore-add':
|
|
185
251
|
return this.handleGitignoreAdd(message, peerAddress);
|
|
186
252
|
case 'git:gitignore-read':
|
|
@@ -201,14 +267,24 @@ export class MessageHandler {
|
|
|
201
267
|
return this.handleBrowseDirectory(message);
|
|
202
268
|
case 'agent:add-repo':
|
|
203
269
|
return this.handleAddRepo(message);
|
|
270
|
+
case 'agent:remove-repo':
|
|
271
|
+
return this.handleRemoveRepo(message);
|
|
272
|
+
case 'agent:clone-repo':
|
|
273
|
+
return this.handleCloneRepo(message);
|
|
204
274
|
case 'agent:list-coding-paths':
|
|
205
275
|
return this.handleListCodingPaths(message);
|
|
206
276
|
case 'agent:add-coding-path':
|
|
207
277
|
return this.handleAddCodingPath(message);
|
|
278
|
+
case 'agent:remove-coding-path':
|
|
279
|
+
return this.handleRemoveCodingPath(message);
|
|
208
280
|
case 'agent:check-update':
|
|
209
281
|
return this.handleAgentCheckUpdate(message);
|
|
210
282
|
case 'agent:update':
|
|
211
283
|
return this.handleAgentUpdate(message);
|
|
284
|
+
case 'agent:restart':
|
|
285
|
+
return this.handleAgentRestart(message);
|
|
286
|
+
case 'codex:list-models':
|
|
287
|
+
return this.handleCodexListModels(message);
|
|
212
288
|
// Claude Code SDK
|
|
213
289
|
case 'claude:list-sessions':
|
|
214
290
|
return this.handleClaudeListSessions(message, peerAddress);
|
|
@@ -247,6 +323,10 @@ export class MessageHandler {
|
|
|
247
323
|
return this.handleUpdateHistory(message);
|
|
248
324
|
case 'session:delete-history':
|
|
249
325
|
return this.handleDeleteHistory(message);
|
|
326
|
+
case 'project:list-summaries':
|
|
327
|
+
return this.handleListProjectSummaries(message);
|
|
328
|
+
case 'project:list-repos':
|
|
329
|
+
return await this.handleListProjectRepos(message);
|
|
250
330
|
default:
|
|
251
331
|
return this.createErrorResponse(message.id, 'UNKNOWN_MESSAGE_TYPE', `Unknown message type: ${message.type}`);
|
|
252
332
|
}
|
|
@@ -257,8 +337,9 @@ export class MessageHandler {
|
|
|
257
337
|
}
|
|
258
338
|
}
|
|
259
339
|
handleHandshake(message, peerAddress) {
|
|
260
|
-
// Fire-and-forget: check npm registry
|
|
340
|
+
// Fire-and-forget: check npm registry + OpenAI models (12h dedup each)
|
|
261
341
|
this.checkLatestVersion().catch(() => { });
|
|
342
|
+
this.fetchCodexModels().catch(() => { });
|
|
262
343
|
const response = createMessage('handshake:ack', {
|
|
263
344
|
success: true,
|
|
264
345
|
agentVersion: this.agentVersion,
|
|
@@ -268,6 +349,7 @@ export class MessageHandler {
|
|
|
268
349
|
preferences: this.claudeService.getPreferences(),
|
|
269
350
|
latestVersion: this.latestVersionCache?.version,
|
|
270
351
|
devBuild: !this.productionBuild || undefined,
|
|
352
|
+
codexModels: this.codexModelsCache?.models,
|
|
271
353
|
});
|
|
272
354
|
response.id = message.id;
|
|
273
355
|
return response;
|
|
@@ -528,6 +610,45 @@ export class MessageHandler {
|
|
|
528
610
|
this.releaseRepoLock(repoPath, peerAddress);
|
|
529
611
|
}
|
|
530
612
|
}
|
|
613
|
+
async handleSubmodules(message, peerAddress) {
|
|
614
|
+
const git = this.getGit(peerAddress);
|
|
615
|
+
const subs = await git.getSubmodules();
|
|
616
|
+
// Auto-register submodule paths so switch-repo works
|
|
617
|
+
for (const sub of subs) {
|
|
618
|
+
if (!this.repos.has(sub.path)) {
|
|
619
|
+
this.repos.set(sub.path, new GitOperations(sub.path));
|
|
620
|
+
this.availableRepos.push({ path: sub.path, name: sub.name, currentBranch: sub.branch });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const response = createMessage('git:submodules:response', {
|
|
624
|
+
submodules: subs.map((s) => ({ name: s.name, path: s.path, branch: s.branch })),
|
|
625
|
+
});
|
|
626
|
+
response.id = message.id;
|
|
627
|
+
return response;
|
|
628
|
+
}
|
|
629
|
+
async handleGitConfigGet(message, peerAddress) {
|
|
630
|
+
const identity = await this.getGit(peerAddress).getIdentity();
|
|
631
|
+
const response = createMessage('git:config-get:response', identity);
|
|
632
|
+
response.id = message.id;
|
|
633
|
+
return response;
|
|
634
|
+
}
|
|
635
|
+
async handleGitConfigSet(message, peerAddress) {
|
|
636
|
+
try {
|
|
637
|
+
const { name, email } = message.payload;
|
|
638
|
+
await this.getGit(peerAddress).setIdentity(name, email);
|
|
639
|
+
const response = createMessage('git:config-set:response', { success: true });
|
|
640
|
+
response.id = message.id;
|
|
641
|
+
return response;
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
const response = createMessage('git:config-set:response', {
|
|
645
|
+
success: false,
|
|
646
|
+
error: error instanceof Error ? error.message : 'Failed to set git identity',
|
|
647
|
+
});
|
|
648
|
+
response.id = message.id;
|
|
649
|
+
return response;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
531
652
|
async handleGitignoreAdd(message, peerAddress) {
|
|
532
653
|
const repoPath = this.getClientRepoPath(peerAddress);
|
|
533
654
|
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
@@ -829,6 +950,81 @@ export class MessageHandler {
|
|
|
829
950
|
return response;
|
|
830
951
|
}
|
|
831
952
|
}
|
|
953
|
+
handleRemoveRepo(message) {
|
|
954
|
+
const { path: repoPath } = message.payload;
|
|
955
|
+
if (!this.repos.has(repoPath)) {
|
|
956
|
+
const response = createMessage('agent:remove-repo:response', {
|
|
957
|
+
success: false,
|
|
958
|
+
error: 'Repository not found',
|
|
959
|
+
});
|
|
960
|
+
response.id = message.id;
|
|
961
|
+
return response;
|
|
962
|
+
}
|
|
963
|
+
this.removeRepo(repoPath);
|
|
964
|
+
removeManagedRepo(repoPath);
|
|
965
|
+
const response = createMessage('agent:remove-repo:response', {
|
|
966
|
+
success: true,
|
|
967
|
+
});
|
|
968
|
+
response.id = message.id;
|
|
969
|
+
return response;
|
|
970
|
+
}
|
|
971
|
+
async handleCloneRepo(message) {
|
|
972
|
+
const { url, targetDir } = message.payload;
|
|
973
|
+
if (!url || !url.trim()) {
|
|
974
|
+
const response = createMessage('agent:clone-repo:response', {
|
|
975
|
+
success: false,
|
|
976
|
+
error: 'Repository URL is required',
|
|
977
|
+
});
|
|
978
|
+
response.id = message.id;
|
|
979
|
+
return response;
|
|
980
|
+
}
|
|
981
|
+
try {
|
|
982
|
+
const { execFile } = await import('child_process');
|
|
983
|
+
const { promisify } = await import('util');
|
|
984
|
+
const execFileAsync = promisify(execFile);
|
|
985
|
+
// Clone into the target directory
|
|
986
|
+
await execFileAsync('git', ['clone', url.trim(), targetDir], {
|
|
987
|
+
timeout: 120_000,
|
|
988
|
+
});
|
|
989
|
+
// Verify it's a valid git repo and add it
|
|
990
|
+
const git = new GitOperations(targetDir);
|
|
991
|
+
const { current } = await git.getBranches();
|
|
992
|
+
const rootPath = await git.getGitRoot();
|
|
993
|
+
// Check if already added (by resolved root path)
|
|
994
|
+
if (this.repos.has(rootPath)) {
|
|
995
|
+
const response = createMessage('agent:clone-repo:response', {
|
|
996
|
+
success: false,
|
|
997
|
+
error: 'Repository already added',
|
|
998
|
+
});
|
|
999
|
+
response.id = message.id;
|
|
1000
|
+
return response;
|
|
1001
|
+
}
|
|
1002
|
+
// Add to maps and persist
|
|
1003
|
+
this.repos.set(rootPath, git);
|
|
1004
|
+
const newRepo = {
|
|
1005
|
+
path: rootPath,
|
|
1006
|
+
name: basename(rootPath),
|
|
1007
|
+
currentBranch: current,
|
|
1008
|
+
};
|
|
1009
|
+
this.availableRepos.push(newRepo);
|
|
1010
|
+
addManagedRepo(rootPath);
|
|
1011
|
+
const response = createMessage('agent:clone-repo:response', {
|
|
1012
|
+
success: true,
|
|
1013
|
+
repo: newRepo,
|
|
1014
|
+
clonedPath: rootPath,
|
|
1015
|
+
});
|
|
1016
|
+
response.id = message.id;
|
|
1017
|
+
return response;
|
|
1018
|
+
}
|
|
1019
|
+
catch (error) {
|
|
1020
|
+
const response = createMessage('agent:clone-repo:response', {
|
|
1021
|
+
success: false,
|
|
1022
|
+
error: error instanceof Error ? error.message : 'Failed to clone repository',
|
|
1023
|
+
});
|
|
1024
|
+
response.id = message.id;
|
|
1025
|
+
return response;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
832
1028
|
// ============================================================================
|
|
833
1029
|
// Coding Path Handlers
|
|
834
1030
|
// ============================================================================
|
|
@@ -865,6 +1061,19 @@ export class MessageHandler {
|
|
|
865
1061
|
return response;
|
|
866
1062
|
}
|
|
867
1063
|
}
|
|
1064
|
+
handleRemoveCodingPath(message) {
|
|
1065
|
+
const { path: codingPath } = message.payload;
|
|
1066
|
+
if (!this.codingPaths.has(codingPath)) {
|
|
1067
|
+
const response = createMessage('agent:remove-coding-path:response', { success: false, error: 'Coding path not found' });
|
|
1068
|
+
response.id = message.id;
|
|
1069
|
+
return response;
|
|
1070
|
+
}
|
|
1071
|
+
this.codingPaths.delete(codingPath);
|
|
1072
|
+
removeManagedCodingPath(codingPath);
|
|
1073
|
+
const response = createMessage('agent:remove-coding-path:response', { success: true });
|
|
1074
|
+
response.id = message.id;
|
|
1075
|
+
return response;
|
|
1076
|
+
}
|
|
868
1077
|
// ============================================================================
|
|
869
1078
|
// Agent Self-Update
|
|
870
1079
|
// ============================================================================
|
|
@@ -944,6 +1153,50 @@ export class MessageHandler {
|
|
|
944
1153
|
return response;
|
|
945
1154
|
}
|
|
946
1155
|
}
|
|
1156
|
+
async handleAgentRestart(message) {
|
|
1157
|
+
if (this.productionBuild) {
|
|
1158
|
+
const response = createMessage('agent:restart:response', { success: false, error: 'Restart is only available for dev builds' });
|
|
1159
|
+
response.id = message.id;
|
|
1160
|
+
return response;
|
|
1161
|
+
}
|
|
1162
|
+
try {
|
|
1163
|
+
const { spawn } = await import('child_process');
|
|
1164
|
+
const { fileURLToPath } = await import('url');
|
|
1165
|
+
const { resolve: resolvePath } = await import('path');
|
|
1166
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
1167
|
+
const isTs = thisFile.endsWith('.ts');
|
|
1168
|
+
const entryPath = resolvePath(dirname(thisFile), isTs ? '../index.ts' : '../../index.js');
|
|
1169
|
+
const agentRoot = resolvePath(dirname(thisFile), isTs ? '..' : '..');
|
|
1170
|
+
const node = process.execPath;
|
|
1171
|
+
const execArgv = isTs ? ['--import', 'tsx'] : [];
|
|
1172
|
+
spawn(node, [...execArgv, entryPath, '--restart'], {
|
|
1173
|
+
detached: true,
|
|
1174
|
+
stdio: 'ignore',
|
|
1175
|
+
cwd: agentRoot,
|
|
1176
|
+
}).unref();
|
|
1177
|
+
const response = createMessage('agent:restart:response', { success: true });
|
|
1178
|
+
response.id = message.id;
|
|
1179
|
+
return response;
|
|
1180
|
+
}
|
|
1181
|
+
catch (error) {
|
|
1182
|
+
const response = createMessage('agent:restart:response', { success: false, error: error instanceof Error ? error.message : 'Failed to restart agent' });
|
|
1183
|
+
response.id = message.id;
|
|
1184
|
+
return response;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async handleCodexListModels(message) {
|
|
1188
|
+
try {
|
|
1189
|
+
const models = await this.fetchCodexModels(/* force */ true);
|
|
1190
|
+
const response = createMessage('codex:list-models:response', { models: models ?? [] });
|
|
1191
|
+
response.id = message.id;
|
|
1192
|
+
return response;
|
|
1193
|
+
}
|
|
1194
|
+
catch (error) {
|
|
1195
|
+
const response = createMessage('codex:list-models:response', { models: [], error: error instanceof Error ? error.message : 'Failed to fetch models' });
|
|
1196
|
+
response.id = message.id;
|
|
1197
|
+
return response;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
947
1200
|
// ============================================================================
|
|
948
1201
|
// Claude Code SDK Handlers
|
|
949
1202
|
// ============================================================================
|
|
@@ -962,27 +1215,41 @@ export class MessageHandler {
|
|
|
962
1215
|
}
|
|
963
1216
|
}
|
|
964
1217
|
async handleClaudeStart(message, peerAddress) {
|
|
965
|
-
const { prompt, cwd: payloadCwd, allowedTools, systemPrompt, model, permissionMode, sandboxed } = message.payload;
|
|
1218
|
+
const { prompt, cwd: payloadCwd, agent, allowedTools, systemPrompt, model, permissionMode, sandboxed } = message.payload;
|
|
1219
|
+
const legacyProvider = message.payload.provider;
|
|
966
1220
|
const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
|
|
967
1221
|
const streamId = generateMessageId();
|
|
968
|
-
|
|
1222
|
+
const resolvedAgent = agent ?? (legacyProvider === 'codex-mcp' ? 'codex' : legacyProvider ? 'claude-code' : undefined);
|
|
1223
|
+
console.log(`[agent:start] agent=${resolvedAgent ?? 'default'} cwd=${cwd} prompt=${prompt.slice(0, 80)}${sandboxed ? ' [sandboxed]' : ''}`);
|
|
969
1224
|
try {
|
|
970
1225
|
const sessionId = await this.claudeService.startSession({
|
|
971
|
-
prompt,
|
|
1226
|
+
prompt,
|
|
1227
|
+
cwd,
|
|
1228
|
+
streamId,
|
|
1229
|
+
agent: resolvedAgent,
|
|
1230
|
+
allowedTools,
|
|
1231
|
+
systemPrompt,
|
|
1232
|
+
model,
|
|
1233
|
+
permissionMode,
|
|
1234
|
+
sandboxed,
|
|
972
1235
|
});
|
|
973
|
-
console.log(`[
|
|
1236
|
+
console.log(`[agent:start] session created: ${sessionId} agent=${resolvedAgent ?? 'default'}`);
|
|
974
1237
|
this.onPeerSubscribed?.(peerAddress, sessionId);
|
|
975
1238
|
// Register in session history
|
|
976
1239
|
const now = Date.now();
|
|
977
1240
|
const registry = getSessionRegistry();
|
|
978
1241
|
const gitBranch = await this.getGitBranchQuiet(cwd);
|
|
1242
|
+
const actualAgent = this.claudeService.getSessionAgent(sessionId, cwd);
|
|
979
1243
|
registry.upsertEntry({
|
|
980
1244
|
sessionId, cwd,
|
|
1245
|
+
agent: actualAgent,
|
|
981
1246
|
repoName: basename(cwd),
|
|
982
1247
|
gitBranch,
|
|
983
1248
|
firstPrompt: prompt.slice(0, 100),
|
|
984
1249
|
createdAt: now,
|
|
985
1250
|
lastAccessedAt: now,
|
|
1251
|
+
permissionMode,
|
|
1252
|
+
sandboxed: sandboxed || undefined,
|
|
986
1253
|
});
|
|
987
1254
|
this.onHistoryUpdated?.(cwd, registry.getEntry(cwd, sessionId), 'upsert');
|
|
988
1255
|
const response = createMessage('claude:start:response', { success: true, sessionId, streamId });
|
|
@@ -990,26 +1257,32 @@ export class MessageHandler {
|
|
|
990
1257
|
return response;
|
|
991
1258
|
}
|
|
992
1259
|
catch (error) {
|
|
993
|
-
console.error(`[
|
|
1260
|
+
console.error(`[agent:start] error:`, error);
|
|
994
1261
|
const response = createMessage('claude:start:response', { success: false, error: error instanceof Error ? error.message : 'Failed to start session' });
|
|
995
1262
|
response.id = message.id;
|
|
996
1263
|
return response;
|
|
997
1264
|
}
|
|
998
1265
|
}
|
|
999
1266
|
async handleClaudeResume(message, peerAddress) {
|
|
1000
|
-
const { sessionId: requestedId, prompt, cwd: payloadCwd } = message.payload;
|
|
1267
|
+
const { sessionId: requestedId, prompt, cwd: payloadCwd, agent } = message.payload;
|
|
1268
|
+
const legacyProvider = message.payload.provider;
|
|
1001
1269
|
const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
|
|
1002
1270
|
const streamId = generateMessageId();
|
|
1003
|
-
|
|
1271
|
+
const resolvedAgent = agent ?? (legacyProvider === 'codex-mcp' ? 'codex' : legacyProvider ? 'claude-code' : undefined);
|
|
1272
|
+
console.log(`[agent:resume] session=${requestedId} agent=${resolvedAgent ?? 'stored'} cwd=${cwd} prompt=${prompt.slice(0, 80)}`);
|
|
1004
1273
|
// Subscribe before resumeSession starts streaming — hot resume emits events
|
|
1005
1274
|
// immediately (user_message, then assistant turns), so we must be subscribed
|
|
1006
1275
|
// before session.send() fires, not after resumeSession() returns.
|
|
1007
1276
|
this.onPeerSubscribed?.(peerAddress, requestedId);
|
|
1008
1277
|
try {
|
|
1009
1278
|
const actualSessionId = await this.claudeService.resumeSession({
|
|
1010
|
-
sessionId: requestedId,
|
|
1279
|
+
sessionId: requestedId,
|
|
1280
|
+
prompt,
|
|
1281
|
+
cwd,
|
|
1282
|
+
streamId,
|
|
1283
|
+
agent: resolvedAgent,
|
|
1011
1284
|
});
|
|
1012
|
-
console.log(`[
|
|
1285
|
+
console.log(`[agent:resume] session resumed: ${actualSessionId}`);
|
|
1013
1286
|
// Re-subscribe with actual session ID (cold resume may return a different ID)
|
|
1014
1287
|
if (actualSessionId !== requestedId) {
|
|
1015
1288
|
this.onPeerSubscribed?.(peerAddress, actualSessionId);
|
|
@@ -1018,9 +1291,11 @@ export class MessageHandler {
|
|
|
1018
1291
|
const registry = getSessionRegistry();
|
|
1019
1292
|
const existing = registry.getEntry(cwd, requestedId) ?? registry.getEntry(cwd, actualSessionId);
|
|
1020
1293
|
const gitBranch = await this.getGitBranchQuiet(cwd);
|
|
1294
|
+
const actualAgent = this.claudeService.getSessionAgent(actualSessionId, cwd);
|
|
1021
1295
|
registry.upsertEntry({
|
|
1022
1296
|
...(existing ?? { sessionId: actualSessionId, cwd, repoName: basename(cwd), createdAt: Date.now() }),
|
|
1023
1297
|
sessionId: actualSessionId,
|
|
1298
|
+
agent: actualAgent,
|
|
1024
1299
|
lastAccessedAt: Date.now(),
|
|
1025
1300
|
gitBranch,
|
|
1026
1301
|
});
|
|
@@ -1030,7 +1305,7 @@ export class MessageHandler {
|
|
|
1030
1305
|
return response;
|
|
1031
1306
|
}
|
|
1032
1307
|
catch (error) {
|
|
1033
|
-
console.error(`[
|
|
1308
|
+
console.error(`[agent:resume] error:`, error);
|
|
1034
1309
|
const response = createMessage('claude:resume:response', { success: false, error: error instanceof Error ? error.message : 'Failed to resume session' });
|
|
1035
1310
|
response.id = message.id;
|
|
1036
1311
|
return response;
|
|
@@ -1046,14 +1321,14 @@ export class MessageHandler {
|
|
|
1046
1321
|
handleClaudeClose(message) {
|
|
1047
1322
|
const { sessionId } = message.payload;
|
|
1048
1323
|
const success = this.claudeService.closeSession(sessionId);
|
|
1049
|
-
console.log(`[
|
|
1324
|
+
console.log(`[agent:close] session=${sessionId} success=${success}`);
|
|
1050
1325
|
const response = createMessage('claude:close:response', { success, error: success ? undefined : 'Session not found or already closed' });
|
|
1051
1326
|
response.id = message.id;
|
|
1052
1327
|
return response;
|
|
1053
1328
|
}
|
|
1054
1329
|
handleClaudeUserInputResponse(message) {
|
|
1055
1330
|
const resolved = this.claudeService.resolveUserInput(message.payload);
|
|
1056
|
-
console.log(`[
|
|
1331
|
+
console.log(`[agent:user-input-response] requestId=${message.payload.requestId} action=${message.payload.action} resolved=${resolved}`);
|
|
1057
1332
|
// No dedicated response type — just acknowledge
|
|
1058
1333
|
const response = createMessage('claude:user-input-response', { success: resolved });
|
|
1059
1334
|
response.id = message.id;
|
|
@@ -1122,7 +1397,8 @@ export class MessageHandler {
|
|
|
1122
1397
|
// ── Session Registry (History) ──────────────────────────────────────
|
|
1123
1398
|
handleListHistory(message, peerAddress) {
|
|
1124
1399
|
const cwd = message.payload.cwd || this.getClientRepoPath(peerAddress);
|
|
1125
|
-
const entries = getSessionRegistry().getEntriesForProject(cwd)
|
|
1400
|
+
const entries = getSessionRegistry().getEntriesForProject(cwd)
|
|
1401
|
+
.filter(e => !e.archived);
|
|
1126
1402
|
const response = createMessage('session:list-history:response', { entries });
|
|
1127
1403
|
response.id = message.id;
|
|
1128
1404
|
return response;
|
|
@@ -1148,6 +1424,130 @@ export class MessageHandler {
|
|
|
1148
1424
|
}
|
|
1149
1425
|
return response;
|
|
1150
1426
|
}
|
|
1427
|
+
handleListProjectSummaries(message) {
|
|
1428
|
+
const allEntries = getSessionRegistry().getEntriesForProject()
|
|
1429
|
+
.filter(e => !e.archived);
|
|
1430
|
+
// Group by cwd
|
|
1431
|
+
const byCwd = new Map();
|
|
1432
|
+
for (const entry of allEntries) {
|
|
1433
|
+
let group = byCwd.get(entry.cwd);
|
|
1434
|
+
if (!group) {
|
|
1435
|
+
group = [];
|
|
1436
|
+
byCwd.set(entry.cwd, group);
|
|
1437
|
+
}
|
|
1438
|
+
group.push(entry);
|
|
1439
|
+
}
|
|
1440
|
+
// Build active session set from session manager
|
|
1441
|
+
const activeSessions = this.claudeService.getActiveSessions();
|
|
1442
|
+
const activeCwds = new Set();
|
|
1443
|
+
for (const s of activeSessions) {
|
|
1444
|
+
activeCwds.add(s.cwd);
|
|
1445
|
+
}
|
|
1446
|
+
const projects = [];
|
|
1447
|
+
for (const [cwd, entries] of byCwd) {
|
|
1448
|
+
// entries are already sorted by lastAccessedAt desc from getEntriesForProject
|
|
1449
|
+
const latest = entries[0];
|
|
1450
|
+
projects.push({
|
|
1451
|
+
cwd,
|
|
1452
|
+
sessionCount: entries.length,
|
|
1453
|
+
lastActivityAt: latest.lastAccessedAt,
|
|
1454
|
+
lastSessionTitle: latest.title ?? latest.firstPrompt?.slice(0, 100),
|
|
1455
|
+
hasActiveSession: activeCwds.has(cwd),
|
|
1456
|
+
isGitRepo: existsSync(join(cwd, '.git')),
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
// Sort by lastActivityAt desc
|
|
1460
|
+
projects.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
1461
|
+
const response = createMessage('project:list-summaries:response', { projects });
|
|
1462
|
+
response.id = message.id;
|
|
1463
|
+
return response;
|
|
1464
|
+
}
|
|
1465
|
+
async handleListProjectRepos(message) {
|
|
1466
|
+
const { cwd } = message.payload;
|
|
1467
|
+
const repos = [];
|
|
1468
|
+
const seen = new Set();
|
|
1469
|
+
try {
|
|
1470
|
+
// 1. Check if cwd itself is a git repo
|
|
1471
|
+
const rootBranch = await this.getGitBranchQuiet(cwd);
|
|
1472
|
+
if (rootBranch !== undefined) {
|
|
1473
|
+
repos.push({
|
|
1474
|
+
path: cwd,
|
|
1475
|
+
name: basename(cwd),
|
|
1476
|
+
currentBranch: rootBranch || undefined,
|
|
1477
|
+
});
|
|
1478
|
+
seen.add(cwd);
|
|
1479
|
+
}
|
|
1480
|
+
// 2. Find submodules via git
|
|
1481
|
+
if (rootBranch !== undefined) {
|
|
1482
|
+
try {
|
|
1483
|
+
const git = new GitOperations(cwd);
|
|
1484
|
+
const submodules = await git.getSubmodules();
|
|
1485
|
+
for (const sub of submodules) {
|
|
1486
|
+
const subPath = join(cwd, sub.path);
|
|
1487
|
+
if (seen.has(subPath))
|
|
1488
|
+
continue;
|
|
1489
|
+
seen.add(subPath);
|
|
1490
|
+
const branch = await this.getGitBranchQuiet(subPath);
|
|
1491
|
+
repos.push({
|
|
1492
|
+
path: subPath,
|
|
1493
|
+
name: sub.path,
|
|
1494
|
+
currentBranch: branch || undefined,
|
|
1495
|
+
isSubmodule: true,
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
catch {
|
|
1500
|
+
// git submodule may fail — ignore
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
// 3. Scan for nested git repos (depth-limited, skips node_modules etc.)
|
|
1504
|
+
const MAX_DEPTH = 3;
|
|
1505
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'vendor', 'dist', 'build', '.next', '__pycache__']);
|
|
1506
|
+
const scan = async (dir, depth) => {
|
|
1507
|
+
if (depth > MAX_DEPTH)
|
|
1508
|
+
return;
|
|
1509
|
+
let names;
|
|
1510
|
+
try {
|
|
1511
|
+
names = await readdir(dir);
|
|
1512
|
+
}
|
|
1513
|
+
catch {
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
for (const name of names) {
|
|
1517
|
+
if (name.startsWith('.') || SKIP_DIRS.has(name))
|
|
1518
|
+
continue;
|
|
1519
|
+
const full = join(dir, name);
|
|
1520
|
+
try {
|
|
1521
|
+
const s = await stat(full);
|
|
1522
|
+
if (!s.isDirectory())
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
catch {
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1528
|
+
if (existsSync(join(full, '.git')) && !seen.has(full)) {
|
|
1529
|
+
seen.add(full);
|
|
1530
|
+
const branch = await this.getGitBranchQuiet(full);
|
|
1531
|
+
repos.push({
|
|
1532
|
+
path: full,
|
|
1533
|
+
name: full.slice(cwd.length + 1),
|
|
1534
|
+
currentBranch: branch || undefined,
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
await scan(full, depth + 1);
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
await scan(cwd, 0);
|
|
1541
|
+
}
|
|
1542
|
+
catch (error) {
|
|
1543
|
+
const response = createMessage('project:list-repos:response', { repos: [], error: error instanceof Error ? error.message : 'Failed to scan repos' });
|
|
1544
|
+
response.id = message.id;
|
|
1545
|
+
return response;
|
|
1546
|
+
}
|
|
1547
|
+
const response = createMessage('project:list-repos:response', { repos });
|
|
1548
|
+
response.id = message.id;
|
|
1549
|
+
return response;
|
|
1550
|
+
}
|
|
1151
1551
|
async getGitBranchQuiet(cwd) {
|
|
1152
1552
|
try {
|
|
1153
1553
|
const git = new GitOperations(cwd);
|