@sumicom/quicksave 0.6.1 → 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 (89) hide show
  1. package/dist/ai/cardBuilder.d.ts +14 -3
  2. package/dist/ai/cardBuilder.d.ts.map +1 -1
  3. package/dist/ai/cardBuilder.js +219 -66
  4. package/dist/ai/cardBuilder.js.map +1 -1
  5. package/dist/ai/claudeCliProvider.d.ts +2 -0
  6. package/dist/ai/claudeCliProvider.d.ts.map +1 -1
  7. package/dist/ai/claudeCliProvider.js +81 -20
  8. package/dist/ai/claudeCliProvider.js.map +1 -1
  9. package/dist/ai/claudeCodeProvider.d.ts +18 -0
  10. package/dist/ai/claudeCodeProvider.d.ts.map +1 -0
  11. package/dist/ai/claudeCodeProvider.js +30 -0
  12. package/dist/ai/claudeCodeProvider.js.map +1 -0
  13. package/dist/ai/claudeSdkProvider.d.ts +2 -0
  14. package/dist/ai/claudeSdkProvider.d.ts.map +1 -1
  15. package/dist/ai/claudeSdkProvider.js +13 -3
  16. package/dist/ai/claudeSdkProvider.js.map +1 -1
  17. package/dist/ai/codexMcpProvider.d.ts +15 -0
  18. package/dist/ai/codexMcpProvider.d.ts.map +1 -0
  19. package/dist/ai/codexMcpProvider.js +442 -0
  20. package/dist/ai/codexMcpProvider.js.map +1 -0
  21. package/dist/ai/codexSdkProvider.d.ts +35 -0
  22. package/dist/ai/codexSdkProvider.d.ts.map +1 -0
  23. package/dist/ai/codexSdkProvider.js +422 -0
  24. package/dist/ai/codexSdkProvider.js.map +1 -0
  25. package/dist/ai/debugLogger.d.ts +24 -0
  26. package/dist/ai/debugLogger.d.ts.map +1 -0
  27. package/dist/ai/debugLogger.js +55 -0
  28. package/dist/ai/debugLogger.js.map +1 -0
  29. package/dist/ai/provider.d.ts +4 -1
  30. package/dist/ai/provider.d.ts.map +1 -1
  31. package/dist/ai/sessionManager.d.ts +13 -8
  32. package/dist/ai/sessionManager.d.ts.map +1 -1
  33. package/dist/ai/sessionManager.js +148 -27
  34. package/dist/ai/sessionManager.js.map +1 -1
  35. package/dist/config.d.ts +1 -0
  36. package/dist/config.d.ts.map +1 -1
  37. package/dist/config.js +12 -0
  38. package/dist/config.js.map +1 -1
  39. package/dist/connection/connection.d.ts +1 -0
  40. package/dist/connection/connection.d.ts.map +1 -1
  41. package/dist/connection/connection.js +19 -1
  42. package/dist/connection/connection.js.map +1 -1
  43. package/dist/git/operations.d.ts +20 -0
  44. package/dist/git/operations.d.ts.map +1 -1
  45. package/dist/git/operations.js +52 -0
  46. package/dist/git/operations.js.map +1 -1
  47. package/dist/handlers/messageHandler.d.ts +16 -0
  48. package/dist/handlers/messageHandler.d.ts.map +1 -1
  49. package/dist/handlers/messageHandler.js +289 -19
  50. package/dist/handlers/messageHandler.js.map +1 -1
  51. package/dist/index.js +22 -2
  52. package/dist/index.js.map +1 -1
  53. package/dist/service/debugHttpServer.d.ts +27 -0
  54. package/dist/service/debugHttpServer.d.ts.map +1 -0
  55. package/dist/service/debugHttpServer.js +189 -0
  56. package/dist/service/debugHttpServer.js.map +1 -0
  57. package/dist/service/ipcClient.js +1 -1
  58. package/dist/service/run.d.ts.map +1 -1
  59. package/dist/service/run.js +21 -1
  60. package/dist/service/run.js.map +1 -1
  61. package/package.json +4 -2
  62. package/dist/ai/asyncQueue.test.d.ts +0 -2
  63. package/dist/ai/asyncQueue.test.d.ts.map +0 -1
  64. package/dist/ai/asyncQueue.test.js +0 -56
  65. package/dist/ai/asyncQueue.test.js.map +0 -1
  66. package/dist/ai/sessionStore.test.d.ts +0 -2
  67. package/dist/ai/sessionStore.test.d.ts.map +0 -1
  68. package/dist/ai/sessionStore.test.js +0 -99
  69. package/dist/ai/sessionStore.test.js.map +0 -1
  70. package/dist/connection/pubsub.test.d.ts +0 -2
  71. package/dist/connection/pubsub.test.d.ts.map +0 -1
  72. package/dist/connection/pubsub.test.js +0 -96
  73. package/dist/connection/pubsub.test.js.map +0 -1
  74. package/dist/git/operations.test.d.ts +0 -2
  75. package/dist/git/operations.test.d.ts.map +0 -1
  76. package/dist/git/operations.test.js +0 -300
  77. package/dist/git/operations.test.js.map +0 -1
  78. package/dist/handlers/messageHandler.test.d.ts +0 -2
  79. package/dist/handlers/messageHandler.test.d.ts.map +0 -1
  80. package/dist/handlers/messageHandler.test.js +0 -361
  81. package/dist/handlers/messageHandler.test.js.map +0 -1
  82. package/dist/service/ipc.test.d.ts +0 -2
  83. package/dist/service/ipc.test.d.ts.map +0 -1
  84. package/dist/service/ipc.test.js +0 -140
  85. package/dist/service/ipc.test.js.map +0 -1
  86. package/dist/service/singleton.test.d.ts +0 -2
  87. package/dist/service/singleton.test.d.ts.map +0 -1
  88. package/dist/service/singleton.test.js +0 -39
  89. package/dist/service/singleton.test.js.map +0 -1
@@ -1,27 +1,33 @@
1
1
  import { createMessage, generateMessageId, } from '@sumicom/quicksave-shared';
2
2
  import { GitOperations } from '../git/operations.js';
3
- import { getAnthropicApiKey, setAnthropicApiKey, hasAnthropicApiKey, addManagedRepo, addManagedCodingPath } from '../config.js';
3
+ import { getAnthropicApiKey, setAnthropicApiKey, hasAnthropicApiKey, addManagedRepo, removeManagedRepo, addManagedCodingPath, removeManagedCodingPath } from '../config.js';
4
4
  import { CommitSummaryService } from '../ai/commitSummary.js';
5
5
  import { SessionManager } from '../ai/sessionManager.js';
6
- import { ClaudeCliProvider } from '../ai/claudeCliProvider.js';
7
- import { ClaudeSdkProvider } from '../ai/claudeSdkProvider.js';
6
+ import { ClaudeCodeProvider } from '../ai/claudeCodeProvider.js';
7
+ import { CodexSdkProvider } from '../ai/codexSdkProvider.js';
8
8
  import { getSessionRegistry } from '../ai/sessionRegistry.js';
9
- import { readdir, stat } from 'fs/promises';
9
+ import { readdir, stat, readFile } from 'fs/promises';
10
10
  import { join, dirname, basename } from 'path';
11
11
  import { homedir } from 'os';
12
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
13
14
  export class MessageHandler {
14
15
  repos;
15
- agentVersion = '0.6.1';
16
+ agentVersion = '0.6.2';
16
17
  defaultRepoPath;
17
18
  clientRepos = new Map(); // peerAddress -> repoPath
18
19
  repoLocks = new Map(); // repoPath -> peerAddress holding lock
19
20
  availableRepos;
20
21
  codingPaths = new Map(); // path -> CodingPath
21
22
  aiService = null;
22
- claudeService = new SessionManager(new ClaudeCliProvider(), new ClaudeSdkProvider());
23
+ claudeService = new SessionManager([
24
+ new ClaudeCodeProvider(),
25
+ new CodexSdkProvider(),
26
+ ]);
23
27
  latestVersionCache = null;
24
28
  versionCheckInFlight = null;
29
+ codexModelsCache = null;
30
+ codexModelsCheckInFlight = null;
25
31
  onPeerSubscribed;
26
32
  onPeerUnsubscribed;
27
33
  onHistoryUpdated;
@@ -77,6 +83,59 @@ export class MessageHandler {
77
83
  })();
78
84
  return this.versionCheckInFlight;
79
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
+ }
80
139
  addRepo(repo) {
81
140
  if (this.repos.has(repo.path))
82
141
  return;
@@ -181,6 +240,12 @@ export class MessageHandler {
181
240
  return this.handleDiscard(message, peerAddress);
182
241
  case 'git:untrack':
183
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);
184
249
  case 'git:gitignore-add':
185
250
  return this.handleGitignoreAdd(message, peerAddress);
186
251
  case 'git:gitignore-read':
@@ -201,14 +266,24 @@ export class MessageHandler {
201
266
  return this.handleBrowseDirectory(message);
202
267
  case 'agent:add-repo':
203
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);
204
273
  case 'agent:list-coding-paths':
205
274
  return this.handleListCodingPaths(message);
206
275
  case 'agent:add-coding-path':
207
276
  return this.handleAddCodingPath(message);
277
+ case 'agent:remove-coding-path':
278
+ return this.handleRemoveCodingPath(message);
208
279
  case 'agent:check-update':
209
280
  return this.handleAgentCheckUpdate(message);
210
281
  case 'agent:update':
211
282
  return this.handleAgentUpdate(message);
283
+ case 'agent:restart':
284
+ return this.handleAgentRestart(message);
285
+ case 'codex:list-models':
286
+ return this.handleCodexListModels(message);
212
287
  // Claude Code SDK
213
288
  case 'claude:list-sessions':
214
289
  return this.handleClaudeListSessions(message, peerAddress);
@@ -257,8 +332,9 @@ export class MessageHandler {
257
332
  }
258
333
  }
259
334
  handleHandshake(message, peerAddress) {
260
- // 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)
261
336
  this.checkLatestVersion().catch(() => { });
337
+ this.fetchCodexModels().catch(() => { });
262
338
  const response = createMessage('handshake:ack', {
263
339
  success: true,
264
340
  agentVersion: this.agentVersion,
@@ -268,6 +344,7 @@ export class MessageHandler {
268
344
  preferences: this.claudeService.getPreferences(),
269
345
  latestVersion: this.latestVersionCache?.version,
270
346
  devBuild: !this.productionBuild || undefined,
347
+ codexModels: this.codexModelsCache?.models,
271
348
  });
272
349
  response.id = message.id;
273
350
  return response;
@@ -528,6 +605,45 @@ export class MessageHandler {
528
605
  this.releaseRepoLock(repoPath, peerAddress);
529
606
  }
530
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
+ }
531
647
  async handleGitignoreAdd(message, peerAddress) {
532
648
  const repoPath = this.getClientRepoPath(peerAddress);
533
649
  if (!this.acquireRepoLock(repoPath, peerAddress)) {
@@ -829,6 +945,81 @@ export class MessageHandler {
829
945
  return response;
830
946
  }
831
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
+ }
832
1023
  // ============================================================================
833
1024
  // Coding Path Handlers
834
1025
  // ============================================================================
@@ -865,6 +1056,19 @@ export class MessageHandler {
865
1056
  return response;
866
1057
  }
867
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
+ }
868
1072
  // ============================================================================
869
1073
  // Agent Self-Update
870
1074
  // ============================================================================
@@ -944,6 +1148,50 @@ export class MessageHandler {
944
1148
  return response;
945
1149
  }
946
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
+ }
947
1195
  // ============================================================================
948
1196
  // Claude Code SDK Handlers
949
1197
  // ============================================================================
@@ -962,27 +1210,41 @@ export class MessageHandler {
962
1210
  }
963
1211
  }
964
1212
  async handleClaudeStart(message, peerAddress) {
965
- const { prompt, cwd: payloadCwd, allowedTools, systemPrompt, model, permissionMode, sandboxed } = message.payload;
1213
+ const { prompt, cwd: payloadCwd, agent, allowedTools, systemPrompt, model, permissionMode, sandboxed } = message.payload;
1214
+ const legacyProvider = message.payload.provider;
966
1215
  const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
967
1216
  const streamId = generateMessageId();
968
- console.log(`[claude:start] cwd=${cwd} prompt=${prompt.slice(0, 80)}${sandboxed ? ' [sandboxed]' : ''}`);
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]' : ''}`);
969
1219
  try {
970
1220
  const sessionId = await this.claudeService.startSession({
971
- prompt, cwd, streamId, allowedTools, systemPrompt, model, permissionMode, sandboxed,
1221
+ prompt,
1222
+ cwd,
1223
+ streamId,
1224
+ agent: resolvedAgent,
1225
+ allowedTools,
1226
+ systemPrompt,
1227
+ model,
1228
+ permissionMode,
1229
+ sandboxed,
972
1230
  });
973
- console.log(`[claude:start] session created: ${sessionId}`);
1231
+ console.log(`[agent:start] session created: ${sessionId} agent=${resolvedAgent ?? 'default'}`);
974
1232
  this.onPeerSubscribed?.(peerAddress, sessionId);
975
1233
  // Register in session history
976
1234
  const now = Date.now();
977
1235
  const registry = getSessionRegistry();
978
1236
  const gitBranch = await this.getGitBranchQuiet(cwd);
1237
+ const actualAgent = this.claudeService.getSessionAgent(sessionId, cwd);
979
1238
  registry.upsertEntry({
980
1239
  sessionId, cwd,
1240
+ agent: actualAgent,
981
1241
  repoName: basename(cwd),
982
1242
  gitBranch,
983
1243
  firstPrompt: prompt.slice(0, 100),
984
1244
  createdAt: now,
985
1245
  lastAccessedAt: now,
1246
+ permissionMode,
1247
+ sandboxed: sandboxed || undefined,
986
1248
  });
987
1249
  this.onHistoryUpdated?.(cwd, registry.getEntry(cwd, sessionId), 'upsert');
988
1250
  const response = createMessage('claude:start:response', { success: true, sessionId, streamId });
@@ -990,26 +1252,32 @@ export class MessageHandler {
990
1252
  return response;
991
1253
  }
992
1254
  catch (error) {
993
- console.error(`[claude:start] error:`, error);
1255
+ console.error(`[agent:start] error:`, error);
994
1256
  const response = createMessage('claude:start:response', { success: false, error: error instanceof Error ? error.message : 'Failed to start session' });
995
1257
  response.id = message.id;
996
1258
  return response;
997
1259
  }
998
1260
  }
999
1261
  async handleClaudeResume(message, peerAddress) {
1000
- const { sessionId: requestedId, prompt, cwd: payloadCwd } = message.payload;
1262
+ const { sessionId: requestedId, prompt, cwd: payloadCwd, agent } = message.payload;
1263
+ const legacyProvider = message.payload.provider;
1001
1264
  const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
1002
1265
  const streamId = generateMessageId();
1003
- 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)}`);
1004
1268
  // Subscribe before resumeSession starts streaming — hot resume emits events
1005
1269
  // immediately (user_message, then assistant turns), so we must be subscribed
1006
1270
  // before session.send() fires, not after resumeSession() returns.
1007
1271
  this.onPeerSubscribed?.(peerAddress, requestedId);
1008
1272
  try {
1009
1273
  const actualSessionId = await this.claudeService.resumeSession({
1010
- sessionId: requestedId, prompt, cwd, streamId,
1274
+ sessionId: requestedId,
1275
+ prompt,
1276
+ cwd,
1277
+ streamId,
1278
+ agent: resolvedAgent,
1011
1279
  });
1012
- console.log(`[claude:resume] session resumed: ${actualSessionId}`);
1280
+ console.log(`[agent:resume] session resumed: ${actualSessionId}`);
1013
1281
  // Re-subscribe with actual session ID (cold resume may return a different ID)
1014
1282
  if (actualSessionId !== requestedId) {
1015
1283
  this.onPeerSubscribed?.(peerAddress, actualSessionId);
@@ -1018,9 +1286,11 @@ export class MessageHandler {
1018
1286
  const registry = getSessionRegistry();
1019
1287
  const existing = registry.getEntry(cwd, requestedId) ?? registry.getEntry(cwd, actualSessionId);
1020
1288
  const gitBranch = await this.getGitBranchQuiet(cwd);
1289
+ const actualAgent = this.claudeService.getSessionAgent(actualSessionId, cwd);
1021
1290
  registry.upsertEntry({
1022
1291
  ...(existing ?? { sessionId: actualSessionId, cwd, repoName: basename(cwd), createdAt: Date.now() }),
1023
1292
  sessionId: actualSessionId,
1293
+ agent: actualAgent,
1024
1294
  lastAccessedAt: Date.now(),
1025
1295
  gitBranch,
1026
1296
  });
@@ -1030,7 +1300,7 @@ export class MessageHandler {
1030
1300
  return response;
1031
1301
  }
1032
1302
  catch (error) {
1033
- console.error(`[claude:resume] error:`, error);
1303
+ console.error(`[agent:resume] error:`, error);
1034
1304
  const response = createMessage('claude:resume:response', { success: false, error: error instanceof Error ? error.message : 'Failed to resume session' });
1035
1305
  response.id = message.id;
1036
1306
  return response;
@@ -1046,14 +1316,14 @@ export class MessageHandler {
1046
1316
  handleClaudeClose(message) {
1047
1317
  const { sessionId } = message.payload;
1048
1318
  const success = this.claudeService.closeSession(sessionId);
1049
- console.log(`[claude:close] session=${sessionId} success=${success}`);
1319
+ console.log(`[agent:close] session=${sessionId} success=${success}`);
1050
1320
  const response = createMessage('claude:close:response', { success, error: success ? undefined : 'Session not found or already closed' });
1051
1321
  response.id = message.id;
1052
1322
  return response;
1053
1323
  }
1054
1324
  handleClaudeUserInputResponse(message) {
1055
1325
  const resolved = this.claudeService.resolveUserInput(message.payload);
1056
- 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}`);
1057
1327
  // No dedicated response type — just acknowledge
1058
1328
  const response = createMessage('claude:user-input-response', { success: resolved });
1059
1329
  response.id = message.id;