@varveai/adit-cli 0.3.1 → 0.3.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 (50) hide show
  1. package/dist/commands/cli-agent/claude-code-provider.d.ts +86 -0
  2. package/dist/commands/cli-agent/claude-code-provider.d.ts.map +1 -0
  3. package/dist/commands/cli-agent/claude-code-provider.js +1347 -0
  4. package/dist/commands/cli-agent/claude-code-provider.js.map +1 -0
  5. package/dist/commands/cli-agent/claude-transcript-sync.d.ts +19 -0
  6. package/dist/commands/cli-agent/claude-transcript-sync.d.ts.map +1 -0
  7. package/dist/commands/cli-agent/claude-transcript-sync.js +365 -0
  8. package/dist/commands/cli-agent/claude-transcript-sync.js.map +1 -0
  9. package/dist/commands/cli-agent/codex-app-server-client.d.ts +42 -0
  10. package/dist/commands/cli-agent/codex-app-server-client.d.ts.map +1 -0
  11. package/dist/commands/cli-agent/codex-app-server-client.js +160 -0
  12. package/dist/commands/cli-agent/codex-app-server-client.js.map +1 -0
  13. package/dist/commands/cli-agent/codex-cli-provider.d.ts +78 -0
  14. package/dist/commands/cli-agent/codex-cli-provider.d.ts.map +1 -0
  15. package/dist/commands/cli-agent/codex-cli-provider.js +974 -0
  16. package/dist/commands/cli-agent/codex-cli-provider.js.map +1 -0
  17. package/dist/commands/cli-agent/codex-transcript-sync.d.ts +34 -0
  18. package/dist/commands/cli-agent/codex-transcript-sync.d.ts.map +1 -0
  19. package/dist/commands/cli-agent/codex-transcript-sync.js +522 -0
  20. package/dist/commands/cli-agent/codex-transcript-sync.js.map +1 -0
  21. package/dist/commands/cli-agent/hook-server.d.ts +13 -0
  22. package/dist/commands/cli-agent/hook-server.d.ts.map +1 -0
  23. package/dist/commands/cli-agent/hook-server.js +74 -0
  24. package/dist/commands/cli-agent/hook-server.js.map +1 -0
  25. package/dist/commands/cli-agent/hooks-bootstrap.d.ts +17 -0
  26. package/dist/commands/cli-agent/hooks-bootstrap.d.ts.map +1 -0
  27. package/dist/commands/cli-agent/hooks-bootstrap.js +449 -0
  28. package/dist/commands/cli-agent/hooks-bootstrap.js.map +1 -0
  29. package/dist/commands/cli-agent/relay-client.d.ts +50 -0
  30. package/dist/commands/cli-agent/relay-client.d.ts.map +1 -0
  31. package/dist/commands/cli-agent/relay-client.js +202 -0
  32. package/dist/commands/cli-agent/relay-client.js.map +1 -0
  33. package/dist/commands/cli-agent/types.d.ts +58 -0
  34. package/dist/commands/cli-agent/types.d.ts.map +1 -0
  35. package/dist/commands/cli-agent/types.js +2 -0
  36. package/dist/commands/cli-agent/types.js.map +1 -0
  37. package/dist/commands/cloud-claude.d.ts +7 -0
  38. package/dist/commands/cloud-claude.d.ts.map +1 -0
  39. package/dist/commands/cloud-claude.js +331 -0
  40. package/dist/commands/cloud-claude.js.map +1 -0
  41. package/dist/commands/cloud-codex.d.ts +7 -0
  42. package/dist/commands/cloud-codex.d.ts.map +1 -0
  43. package/dist/commands/cloud-codex.js +373 -0
  44. package/dist/commands/cloud-codex.js.map +1 -0
  45. package/dist/commands/doctor.d.ts.map +1 -1
  46. package/dist/commands/doctor.js +7 -5
  47. package/dist/commands/doctor.js.map +1 -1
  48. package/dist/index.js +14 -0
  49. package/dist/index.js.map +1 -1
  50. package/package.json +9 -6
@@ -0,0 +1,1347 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { spawn } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
4
+ import fs from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import path from "node:path";
7
+ import { AbortError, query, } from "@anthropic-ai/claude-agent-sdk";
8
+ const RECLAIM_COMMAND = "/local";
9
+ const ASK_USER_QUESTION_TOOL = "AskUserQuestion";
10
+ const EXIT_PLAN_MODE_TOOL = "ExitPlanMode";
11
+ export class ClaudeCodeProvider extends EventEmitter {
12
+ opts;
13
+ provider = "claude-code";
14
+ local = null;
15
+ remoteQuery = null;
16
+ remoteAbortController = null;
17
+ remoteLoopGeneration = 0;
18
+ ownerValue = "stopped";
19
+ busyValue = false;
20
+ thinkingValue = false;
21
+ activeSessionId = null;
22
+ resumeSessionId = null;
23
+ sdkSessionId = null;
24
+ remoteSdkSessionId = null;
25
+ remoteSdkSessionIdsByActiveSession = new Map();
26
+ activeModelId = null;
27
+ promptQueue = [];
28
+ promptResolvers = [];
29
+ activePromptEvent = null;
30
+ pendingPermissions = new Map();
31
+ pendingQuestions = new Map();
32
+ boundPendingSessionIds = new Set();
33
+ lastAssistantMessageBySession = new Map();
34
+ reclaimingToLocal = false;
35
+ reclaimBuffer = "";
36
+ reclaimAttached = false;
37
+ suppressNextLocalExit = false;
38
+ constructor(opts) {
39
+ super();
40
+ this.opts = opts;
41
+ this.startLocal();
42
+ }
43
+ get state() {
44
+ return {
45
+ owner: this.ownerValue,
46
+ busy: this.busyValue,
47
+ thinking: this.thinkingValue,
48
+ activeSessionId: this.activeSessionId,
49
+ resumeSessionId: this.resumeSessionId,
50
+ sdkSessionId: this.sdkSessionId,
51
+ activeModelId: this.activeModelId,
52
+ };
53
+ }
54
+ get permissions() {
55
+ return [...this.pendingPermissions.values()].map((item) => item.request);
56
+ }
57
+ noteModel(modelId) {
58
+ if (!modelId || modelId === this.activeModelId)
59
+ return;
60
+ this.activeModelId = modelId;
61
+ this.emitState();
62
+ }
63
+ noteLocalSession(id) {
64
+ if (!id)
65
+ return;
66
+ const changed = this.activeSessionId !== id || this.resumeSessionId !== id;
67
+ this.activeSessionId = id;
68
+ this.resumeSessionId = id;
69
+ this.sdkSessionId = null;
70
+ this.remoteSdkSessionId = null;
71
+ this.refreshActiveModel({ sessionId: id });
72
+ if (!changed)
73
+ return;
74
+ this.emitState();
75
+ }
76
+ markLocalBusy() {
77
+ this.setBusy(true);
78
+ this.setThinking(true);
79
+ }
80
+ markLocalIdle() {
81
+ this.setThinking(false);
82
+ this.setBusy(false);
83
+ }
84
+ async takeover() {
85
+ if (this.ownerValue === "web")
86
+ return;
87
+ if (this.ownerValue !== "local") {
88
+ throw Object.assign(new Error("local Claude owner is not available"), {
89
+ statusCode: 409,
90
+ });
91
+ }
92
+ if (this.busyValue || this.thinkingValue) {
93
+ throw Object.assign(new Error("local Claude is busy"), {
94
+ statusCode: 409,
95
+ });
96
+ }
97
+ this.suppressNextLocalExit = true;
98
+ const oldLocal = this.local;
99
+ this.local = null;
100
+ try {
101
+ oldLocal?.kill("SIGTERM");
102
+ }
103
+ catch { }
104
+ this.ownerValue = "web";
105
+ this.reclaimingToLocal = false;
106
+ this.emitState();
107
+ process.stderr.write(`\n[adit cloud claude] Web has taken over Claude Code. Type ${RECLAIM_COMMAND} here to reclaim local control.\n`);
108
+ this.attachReclaimInput();
109
+ void this.runRemoteLoop(++this.remoteLoopGeneration);
110
+ }
111
+ async releaseToLocal() {
112
+ if (this.ownerValue !== "web")
113
+ return;
114
+ if (this.reclaimingToLocal)
115
+ return;
116
+ this.reclaimingToLocal = true;
117
+ this.detachReclaimInput();
118
+ process.stderr.write("\n[adit cloud claude] releasing Web control back to local Claude CLI...\n");
119
+ this.finishWebPrompts(new Error("Web control released to local CLI"));
120
+ for (const pending of this.pendingPermissions.values()) {
121
+ pending.reject(new Error("Web control released to local CLI"));
122
+ }
123
+ this.pendingPermissions.clear();
124
+ for (const pending of this.pendingQuestions.values()) {
125
+ pending.reject(new Error("Web control released to local CLI"));
126
+ }
127
+ this.pendingQuestions.clear();
128
+ this.pushEvent("permission-resolved", { id: "all", approved: false });
129
+ this.pushEvent("question.rejected", { id: "all" });
130
+ this.remoteAbortController?.abort();
131
+ try {
132
+ await this.remoteQuery?.interrupt?.();
133
+ }
134
+ catch { }
135
+ this.remoteQuery?.close?.();
136
+ this.remoteQuery = null;
137
+ this.remoteAbortController = null;
138
+ this.mergeRemoteTranscriptsIntoActive();
139
+ const resumeId = this.pickResumeSessionId({ fallbackToLatest: true });
140
+ this.startLocal(resumeId ? ["--resume", resumeId] : []);
141
+ }
142
+ async switchSession(sessionId) {
143
+ if (!isUuid(sessionId)) {
144
+ throw Object.assign(new Error("Claude session not found for this project"), {
145
+ statusCode: 404,
146
+ });
147
+ }
148
+ const hasLocalTranscript = isValidClaudeSession(sessionId, this.opts.cwd);
149
+ if (!hasLocalTranscript && this.ownerValue !== "web") {
150
+ throw Object.assign(new Error("Claude session not found for this project"), {
151
+ statusCode: 404,
152
+ });
153
+ }
154
+ this.activeSessionId = sessionId;
155
+ this.resumeSessionId = sessionId;
156
+ this.sdkSessionId = null;
157
+ this.remoteSdkSessionId = null;
158
+ this.refreshActiveModel({ sessionId });
159
+ this.emitState();
160
+ if (this.ownerValue === "local") {
161
+ this.suppressNextLocalExit = true;
162
+ const oldLocal = this.local;
163
+ this.local = null;
164
+ try {
165
+ oldLocal?.kill("SIGTERM");
166
+ }
167
+ catch { }
168
+ this.startLocal(["--resume", sessionId]);
169
+ return;
170
+ }
171
+ if (this.ownerValue === "web") {
172
+ const generation = ++this.remoteLoopGeneration;
173
+ this.finishWebPrompts(new Error("Claude session switched"));
174
+ this.remoteAbortController?.abort();
175
+ try {
176
+ await this.remoteQuery?.interrupt?.();
177
+ }
178
+ catch { }
179
+ this.remoteQuery?.close?.();
180
+ this.remoteQuery = null;
181
+ this.remoteAbortController = null;
182
+ this.setThinking(false);
183
+ this.setBusy(false);
184
+ void this.runRemoteLoop(generation);
185
+ }
186
+ }
187
+ async sendPrompt(prompt, opts = {}) {
188
+ if (this.ownerValue !== "web") {
189
+ throw Object.assign(new Error("Web has not taken over this Claude session"), {
190
+ statusCode: 409,
191
+ });
192
+ }
193
+ const trimmed = prompt.trim();
194
+ if (!trimmed)
195
+ return;
196
+ const sessionId = this.activeSessionId ?? this.resumeSessionId;
197
+ this.refreshActiveModel({ sessionId });
198
+ await new Promise((resolve, reject) => {
199
+ this.promptQueue.push({
200
+ message: trimmed,
201
+ mode: opts.mode === "plan" ? "plan" : "build",
202
+ pendingSessionId: opts.pendingSessionId ?? null,
203
+ promptEvent: {
204
+ text: trimmed,
205
+ createdAt: Date.now(),
206
+ },
207
+ resolve,
208
+ reject,
209
+ });
210
+ this.drainPromptResolvers();
211
+ });
212
+ }
213
+ async answerPermission(id, approved, reason) {
214
+ const pending = this.pendingPermissions.get(id);
215
+ if (!pending) {
216
+ throw Object.assign(new Error("permission request not found"), {
217
+ statusCode: 404,
218
+ });
219
+ }
220
+ this.pendingPermissions.delete(id);
221
+ this.pushEvent("permission-resolved", { id, approved });
222
+ const result = approved
223
+ ? {
224
+ behavior: "allow",
225
+ updatedInput: asRecord(pending.request.input),
226
+ toolUseID: id,
227
+ }
228
+ : {
229
+ behavior: "deny",
230
+ message: reason || "The user rejected this tool use from adit-cloud.",
231
+ toolUseID: id,
232
+ };
233
+ if (pending.request.toolName === EXIT_PLAN_MODE_TOOL) {
234
+ this.pushEvent("plan.approval.resolved", {
235
+ id,
236
+ requestID: id,
237
+ approved,
238
+ sessionId: this.activeSessionId ?? this.resumeSessionId,
239
+ });
240
+ }
241
+ pending.resolve(result);
242
+ }
243
+ async answerQuestion(response) {
244
+ const pending = this.pendingQuestions.get(response.id);
245
+ if (!pending) {
246
+ const pendingPermission = this.pendingPermissions.get(response.id);
247
+ if (pendingPermission?.request.toolName === ASK_USER_QUESTION_TOOL) {
248
+ this.pendingPermissions.delete(response.id);
249
+ this.resolveAskUserQuestionPermission(response, pendingPermission.request, pendingPermission.resolve);
250
+ return;
251
+ }
252
+ throw Object.assign(new Error("question request not found"), {
253
+ statusCode: 404,
254
+ });
255
+ }
256
+ this.pendingQuestions.delete(response.id);
257
+ this.resolveAskUserQuestionPermission(response, pending.request, pending.resolve);
258
+ }
259
+ resolveAskUserQuestionPermission(response, request, resolve) {
260
+ if (response.rejected) {
261
+ this.pushEvent("question.rejected", {
262
+ id: response.id,
263
+ requestID: response.id,
264
+ });
265
+ resolve({
266
+ behavior: "deny",
267
+ message: "The user ignored this question from adit-cloud.",
268
+ toolUseID: response.id,
269
+ });
270
+ return;
271
+ }
272
+ const inputRecord = asRecord(request.input);
273
+ const questions = normalizeQuestionInput(inputRecord);
274
+ const answers = buildQuestionAnswerMap(questions, response.answers);
275
+ this.pushEvent("question.replied", {
276
+ id: response.id,
277
+ requestID: response.id,
278
+ });
279
+ resolve({
280
+ behavior: "allow",
281
+ updatedInput: {
282
+ ...inputRecord,
283
+ answers,
284
+ },
285
+ toolUseID: response.id,
286
+ });
287
+ }
288
+ async abort() {
289
+ if (this.ownerValue !== "web")
290
+ return;
291
+ this.finishWebPrompts(new Error("Claude run aborted"));
292
+ this.remoteAbortController?.abort();
293
+ try {
294
+ await this.remoteQuery?.interrupt?.();
295
+ }
296
+ catch { }
297
+ this.remoteQuery?.close?.();
298
+ this.remoteQuery = null;
299
+ this.remoteAbortController = null;
300
+ this.setThinking(false);
301
+ this.setBusy(false);
302
+ this.pushEvent("error", { message: "Claude run aborted." });
303
+ }
304
+ stop() {
305
+ this.finishWebPrompts(new Error("Claude provider stopped"));
306
+ this.remoteAbortController?.abort();
307
+ this.remoteQuery?.close?.();
308
+ this.remoteQuery = null;
309
+ this.remoteAbortController = null;
310
+ for (const pending of this.pendingPermissions.values()) {
311
+ pending.reject(new Error("Claude provider stopped"));
312
+ }
313
+ this.pendingPermissions.clear();
314
+ for (const pending of this.pendingQuestions.values()) {
315
+ pending.reject(new Error("Claude provider stopped"));
316
+ }
317
+ this.pendingQuestions.clear();
318
+ this.detachReclaimInput();
319
+ try {
320
+ this.local?.kill("SIGTERM");
321
+ }
322
+ catch { }
323
+ this.local = null;
324
+ this.ownerValue = "stopped";
325
+ this.setThinking(false);
326
+ this.setBusy(false);
327
+ this.emitState();
328
+ }
329
+ startLocal(extraArgs = []) {
330
+ this.detachReclaimInput();
331
+ this.reclaimingToLocal = false;
332
+ this.finishWebPrompts(new Error("local mode active"));
333
+ this.remoteQuery = null;
334
+ this.remoteAbortController = null;
335
+ const args = [
336
+ ...extraArgs,
337
+ ...this.opts.args,
338
+ ...(this.opts.hookSettingsPath ? ["--settings", this.opts.hookSettingsPath] : []),
339
+ ];
340
+ const child = spawn(this.opts.bin, args, {
341
+ cwd: this.opts.cwd,
342
+ env: this.buildEnv(),
343
+ stdio: "inherit",
344
+ windowsHide: true,
345
+ });
346
+ this.local = child;
347
+ this.ownerValue = "local";
348
+ this.emitState();
349
+ child.on("error", (error) => {
350
+ this.pushEvent("error", { message: error.message });
351
+ process.stderr.write(`\n[adit cloud claude] failed to start Claude CLI: ${error.message}\n`);
352
+ });
353
+ child.on("exit", (code, signal) => {
354
+ this.setThinking(false);
355
+ this.setBusy(false);
356
+ if (this.suppressNextLocalExit) {
357
+ this.suppressNextLocalExit = false;
358
+ return;
359
+ }
360
+ if (this.local === child) {
361
+ this.local = null;
362
+ this.ownerValue = "stopped";
363
+ this.emitState();
364
+ this.emit("exit", { code, signal });
365
+ }
366
+ });
367
+ }
368
+ async runRemoteLoop(generation) {
369
+ while (this.ownerValue === "web" && this.remoteLoopGeneration === generation) {
370
+ let first;
371
+ try {
372
+ first = await this.nextPrompt();
373
+ }
374
+ catch {
375
+ return;
376
+ }
377
+ if (!first || this.ownerValue !== "web" || this.remoteLoopGeneration !== generation)
378
+ return;
379
+ const pendingSessionId = first.pendingSessionId;
380
+ const pendingClaudeSessionId = pendingSessionId ? randomUUID() : null;
381
+ const canonicalSessionId = pendingSessionId
382
+ ? pendingClaudeSessionId
383
+ : this.activeSessionId ?? this.resumeSessionId;
384
+ const resumeId = pendingSessionId
385
+ ? null
386
+ : this.pickResumeSessionId({ fallbackToLatest: false });
387
+ this.refreshActiveModel({
388
+ sessionId: canonicalSessionId ?? resumeId,
389
+ });
390
+ const abortController = new AbortController();
391
+ this.remoteAbortController = abortController;
392
+ this.activePromptEvent = first.promptEvent;
393
+ const permissionMode = first.mode === "plan" ? "plan" : "bypassPermissions";
394
+ const explicitSessionId = !resumeId && canonicalSessionId && isUuid(canonicalSessionId)
395
+ ? canonicalSessionId
396
+ : undefined;
397
+ const options = {
398
+ cwd: this.opts.cwd,
399
+ pathToClaudeCodeExecutable: this.opts.bin,
400
+ env: this.buildEnv(),
401
+ resume: resumeId ?? undefined,
402
+ sessionId: explicitSessionId,
403
+ settings: this.opts.hookSettingsPath,
404
+ permissionMode,
405
+ ...(permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}),
406
+ forkSession: false,
407
+ abortController,
408
+ includePartialMessages: true,
409
+ forwardSubagentText: true,
410
+ ...(permissionMode === "bypassPermissions"
411
+ ? {}
412
+ : {
413
+ canUseTool: (toolName, input, requestOptions) => this.handleToolPermission(toolName, input, requestOptions.toolUseID, requestOptions.signal),
414
+ }),
415
+ };
416
+ this.remoteQuery = query({
417
+ prompt: this.createPromptStream(toUserMessage(first.message, pendingClaudeSessionId ?? undefined)),
418
+ options,
419
+ });
420
+ this.setBusy(true);
421
+ this.setThinking(true);
422
+ this.emitState();
423
+ let observedSdkSessionId = null;
424
+ try {
425
+ for await (const message of this.remoteQuery) {
426
+ if (this.remoteLoopGeneration !== generation)
427
+ break;
428
+ const sdkSessionId = this.handleSdkMessage(message, canonicalSessionId ?? resumeId, {
429
+ suppressActiveFallback: Boolean(pendingSessionId),
430
+ });
431
+ const boundSessionId = sdkSessionId ?? pendingClaudeSessionId;
432
+ if (pendingSessionId && boundSessionId) {
433
+ this.bindPendingSession(pendingSessionId, boundSessionId);
434
+ }
435
+ if (sdkSessionId &&
436
+ canonicalSessionId &&
437
+ sdkSessionId !== canonicalSessionId) {
438
+ observedSdkSessionId = sdkSessionId;
439
+ }
440
+ }
441
+ }
442
+ catch (error) {
443
+ if (!(error instanceof AbortError)) {
444
+ const messageText = error instanceof Error ? error.message : String(error);
445
+ this.pushEvent("error", { message: messageText });
446
+ process.stderr.write(`\n[adit cloud claude] SDK error: ${messageText}\n`);
447
+ }
448
+ }
449
+ finally {
450
+ if (!pendingSessionId) {
451
+ this.mergeRemoteTranscriptsIntoActive(canonicalSessionId, observedSdkSessionId);
452
+ }
453
+ if (this.activePromptEvent === first.promptEvent) {
454
+ this.activePromptEvent = null;
455
+ }
456
+ this.setThinking(false);
457
+ this.setBusy(false);
458
+ this.remoteQuery = null;
459
+ this.remoteAbortController = null;
460
+ }
461
+ }
462
+ }
463
+ async *createPromptStream(first) {
464
+ yield first;
465
+ }
466
+ nextPrompt() {
467
+ if (this.promptQueue.length > 0) {
468
+ const item = this.promptQueue.shift();
469
+ if (!item)
470
+ return Promise.resolve(null);
471
+ item.resolve();
472
+ return Promise.resolve({
473
+ message: item.message,
474
+ mode: item.mode,
475
+ pendingSessionId: item.pendingSessionId,
476
+ promptEvent: item.promptEvent,
477
+ });
478
+ }
479
+ return new Promise((resolve) => {
480
+ this.promptResolvers.push(resolve);
481
+ });
482
+ }
483
+ drainPromptResolvers() {
484
+ while (this.promptResolvers.length > 0 && this.promptQueue.length > 0) {
485
+ const resolve = this.promptResolvers.shift();
486
+ const item = this.promptQueue.shift();
487
+ if (!resolve || !item)
488
+ return;
489
+ item.resolve();
490
+ resolve({
491
+ message: item.message,
492
+ mode: item.mode,
493
+ pendingSessionId: item.pendingSessionId,
494
+ promptEvent: item.promptEvent,
495
+ });
496
+ }
497
+ }
498
+ finishWebPrompts(error) {
499
+ this.activePromptEvent = null;
500
+ for (const item of this.promptQueue.splice(0)) {
501
+ item.reject(error);
502
+ }
503
+ for (const resolve of this.promptResolvers.splice(0)) {
504
+ resolve(null);
505
+ }
506
+ }
507
+ handleSdkMessage(message, canonicalSessionId, opts = {}) {
508
+ const sdkSessionId = extractSessionId(message);
509
+ if (sdkSessionId) {
510
+ this.noteSdkSession(sdkSessionId, {
511
+ keepActiveSession: Boolean(canonicalSessionId && sdkSessionId !== canonicalSessionId),
512
+ });
513
+ }
514
+ const sessionId = canonicalSessionId ??
515
+ (opts.suppressActiveFallback ? null : this.activeSessionId) ??
516
+ (opts.suppressActiveFallback ? null : this.resumeSessionId) ??
517
+ sdkSessionId ??
518
+ "pending";
519
+ if (sdkSessionId)
520
+ this.flushActivePromptEvent(sessionId);
521
+ const messageModelId = extractModelId(message);
522
+ if (messageModelId)
523
+ this.activeModelId = messageModelId;
524
+ this.emitState();
525
+ if (message.type === "system" && message.subtype === "init") {
526
+ const init = message;
527
+ if (init.session_id) {
528
+ this.noteSdkSession(init.session_id, {
529
+ keepActiveSession: Boolean(canonicalSessionId && init.session_id !== canonicalSessionId),
530
+ });
531
+ this.flushActivePromptEvent(sessionId);
532
+ }
533
+ return sdkSessionId;
534
+ }
535
+ if (message.type === "assistant") {
536
+ this.emitAssistantMessage(message, sessionId);
537
+ return sdkSessionId;
538
+ }
539
+ if (message.type === "user") {
540
+ this.emitToolResults(message, sessionId);
541
+ return sdkSessionId;
542
+ }
543
+ if (message.type === "result") {
544
+ this.emitResultUsage(message, sessionId);
545
+ this.setThinking(false);
546
+ this.setBusy(false);
547
+ }
548
+ return sdkSessionId;
549
+ }
550
+ noteSdkSession(id, opts = {}) {
551
+ if (!id)
552
+ return;
553
+ const nextActiveSessionId = opts.keepActiveSession
554
+ ? this.activeSessionId ?? this.resumeSessionId
555
+ : id;
556
+ const nextResumeSessionId = opts.keepActiveSession
557
+ ? this.resumeSessionId ?? this.activeSessionId
558
+ : id;
559
+ const changed = this.sdkSessionId !== id ||
560
+ this.remoteSdkSessionId !== id ||
561
+ this.activeSessionId !== nextActiveSessionId ||
562
+ this.resumeSessionId !== nextResumeSessionId;
563
+ this.sdkSessionId = id;
564
+ this.remoteSdkSessionId = id;
565
+ this.activeSessionId = nextActiveSessionId;
566
+ this.resumeSessionId = nextResumeSessionId;
567
+ if (nextActiveSessionId) {
568
+ this.rememberRemoteSdkSession(nextActiveSessionId, id);
569
+ }
570
+ this.refreshActiveModel({ sessionId: id });
571
+ if (changed)
572
+ this.emitState();
573
+ }
574
+ bindPendingSession(pendingSessionId, sessionId) {
575
+ if (!pendingSessionId || !sessionId)
576
+ return;
577
+ const changed = this.activeSessionId !== sessionId ||
578
+ this.resumeSessionId !== sessionId ||
579
+ this.sdkSessionId !== sessionId ||
580
+ this.remoteSdkSessionId !== sessionId;
581
+ this.activeSessionId = sessionId;
582
+ this.resumeSessionId = sessionId;
583
+ this.sdkSessionId = sessionId;
584
+ this.remoteSdkSessionId = sessionId;
585
+ this.refreshActiveModel({ sessionId });
586
+ if (!this.boundPendingSessionIds.has(pendingSessionId)) {
587
+ this.boundPendingSessionIds.add(pendingSessionId);
588
+ this.pushEvent("session-bound", {
589
+ pendingSessionId,
590
+ sessionId,
591
+ createdAt: Date.now(),
592
+ });
593
+ }
594
+ if (changed)
595
+ this.emitState();
596
+ }
597
+ flushActivePromptEvent(sessionId) {
598
+ if (!this.activePromptEvent)
599
+ return;
600
+ const prompt = this.activePromptEvent;
601
+ this.activePromptEvent = null;
602
+ this.pushEvent("message", {
603
+ role: "user",
604
+ sessionId,
605
+ text: prompt.text,
606
+ createdAt: prompt.createdAt,
607
+ });
608
+ }
609
+ emitAssistantMessage(message, sessionId) {
610
+ const content = message.message?.content;
611
+ if (!Array.isArray(content))
612
+ return;
613
+ const messageId = makeMessageId(message, sessionId);
614
+ const modelId = typeof message.message?.model === "string"
615
+ ? message.message.model
616
+ : undefined;
617
+ if (modelId)
618
+ this.activeModelId = modelId;
619
+ const usage = normalizeClaudeUsage(message.message.usage);
620
+ this.lastAssistantMessageBySession.set(sessionId, messageId);
621
+ for (const part of content) {
622
+ if (part.type === "text" && typeof part.text === "string" && part.text) {
623
+ this.pushEvent("message", {
624
+ role: "assistant",
625
+ sessionId,
626
+ messageId,
627
+ modelId,
628
+ usage,
629
+ text: part.text,
630
+ createdAt: Date.now(),
631
+ });
632
+ }
633
+ else if (part.type === "thinking" &&
634
+ typeof part.thinking === "string" &&
635
+ part.thinking) {
636
+ this.pushEvent("reasoning", {
637
+ sessionId,
638
+ messageId,
639
+ modelId,
640
+ usage,
641
+ text: part.thinking,
642
+ createdAt: Date.now(),
643
+ });
644
+ }
645
+ else if (part.type === "tool_use" || part.type === "server_tool_use") {
646
+ this.pushEvent("tool", {
647
+ sessionId,
648
+ messageId,
649
+ modelId,
650
+ usage,
651
+ toolUseId: typeof part.id === "string" ? part.id : undefined,
652
+ toolName: typeof part.name === "string" ? part.name : "tool",
653
+ input: part.input ?? {},
654
+ status: "running",
655
+ createdAt: Date.now(),
656
+ });
657
+ }
658
+ }
659
+ }
660
+ emitResultUsage(message, fallbackSessionId) {
661
+ const result = message;
662
+ const usage = normalizeClaudeUsage(result.usage);
663
+ if (!usage)
664
+ return;
665
+ const sessionId = fallbackSessionId ||
666
+ (typeof result.session_id === "string"
667
+ ? result.session_id
668
+ : typeof result.sessionId === "string"
669
+ ? result.sessionId
670
+ : fallbackSessionId);
671
+ this.pushEvent("usage", {
672
+ sessionId,
673
+ messageId: this.lastAssistantMessageBySession.get(sessionId),
674
+ usage,
675
+ cost: typeof result.total_cost_usd === "number" && Number.isFinite(result.total_cost_usd)
676
+ ? result.total_cost_usd
677
+ : undefined,
678
+ createdAt: Date.now(),
679
+ });
680
+ }
681
+ emitToolResults(message, sessionId) {
682
+ const content = message.message?.content;
683
+ if (!Array.isArray(content))
684
+ return;
685
+ const messageId = makeMessageId(message, sessionId);
686
+ for (const part of content) {
687
+ if (part.type !== "tool_result")
688
+ continue;
689
+ const toolUseId = typeof part.tool_use_id === "string"
690
+ ? part.tool_use_id
691
+ : typeof part.toolUseID === "string"
692
+ ? part.toolUseID
693
+ : "tool";
694
+ this.pushEvent("tool", {
695
+ sessionId,
696
+ messageId,
697
+ toolUseId,
698
+ toolName: "tool",
699
+ input: {},
700
+ output: formatToolResult(part.content),
701
+ status: "completed",
702
+ createdAt: Date.now(),
703
+ });
704
+ }
705
+ }
706
+ handleToolPermission(toolName, input, id, signal) {
707
+ if (toolName === ASK_USER_QUESTION_TOOL) {
708
+ return this.handleUserQuestion(input, id, signal);
709
+ }
710
+ if (toolName === EXIT_PLAN_MODE_TOOL) {
711
+ return this.handlePlanApproval(input, id, signal);
712
+ }
713
+ return new Promise((resolve, reject) => {
714
+ const request = {
715
+ id,
716
+ toolName,
717
+ input,
718
+ createdAt: Date.now(),
719
+ };
720
+ const abort = () => {
721
+ this.pendingPermissions.delete(id);
722
+ this.pushEvent("permission-resolved", { id, approved: false });
723
+ reject(new Error("permission request aborted"));
724
+ };
725
+ signal.addEventListener("abort", abort, { once: true });
726
+ this.pendingPermissions.set(id, {
727
+ request,
728
+ resolve: (result) => {
729
+ signal.removeEventListener("abort", abort);
730
+ resolve(result);
731
+ },
732
+ reject: (error) => {
733
+ signal.removeEventListener("abort", abort);
734
+ reject(error);
735
+ },
736
+ });
737
+ this.pushEvent("permission", {
738
+ id,
739
+ toolName,
740
+ input,
741
+ createdAt: request.createdAt,
742
+ sessionId: this.activeSessionId ?? this.resumeSessionId,
743
+ });
744
+ this.emitState();
745
+ });
746
+ }
747
+ handlePlanApproval(input, id, signal) {
748
+ return new Promise((resolve, reject) => {
749
+ const request = {
750
+ id,
751
+ toolName: EXIT_PLAN_MODE_TOOL,
752
+ input,
753
+ createdAt: Date.now(),
754
+ };
755
+ const abort = () => {
756
+ this.pendingPermissions.delete(id);
757
+ this.pushEvent("permission-resolved", { id, approved: false });
758
+ this.pushEvent("plan.approval.resolved", {
759
+ id,
760
+ requestID: id,
761
+ approved: false,
762
+ sessionId: this.activeSessionId ?? this.resumeSessionId,
763
+ });
764
+ reject(new Error("plan approval request aborted"));
765
+ };
766
+ signal.addEventListener("abort", abort, { once: true });
767
+ this.pendingPermissions.set(id, {
768
+ request,
769
+ resolve: (result) => {
770
+ signal.removeEventListener("abort", abort);
771
+ resolve(result);
772
+ },
773
+ reject: (error) => {
774
+ signal.removeEventListener("abort", abort);
775
+ reject(error);
776
+ },
777
+ });
778
+ const inputRecord = asRecord(input);
779
+ this.pushEvent("plan.approval.requested", {
780
+ id,
781
+ requestID: id,
782
+ sessionID: this.activeSessionId ?? this.resumeSessionId,
783
+ sessionId: this.activeSessionId ?? this.resumeSessionId,
784
+ input: inputRecord,
785
+ plan: readString(inputRecord.plan),
786
+ allowedPrompts: Array.isArray(inputRecord.allowedPrompts)
787
+ ? inputRecord.allowedPrompts
788
+ : undefined,
789
+ tool: {
790
+ messageID: this.lastAssistantMessageBySession.get(this.activeSessionId ?? this.resumeSessionId ?? "") ?? "",
791
+ callID: id,
792
+ },
793
+ createdAt: request.createdAt,
794
+ });
795
+ this.pushEvent("permission", {
796
+ id,
797
+ toolName: EXIT_PLAN_MODE_TOOL,
798
+ input,
799
+ createdAt: request.createdAt,
800
+ sessionId: this.activeSessionId ?? this.resumeSessionId,
801
+ });
802
+ this.emitState();
803
+ });
804
+ }
805
+ handleUserQuestion(input, id, signal) {
806
+ const inputRecord = asRecord(input);
807
+ const questions = normalizeQuestionInput(inputRecord);
808
+ if (questions.length === 0) {
809
+ return Promise.resolve({
810
+ behavior: "deny",
811
+ message: "AskUserQuestion did not include any valid questions.",
812
+ toolUseID: id,
813
+ });
814
+ }
815
+ return new Promise((resolve, reject) => {
816
+ const request = {
817
+ id,
818
+ input: inputRecord,
819
+ createdAt: Date.now(),
820
+ };
821
+ const abort = () => {
822
+ this.pendingQuestions.delete(id);
823
+ this.pushEvent("question.rejected", {
824
+ id,
825
+ requestID: id,
826
+ sessionId: this.activeSessionId ?? this.resumeSessionId,
827
+ });
828
+ reject(new Error("question request aborted"));
829
+ };
830
+ signal.addEventListener("abort", abort, { once: true });
831
+ this.pendingQuestions.set(id, {
832
+ request,
833
+ resolve: (result) => {
834
+ signal.removeEventListener("abort", abort);
835
+ resolve(result);
836
+ },
837
+ reject: (error) => {
838
+ signal.removeEventListener("abort", abort);
839
+ reject(error);
840
+ },
841
+ });
842
+ this.pushEvent("question.asked", {
843
+ id,
844
+ sessionID: this.activeSessionId ?? this.resumeSessionId,
845
+ sessionId: this.activeSessionId ?? this.resumeSessionId,
846
+ questions,
847
+ tool: {
848
+ messageID: this.lastAssistantMessageBySession.get(this.activeSessionId ?? this.resumeSessionId ?? "") ?? "",
849
+ callID: id,
850
+ },
851
+ createdAt: request.createdAt,
852
+ });
853
+ this.emitState();
854
+ });
855
+ }
856
+ attachReclaimInput() {
857
+ if (this.reclaimAttached || !process.stdin.isTTY)
858
+ return;
859
+ this.reclaimAttached = true;
860
+ this.reclaimBuffer = "";
861
+ try {
862
+ process.stdin.setEncoding("utf8");
863
+ process.stdin.resume();
864
+ process.stdin.on("data", this.onReclaimInput);
865
+ }
866
+ catch {
867
+ this.reclaimAttached = false;
868
+ }
869
+ }
870
+ detachReclaimInput() {
871
+ if (!this.reclaimAttached)
872
+ return;
873
+ this.reclaimAttached = false;
874
+ process.stdin.off("data", this.onReclaimInput);
875
+ this.reclaimBuffer = "";
876
+ try {
877
+ process.stdin.pause();
878
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
879
+ process.stdin.setRawMode(false);
880
+ }
881
+ }
882
+ catch { }
883
+ }
884
+ onReclaimInput = (chunk) => {
885
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
886
+ if (text === "\u0003") {
887
+ this.stop();
888
+ return;
889
+ }
890
+ this.reclaimBuffer += text;
891
+ if (this.reclaimBuffer.length > 200) {
892
+ this.reclaimBuffer = this.reclaimBuffer.slice(-200);
893
+ }
894
+ if (this.reclaimBuffer.includes(RECLAIM_COMMAND)) {
895
+ this.reclaimBuffer = "";
896
+ void this.releaseToLocal();
897
+ return;
898
+ }
899
+ if (text.includes("\n") || text.includes("\r")) {
900
+ process.stderr.write(`[adit cloud claude] Web owns this session. Type ${RECLAIM_COMMAND} to reclaim.\n`);
901
+ }
902
+ };
903
+ pickResumeSessionId(opts) {
904
+ const candidates = [
905
+ this.resumeSessionId,
906
+ this.activeSessionId,
907
+ this.remoteSdkSessionId,
908
+ this.sdkSessionId,
909
+ ];
910
+ for (const id of candidates) {
911
+ if (id && isValidClaudeSession(id, this.opts.cwd)) {
912
+ this.resumeSessionId = id;
913
+ this.activeSessionId = id;
914
+ this.refreshActiveModel({ sessionId: id });
915
+ return id;
916
+ }
917
+ }
918
+ if (!opts.fallbackToLatest)
919
+ return null;
920
+ const latest = findLastClaudeSession(this.opts.cwd);
921
+ if (latest) {
922
+ this.resumeSessionId = latest;
923
+ this.activeSessionId = latest;
924
+ this.refreshActiveModel({ sessionId: latest });
925
+ this.emitState();
926
+ return latest;
927
+ }
928
+ this.resumeSessionId = null;
929
+ return null;
930
+ }
931
+ setBusy(value) {
932
+ if (this.busyValue === value)
933
+ return;
934
+ this.busyValue = value;
935
+ this.emitState();
936
+ }
937
+ setThinking(value) {
938
+ if (this.thinkingValue === value)
939
+ return;
940
+ this.thinkingValue = value;
941
+ this.emitState();
942
+ }
943
+ emitState() {
944
+ this.emit("state", this.state);
945
+ this.pushEvent("state", {
946
+ owner: this.ownerValue,
947
+ busy: this.busyValue,
948
+ thinking: this.thinkingValue,
949
+ activeSessionId: this.activeSessionId,
950
+ resumeSessionId: this.resumeSessionId,
951
+ sdkSessionId: this.sdkSessionId,
952
+ activeModelId: this.activeModelId,
953
+ createdAt: Date.now(),
954
+ });
955
+ }
956
+ pushEvent(type, payload) {
957
+ this.opts.onEvent?.({ type, payload });
958
+ }
959
+ buildEnv() {
960
+ return {
961
+ ...process.env,
962
+ TERM: process.env.TERM || "xterm-256color",
963
+ COLORTERM: process.env.COLORTERM || "truecolor",
964
+ FORCE_COLOR: process.env.FORCE_COLOR || "3",
965
+ DISABLE_AUTOUPDATER: "1",
966
+ };
967
+ }
968
+ refreshActiveModel(opts = {}) {
969
+ const modelId = resolveClaudeModel({
970
+ cwd: this.opts.cwd,
971
+ sessionId: opts.sessionId ?? this.activeSessionId ?? this.resumeSessionId,
972
+ });
973
+ if (modelId && modelId !== this.activeModelId) {
974
+ this.activeModelId = modelId;
975
+ }
976
+ return this.activeModelId;
977
+ }
978
+ rememberRemoteSdkSession(activeSessionId, sdkSessionId) {
979
+ if (!activeSessionId || !sdkSessionId || activeSessionId === sdkSessionId)
980
+ return;
981
+ let ids = this.remoteSdkSessionIdsByActiveSession.get(activeSessionId);
982
+ if (!ids) {
983
+ ids = new Set();
984
+ this.remoteSdkSessionIdsByActiveSession.set(activeSessionId, ids);
985
+ }
986
+ ids.add(sdkSessionId);
987
+ }
988
+ mergeRemoteTranscriptsIntoActive(activeSessionId = this.activeSessionId, remoteSessionId = this.remoteSdkSessionId ?? this.sdkSessionId) {
989
+ if (!activeSessionId)
990
+ return;
991
+ const remoteSessionIds = new Set();
992
+ const remembered = this.remoteSdkSessionIdsByActiveSession.get(activeSessionId);
993
+ if (remembered) {
994
+ for (const id of remembered)
995
+ remoteSessionIds.add(id);
996
+ }
997
+ if (remoteSessionId)
998
+ remoteSessionIds.add(remoteSessionId);
999
+ if (this.remoteSdkSessionId)
1000
+ remoteSessionIds.add(this.remoteSdkSessionId);
1001
+ if (this.sdkSessionId)
1002
+ remoteSessionIds.add(this.sdkSessionId);
1003
+ for (const id of remoteSessionIds) {
1004
+ if (!id || id === activeSessionId)
1005
+ continue;
1006
+ mergeClaudeSessionTranscript({
1007
+ cwd: this.opts.cwd,
1008
+ sourceSessionId: id,
1009
+ targetSessionId: activeSessionId,
1010
+ });
1011
+ }
1012
+ }
1013
+ }
1014
+ function toUserMessage(message, sessionId) {
1015
+ return {
1016
+ type: "user",
1017
+ ...(sessionId ? { session_id: sessionId } : {}),
1018
+ parent_tool_use_id: null,
1019
+ message: {
1020
+ role: "user",
1021
+ content: message,
1022
+ },
1023
+ };
1024
+ }
1025
+ function extractSessionId(message) {
1026
+ const maybe = message;
1027
+ if (typeof maybe.session_id === "string")
1028
+ return maybe.session_id;
1029
+ if (typeof maybe.sessionId === "string")
1030
+ return maybe.sessionId;
1031
+ return null;
1032
+ }
1033
+ function extractModelId(message) {
1034
+ const maybe = message;
1035
+ if (typeof maybe.message?.model === "string" && maybe.message.model.trim()) {
1036
+ return maybe.message.model.trim();
1037
+ }
1038
+ if (typeof maybe.model === "string" && maybe.model.trim()) {
1039
+ return maybe.model.trim();
1040
+ }
1041
+ return null;
1042
+ }
1043
+ function makeMessageId(message, sessionId) {
1044
+ const maybe = message;
1045
+ if (typeof maybe.message?.id === "string")
1046
+ return maybe.message.id;
1047
+ if (typeof maybe.uuid === "string")
1048
+ return maybe.uuid;
1049
+ return `claude-${sessionId}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1050
+ }
1051
+ function normalizeClaudeUsage(value) {
1052
+ if (!value || typeof value !== "object" || Array.isArray(value))
1053
+ return undefined;
1054
+ const usage = value;
1055
+ const normalized = {};
1056
+ for (const key of [
1057
+ "input_tokens",
1058
+ "output_tokens",
1059
+ "cache_read_input_tokens",
1060
+ "cache_creation_input_tokens",
1061
+ "reasoning_tokens",
1062
+ ]) {
1063
+ const numberValue = usage[key];
1064
+ if (typeof numberValue === "number" && Number.isFinite(numberValue)) {
1065
+ normalized[key] = numberValue;
1066
+ }
1067
+ }
1068
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
1069
+ }
1070
+ function asRecord(value) {
1071
+ return value && typeof value === "object" && !Array.isArray(value)
1072
+ ? value
1073
+ : {};
1074
+ }
1075
+ function normalizeQuestionInput(input) {
1076
+ const rawQuestions = Array.isArray(input.questions) ? input.questions : [];
1077
+ const questions = [];
1078
+ for (const rawQuestion of rawQuestions) {
1079
+ const questionRecord = asRecord(rawQuestion);
1080
+ const question = readString(questionRecord.question);
1081
+ if (!question)
1082
+ continue;
1083
+ const options = (Array.isArray(questionRecord.options) ? questionRecord.options : [])
1084
+ .map((rawOption) => {
1085
+ const optionRecord = asRecord(rawOption);
1086
+ const label = readString(optionRecord.label);
1087
+ if (!label)
1088
+ return null;
1089
+ return {
1090
+ label,
1091
+ description: readString(optionRecord.description) ?? "",
1092
+ };
1093
+ })
1094
+ .filter((option) => Boolean(option));
1095
+ if (options.length === 0)
1096
+ continue;
1097
+ questions.push({
1098
+ question,
1099
+ header: readString(questionRecord.header) ?? "Question",
1100
+ options,
1101
+ multiple: questionRecord.multiSelect === true,
1102
+ });
1103
+ }
1104
+ return questions;
1105
+ }
1106
+ function buildQuestionAnswerMap(questions, answers) {
1107
+ const result = {};
1108
+ questions.forEach((question, index) => {
1109
+ const answer = answers[index] ?? [];
1110
+ result[question.question] = answer.filter(Boolean).join(", ");
1111
+ });
1112
+ return result;
1113
+ }
1114
+ function formatToolResult(content) {
1115
+ if (typeof content === "string")
1116
+ return content;
1117
+ try {
1118
+ return JSON.stringify(content, null, 2);
1119
+ }
1120
+ catch {
1121
+ return String(content);
1122
+ }
1123
+ }
1124
+ function getClaudeProjectDir(cwd) {
1125
+ const projectId = path.resolve(cwd).replace(/[^a-zA-Z0-9-]/g, "-");
1126
+ const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), ".claude");
1127
+ return path.join(claudeConfigDir, "projects", projectId);
1128
+ }
1129
+ function getClaudeConfigDir() {
1130
+ return process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), ".claude");
1131
+ }
1132
+ function resolveClaudeModel(input) {
1133
+ return (readLastSessionModel(input.cwd, input.sessionId) ??
1134
+ readSettingsModel(path.join(input.cwd, ".claude", "settings.local.json")) ??
1135
+ readSettingsModel(path.join(input.cwd, ".claude", "settings.json")) ??
1136
+ readSettingsModel(path.join(getClaudeConfigDir(), "settings.json")) ??
1137
+ readString(process.env.ANTHROPIC_MODEL));
1138
+ }
1139
+ function readLastSessionModel(cwd, sessionId) {
1140
+ if (!sessionId || !isUuid(sessionId))
1141
+ return null;
1142
+ const file = path.join(getClaudeProjectDir(cwd), `${sessionId}.jsonl`);
1143
+ let text;
1144
+ try {
1145
+ text = fs.readFileSync(file, "utf8");
1146
+ }
1147
+ catch {
1148
+ return null;
1149
+ }
1150
+ let lastModel = null;
1151
+ for (const line of text.split("\n")) {
1152
+ if (!line.trim())
1153
+ continue;
1154
+ try {
1155
+ const parsed = JSON.parse(line);
1156
+ const obj = asRecord(parsed);
1157
+ const message = asRecord(obj.message);
1158
+ const model = readString(message.model) ?? readString(obj.model);
1159
+ if (model)
1160
+ lastModel = model;
1161
+ }
1162
+ catch { }
1163
+ }
1164
+ return lastModel;
1165
+ }
1166
+ function readSettingsModel(file) {
1167
+ let parsed;
1168
+ try {
1169
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
1170
+ }
1171
+ catch {
1172
+ return null;
1173
+ }
1174
+ const settings = asRecord(parsed);
1175
+ const env = asRecord(settings.env);
1176
+ return (readString(env.ANTHROPIC_MODEL) ??
1177
+ readString(settings.model) ??
1178
+ readString(env.ANTHROPIC_DEFAULT_SONNET_MODEL) ??
1179
+ readString(env.ANTHROPIC_DEFAULT_OPUS_MODEL) ??
1180
+ readString(env.ANTHROPIC_DEFAULT_HAIKU_MODEL));
1181
+ }
1182
+ function readString(value) {
1183
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1184
+ }
1185
+ function isUuid(value) {
1186
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
1187
+ }
1188
+ function isValidClaudeSession(sessionId, cwd) {
1189
+ if (!isUuid(sessionId))
1190
+ return false;
1191
+ const file = path.join(getClaudeProjectDir(cwd), `${sessionId}.jsonl`);
1192
+ let text;
1193
+ try {
1194
+ text = fs.readFileSync(file, "utf8");
1195
+ }
1196
+ catch {
1197
+ return false;
1198
+ }
1199
+ for (const line of text.split("\n")) {
1200
+ if (!line.trim())
1201
+ continue;
1202
+ try {
1203
+ const obj = JSON.parse(line);
1204
+ if (typeof obj?.uuid === "string" && obj.uuid.length > 0)
1205
+ return true;
1206
+ if (typeof obj?.messageId === "string" && obj.messageId.length > 0)
1207
+ return true;
1208
+ if (typeof obj?.leafUuid === "string" && obj.leafUuid.length > 0)
1209
+ return true;
1210
+ if (typeof obj?.message?.id === "string" && obj.message.id.length > 0)
1211
+ return true;
1212
+ }
1213
+ catch { }
1214
+ }
1215
+ return false;
1216
+ }
1217
+ function mergeClaudeSessionTranscript(input) {
1218
+ if (!isUuid(input.sourceSessionId) ||
1219
+ !isUuid(input.targetSessionId) ||
1220
+ input.sourceSessionId === input.targetSessionId) {
1221
+ return;
1222
+ }
1223
+ const projectDir = getClaudeProjectDir(input.cwd);
1224
+ const sourceFile = path.join(projectDir, `${input.sourceSessionId}.jsonl`);
1225
+ const targetFile = path.join(projectDir, `${input.targetSessionId}.jsonl`);
1226
+ let sourceText;
1227
+ try {
1228
+ sourceText = fs.readFileSync(sourceFile, "utf8");
1229
+ }
1230
+ catch {
1231
+ return;
1232
+ }
1233
+ let targetText = "";
1234
+ try {
1235
+ targetText = fs.readFileSync(targetFile, "utf8");
1236
+ }
1237
+ catch { }
1238
+ const targetKeys = new Set();
1239
+ for (const line of targetText.split("\n")) {
1240
+ const key = transcriptLineKey(line);
1241
+ if (key)
1242
+ targetKeys.add(key);
1243
+ }
1244
+ const linesToAppend = [];
1245
+ let lastTargetUuid = readLastTranscriptUuid(targetText);
1246
+ for (const line of sourceText.split("\n")) {
1247
+ if (!line.trim())
1248
+ continue;
1249
+ const normalized = normalizeTranscriptLineForSession(line, input.sourceSessionId, input.targetSessionId, lastTargetUuid);
1250
+ if (!normalized)
1251
+ continue;
1252
+ const key = transcriptLineKey(normalized);
1253
+ if (key && targetKeys.has(key))
1254
+ continue;
1255
+ if (key)
1256
+ targetKeys.add(key);
1257
+ linesToAppend.push(normalized);
1258
+ const uuid = readTranscriptUuid(normalized);
1259
+ if (uuid)
1260
+ lastTargetUuid = uuid;
1261
+ }
1262
+ if (linesToAppend.length === 0)
1263
+ return;
1264
+ fs.mkdirSync(projectDir, { recursive: true });
1265
+ const prefix = targetText && !targetText.endsWith("\n") ? "\n" : "";
1266
+ fs.appendFileSync(targetFile, `${prefix}${linesToAppend.join("\n")}\n`);
1267
+ }
1268
+ function normalizeTranscriptLineForSession(line, sourceSessionId, targetSessionId, fallbackParentUuid) {
1269
+ let parsed;
1270
+ try {
1271
+ parsed = JSON.parse(line);
1272
+ }
1273
+ catch {
1274
+ return null;
1275
+ }
1276
+ const obj = asRecord(parsed);
1277
+ if (readString(obj.sessionId) !== sourceSessionId)
1278
+ return null;
1279
+ obj.sessionId = targetSessionId;
1280
+ if (obj.parentUuid === null && fallbackParentUuid) {
1281
+ obj.parentUuid = fallbackParentUuid;
1282
+ }
1283
+ return JSON.stringify(obj);
1284
+ }
1285
+ function transcriptLineKey(line) {
1286
+ if (!line.trim())
1287
+ return null;
1288
+ let parsed;
1289
+ try {
1290
+ parsed = JSON.parse(line);
1291
+ }
1292
+ catch {
1293
+ return line.trim();
1294
+ }
1295
+ const obj = asRecord(parsed);
1296
+ return (readString(obj.uuid) ??
1297
+ readString(obj.messageId) ??
1298
+ readString(obj.leafUuid) ??
1299
+ readString(obj.promptId) ??
1300
+ line.trim());
1301
+ }
1302
+ function readTranscriptUuid(line) {
1303
+ let parsed;
1304
+ try {
1305
+ parsed = JSON.parse(line);
1306
+ }
1307
+ catch {
1308
+ return null;
1309
+ }
1310
+ return readString(asRecord(parsed).uuid);
1311
+ }
1312
+ function readLastTranscriptUuid(text) {
1313
+ let uuid = null;
1314
+ for (const line of text.split("\n")) {
1315
+ const next = readTranscriptUuid(line);
1316
+ if (next)
1317
+ uuid = next;
1318
+ }
1319
+ return uuid;
1320
+ }
1321
+ function findLastClaudeSession(cwd) {
1322
+ const projectDir = getClaudeProjectDir(cwd);
1323
+ let files;
1324
+ try {
1325
+ files = fs.readdirSync(projectDir);
1326
+ }
1327
+ catch {
1328
+ return null;
1329
+ }
1330
+ const candidates = files
1331
+ .filter((name) => name.endsWith(".jsonl"))
1332
+ .map((name) => {
1333
+ const id = name.slice(0, -".jsonl".length);
1334
+ if (!isUuid(id) || !isValidClaudeSession(id, cwd))
1335
+ return null;
1336
+ try {
1337
+ return { id, mtime: fs.statSync(path.join(projectDir, name)).mtimeMs };
1338
+ }
1339
+ catch {
1340
+ return null;
1341
+ }
1342
+ })
1343
+ .filter((item) => Boolean(item))
1344
+ .sort((a, b) => b.mtime - a.mtime);
1345
+ return candidates[0]?.id ?? null;
1346
+ }
1347
+ //# sourceMappingURL=claude-code-provider.js.map