@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.
Files changed (122) hide show
  1. package/dist/ai/asyncQueue.d.ts +7 -6
  2. package/dist/ai/asyncQueue.d.ts.map +1 -1
  3. package/dist/ai/asyncQueue.js +31 -21
  4. package/dist/ai/asyncQueue.js.map +1 -1
  5. package/dist/ai/cardBuilder.d.ts +22 -2
  6. package/dist/ai/cardBuilder.d.ts.map +1 -1
  7. package/dist/ai/cardBuilder.js +275 -30
  8. package/dist/ai/cardBuilder.js.map +1 -1
  9. package/dist/ai/claudeCliProvider.d.ts +21 -0
  10. package/dist/ai/claudeCliProvider.d.ts.map +1 -0
  11. package/dist/ai/claudeCliProvider.js +527 -0
  12. package/dist/ai/claudeCliProvider.js.map +1 -0
  13. package/dist/ai/claudeCodeProvider.d.ts +18 -0
  14. package/dist/ai/claudeCodeProvider.d.ts.map +1 -0
  15. package/dist/ai/claudeCodeProvider.js +30 -0
  16. package/dist/ai/claudeCodeProvider.js.map +1 -0
  17. package/dist/ai/claudeSdkProvider.d.ts +20 -0
  18. package/dist/ai/claudeSdkProvider.d.ts.map +1 -0
  19. package/dist/ai/claudeSdkProvider.js +428 -0
  20. package/dist/ai/claudeSdkProvider.js.map +1 -0
  21. package/dist/ai/codexMcpProvider.d.ts +15 -0
  22. package/dist/ai/codexMcpProvider.d.ts.map +1 -0
  23. package/dist/ai/codexMcpProvider.js +442 -0
  24. package/dist/ai/codexMcpProvider.js.map +1 -0
  25. package/dist/ai/codexSdkProvider.d.ts +35 -0
  26. package/dist/ai/codexSdkProvider.d.ts.map +1 -0
  27. package/dist/ai/codexSdkProvider.js +422 -0
  28. package/dist/ai/codexSdkProvider.js.map +1 -0
  29. package/dist/ai/commitSummary.d.ts +4 -0
  30. package/dist/ai/commitSummary.d.ts.map +1 -1
  31. package/dist/ai/commitSummary.js +80 -40
  32. package/dist/ai/commitSummary.js.map +1 -1
  33. package/dist/ai/debugLogger.d.ts +24 -0
  34. package/dist/ai/debugLogger.d.ts.map +1 -0
  35. package/dist/ai/debugLogger.js +55 -0
  36. package/dist/ai/debugLogger.js.map +1 -0
  37. package/dist/ai/provider.d.ts +57 -0
  38. package/dist/ai/provider.d.ts.map +1 -0
  39. package/dist/ai/provider.js +2 -0
  40. package/dist/ai/provider.js.map +1 -0
  41. package/dist/ai/sandboxMcp.d.ts +12 -0
  42. package/dist/ai/sandboxMcp.d.ts.map +1 -0
  43. package/dist/ai/sandboxMcp.js +12 -0
  44. package/dist/ai/sandboxMcp.js.map +1 -0
  45. package/dist/ai/sandboxMcpStdio.d.ts +16 -0
  46. package/dist/ai/sandboxMcpStdio.d.ts.map +1 -0
  47. package/dist/ai/sandboxMcpStdio.js +127 -0
  48. package/dist/ai/sandboxMcpStdio.js.map +1 -0
  49. package/dist/ai/sessionManager.d.ts +105 -0
  50. package/dist/ai/sessionManager.d.ts.map +1 -0
  51. package/dist/ai/sessionManager.js +716 -0
  52. package/dist/ai/sessionManager.js.map +1 -0
  53. package/dist/ai/sessionRegistry.d.ts +28 -0
  54. package/dist/ai/sessionRegistry.d.ts.map +1 -0
  55. package/dist/ai/sessionRegistry.js +148 -0
  56. package/dist/ai/sessionRegistry.js.map +1 -0
  57. package/dist/config.d.ts +1 -0
  58. package/dist/config.d.ts.map +1 -1
  59. package/dist/config.js +12 -0
  60. package/dist/config.js.map +1 -1
  61. package/dist/connection/connection.d.ts +1 -0
  62. package/dist/connection/connection.d.ts.map +1 -1
  63. package/dist/connection/connection.js +19 -1
  64. package/dist/connection/connection.js.map +1 -1
  65. package/dist/git/operations.d.ts +26 -1
  66. package/dist/git/operations.d.ts.map +1 -1
  67. package/dist/git/operations.js +83 -2
  68. package/dist/git/operations.js.map +1 -1
  69. package/dist/handlers/messageHandler.d.ts +24 -3
  70. package/dist/handlers/messageHandler.d.ts.map +1 -1
  71. package/dist/handlers/messageHandler.js +378 -24
  72. package/dist/handlers/messageHandler.js.map +1 -1
  73. package/dist/index.js +23 -40
  74. package/dist/index.js.map +1 -1
  75. package/dist/service/debugHttpServer.d.ts +27 -0
  76. package/dist/service/debugHttpServer.d.ts.map +1 -0
  77. package/dist/service/debugHttpServer.js +189 -0
  78. package/dist/service/debugHttpServer.js.map +1 -0
  79. package/dist/service/ensureDaemon.d.ts.map +1 -1
  80. package/dist/service/ensureDaemon.js +15 -2
  81. package/dist/service/ensureDaemon.js.map +1 -1
  82. package/dist/service/ipcClient.js +1 -1
  83. package/dist/service/run.d.ts.map +1 -1
  84. package/dist/service/run.js +42 -15
  85. package/dist/service/run.js.map +1 -1
  86. package/dist/service/singleton.d.ts +4 -0
  87. package/dist/service/singleton.d.ts.map +1 -1
  88. package/dist/service/singleton.js +28 -2
  89. package/dist/service/singleton.js.map +1 -1
  90. package/package.json +7 -3
  91. package/dist/ai/asyncQueue.test.d.ts +0 -2
  92. package/dist/ai/asyncQueue.test.d.ts.map +0 -1
  93. package/dist/ai/asyncQueue.test.js +0 -56
  94. package/dist/ai/asyncQueue.test.js.map +0 -1
  95. package/dist/ai/claudeCodeService.d.ts +0 -122
  96. package/dist/ai/claudeCodeService.d.ts.map +0 -1
  97. package/dist/ai/claudeCodeService.js +0 -869
  98. package/dist/ai/claudeCodeService.js.map +0 -1
  99. package/dist/ai/sessionStore.test.d.ts +0 -2
  100. package/dist/ai/sessionStore.test.d.ts.map +0 -1
  101. package/dist/ai/sessionStore.test.js +0 -99
  102. package/dist/ai/sessionStore.test.js.map +0 -1
  103. package/dist/connection/pubsub.test.d.ts +0 -2
  104. package/dist/connection/pubsub.test.d.ts.map +0 -1
  105. package/dist/connection/pubsub.test.js +0 -96
  106. package/dist/connection/pubsub.test.js.map +0 -1
  107. package/dist/git/operations.test.d.ts +0 -2
  108. package/dist/git/operations.test.d.ts.map +0 -1
  109. package/dist/git/operations.test.js +0 -300
  110. package/dist/git/operations.test.js.map +0 -1
  111. package/dist/handlers/messageHandler.test.d.ts +0 -2
  112. package/dist/handlers/messageHandler.test.d.ts.map +0 -1
  113. package/dist/handlers/messageHandler.test.js +0 -361
  114. package/dist/handlers/messageHandler.test.js.map +0 -1
  115. package/dist/service/ipc.test.d.ts +0 -2
  116. package/dist/service/ipc.test.d.ts.map +0 -1
  117. package/dist/service/ipc.test.js +0 -140
  118. package/dist/service/ipc.test.js.map +0 -1
  119. package/dist/service/singleton.test.d.ts +0 -2
  120. package/dist/service/singleton.test.d.ts.map +0 -1
  121. package/dist/service/singleton.test.js +0 -39
  122. 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 { ClaudeCodeService } from '../ai/claudeCodeService.js';
6
- import { readdir, stat } from 'fs/promises';
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.0';
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 ClaudeCodeService();
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 for latest version (12h dedup, production only)
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 for all staged files
607
- const diffs = await Promise.all(status.staged.map((file) => git.getDiff(file.path, true)));
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
- console.log(`[claude:start] cwd=${cwd} prompt=${prompt.slice(0, 80)}`);
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, cwd, streamId, allowedTools, systemPrompt, model, permissionMode,
1221
+ prompt,
1222
+ cwd,
1223
+ streamId,
1224
+ agent: resolvedAgent,
1225
+ allowedTools,
1226
+ systemPrompt,
1227
+ model,
1228
+ permissionMode,
1229
+ sandboxed,
951
1230
  });
952
- console.log(`[claude:start] session created: ${sessionId}`);
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(`[claude:start] error:`, 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
- console.log(`[claude:resume] session=${requestedId} cwd=${cwd} prompt=${prompt.slice(0, 80)}`);
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, prompt, cwd, streamId,
1274
+ sessionId: requestedId,
1275
+ prompt,
1276
+ cwd,
1277
+ streamId,
1278
+ agent: resolvedAgent,
977
1279
  });
978
- console.log(`[claude:resume] session resumed: ${actualSessionId}`);
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(`[claude:resume] error:`, 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(`[claude:close] session=${sessionId} success=${success}`);
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(`[claude:user-input-response] requestId=${message.payload.requestId} action=${message.payload.action} resolved=${resolved}`);
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;