@sumicom/quicksave 0.3.1 → 0.5.0
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/claudeCodeService.d.ts +89 -0
- package/dist/ai/claudeCodeService.d.ts.map +1 -0
- package/dist/ai/claudeCodeService.js +602 -0
- package/dist/ai/claudeCodeService.js.map +1 -0
- package/dist/ai/sessionStore.d.ts +31 -0
- package/dist/ai/sessionStore.d.ts.map +1 -0
- package/dist/ai/sessionStore.js +72 -0
- package/dist/ai/sessionStore.js.map +1 -0
- package/dist/ai/sessionStore.test.d.ts +2 -0
- package/dist/ai/sessionStore.test.d.ts.map +1 -0
- package/dist/ai/sessionStore.test.js +99 -0
- package/dist/ai/sessionStore.test.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +39 -0
- package/dist/config.js.map +1 -1
- package/dist/connection/connection.d.ts +16 -17
- package/dist/connection/connection.d.ts.map +1 -1
- package/dist/connection/connection.js +72 -57
- package/dist/connection/connection.js.map +1 -1
- package/dist/connection/{signaling.d.ts → relay.d.ts} +3 -5
- package/dist/connection/relay.d.ts.map +1 -0
- package/dist/connection/{signaling.js → relay.js} +18 -19
- package/dist/connection/relay.js.map +1 -0
- package/dist/git/operations.d.ts +27 -1
- package/dist/git/operations.d.ts.map +1 -1
- package/dist/git/operations.js +113 -4
- package/dist/git/operations.js.map +1 -1
- package/dist/git/operations.test.js +69 -1
- package/dist/git/operations.test.js.map +1 -1
- package/dist/handlers/messageHandler.d.ts +31 -4
- package/dist/handlers/messageHandler.d.ts.map +1 -1
- package/dist/handlers/messageHandler.js +491 -62
- package/dist/handlers/messageHandler.js.map +1 -1
- package/dist/handlers/messageHandler.test.js +130 -2
- package/dist/handlers/messageHandler.test.js.map +1 -1
- package/dist/index.js +229 -89
- package/dist/index.js.map +1 -1
- package/dist/service/ensureDaemon.d.ts +23 -0
- package/dist/service/ensureDaemon.d.ts.map +1 -0
- package/dist/service/ensureDaemon.js +138 -0
- package/dist/service/ensureDaemon.js.map +1 -0
- package/dist/service/index.d.ts +8 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +8 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/ipc.test.d.ts +2 -0
- package/dist/service/ipc.test.d.ts.map +1 -0
- package/dist/service/ipc.test.js +140 -0
- package/dist/service/ipc.test.js.map +1 -0
- package/dist/service/ipcClient.d.ts +36 -0
- package/dist/service/ipcClient.d.ts.map +1 -0
- package/dist/service/ipcClient.js +167 -0
- package/dist/service/ipcClient.js.map +1 -0
- package/dist/service/ipcServer.d.ts +53 -0
- package/dist/service/ipcServer.d.ts.map +1 -0
- package/dist/service/ipcServer.js +245 -0
- package/dist/service/ipcServer.js.map +1 -0
- package/dist/service/run.d.ts +13 -0
- package/dist/service/run.d.ts.map +1 -0
- package/dist/service/run.js +292 -0
- package/dist/service/run.js.map +1 -0
- package/dist/service/singleton.d.ts +31 -0
- package/dist/service/singleton.d.ts.map +1 -0
- package/dist/service/singleton.js +96 -0
- package/dist/service/singleton.js.map +1 -0
- package/dist/service/singleton.test.d.ts +2 -0
- package/dist/service/singleton.test.d.ts.map +1 -0
- package/dist/service/singleton.test.js +39 -0
- package/dist/service/singleton.test.js.map +1 -0
- package/dist/service/stateStore.d.ts +11 -0
- package/dist/service/stateStore.d.ts.map +1 -0
- package/dist/service/stateStore.js +35 -0
- package/dist/service/stateStore.js.map +1 -0
- package/dist/service/types.d.ts +111 -0
- package/dist/service/types.d.ts.map +1 -0
- package/dist/service/types.js +76 -0
- package/dist/service/types.js.map +1 -0
- package/package.json +3 -2
- package/dist/connection/signaling.d.ts.map +0 -1
- package/dist/connection/signaling.js.map +0 -1
|
@@ -1,27 +1,93 @@
|
|
|
1
|
-
import { createMessage, } from '@sumicom/quicksave-shared';
|
|
1
|
+
import { createMessage, generateMessageId, } from '@sumicom/quicksave-shared';
|
|
2
2
|
import { GitOperations } from '../git/operations.js';
|
|
3
|
-
import { getAnthropicApiKey, setAnthropicApiKey, hasAnthropicApiKey } from '../config.js';
|
|
3
|
+
import { getAnthropicApiKey, setAnthropicApiKey, hasAnthropicApiKey, addManagedRepo, addManagedCodingPath } from '../config.js';
|
|
4
4
|
import { CommitSummaryService } from '../ai/commitSummary.js';
|
|
5
|
+
import { ClaudeCodeService } from '../ai/claudeCodeService.js';
|
|
5
6
|
import { readdir, stat } from 'fs/promises';
|
|
6
7
|
import { join, dirname, basename } from 'path';
|
|
7
8
|
import { homedir } from 'os';
|
|
8
9
|
export class MessageHandler {
|
|
9
10
|
repos;
|
|
10
|
-
agentVersion = '0.
|
|
11
|
-
|
|
11
|
+
agentVersion = '0.5.0';
|
|
12
|
+
defaultRepoPath;
|
|
13
|
+
clientRepos = new Map(); // peerAddress -> repoPath
|
|
14
|
+
repoLocks = new Map(); // repoPath -> peerAddress holding lock
|
|
12
15
|
availableRepos;
|
|
16
|
+
codingPaths = new Map(); // path -> CodingPath
|
|
13
17
|
aiService = null;
|
|
14
|
-
|
|
18
|
+
claudeService = new ClaudeCodeService();
|
|
19
|
+
constructor(repos, _license, codingPaths) {
|
|
15
20
|
this.repos = new Map();
|
|
16
21
|
for (const repo of repos) {
|
|
17
22
|
this.repos.set(repo.path, new GitOperations(repo.path));
|
|
18
23
|
}
|
|
19
24
|
this.availableRepos = repos;
|
|
20
|
-
this.
|
|
21
|
-
//
|
|
25
|
+
this.defaultRepoPath = repos.length > 0 ? repos[0].path : '';
|
|
26
|
+
// Load explicit coding paths only (repos and coding paths are independent)
|
|
27
|
+
if (codingPaths) {
|
|
28
|
+
for (const p of codingPaths) {
|
|
29
|
+
this.codingPaths.set(p, { path: p, name: basename(p) });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
22
32
|
}
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
addRepo(repo) {
|
|
34
|
+
if (this.repos.has(repo.path))
|
|
35
|
+
return;
|
|
36
|
+
this.repos.set(repo.path, new GitOperations(repo.path));
|
|
37
|
+
this.availableRepos.push(repo);
|
|
38
|
+
if (!this.defaultRepoPath) {
|
|
39
|
+
this.defaultRepoPath = repo.path;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
removeRepo(path) {
|
|
43
|
+
this.repos.delete(path);
|
|
44
|
+
this.availableRepos = this.availableRepos.filter((r) => r.path !== path);
|
|
45
|
+
if (this.defaultRepoPath === path) {
|
|
46
|
+
this.defaultRepoPath = this.availableRepos.length > 0 ? this.availableRepos[0].path : '';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
getClientRepoPath(peerAddress) {
|
|
50
|
+
return this.clientRepos.get(peerAddress) || this.defaultRepoPath;
|
|
51
|
+
}
|
|
52
|
+
getGit(peerAddress) {
|
|
53
|
+
const repoPath = this.getClientRepoPath(peerAddress);
|
|
54
|
+
const git = this.repos.get(repoPath);
|
|
55
|
+
if (!git) {
|
|
56
|
+
throw new Error('No repository selected. Please add a repository first.');
|
|
57
|
+
}
|
|
58
|
+
return git;
|
|
59
|
+
}
|
|
60
|
+
acquireRepoLock(repoPath, peerAddress) {
|
|
61
|
+
const holder = this.repoLocks.get(repoPath);
|
|
62
|
+
if (holder && holder !== peerAddress) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
this.repoLocks.set(repoPath, peerAddress);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
releaseRepoLock(repoPath, peerAddress) {
|
|
69
|
+
if (this.repoLocks.get(repoPath) === peerAddress) {
|
|
70
|
+
this.repoLocks.delete(repoPath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
removeClient(peerAddress) {
|
|
74
|
+
this.clientRepos.delete(peerAddress);
|
|
75
|
+
for (const [repoPath, holder] of this.repoLocks) {
|
|
76
|
+
if (holder === peerAddress) {
|
|
77
|
+
this.repoLocks.delete(repoPath);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Pending user input requests are NOT auto-approved on disconnect.
|
|
81
|
+
// They persist until the user reconnects and explicitly responds.
|
|
82
|
+
}
|
|
83
|
+
cleanup() {
|
|
84
|
+
this.claudeService.cleanup();
|
|
85
|
+
}
|
|
86
|
+
getActiveSessionCount() {
|
|
87
|
+
return this.claudeService.getActiveSessionCount();
|
|
88
|
+
}
|
|
89
|
+
getClaudeService() {
|
|
90
|
+
return this.claudeService;
|
|
25
91
|
}
|
|
26
92
|
getAiService() {
|
|
27
93
|
const apiKey = getAnthropicApiKey();
|
|
@@ -33,49 +99,80 @@ export class MessageHandler {
|
|
|
33
99
|
}
|
|
34
100
|
return this.aiService;
|
|
35
101
|
}
|
|
36
|
-
async handleMessage(message) {
|
|
102
|
+
async handleMessage(message, peerAddress = 'default') {
|
|
103
|
+
const isVerbose = message.type.startsWith('claude:') || message.type.startsWith('agent:add');
|
|
104
|
+
if (isVerbose) {
|
|
105
|
+
console.log(`[msg] ${message.type} from ${peerAddress.slice(0, 12)}`);
|
|
106
|
+
}
|
|
37
107
|
try {
|
|
38
108
|
switch (message.type) {
|
|
39
109
|
case 'handshake':
|
|
40
|
-
return this.handleHandshake(message);
|
|
110
|
+
return this.handleHandshake(message, peerAddress);
|
|
41
111
|
case 'ping':
|
|
42
112
|
return createMessage('pong', { timestamp: Date.now() });
|
|
43
113
|
case 'git:status':
|
|
44
|
-
return this.handleStatus(message);
|
|
114
|
+
return this.handleStatus(message, peerAddress);
|
|
45
115
|
case 'git:diff':
|
|
46
|
-
return this.handleDiff(message);
|
|
116
|
+
return this.handleDiff(message, peerAddress);
|
|
47
117
|
case 'git:stage':
|
|
48
|
-
return this.handleStage(message);
|
|
118
|
+
return this.handleStage(message, peerAddress);
|
|
49
119
|
case 'git:unstage':
|
|
50
|
-
return this.handleUnstage(message);
|
|
120
|
+
return this.handleUnstage(message, peerAddress);
|
|
51
121
|
case 'git:stage-patch':
|
|
52
|
-
return this.handleStagePatch(message);
|
|
122
|
+
return this.handleStagePatch(message, peerAddress);
|
|
53
123
|
case 'git:unstage-patch':
|
|
54
|
-
return this.handleUnstagePatch(message);
|
|
124
|
+
return this.handleUnstagePatch(message, peerAddress);
|
|
55
125
|
case 'git:commit':
|
|
56
|
-
return this.handleCommit(message);
|
|
126
|
+
return this.handleCommit(message, peerAddress);
|
|
57
127
|
case 'git:log':
|
|
58
|
-
return this.handleLog(message);
|
|
128
|
+
return this.handleLog(message, peerAddress);
|
|
59
129
|
case 'git:branches':
|
|
60
|
-
return this.handleBranches();
|
|
130
|
+
return this.handleBranches(peerAddress);
|
|
61
131
|
case 'git:checkout':
|
|
62
|
-
return this.handleCheckout(message);
|
|
132
|
+
return this.handleCheckout(message, peerAddress);
|
|
63
133
|
case 'git:discard':
|
|
64
|
-
return this.handleDiscard(message);
|
|
134
|
+
return this.handleDiscard(message, peerAddress);
|
|
135
|
+
case 'git:untrack':
|
|
136
|
+
return this.handleUntrack(message, peerAddress);
|
|
137
|
+
case 'git:gitignore-add':
|
|
138
|
+
return this.handleGitignoreAdd(message, peerAddress);
|
|
139
|
+
case 'git:gitignore-read':
|
|
140
|
+
return this.handleGitignoreRead(message, peerAddress);
|
|
141
|
+
case 'git:gitignore-write':
|
|
142
|
+
return this.handleGitignoreWrite(message, peerAddress);
|
|
65
143
|
case 'ai:generate-commit-summary':
|
|
66
|
-
return this.handleGenerateCommitSummary(message);
|
|
144
|
+
return this.handleGenerateCommitSummary(message, peerAddress);
|
|
67
145
|
case 'ai:set-api-key':
|
|
68
146
|
return this.handleSetApiKey(message);
|
|
69
147
|
case 'ai:get-api-key-status':
|
|
70
148
|
return this.handleGetApiKeyStatus(message);
|
|
71
149
|
case 'agent:list-repos':
|
|
72
|
-
return this.handleListRepos(message);
|
|
150
|
+
return this.handleListRepos(message, peerAddress);
|
|
73
151
|
case 'agent:switch-repo':
|
|
74
|
-
return this.handleSwitchRepo(message);
|
|
152
|
+
return this.handleSwitchRepo(message, peerAddress);
|
|
75
153
|
case 'agent:browse-directory':
|
|
76
154
|
return this.handleBrowseDirectory(message);
|
|
77
155
|
case 'agent:add-repo':
|
|
78
156
|
return this.handleAddRepo(message);
|
|
157
|
+
case 'agent:list-coding-paths':
|
|
158
|
+
return this.handleListCodingPaths(message);
|
|
159
|
+
case 'agent:add-coding-path':
|
|
160
|
+
return this.handleAddCodingPath(message);
|
|
161
|
+
// Claude Code SDK
|
|
162
|
+
case 'claude:list-sessions':
|
|
163
|
+
return this.handleClaudeListSessions(message, peerAddress);
|
|
164
|
+
case 'claude:start':
|
|
165
|
+
return this.handleClaudeStart(message, peerAddress);
|
|
166
|
+
case 'claude:resume':
|
|
167
|
+
return this.handleClaudeResume(message, peerAddress);
|
|
168
|
+
case 'claude:cancel':
|
|
169
|
+
return this.handleClaudeCancel(message);
|
|
170
|
+
case 'claude:close':
|
|
171
|
+
return this.handleClaudeClose(message);
|
|
172
|
+
case 'claude:get-messages':
|
|
173
|
+
return this.handleClaudeGetMessages(message, peerAddress);
|
|
174
|
+
case 'claude:user-input-response':
|
|
175
|
+
return this.handleClaudeUserInputResponse(message);
|
|
79
176
|
default:
|
|
80
177
|
return this.createErrorResponse(message.id, 'UNKNOWN_MESSAGE_TYPE', `Unknown message type: ${message.type}`);
|
|
81
178
|
}
|
|
@@ -85,32 +182,48 @@ export class MessageHandler {
|
|
|
85
182
|
return this.createErrorResponse(message.id, 'HANDLER_ERROR', errorMessage);
|
|
86
183
|
}
|
|
87
184
|
}
|
|
88
|
-
handleHandshake(message) {
|
|
185
|
+
handleHandshake(message, peerAddress) {
|
|
89
186
|
const response = createMessage('handshake:ack', {
|
|
90
187
|
success: true,
|
|
91
188
|
agentVersion: this.agentVersion,
|
|
92
|
-
repoPath: this.
|
|
189
|
+
repoPath: this.getClientRepoPath(peerAddress),
|
|
93
190
|
availableRepos: this.availableRepos,
|
|
191
|
+
availableCodingPaths: [...this.codingPaths.values()],
|
|
94
192
|
});
|
|
95
193
|
response.id = message.id;
|
|
194
|
+
// Re-emit pending user input requests so they get broadcast to the new peer
|
|
195
|
+
const pending = this.claudeService.getPendingInputRequests();
|
|
196
|
+
for (const request of pending) {
|
|
197
|
+
console.log(`[handshake] re-emitting pending user input: requestId=${request.requestId}`);
|
|
198
|
+
this.claudeService.emit('user-input-request', request);
|
|
199
|
+
}
|
|
96
200
|
return response;
|
|
97
201
|
}
|
|
98
|
-
async handleStatus(message) {
|
|
99
|
-
const status = await this.
|
|
202
|
+
async handleStatus(message, peerAddress) {
|
|
203
|
+
const status = await this.getGit(peerAddress).getStatus();
|
|
100
204
|
const response = createMessage('git:status:response', status);
|
|
101
205
|
response.id = message.id;
|
|
102
206
|
return response;
|
|
103
207
|
}
|
|
104
|
-
async handleDiff(message) {
|
|
208
|
+
async handleDiff(message, peerAddress) {
|
|
105
209
|
const { path, staged } = message.payload;
|
|
106
|
-
const diff = await this.
|
|
210
|
+
const diff = await this.getGit(peerAddress).getDiff(path, staged);
|
|
107
211
|
const response = createMessage('git:diff:response', diff);
|
|
108
212
|
response.id = message.id;
|
|
109
213
|
return response;
|
|
110
214
|
}
|
|
111
|
-
async handleStage(message) {
|
|
215
|
+
async handleStage(message, peerAddress) {
|
|
216
|
+
const repoPath = this.getClientRepoPath(peerAddress);
|
|
217
|
+
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
218
|
+
const response = createMessage('git:stage:response', {
|
|
219
|
+
success: false,
|
|
220
|
+
error: 'Repository is busy — another device is performing an operation',
|
|
221
|
+
});
|
|
222
|
+
response.id = message.id;
|
|
223
|
+
return response;
|
|
224
|
+
}
|
|
112
225
|
try {
|
|
113
|
-
await this.
|
|
226
|
+
await this.getGit(peerAddress).stage(message.payload.paths);
|
|
114
227
|
const response = createMessage('git:stage:response', { success: true });
|
|
115
228
|
response.id = message.id;
|
|
116
229
|
return response;
|
|
@@ -123,10 +236,22 @@ export class MessageHandler {
|
|
|
123
236
|
response.id = message.id;
|
|
124
237
|
return response;
|
|
125
238
|
}
|
|
239
|
+
finally {
|
|
240
|
+
this.releaseRepoLock(repoPath, peerAddress);
|
|
241
|
+
}
|
|
126
242
|
}
|
|
127
|
-
async handleUnstage(message) {
|
|
243
|
+
async handleUnstage(message, peerAddress) {
|
|
244
|
+
const repoPath = this.getClientRepoPath(peerAddress);
|
|
245
|
+
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
246
|
+
const response = createMessage('git:unstage:response', {
|
|
247
|
+
success: false,
|
|
248
|
+
error: 'Repository is busy — another device is performing an operation',
|
|
249
|
+
});
|
|
250
|
+
response.id = message.id;
|
|
251
|
+
return response;
|
|
252
|
+
}
|
|
128
253
|
try {
|
|
129
|
-
await this.
|
|
254
|
+
await this.getGit(peerAddress).unstage(message.payload.paths);
|
|
130
255
|
const response = createMessage('git:unstage:response', { success: true });
|
|
131
256
|
response.id = message.id;
|
|
132
257
|
return response;
|
|
@@ -139,10 +264,22 @@ export class MessageHandler {
|
|
|
139
264
|
response.id = message.id;
|
|
140
265
|
return response;
|
|
141
266
|
}
|
|
267
|
+
finally {
|
|
268
|
+
this.releaseRepoLock(repoPath, peerAddress);
|
|
269
|
+
}
|
|
142
270
|
}
|
|
143
|
-
async handleStagePatch(message) {
|
|
271
|
+
async handleStagePatch(message, peerAddress) {
|
|
272
|
+
const repoPath = this.getClientRepoPath(peerAddress);
|
|
273
|
+
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
274
|
+
const response = createMessage('git:stage-patch:response', {
|
|
275
|
+
success: false,
|
|
276
|
+
error: 'Repository is busy — another device is performing an operation',
|
|
277
|
+
});
|
|
278
|
+
response.id = message.id;
|
|
279
|
+
return response;
|
|
280
|
+
}
|
|
144
281
|
try {
|
|
145
|
-
await this.
|
|
282
|
+
await this.getGit(peerAddress).stagePatch(message.payload.patch);
|
|
146
283
|
const response = createMessage('git:stage-patch:response', { success: true });
|
|
147
284
|
response.id = message.id;
|
|
148
285
|
return response;
|
|
@@ -155,10 +292,22 @@ export class MessageHandler {
|
|
|
155
292
|
response.id = message.id;
|
|
156
293
|
return response;
|
|
157
294
|
}
|
|
295
|
+
finally {
|
|
296
|
+
this.releaseRepoLock(repoPath, peerAddress);
|
|
297
|
+
}
|
|
158
298
|
}
|
|
159
|
-
async handleUnstagePatch(message) {
|
|
299
|
+
async handleUnstagePatch(message, peerAddress) {
|
|
300
|
+
const repoPath = this.getClientRepoPath(peerAddress);
|
|
301
|
+
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
302
|
+
const response = createMessage('git:unstage-patch:response', {
|
|
303
|
+
success: false,
|
|
304
|
+
error: 'Repository is busy — another device is performing an operation',
|
|
305
|
+
});
|
|
306
|
+
response.id = message.id;
|
|
307
|
+
return response;
|
|
308
|
+
}
|
|
160
309
|
try {
|
|
161
|
-
await this.
|
|
310
|
+
await this.getGit(peerAddress).unstagePatch(message.payload.patch);
|
|
162
311
|
const response = createMessage('git:unstage-patch:response', { success: true });
|
|
163
312
|
response.id = message.id;
|
|
164
313
|
return response;
|
|
@@ -171,11 +320,23 @@ export class MessageHandler {
|
|
|
171
320
|
response.id = message.id;
|
|
172
321
|
return response;
|
|
173
322
|
}
|
|
323
|
+
finally {
|
|
324
|
+
this.releaseRepoLock(repoPath, peerAddress);
|
|
325
|
+
}
|
|
174
326
|
}
|
|
175
|
-
async handleCommit(message) {
|
|
327
|
+
async handleCommit(message, peerAddress) {
|
|
328
|
+
const repoPath = this.getClientRepoPath(peerAddress);
|
|
329
|
+
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
330
|
+
const response = createMessage('git:commit:response', {
|
|
331
|
+
success: false,
|
|
332
|
+
error: 'Repository is busy — another device is performing an operation',
|
|
333
|
+
});
|
|
334
|
+
response.id = message.id;
|
|
335
|
+
return response;
|
|
336
|
+
}
|
|
176
337
|
try {
|
|
177
338
|
const { message: commitMessage, description } = message.payload;
|
|
178
|
-
const hash = await this.
|
|
339
|
+
const hash = await this.getGit(peerAddress).commit(commitMessage, description);
|
|
179
340
|
const response = createMessage('git:commit:response', {
|
|
180
341
|
success: true,
|
|
181
342
|
hash,
|
|
@@ -191,25 +352,37 @@ export class MessageHandler {
|
|
|
191
352
|
response.id = message.id;
|
|
192
353
|
return response;
|
|
193
354
|
}
|
|
355
|
+
finally {
|
|
356
|
+
this.releaseRepoLock(repoPath, peerAddress);
|
|
357
|
+
}
|
|
194
358
|
}
|
|
195
|
-
async handleLog(message) {
|
|
359
|
+
async handleLog(message, peerAddress) {
|
|
196
360
|
const limit = message.payload.limit || 50;
|
|
197
|
-
const commits = await this.
|
|
361
|
+
const commits = await this.getGit(peerAddress).getLog(limit);
|
|
198
362
|
const response = createMessage('git:log:response', { commits });
|
|
199
363
|
response.id = message.id;
|
|
200
364
|
return response;
|
|
201
365
|
}
|
|
202
|
-
async handleBranches() {
|
|
203
|
-
const { branches, current } = await this.
|
|
366
|
+
async handleBranches(peerAddress) {
|
|
367
|
+
const { branches, current } = await this.getGit(peerAddress).getBranches();
|
|
204
368
|
return createMessage('git:branches:response', {
|
|
205
369
|
branches,
|
|
206
370
|
current,
|
|
207
371
|
});
|
|
208
372
|
}
|
|
209
|
-
async handleCheckout(message) {
|
|
373
|
+
async handleCheckout(message, peerAddress) {
|
|
374
|
+
const repoPath = this.getClientRepoPath(peerAddress);
|
|
375
|
+
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
376
|
+
const response = createMessage('git:checkout:response', {
|
|
377
|
+
success: false,
|
|
378
|
+
error: 'Repository is busy — another device is performing an operation',
|
|
379
|
+
});
|
|
380
|
+
response.id = message.id;
|
|
381
|
+
return response;
|
|
382
|
+
}
|
|
210
383
|
try {
|
|
211
384
|
const { branch, create } = message.payload;
|
|
212
|
-
await this.
|
|
385
|
+
await this.getGit(peerAddress).checkout(branch, create);
|
|
213
386
|
const response = createMessage('git:checkout:response', { success: true });
|
|
214
387
|
response.id = message.id;
|
|
215
388
|
return response;
|
|
@@ -222,10 +395,22 @@ export class MessageHandler {
|
|
|
222
395
|
response.id = message.id;
|
|
223
396
|
return response;
|
|
224
397
|
}
|
|
398
|
+
finally {
|
|
399
|
+
this.releaseRepoLock(repoPath, peerAddress);
|
|
400
|
+
}
|
|
225
401
|
}
|
|
226
|
-
async handleDiscard(message) {
|
|
402
|
+
async handleDiscard(message, peerAddress) {
|
|
403
|
+
const repoPath = this.getClientRepoPath(peerAddress);
|
|
404
|
+
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
405
|
+
const response = createMessage('git:discard:response', {
|
|
406
|
+
success: false,
|
|
407
|
+
error: 'Repository is busy — another device is performing an operation',
|
|
408
|
+
});
|
|
409
|
+
response.id = message.id;
|
|
410
|
+
return response;
|
|
411
|
+
}
|
|
227
412
|
try {
|
|
228
|
-
await this.
|
|
413
|
+
await this.getGit(peerAddress).discard(message.payload.paths);
|
|
229
414
|
const response = createMessage('git:discard:response', { success: true });
|
|
230
415
|
response.id = message.id;
|
|
231
416
|
return response;
|
|
@@ -238,8 +423,101 @@ export class MessageHandler {
|
|
|
238
423
|
response.id = message.id;
|
|
239
424
|
return response;
|
|
240
425
|
}
|
|
426
|
+
finally {
|
|
427
|
+
this.releaseRepoLock(repoPath, peerAddress);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async handleUntrack(message, peerAddress) {
|
|
431
|
+
const repoPath = this.getClientRepoPath(peerAddress);
|
|
432
|
+
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
433
|
+
const response = createMessage('git:untrack:response', {
|
|
434
|
+
success: false,
|
|
435
|
+
error: 'Repository is busy — another device is performing an operation',
|
|
436
|
+
});
|
|
437
|
+
response.id = message.id;
|
|
438
|
+
return response;
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
await this.getGit(peerAddress).untrack(message.payload.paths);
|
|
442
|
+
const response = createMessage('git:untrack:response', { success: true });
|
|
443
|
+
response.id = message.id;
|
|
444
|
+
return response;
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
const response = createMessage('git:untrack:response', {
|
|
448
|
+
success: false,
|
|
449
|
+
error: error instanceof Error ? error.message : 'Failed to untrack files',
|
|
450
|
+
});
|
|
451
|
+
response.id = message.id;
|
|
452
|
+
return response;
|
|
453
|
+
}
|
|
454
|
+
finally {
|
|
455
|
+
this.releaseRepoLock(repoPath, peerAddress);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async handleGitignoreAdd(message, peerAddress) {
|
|
459
|
+
const repoPath = this.getClientRepoPath(peerAddress);
|
|
460
|
+
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
461
|
+
const response = createMessage('git:gitignore-add:response', {
|
|
462
|
+
success: false,
|
|
463
|
+
error: 'Repository is busy — another device is performing an operation',
|
|
464
|
+
});
|
|
465
|
+
response.id = message.id;
|
|
466
|
+
return response;
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
await this.getGit(peerAddress).addToGitignore(message.payload.pattern);
|
|
470
|
+
const response = createMessage('git:gitignore-add:response', { success: true });
|
|
471
|
+
response.id = message.id;
|
|
472
|
+
return response;
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
const response = createMessage('git:gitignore-add:response', {
|
|
476
|
+
success: false,
|
|
477
|
+
error: error instanceof Error ? error.message : 'Failed to add to .gitignore',
|
|
478
|
+
});
|
|
479
|
+
response.id = message.id;
|
|
480
|
+
return response;
|
|
481
|
+
}
|
|
482
|
+
finally {
|
|
483
|
+
this.releaseRepoLock(repoPath, peerAddress);
|
|
484
|
+
}
|
|
241
485
|
}
|
|
242
|
-
async
|
|
486
|
+
async handleGitignoreRead(message, peerAddress) {
|
|
487
|
+
const { content, exists } = await this.getGit(peerAddress).readGitignore();
|
|
488
|
+
const response = createMessage('git:gitignore-read:response', { content, exists });
|
|
489
|
+
response.id = message.id;
|
|
490
|
+
return response;
|
|
491
|
+
}
|
|
492
|
+
async handleGitignoreWrite(message, peerAddress) {
|
|
493
|
+
const repoPath = this.getClientRepoPath(peerAddress);
|
|
494
|
+
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
495
|
+
const response = createMessage('git:gitignore-write:response', {
|
|
496
|
+
success: false,
|
|
497
|
+
error: 'Repository is busy — another device is performing an operation',
|
|
498
|
+
});
|
|
499
|
+
response.id = message.id;
|
|
500
|
+
return response;
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
await this.getGit(peerAddress).writeGitignore(message.payload.content);
|
|
504
|
+
const response = createMessage('git:gitignore-write:response', { success: true });
|
|
505
|
+
response.id = message.id;
|
|
506
|
+
return response;
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
const response = createMessage('git:gitignore-write:response', {
|
|
510
|
+
success: false,
|
|
511
|
+
error: error instanceof Error ? error.message : 'Failed to write .gitignore',
|
|
512
|
+
});
|
|
513
|
+
response.id = message.id;
|
|
514
|
+
return response;
|
|
515
|
+
}
|
|
516
|
+
finally {
|
|
517
|
+
this.releaseRepoLock(repoPath, peerAddress);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async handleGenerateCommitSummary(message, peerAddress) {
|
|
243
521
|
const aiService = this.getAiService();
|
|
244
522
|
if (!aiService) {
|
|
245
523
|
const response = createMessage('ai:generate-commit-summary:response', {
|
|
@@ -251,7 +529,8 @@ export class MessageHandler {
|
|
|
251
529
|
return response;
|
|
252
530
|
}
|
|
253
531
|
try {
|
|
254
|
-
const
|
|
532
|
+
const git = this.getGit(peerAddress);
|
|
533
|
+
const status = await git.getStatus();
|
|
255
534
|
if (status.staged.length === 0) {
|
|
256
535
|
const response = createMessage('ai:generate-commit-summary:response', {
|
|
257
536
|
success: false,
|
|
@@ -262,7 +541,7 @@ export class MessageHandler {
|
|
|
262
541
|
return response;
|
|
263
542
|
}
|
|
264
543
|
// Collect diffs for all staged files
|
|
265
|
-
const diffs = await Promise.all(status.staged.map((file) =>
|
|
544
|
+
const diffs = await Promise.all(status.staged.map((file) => git.getDiff(file.path, true)));
|
|
266
545
|
// Generate summary (uses internal queue and cache)
|
|
267
546
|
const result = await aiService.generateSummary({
|
|
268
547
|
diffs,
|
|
@@ -316,7 +595,7 @@ export class MessageHandler {
|
|
|
316
595
|
response.id = message.id;
|
|
317
596
|
return response;
|
|
318
597
|
}
|
|
319
|
-
async handleListRepos(message) {
|
|
598
|
+
async handleListRepos(message, peerAddress) {
|
|
320
599
|
// Refresh branch info for all repos
|
|
321
600
|
const repos = [];
|
|
322
601
|
for (const repo of this.availableRepos) {
|
|
@@ -331,24 +610,24 @@ export class MessageHandler {
|
|
|
331
610
|
}
|
|
332
611
|
const response = createMessage('agent:list-repos:response', {
|
|
333
612
|
repos,
|
|
334
|
-
current: this.
|
|
613
|
+
current: this.getClientRepoPath(peerAddress),
|
|
335
614
|
});
|
|
336
615
|
response.id = message.id;
|
|
337
616
|
return response;
|
|
338
617
|
}
|
|
339
|
-
handleSwitchRepo(message) {
|
|
618
|
+
handleSwitchRepo(message, peerAddress) {
|
|
340
619
|
const { path } = message.payload;
|
|
341
620
|
// Check if the requested repo is in our available repos
|
|
342
621
|
if (!this.repos.has(path)) {
|
|
343
622
|
const response = createMessage('agent:switch-repo:response', {
|
|
344
623
|
success: false,
|
|
345
|
-
newPath: this.
|
|
624
|
+
newPath: this.getClientRepoPath(peerAddress),
|
|
346
625
|
error: `Repository not available: ${path}`,
|
|
347
626
|
});
|
|
348
627
|
response.id = message.id;
|
|
349
628
|
return response;
|
|
350
629
|
}
|
|
351
|
-
this.
|
|
630
|
+
this.clientRepos.set(peerAddress, path);
|
|
352
631
|
const response = createMessage('agent:switch-repo:response', {
|
|
353
632
|
success: true,
|
|
354
633
|
newPath: path,
|
|
@@ -427,17 +706,29 @@ export class MessageHandler {
|
|
|
427
706
|
return response;
|
|
428
707
|
}
|
|
429
708
|
try {
|
|
430
|
-
// Verify it's a git repo
|
|
709
|
+
// Verify it's a git repo and resolve to actual root
|
|
431
710
|
const git = new GitOperations(repoPath);
|
|
432
711
|
const { current } = await git.getBranches();
|
|
433
|
-
|
|
434
|
-
|
|
712
|
+
const rootPath = await git.getGitRoot();
|
|
713
|
+
// Check if already added (by resolved root path)
|
|
714
|
+
if (this.repos.has(rootPath)) {
|
|
715
|
+
const response = createMessage('agent:add-repo:response', {
|
|
716
|
+
success: false,
|
|
717
|
+
error: 'Repository already added',
|
|
718
|
+
});
|
|
719
|
+
response.id = message.id;
|
|
720
|
+
return response;
|
|
721
|
+
}
|
|
722
|
+
// Add to our maps using the actual git root
|
|
723
|
+
this.repos.set(rootPath, git);
|
|
435
724
|
const newRepo = {
|
|
436
|
-
path:
|
|
437
|
-
name: basename(
|
|
725
|
+
path: rootPath,
|
|
726
|
+
name: basename(rootPath),
|
|
438
727
|
currentBranch: current,
|
|
439
728
|
};
|
|
440
729
|
this.availableRepos.push(newRepo);
|
|
730
|
+
// Persist to agent.json
|
|
731
|
+
addManagedRepo(rootPath);
|
|
441
732
|
const response = createMessage('agent:add-repo:response', {
|
|
442
733
|
success: true,
|
|
443
734
|
repo: newRepo,
|
|
@@ -454,6 +745,144 @@ export class MessageHandler {
|
|
|
454
745
|
return response;
|
|
455
746
|
}
|
|
456
747
|
}
|
|
748
|
+
// ============================================================================
|
|
749
|
+
// Coding Path Handlers
|
|
750
|
+
// ============================================================================
|
|
751
|
+
handleListCodingPaths(message) {
|
|
752
|
+
const response = createMessage('agent:list-coding-paths:response', { paths: [...this.codingPaths.values()] });
|
|
753
|
+
response.id = message.id;
|
|
754
|
+
return response;
|
|
755
|
+
}
|
|
756
|
+
async handleAddCodingPath(message) {
|
|
757
|
+
const { path: codingPath } = message.payload;
|
|
758
|
+
if (this.codingPaths.has(codingPath)) {
|
|
759
|
+
const response = createMessage('agent:add-coding-path:response', { success: false, error: 'Coding path already added' });
|
|
760
|
+
response.id = message.id;
|
|
761
|
+
return response;
|
|
762
|
+
}
|
|
763
|
+
try {
|
|
764
|
+
// Verify directory exists
|
|
765
|
+
const dirStat = await stat(codingPath);
|
|
766
|
+
if (!dirStat.isDirectory()) {
|
|
767
|
+
const response = createMessage('agent:add-coding-path:response', { success: false, error: 'Path is not a directory' });
|
|
768
|
+
response.id = message.id;
|
|
769
|
+
return response;
|
|
770
|
+
}
|
|
771
|
+
const newPath = { path: codingPath, name: basename(codingPath) };
|
|
772
|
+
this.codingPaths.set(codingPath, newPath);
|
|
773
|
+
addManagedCodingPath(codingPath);
|
|
774
|
+
const response = createMessage('agent:add-coding-path:response', { success: true, path: newPath });
|
|
775
|
+
response.id = message.id;
|
|
776
|
+
return response;
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
const response = createMessage('agent:add-coding-path:response', { success: false, error: error instanceof Error ? error.message : 'Failed to add coding path' });
|
|
780
|
+
response.id = message.id;
|
|
781
|
+
return response;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
// ============================================================================
|
|
785
|
+
// Claude Code SDK Handlers
|
|
786
|
+
// ============================================================================
|
|
787
|
+
async handleClaudeListSessions(message, peerAddress) {
|
|
788
|
+
try {
|
|
789
|
+
const cwd = message.payload.cwd || this.getClientRepoPath(peerAddress);
|
|
790
|
+
const sessions = await this.claudeService.listAvailableSessions(cwd);
|
|
791
|
+
const response = createMessage('claude:list-sessions:response', { sessions });
|
|
792
|
+
response.id = message.id;
|
|
793
|
+
return response;
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
const response = createMessage('claude:list-sessions:response', { sessions: [], error: error instanceof Error ? error.message : 'Failed to list sessions' });
|
|
797
|
+
response.id = message.id;
|
|
798
|
+
return response;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
async handleClaudeStart(message, peerAddress) {
|
|
802
|
+
const { prompt, cwd: payloadCwd, allowedTools, systemPrompt, model, permissionMode } = message.payload;
|
|
803
|
+
const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
|
|
804
|
+
const streamId = generateMessageId();
|
|
805
|
+
console.log(`[claude:start] cwd=${cwd} prompt=${prompt.slice(0, 80)}`);
|
|
806
|
+
try {
|
|
807
|
+
const sessionId = await this.claudeService.startSession({
|
|
808
|
+
prompt, cwd, streamId, allowedTools, systemPrompt, model, permissionMode,
|
|
809
|
+
});
|
|
810
|
+
console.log(`[claude:start] session created: ${sessionId}`);
|
|
811
|
+
const response = createMessage('claude:start:response', { success: true, sessionId, streamId });
|
|
812
|
+
response.id = message.id;
|
|
813
|
+
return response;
|
|
814
|
+
}
|
|
815
|
+
catch (error) {
|
|
816
|
+
console.error(`[claude:start] error:`, error);
|
|
817
|
+
const response = createMessage('claude:start:response', { success: false, error: error instanceof Error ? error.message : 'Failed to start session' });
|
|
818
|
+
response.id = message.id;
|
|
819
|
+
return response;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
async handleClaudeResume(message, peerAddress) {
|
|
823
|
+
const { sessionId: requestedId, prompt, cwd: payloadCwd } = message.payload;
|
|
824
|
+
const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
|
|
825
|
+
const streamId = generateMessageId();
|
|
826
|
+
console.log(`[claude:resume] session=${requestedId} cwd=${cwd} prompt=${prompt.slice(0, 80)}`);
|
|
827
|
+
try {
|
|
828
|
+
const actualSessionId = await this.claudeService.resumeSession({
|
|
829
|
+
sessionId: requestedId, prompt, cwd, streamId,
|
|
830
|
+
});
|
|
831
|
+
console.log(`[claude:resume] session resumed: ${actualSessionId}`);
|
|
832
|
+
const response = createMessage('claude:resume:response', { success: true, sessionId: actualSessionId, streamId });
|
|
833
|
+
response.id = message.id;
|
|
834
|
+
return response;
|
|
835
|
+
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
console.error(`[claude:resume] error:`, error);
|
|
838
|
+
const response = createMessage('claude:resume:response', { success: false, error: error instanceof Error ? error.message : 'Failed to resume session' });
|
|
839
|
+
response.id = message.id;
|
|
840
|
+
return response;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
handleClaudeCancel(message) {
|
|
844
|
+
const { sessionId } = message.payload;
|
|
845
|
+
const success = this.claudeService.cancelSession(sessionId);
|
|
846
|
+
const response = createMessage('claude:cancel:response', { success, error: success ? undefined : 'Session not found or already ended' });
|
|
847
|
+
response.id = message.id;
|
|
848
|
+
return response;
|
|
849
|
+
}
|
|
850
|
+
handleClaudeClose(message) {
|
|
851
|
+
const { sessionId } = message.payload;
|
|
852
|
+
const success = this.claudeService.closeSession(sessionId);
|
|
853
|
+
console.log(`[claude:close] session=${sessionId} success=${success}`);
|
|
854
|
+
const response = createMessage('claude:close:response', { success, error: success ? undefined : 'Session not found or already closed' });
|
|
855
|
+
response.id = message.id;
|
|
856
|
+
return response;
|
|
857
|
+
}
|
|
858
|
+
handleClaudeUserInputResponse(message) {
|
|
859
|
+
const resolved = this.claudeService.resolveUserInput(message.payload);
|
|
860
|
+
console.log(`[claude:user-input-response] requestId=${message.payload.requestId} action=${message.payload.action} resolved=${resolved}`);
|
|
861
|
+
// No dedicated response type — just acknowledge
|
|
862
|
+
const response = createMessage('claude:user-input-response', { success: resolved });
|
|
863
|
+
response.id = message.id;
|
|
864
|
+
return response;
|
|
865
|
+
}
|
|
866
|
+
async handleClaudeGetMessages(message, peerAddress) {
|
|
867
|
+
const { sessionId, cwd: payloadCwd, offset = 0, limit = 50 } = message.payload;
|
|
868
|
+
const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
|
|
869
|
+
try {
|
|
870
|
+
const result = await this.claudeService.getMessages(sessionId, cwd, offset, limit);
|
|
871
|
+
const response = createMessage('claude:get-messages:response', result);
|
|
872
|
+
response.id = message.id;
|
|
873
|
+
return response;
|
|
874
|
+
}
|
|
875
|
+
catch (error) {
|
|
876
|
+
const response = createMessage('claude:get-messages:response', {
|
|
877
|
+
messages: [],
|
|
878
|
+
total: 0,
|
|
879
|
+
hasMore: false,
|
|
880
|
+
error: error instanceof Error ? error.message : 'Failed to get messages',
|
|
881
|
+
});
|
|
882
|
+
response.id = message.id;
|
|
883
|
+
return response;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
457
886
|
createErrorResponse(id, code, message) {
|
|
458
887
|
const response = createMessage('error', { code, message });
|
|
459
888
|
response.id = id;
|