@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.
Files changed (81) hide show
  1. package/dist/ai/claudeCodeService.d.ts +89 -0
  2. package/dist/ai/claudeCodeService.d.ts.map +1 -0
  3. package/dist/ai/claudeCodeService.js +602 -0
  4. package/dist/ai/claudeCodeService.js.map +1 -0
  5. package/dist/ai/sessionStore.d.ts +31 -0
  6. package/dist/ai/sessionStore.d.ts.map +1 -0
  7. package/dist/ai/sessionStore.js +72 -0
  8. package/dist/ai/sessionStore.js.map +1 -0
  9. package/dist/ai/sessionStore.test.d.ts +2 -0
  10. package/dist/ai/sessionStore.test.d.ts.map +1 -0
  11. package/dist/ai/sessionStore.test.js +99 -0
  12. package/dist/ai/sessionStore.test.js.map +1 -0
  13. package/dist/config.d.ts +7 -0
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +39 -0
  16. package/dist/config.js.map +1 -1
  17. package/dist/connection/connection.d.ts +16 -17
  18. package/dist/connection/connection.d.ts.map +1 -1
  19. package/dist/connection/connection.js +72 -57
  20. package/dist/connection/connection.js.map +1 -1
  21. package/dist/connection/{signaling.d.ts → relay.d.ts} +3 -5
  22. package/dist/connection/relay.d.ts.map +1 -0
  23. package/dist/connection/{signaling.js → relay.js} +18 -19
  24. package/dist/connection/relay.js.map +1 -0
  25. package/dist/git/operations.d.ts +27 -1
  26. package/dist/git/operations.d.ts.map +1 -1
  27. package/dist/git/operations.js +113 -4
  28. package/dist/git/operations.js.map +1 -1
  29. package/dist/git/operations.test.js +69 -1
  30. package/dist/git/operations.test.js.map +1 -1
  31. package/dist/handlers/messageHandler.d.ts +31 -4
  32. package/dist/handlers/messageHandler.d.ts.map +1 -1
  33. package/dist/handlers/messageHandler.js +491 -62
  34. package/dist/handlers/messageHandler.js.map +1 -1
  35. package/dist/handlers/messageHandler.test.js +130 -2
  36. package/dist/handlers/messageHandler.test.js.map +1 -1
  37. package/dist/index.js +229 -89
  38. package/dist/index.js.map +1 -1
  39. package/dist/service/ensureDaemon.d.ts +23 -0
  40. package/dist/service/ensureDaemon.d.ts.map +1 -0
  41. package/dist/service/ensureDaemon.js +138 -0
  42. package/dist/service/ensureDaemon.js.map +1 -0
  43. package/dist/service/index.d.ts +8 -0
  44. package/dist/service/index.d.ts.map +1 -0
  45. package/dist/service/index.js +8 -0
  46. package/dist/service/index.js.map +1 -0
  47. package/dist/service/ipc.test.d.ts +2 -0
  48. package/dist/service/ipc.test.d.ts.map +1 -0
  49. package/dist/service/ipc.test.js +140 -0
  50. package/dist/service/ipc.test.js.map +1 -0
  51. package/dist/service/ipcClient.d.ts +36 -0
  52. package/dist/service/ipcClient.d.ts.map +1 -0
  53. package/dist/service/ipcClient.js +167 -0
  54. package/dist/service/ipcClient.js.map +1 -0
  55. package/dist/service/ipcServer.d.ts +53 -0
  56. package/dist/service/ipcServer.d.ts.map +1 -0
  57. package/dist/service/ipcServer.js +245 -0
  58. package/dist/service/ipcServer.js.map +1 -0
  59. package/dist/service/run.d.ts +13 -0
  60. package/dist/service/run.d.ts.map +1 -0
  61. package/dist/service/run.js +292 -0
  62. package/dist/service/run.js.map +1 -0
  63. package/dist/service/singleton.d.ts +31 -0
  64. package/dist/service/singleton.d.ts.map +1 -0
  65. package/dist/service/singleton.js +96 -0
  66. package/dist/service/singleton.js.map +1 -0
  67. package/dist/service/singleton.test.d.ts +2 -0
  68. package/dist/service/singleton.test.d.ts.map +1 -0
  69. package/dist/service/singleton.test.js +39 -0
  70. package/dist/service/singleton.test.js.map +1 -0
  71. package/dist/service/stateStore.d.ts +11 -0
  72. package/dist/service/stateStore.d.ts.map +1 -0
  73. package/dist/service/stateStore.js +35 -0
  74. package/dist/service/stateStore.js.map +1 -0
  75. package/dist/service/types.d.ts +111 -0
  76. package/dist/service/types.d.ts.map +1 -0
  77. package/dist/service/types.js +76 -0
  78. package/dist/service/types.js.map +1 -0
  79. package/package.json +3 -2
  80. package/dist/connection/signaling.d.ts.map +0 -1
  81. 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.1.0';
11
- currentRepoPath;
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
- constructor(repos, _license) {
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.currentRepoPath = repos[0].path;
21
- // License handling will be implemented later
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
- get git() {
24
- return this.repos.get(this.currentRepoPath);
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.currentRepoPath,
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.git.getStatus();
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.git.getDiff(path, staged);
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.git.stage(message.payload.paths);
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.git.unstage(message.payload.paths);
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.git.stagePatch(message.payload.patch);
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.git.unstagePatch(message.payload.patch);
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.git.commit(commitMessage, description);
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.git.getLog(limit);
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.git.getBranches();
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.git.checkout(branch, create);
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.git.discard(message.payload.paths);
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 handleGenerateCommitSummary(message) {
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 status = await this.git.getStatus();
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) => this.git.getDiff(file.path, true)));
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.currentRepoPath,
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.currentRepoPath,
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.currentRepoPath = path;
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 by trying to get branches
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
- // Add to our maps
434
- this.repos.set(repoPath, git);
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: repoPath,
437
- name: basename(repoPath),
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;