@sumicom/quicksave 0.6.0 → 0.6.2
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/asyncQueue.d.ts +7 -6
- package/dist/ai/asyncQueue.d.ts.map +1 -1
- package/dist/ai/asyncQueue.js +31 -21
- package/dist/ai/asyncQueue.js.map +1 -1
- package/dist/ai/cardBuilder.d.ts +22 -2
- package/dist/ai/cardBuilder.d.ts.map +1 -1
- package/dist/ai/cardBuilder.js +275 -30
- package/dist/ai/cardBuilder.js.map +1 -1
- package/dist/ai/claudeCliProvider.d.ts +21 -0
- package/dist/ai/claudeCliProvider.d.ts.map +1 -0
- package/dist/ai/claudeCliProvider.js +527 -0
- package/dist/ai/claudeCliProvider.js.map +1 -0
- 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 +20 -0
- package/dist/ai/claudeSdkProvider.d.ts.map +1 -0
- package/dist/ai/claudeSdkProvider.js +428 -0
- package/dist/ai/claudeSdkProvider.js.map +1 -0
- 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/commitSummary.d.ts +4 -0
- package/dist/ai/commitSummary.d.ts.map +1 -1
- package/dist/ai/commitSummary.js +80 -40
- package/dist/ai/commitSummary.js.map +1 -1
- package/dist/ai/debugLogger.d.ts +24 -0
- package/dist/ai/debugLogger.d.ts.map +1 -0
- package/dist/ai/debugLogger.js +55 -0
- package/dist/ai/debugLogger.js.map +1 -0
- package/dist/ai/provider.d.ts +57 -0
- package/dist/ai/provider.d.ts.map +1 -0
- package/dist/ai/provider.js +2 -0
- package/dist/ai/provider.js.map +1 -0
- package/dist/ai/sandboxMcp.d.ts +12 -0
- package/dist/ai/sandboxMcp.d.ts.map +1 -0
- package/dist/ai/sandboxMcp.js +12 -0
- package/dist/ai/sandboxMcp.js.map +1 -0
- package/dist/ai/sandboxMcpStdio.d.ts +16 -0
- package/dist/ai/sandboxMcpStdio.d.ts.map +1 -0
- package/dist/ai/sandboxMcpStdio.js +127 -0
- package/dist/ai/sandboxMcpStdio.js.map +1 -0
- package/dist/ai/sessionManager.d.ts +105 -0
- package/dist/ai/sessionManager.d.ts.map +1 -0
- package/dist/ai/sessionManager.js +716 -0
- package/dist/ai/sessionManager.js.map +1 -0
- package/dist/ai/sessionRegistry.d.ts +28 -0
- package/dist/ai/sessionRegistry.d.ts.map +1 -0
- package/dist/ai/sessionRegistry.js +148 -0
- package/dist/ai/sessionRegistry.js.map +1 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -0
- 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 +26 -1
- package/dist/git/operations.d.ts.map +1 -1
- package/dist/git/operations.js +83 -2
- package/dist/git/operations.js.map +1 -1
- package/dist/handlers/messageHandler.d.ts +24 -3
- package/dist/handlers/messageHandler.d.ts.map +1 -1
- package/dist/handlers/messageHandler.js +378 -24
- package/dist/handlers/messageHandler.js.map +1 -1
- package/dist/index.js +23 -40
- 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/ensureDaemon.d.ts.map +1 -1
- package/dist/service/ensureDaemon.js +15 -2
- package/dist/service/ensureDaemon.js.map +1 -1
- package/dist/service/ipcClient.js +1 -1
- package/dist/service/run.d.ts.map +1 -1
- package/dist/service/run.js +42 -15
- package/dist/service/run.js.map +1 -1
- package/dist/service/singleton.d.ts +4 -0
- package/dist/service/singleton.d.ts.map +1 -1
- package/dist/service/singleton.js +28 -2
- package/dist/service/singleton.js.map +1 -1
- package/package.json +7 -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/claudeCodeService.d.ts +0 -122
- package/dist/ai/claudeCodeService.d.ts.map +0 -1
- package/dist/ai/claudeCodeService.js +0 -869
- package/dist/ai/claudeCodeService.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,26 +1,36 @@
|
|
|
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
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { SessionManager } from '../ai/sessionManager.js';
|
|
6
|
+
import { ClaudeCodeProvider } from '../ai/claudeCodeProvider.js';
|
|
7
|
+
import { CodexSdkProvider } from '../ai/codexSdkProvider.js';
|
|
8
|
+
import { getSessionRegistry } from '../ai/sessionRegistry.js';
|
|
9
|
+
import { readdir, stat, readFile } from 'fs/promises';
|
|
7
10
|
import { join, dirname, basename } from 'path';
|
|
8
11
|
import { homedir } from 'os';
|
|
9
12
|
const VERSION_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000; // 12 hours
|
|
13
|
+
const CODEX_MODELS_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000; // 12 hours
|
|
10
14
|
export class MessageHandler {
|
|
11
15
|
repos;
|
|
12
|
-
agentVersion = '0.6.
|
|
16
|
+
agentVersion = '0.6.2';
|
|
13
17
|
defaultRepoPath;
|
|
14
18
|
clientRepos = new Map(); // peerAddress -> repoPath
|
|
15
19
|
repoLocks = new Map(); // repoPath -> peerAddress holding lock
|
|
16
20
|
availableRepos;
|
|
17
21
|
codingPaths = new Map(); // path -> CodingPath
|
|
18
22
|
aiService = null;
|
|
19
|
-
claudeService = new
|
|
23
|
+
claudeService = new SessionManager([
|
|
24
|
+
new ClaudeCodeProvider(),
|
|
25
|
+
new CodexSdkProvider(),
|
|
26
|
+
]);
|
|
20
27
|
latestVersionCache = null;
|
|
21
28
|
versionCheckInFlight = null;
|
|
29
|
+
codexModelsCache = null;
|
|
30
|
+
codexModelsCheckInFlight = null;
|
|
22
31
|
onPeerSubscribed;
|
|
23
32
|
onPeerUnsubscribed;
|
|
33
|
+
onHistoryUpdated;
|
|
24
34
|
productionBuild;
|
|
25
35
|
constructor(repos, _license, codingPaths, productionBuild = false) {
|
|
26
36
|
this.repos = new Map();
|
|
@@ -73,6 +83,59 @@ export class MessageHandler {
|
|
|
73
83
|
})();
|
|
74
84
|
return this.versionCheckInFlight;
|
|
75
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Fetch available Codex models from OpenAI /v1/models. Reads API key from
|
|
88
|
+
* ~/.codex/auth.json (ChatGPT OAuth token or OPENAI_API_KEY). Cached for 12h
|
|
89
|
+
* with concurrent-request dedup — same pattern as checkLatestVersion.
|
|
90
|
+
*/
|
|
91
|
+
async fetchCodexModels(force = false) {
|
|
92
|
+
if (!force && this.codexModelsCache &&
|
|
93
|
+
Date.now() - this.codexModelsCache.checkedAt < CODEX_MODELS_CHECK_INTERVAL_MS) {
|
|
94
|
+
return this.codexModelsCache.models;
|
|
95
|
+
}
|
|
96
|
+
if (this.codexModelsCheckInFlight)
|
|
97
|
+
return this.codexModelsCheckInFlight;
|
|
98
|
+
this.codexModelsCheckInFlight = (async () => {
|
|
99
|
+
try {
|
|
100
|
+
// Resolve API key: env var first, then ~/.codex/auth.json
|
|
101
|
+
let apiKey = process.env.OPENAI_API_KEY;
|
|
102
|
+
if (!apiKey) {
|
|
103
|
+
try {
|
|
104
|
+
const authPath = join(homedir(), '.codex', 'auth.json');
|
|
105
|
+
const raw = await readFile(authPath, 'utf-8');
|
|
106
|
+
const auth = JSON.parse(raw);
|
|
107
|
+
apiKey = auth.OPENAI_API_KEY || auth.tokens?.access_token;
|
|
108
|
+
}
|
|
109
|
+
catch { /* no auth file */ }
|
|
110
|
+
}
|
|
111
|
+
if (!apiKey)
|
|
112
|
+
return null;
|
|
113
|
+
const res = await fetch('https://api.openai.com/v1/models', {
|
|
114
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
115
|
+
signal: AbortSignal.timeout(10_000),
|
|
116
|
+
});
|
|
117
|
+
if (!res.ok)
|
|
118
|
+
return null;
|
|
119
|
+
const data = await res.json();
|
|
120
|
+
if (!Array.isArray(data.data))
|
|
121
|
+
return null;
|
|
122
|
+
// Filter to chat-capable models (gpt-*, o*) and sort descending
|
|
123
|
+
const models = data.data
|
|
124
|
+
.filter((m) => /^(gpt-|o\d)/.test(m.id))
|
|
125
|
+
.map((m) => ({ id: m.id, name: m.id }))
|
|
126
|
+
.sort((a, b) => b.id.localeCompare(a.id));
|
|
127
|
+
this.codexModelsCache = { models, checkedAt: Date.now() };
|
|
128
|
+
return models;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
this.codexModelsCheckInFlight = null;
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
137
|
+
return this.codexModelsCheckInFlight;
|
|
138
|
+
}
|
|
76
139
|
addRepo(repo) {
|
|
77
140
|
if (this.repos.has(repo.path))
|
|
78
141
|
return;
|
|
@@ -177,6 +240,12 @@ export class MessageHandler {
|
|
|
177
240
|
return this.handleDiscard(message, peerAddress);
|
|
178
241
|
case 'git:untrack':
|
|
179
242
|
return this.handleUntrack(message, peerAddress);
|
|
243
|
+
case 'git:submodules':
|
|
244
|
+
return this.handleSubmodules(message, peerAddress);
|
|
245
|
+
case 'git:config-get':
|
|
246
|
+
return this.handleGitConfigGet(message, peerAddress);
|
|
247
|
+
case 'git:config-set':
|
|
248
|
+
return this.handleGitConfigSet(message, peerAddress);
|
|
180
249
|
case 'git:gitignore-add':
|
|
181
250
|
return this.handleGitignoreAdd(message, peerAddress);
|
|
182
251
|
case 'git:gitignore-read':
|
|
@@ -197,14 +266,24 @@ export class MessageHandler {
|
|
|
197
266
|
return this.handleBrowseDirectory(message);
|
|
198
267
|
case 'agent:add-repo':
|
|
199
268
|
return this.handleAddRepo(message);
|
|
269
|
+
case 'agent:remove-repo':
|
|
270
|
+
return this.handleRemoveRepo(message);
|
|
271
|
+
case 'agent:clone-repo':
|
|
272
|
+
return this.handleCloneRepo(message);
|
|
200
273
|
case 'agent:list-coding-paths':
|
|
201
274
|
return this.handleListCodingPaths(message);
|
|
202
275
|
case 'agent:add-coding-path':
|
|
203
276
|
return this.handleAddCodingPath(message);
|
|
277
|
+
case 'agent:remove-coding-path':
|
|
278
|
+
return this.handleRemoveCodingPath(message);
|
|
204
279
|
case 'agent:check-update':
|
|
205
280
|
return this.handleAgentCheckUpdate(message);
|
|
206
281
|
case 'agent:update':
|
|
207
282
|
return this.handleAgentUpdate(message);
|
|
283
|
+
case 'agent:restart':
|
|
284
|
+
return this.handleAgentRestart(message);
|
|
285
|
+
case 'codex:list-models':
|
|
286
|
+
return this.handleCodexListModels(message);
|
|
208
287
|
// Claude Code SDK
|
|
209
288
|
case 'claude:list-sessions':
|
|
210
289
|
return this.handleClaudeListSessions(message, peerAddress);
|
|
@@ -237,6 +316,12 @@ export class MessageHandler {
|
|
|
237
316
|
return this.handleGetSessionConfig(message);
|
|
238
317
|
case 'session:set-config':
|
|
239
318
|
return this.handleSetSessionConfig(message);
|
|
319
|
+
case 'session:list-history':
|
|
320
|
+
return this.handleListHistory(message, peerAddress);
|
|
321
|
+
case 'session:update-history':
|
|
322
|
+
return this.handleUpdateHistory(message);
|
|
323
|
+
case 'session:delete-history':
|
|
324
|
+
return this.handleDeleteHistory(message);
|
|
240
325
|
default:
|
|
241
326
|
return this.createErrorResponse(message.id, 'UNKNOWN_MESSAGE_TYPE', `Unknown message type: ${message.type}`);
|
|
242
327
|
}
|
|
@@ -247,8 +332,9 @@ export class MessageHandler {
|
|
|
247
332
|
}
|
|
248
333
|
}
|
|
249
334
|
handleHandshake(message, peerAddress) {
|
|
250
|
-
// Fire-and-forget: check npm registry
|
|
335
|
+
// Fire-and-forget: check npm registry + OpenAI models (12h dedup each)
|
|
251
336
|
this.checkLatestVersion().catch(() => { });
|
|
337
|
+
this.fetchCodexModels().catch(() => { });
|
|
252
338
|
const response = createMessage('handshake:ack', {
|
|
253
339
|
success: true,
|
|
254
340
|
agentVersion: this.agentVersion,
|
|
@@ -258,6 +344,7 @@ export class MessageHandler {
|
|
|
258
344
|
preferences: this.claudeService.getPreferences(),
|
|
259
345
|
latestVersion: this.latestVersionCache?.version,
|
|
260
346
|
devBuild: !this.productionBuild || undefined,
|
|
347
|
+
codexModels: this.codexModelsCache?.models,
|
|
261
348
|
});
|
|
262
349
|
response.id = message.id;
|
|
263
350
|
return response;
|
|
@@ -398,8 +485,8 @@ export class MessageHandler {
|
|
|
398
485
|
return response;
|
|
399
486
|
}
|
|
400
487
|
try {
|
|
401
|
-
const { message: commitMessage, description } = message.payload;
|
|
402
|
-
const hash = await this.getGit(peerAddress).commit(commitMessage, description);
|
|
488
|
+
const { message: commitMessage, description, attribution } = message.payload;
|
|
489
|
+
const hash = await this.getGit(peerAddress).commit(commitMessage, description, attribution ?? true);
|
|
403
490
|
const response = createMessage('git:commit:response', {
|
|
404
491
|
success: true,
|
|
405
492
|
hash,
|
|
@@ -518,6 +605,45 @@ export class MessageHandler {
|
|
|
518
605
|
this.releaseRepoLock(repoPath, peerAddress);
|
|
519
606
|
}
|
|
520
607
|
}
|
|
608
|
+
async handleSubmodules(message, peerAddress) {
|
|
609
|
+
const git = this.getGit(peerAddress);
|
|
610
|
+
const subs = await git.getSubmodules();
|
|
611
|
+
// Auto-register submodule paths so switch-repo works
|
|
612
|
+
for (const sub of subs) {
|
|
613
|
+
if (!this.repos.has(sub.path)) {
|
|
614
|
+
this.repos.set(sub.path, new GitOperations(sub.path));
|
|
615
|
+
this.availableRepos.push({ path: sub.path, name: sub.name, currentBranch: sub.branch });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const response = createMessage('git:submodules:response', {
|
|
619
|
+
submodules: subs.map((s) => ({ name: s.name, path: s.path, branch: s.branch })),
|
|
620
|
+
});
|
|
621
|
+
response.id = message.id;
|
|
622
|
+
return response;
|
|
623
|
+
}
|
|
624
|
+
async handleGitConfigGet(message, peerAddress) {
|
|
625
|
+
const identity = await this.getGit(peerAddress).getIdentity();
|
|
626
|
+
const response = createMessage('git:config-get:response', identity);
|
|
627
|
+
response.id = message.id;
|
|
628
|
+
return response;
|
|
629
|
+
}
|
|
630
|
+
async handleGitConfigSet(message, peerAddress) {
|
|
631
|
+
try {
|
|
632
|
+
const { name, email } = message.payload;
|
|
633
|
+
await this.getGit(peerAddress).setIdentity(name, email);
|
|
634
|
+
const response = createMessage('git:config-set:response', { success: true });
|
|
635
|
+
response.id = message.id;
|
|
636
|
+
return response;
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
const response = createMessage('git:config-set:response', {
|
|
640
|
+
success: false,
|
|
641
|
+
error: error instanceof Error ? error.message : 'Failed to set git identity',
|
|
642
|
+
});
|
|
643
|
+
response.id = message.id;
|
|
644
|
+
return response;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
521
647
|
async handleGitignoreAdd(message, peerAddress) {
|
|
522
648
|
const repoPath = this.getClientRepoPath(peerAddress);
|
|
523
649
|
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
@@ -603,13 +729,24 @@ export class MessageHandler {
|
|
|
603
729
|
response.id = message.id;
|
|
604
730
|
return response;
|
|
605
731
|
}
|
|
606
|
-
// Collect diffs
|
|
607
|
-
const diffs = await Promise.all(
|
|
732
|
+
// Collect diffs and context in parallel
|
|
733
|
+
const [diffs, recentLog, branchInfo, conventions] = await Promise.all([
|
|
734
|
+
Promise.all(status.staged.map((file) => git.getDiff(file.path, true))),
|
|
735
|
+
git.getLog(10).catch(() => []),
|
|
736
|
+
git.getBranches().catch(() => ({ current: '' })),
|
|
737
|
+
git.readCommitConventions().catch(() => undefined),
|
|
738
|
+
]);
|
|
739
|
+
const recentCommits = recentLog.map((c) => c.message);
|
|
740
|
+
const branchName = branchInfo.current;
|
|
608
741
|
// Generate summary (uses internal queue and cache)
|
|
609
742
|
const result = await aiService.generateSummary({
|
|
610
743
|
diffs,
|
|
611
744
|
context: message.payload.context,
|
|
612
745
|
model: message.payload.model,
|
|
746
|
+
recentCommits: recentCommits.length > 0 ? recentCommits : undefined,
|
|
747
|
+
branchName: branchName || undefined,
|
|
748
|
+
conventions,
|
|
749
|
+
attribution: message.payload.attribution,
|
|
613
750
|
});
|
|
614
751
|
const response = createMessage('ai:generate-commit-summary:response', {
|
|
615
752
|
success: true,
|
|
@@ -808,6 +945,81 @@ export class MessageHandler {
|
|
|
808
945
|
return response;
|
|
809
946
|
}
|
|
810
947
|
}
|
|
948
|
+
handleRemoveRepo(message) {
|
|
949
|
+
const { path: repoPath } = message.payload;
|
|
950
|
+
if (!this.repos.has(repoPath)) {
|
|
951
|
+
const response = createMessage('agent:remove-repo:response', {
|
|
952
|
+
success: false,
|
|
953
|
+
error: 'Repository not found',
|
|
954
|
+
});
|
|
955
|
+
response.id = message.id;
|
|
956
|
+
return response;
|
|
957
|
+
}
|
|
958
|
+
this.removeRepo(repoPath);
|
|
959
|
+
removeManagedRepo(repoPath);
|
|
960
|
+
const response = createMessage('agent:remove-repo:response', {
|
|
961
|
+
success: true,
|
|
962
|
+
});
|
|
963
|
+
response.id = message.id;
|
|
964
|
+
return response;
|
|
965
|
+
}
|
|
966
|
+
async handleCloneRepo(message) {
|
|
967
|
+
const { url, targetDir } = message.payload;
|
|
968
|
+
if (!url || !url.trim()) {
|
|
969
|
+
const response = createMessage('agent:clone-repo:response', {
|
|
970
|
+
success: false,
|
|
971
|
+
error: 'Repository URL is required',
|
|
972
|
+
});
|
|
973
|
+
response.id = message.id;
|
|
974
|
+
return response;
|
|
975
|
+
}
|
|
976
|
+
try {
|
|
977
|
+
const { execFile } = await import('child_process');
|
|
978
|
+
const { promisify } = await import('util');
|
|
979
|
+
const execFileAsync = promisify(execFile);
|
|
980
|
+
// Clone into the target directory
|
|
981
|
+
await execFileAsync('git', ['clone', url.trim(), targetDir], {
|
|
982
|
+
timeout: 120_000,
|
|
983
|
+
});
|
|
984
|
+
// Verify it's a valid git repo and add it
|
|
985
|
+
const git = new GitOperations(targetDir);
|
|
986
|
+
const { current } = await git.getBranches();
|
|
987
|
+
const rootPath = await git.getGitRoot();
|
|
988
|
+
// Check if already added (by resolved root path)
|
|
989
|
+
if (this.repos.has(rootPath)) {
|
|
990
|
+
const response = createMessage('agent:clone-repo:response', {
|
|
991
|
+
success: false,
|
|
992
|
+
error: 'Repository already added',
|
|
993
|
+
});
|
|
994
|
+
response.id = message.id;
|
|
995
|
+
return response;
|
|
996
|
+
}
|
|
997
|
+
// Add to maps and persist
|
|
998
|
+
this.repos.set(rootPath, git);
|
|
999
|
+
const newRepo = {
|
|
1000
|
+
path: rootPath,
|
|
1001
|
+
name: basename(rootPath),
|
|
1002
|
+
currentBranch: current,
|
|
1003
|
+
};
|
|
1004
|
+
this.availableRepos.push(newRepo);
|
|
1005
|
+
addManagedRepo(rootPath);
|
|
1006
|
+
const response = createMessage('agent:clone-repo:response', {
|
|
1007
|
+
success: true,
|
|
1008
|
+
repo: newRepo,
|
|
1009
|
+
clonedPath: rootPath,
|
|
1010
|
+
});
|
|
1011
|
+
response.id = message.id;
|
|
1012
|
+
return response;
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
const response = createMessage('agent:clone-repo:response', {
|
|
1016
|
+
success: false,
|
|
1017
|
+
error: error instanceof Error ? error.message : 'Failed to clone repository',
|
|
1018
|
+
});
|
|
1019
|
+
response.id = message.id;
|
|
1020
|
+
return response;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
811
1023
|
// ============================================================================
|
|
812
1024
|
// Coding Path Handlers
|
|
813
1025
|
// ============================================================================
|
|
@@ -844,6 +1056,19 @@ export class MessageHandler {
|
|
|
844
1056
|
return response;
|
|
845
1057
|
}
|
|
846
1058
|
}
|
|
1059
|
+
handleRemoveCodingPath(message) {
|
|
1060
|
+
const { path: codingPath } = message.payload;
|
|
1061
|
+
if (!this.codingPaths.has(codingPath)) {
|
|
1062
|
+
const response = createMessage('agent:remove-coding-path:response', { success: false, error: 'Coding path not found' });
|
|
1063
|
+
response.id = message.id;
|
|
1064
|
+
return response;
|
|
1065
|
+
}
|
|
1066
|
+
this.codingPaths.delete(codingPath);
|
|
1067
|
+
removeManagedCodingPath(codingPath);
|
|
1068
|
+
const response = createMessage('agent:remove-coding-path:response', { success: true });
|
|
1069
|
+
response.id = message.id;
|
|
1070
|
+
return response;
|
|
1071
|
+
}
|
|
847
1072
|
// ============================================================================
|
|
848
1073
|
// Agent Self-Update
|
|
849
1074
|
// ============================================================================
|
|
@@ -923,6 +1148,50 @@ export class MessageHandler {
|
|
|
923
1148
|
return response;
|
|
924
1149
|
}
|
|
925
1150
|
}
|
|
1151
|
+
async handleAgentRestart(message) {
|
|
1152
|
+
if (this.productionBuild) {
|
|
1153
|
+
const response = createMessage('agent:restart:response', { success: false, error: 'Restart is only available for dev builds' });
|
|
1154
|
+
response.id = message.id;
|
|
1155
|
+
return response;
|
|
1156
|
+
}
|
|
1157
|
+
try {
|
|
1158
|
+
const { spawn } = await import('child_process');
|
|
1159
|
+
const { fileURLToPath } = await import('url');
|
|
1160
|
+
const { resolve: resolvePath } = await import('path');
|
|
1161
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
1162
|
+
const isTs = thisFile.endsWith('.ts');
|
|
1163
|
+
const entryPath = resolvePath(dirname(thisFile), isTs ? '../index.ts' : '../../index.js');
|
|
1164
|
+
const agentRoot = resolvePath(dirname(thisFile), isTs ? '..' : '..');
|
|
1165
|
+
const node = process.execPath;
|
|
1166
|
+
const execArgv = isTs ? ['--import', 'tsx'] : [];
|
|
1167
|
+
spawn(node, [...execArgv, entryPath, '--restart'], {
|
|
1168
|
+
detached: true,
|
|
1169
|
+
stdio: 'ignore',
|
|
1170
|
+
cwd: agentRoot,
|
|
1171
|
+
}).unref();
|
|
1172
|
+
const response = createMessage('agent:restart:response', { success: true });
|
|
1173
|
+
response.id = message.id;
|
|
1174
|
+
return response;
|
|
1175
|
+
}
|
|
1176
|
+
catch (error) {
|
|
1177
|
+
const response = createMessage('agent:restart:response', { success: false, error: error instanceof Error ? error.message : 'Failed to restart agent' });
|
|
1178
|
+
response.id = message.id;
|
|
1179
|
+
return response;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
async handleCodexListModels(message) {
|
|
1183
|
+
try {
|
|
1184
|
+
const models = await this.fetchCodexModels(/* force */ true);
|
|
1185
|
+
const response = createMessage('codex:list-models:response', { models: models ?? [] });
|
|
1186
|
+
response.id = message.id;
|
|
1187
|
+
return response;
|
|
1188
|
+
}
|
|
1189
|
+
catch (error) {
|
|
1190
|
+
const response = createMessage('codex:list-models:response', { models: [], error: error instanceof Error ? error.message : 'Failed to fetch models' });
|
|
1191
|
+
response.id = message.id;
|
|
1192
|
+
return response;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
926
1195
|
// ============================================================================
|
|
927
1196
|
// Claude Code SDK Handlers
|
|
928
1197
|
// ============================================================================
|
|
@@ -941,59 +1210,105 @@ export class MessageHandler {
|
|
|
941
1210
|
}
|
|
942
1211
|
}
|
|
943
1212
|
async handleClaudeStart(message, peerAddress) {
|
|
944
|
-
const { prompt, cwd: payloadCwd, allowedTools, systemPrompt, model, permissionMode } = message.payload;
|
|
1213
|
+
const { prompt, cwd: payloadCwd, agent, allowedTools, systemPrompt, model, permissionMode, sandboxed } = message.payload;
|
|
1214
|
+
const legacyProvider = message.payload.provider;
|
|
945
1215
|
const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
|
|
946
1216
|
const streamId = generateMessageId();
|
|
947
|
-
|
|
1217
|
+
const resolvedAgent = agent ?? (legacyProvider === 'codex-mcp' ? 'codex' : legacyProvider ? 'claude-code' : undefined);
|
|
1218
|
+
console.log(`[agent:start] agent=${resolvedAgent ?? 'default'} cwd=${cwd} prompt=${prompt.slice(0, 80)}${sandboxed ? ' [sandboxed]' : ''}`);
|
|
948
1219
|
try {
|
|
949
1220
|
const sessionId = await this.claudeService.startSession({
|
|
950
|
-
prompt,
|
|
1221
|
+
prompt,
|
|
1222
|
+
cwd,
|
|
1223
|
+
streamId,
|
|
1224
|
+
agent: resolvedAgent,
|
|
1225
|
+
allowedTools,
|
|
1226
|
+
systemPrompt,
|
|
1227
|
+
model,
|
|
1228
|
+
permissionMode,
|
|
1229
|
+
sandboxed,
|
|
951
1230
|
});
|
|
952
|
-
console.log(`[
|
|
1231
|
+
console.log(`[agent:start] session created: ${sessionId} agent=${resolvedAgent ?? 'default'}`);
|
|
953
1232
|
this.onPeerSubscribed?.(peerAddress, sessionId);
|
|
1233
|
+
// Register in session history
|
|
1234
|
+
const now = Date.now();
|
|
1235
|
+
const registry = getSessionRegistry();
|
|
1236
|
+
const gitBranch = await this.getGitBranchQuiet(cwd);
|
|
1237
|
+
const actualAgent = this.claudeService.getSessionAgent(sessionId, cwd);
|
|
1238
|
+
registry.upsertEntry({
|
|
1239
|
+
sessionId, cwd,
|
|
1240
|
+
agent: actualAgent,
|
|
1241
|
+
repoName: basename(cwd),
|
|
1242
|
+
gitBranch,
|
|
1243
|
+
firstPrompt: prompt.slice(0, 100),
|
|
1244
|
+
createdAt: now,
|
|
1245
|
+
lastAccessedAt: now,
|
|
1246
|
+
permissionMode,
|
|
1247
|
+
sandboxed: sandboxed || undefined,
|
|
1248
|
+
});
|
|
1249
|
+
this.onHistoryUpdated?.(cwd, registry.getEntry(cwd, sessionId), 'upsert');
|
|
954
1250
|
const response = createMessage('claude:start:response', { success: true, sessionId, streamId });
|
|
955
1251
|
response.id = message.id;
|
|
956
1252
|
return response;
|
|
957
1253
|
}
|
|
958
1254
|
catch (error) {
|
|
959
|
-
console.error(`[
|
|
1255
|
+
console.error(`[agent:start] error:`, error);
|
|
960
1256
|
const response = createMessage('claude:start:response', { success: false, error: error instanceof Error ? error.message : 'Failed to start session' });
|
|
961
1257
|
response.id = message.id;
|
|
962
1258
|
return response;
|
|
963
1259
|
}
|
|
964
1260
|
}
|
|
965
1261
|
async handleClaudeResume(message, peerAddress) {
|
|
966
|
-
const { sessionId: requestedId, prompt, cwd: payloadCwd } = message.payload;
|
|
1262
|
+
const { sessionId: requestedId, prompt, cwd: payloadCwd, agent } = message.payload;
|
|
1263
|
+
const legacyProvider = message.payload.provider;
|
|
967
1264
|
const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
|
|
968
1265
|
const streamId = generateMessageId();
|
|
969
|
-
|
|
1266
|
+
const resolvedAgent = agent ?? (legacyProvider === 'codex-mcp' ? 'codex' : legacyProvider ? 'claude-code' : undefined);
|
|
1267
|
+
console.log(`[agent:resume] session=${requestedId} agent=${resolvedAgent ?? 'stored'} cwd=${cwd} prompt=${prompt.slice(0, 80)}`);
|
|
970
1268
|
// Subscribe before resumeSession starts streaming — hot resume emits events
|
|
971
1269
|
// immediately (user_message, then assistant turns), so we must be subscribed
|
|
972
1270
|
// before session.send() fires, not after resumeSession() returns.
|
|
973
1271
|
this.onPeerSubscribed?.(peerAddress, requestedId);
|
|
974
1272
|
try {
|
|
975
1273
|
const actualSessionId = await this.claudeService.resumeSession({
|
|
976
|
-
sessionId: requestedId,
|
|
1274
|
+
sessionId: requestedId,
|
|
1275
|
+
prompt,
|
|
1276
|
+
cwd,
|
|
1277
|
+
streamId,
|
|
1278
|
+
agent: resolvedAgent,
|
|
977
1279
|
});
|
|
978
|
-
console.log(`[
|
|
1280
|
+
console.log(`[agent:resume] session resumed: ${actualSessionId}`);
|
|
979
1281
|
// Re-subscribe with actual session ID (cold resume may return a different ID)
|
|
980
1282
|
if (actualSessionId !== requestedId) {
|
|
981
1283
|
this.onPeerSubscribed?.(peerAddress, actualSessionId);
|
|
982
1284
|
}
|
|
1285
|
+
// Update session history
|
|
1286
|
+
const registry = getSessionRegistry();
|
|
1287
|
+
const existing = registry.getEntry(cwd, requestedId) ?? registry.getEntry(cwd, actualSessionId);
|
|
1288
|
+
const gitBranch = await this.getGitBranchQuiet(cwd);
|
|
1289
|
+
const actualAgent = this.claudeService.getSessionAgent(actualSessionId, cwd);
|
|
1290
|
+
registry.upsertEntry({
|
|
1291
|
+
...(existing ?? { sessionId: actualSessionId, cwd, repoName: basename(cwd), createdAt: Date.now() }),
|
|
1292
|
+
sessionId: actualSessionId,
|
|
1293
|
+
agent: actualAgent,
|
|
1294
|
+
lastAccessedAt: Date.now(),
|
|
1295
|
+
gitBranch,
|
|
1296
|
+
});
|
|
1297
|
+
this.onHistoryUpdated?.(cwd, registry.getEntry(cwd, actualSessionId), 'upsert');
|
|
983
1298
|
const response = createMessage('claude:resume:response', { success: true, sessionId: actualSessionId, streamId });
|
|
984
1299
|
response.id = message.id;
|
|
985
1300
|
return response;
|
|
986
1301
|
}
|
|
987
1302
|
catch (error) {
|
|
988
|
-
console.error(`[
|
|
1303
|
+
console.error(`[agent:resume] error:`, error);
|
|
989
1304
|
const response = createMessage('claude:resume:response', { success: false, error: error instanceof Error ? error.message : 'Failed to resume session' });
|
|
990
1305
|
response.id = message.id;
|
|
991
1306
|
return response;
|
|
992
1307
|
}
|
|
993
1308
|
}
|
|
994
|
-
handleClaudeCancel(message) {
|
|
1309
|
+
async handleClaudeCancel(message) {
|
|
995
1310
|
const { sessionId } = message.payload;
|
|
996
|
-
const success = this.claudeService.cancelSession(sessionId);
|
|
1311
|
+
const success = await this.claudeService.cancelSession(sessionId);
|
|
997
1312
|
const response = createMessage('claude:cancel:response', { success, error: success ? undefined : 'Session not found or already ended' });
|
|
998
1313
|
response.id = message.id;
|
|
999
1314
|
return response;
|
|
@@ -1001,14 +1316,14 @@ export class MessageHandler {
|
|
|
1001
1316
|
handleClaudeClose(message) {
|
|
1002
1317
|
const { sessionId } = message.payload;
|
|
1003
1318
|
const success = this.claudeService.closeSession(sessionId);
|
|
1004
|
-
console.log(`[
|
|
1319
|
+
console.log(`[agent:close] session=${sessionId} success=${success}`);
|
|
1005
1320
|
const response = createMessage('claude:close:response', { success, error: success ? undefined : 'Session not found or already closed' });
|
|
1006
1321
|
response.id = message.id;
|
|
1007
1322
|
return response;
|
|
1008
1323
|
}
|
|
1009
1324
|
handleClaudeUserInputResponse(message) {
|
|
1010
1325
|
const resolved = this.claudeService.resolveUserInput(message.payload);
|
|
1011
|
-
console.log(`[
|
|
1326
|
+
console.log(`[agent:user-input-response] requestId=${message.payload.requestId} action=${message.payload.action} resolved=${resolved}`);
|
|
1012
1327
|
// No dedicated response type — just acknowledge
|
|
1013
1328
|
const response = createMessage('claude:user-input-response', { success: resolved });
|
|
1014
1329
|
response.id = message.id;
|
|
@@ -1074,6 +1389,45 @@ export class MessageHandler {
|
|
|
1074
1389
|
response.id = message.id;
|
|
1075
1390
|
return response;
|
|
1076
1391
|
}
|
|
1392
|
+
// ── Session Registry (History) ──────────────────────────────────────
|
|
1393
|
+
handleListHistory(message, peerAddress) {
|
|
1394
|
+
const cwd = message.payload.cwd || this.getClientRepoPath(peerAddress);
|
|
1395
|
+
const entries = getSessionRegistry().getEntriesForProject(cwd);
|
|
1396
|
+
const response = createMessage('session:list-history:response', { entries });
|
|
1397
|
+
response.id = message.id;
|
|
1398
|
+
return response;
|
|
1399
|
+
}
|
|
1400
|
+
handleUpdateHistory(message) {
|
|
1401
|
+
const { sessionId, cwd, updates } = message.payload;
|
|
1402
|
+
const entry = getSessionRegistry().updateEntry(cwd, sessionId, updates);
|
|
1403
|
+
const response = createMessage('session:update-history:response', entry ? { success: true, entry } : { success: false, error: 'Entry not found' });
|
|
1404
|
+
response.id = message.id;
|
|
1405
|
+
if (entry) {
|
|
1406
|
+
this.onHistoryUpdated?.(cwd, entry, 'upsert');
|
|
1407
|
+
}
|
|
1408
|
+
return response;
|
|
1409
|
+
}
|
|
1410
|
+
handleDeleteHistory(message) {
|
|
1411
|
+
const { sessionId, cwd } = message.payload;
|
|
1412
|
+
const entry = getSessionRegistry().getEntry(cwd, sessionId);
|
|
1413
|
+
const success = getSessionRegistry().deleteEntry(cwd, sessionId);
|
|
1414
|
+
const response = createMessage('session:delete-history:response', { success, error: success ? undefined : 'Entry not found' });
|
|
1415
|
+
response.id = message.id;
|
|
1416
|
+
if (success && entry) {
|
|
1417
|
+
this.onHistoryUpdated?.(cwd, entry, 'delete');
|
|
1418
|
+
}
|
|
1419
|
+
return response;
|
|
1420
|
+
}
|
|
1421
|
+
async getGitBranchQuiet(cwd) {
|
|
1422
|
+
try {
|
|
1423
|
+
const git = new GitOperations(cwd);
|
|
1424
|
+
const { current } = await git.getBranches();
|
|
1425
|
+
return current || undefined;
|
|
1426
|
+
}
|
|
1427
|
+
catch {
|
|
1428
|
+
return undefined;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1077
1431
|
createErrorResponse(id, code, message) {
|
|
1078
1432
|
const response = createMessage('error', { code, message });
|
|
1079
1433
|
response.id = id;
|