glad-web 1.0.29 → 1.0.31

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.
@@ -0,0 +1,95 @@
1
+ function stripTerminalFormatting(value) {
2
+ return String(value || '')
3
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
4
+ .replace(/\r/g, '');
5
+ }
6
+
7
+ function parseTokenCount(value) {
8
+ const text = String(value || '').trim().replace(/,/g, '');
9
+ const match = text.match(/^([\d.]+)\s*([kmb])?$/i);
10
+ if (!match) return null;
11
+ const amount = Number(match[1]);
12
+ if (!Number.isFinite(amount)) return null;
13
+ const multiplier = { k: 1_000, m: 1_000_000, b: 1_000_000_000 }[String(match[2] || '').toLowerCase()] || 1;
14
+ return Math.round(amount * multiplier);
15
+ }
16
+
17
+ function parseClaudeUsageOutput(output) {
18
+ const text = stripTerminalFormatting(output);
19
+ const cost = text.match(/Total cost:\s*\$([\d,.]+)/i);
20
+ const apiDuration = text.match(/Total duration \(API\):\s*([^\n]+)/i);
21
+ const wallDuration = text.match(/Total duration \(wall\):\s*([^\n]+)/i);
22
+ const codeChanges = text.match(/Total code changes:\s*([\d,]+) lines added,\s*([\d,]+) lines removed/i);
23
+ const tokens = text.match(/Usage:\s*([\d,.kmb]+) input,\s*([\d,.kmb]+) output,\s*([\d,.kmb]+) cache read,\s*([\d,.kmb]+) cache write/i);
24
+ const models = [];
25
+ const modelPattern = /^\s*(.+?):\s*([\d,.kmb]+) input,\s*([\d,.kmb]+) output,\s*([\d,.kmb]+) cache read,\s*([\d,.kmb]+) cache write(?:\s*\(\$([\d,.]+)\))?\s*$/gim;
26
+ for (const match of text.matchAll(modelPattern)) {
27
+ if (/^Usage$/i.test(match[1].trim())) continue;
28
+ models.push({
29
+ model: match[1].trim(),
30
+ inputTokens: parseTokenCount(match[2]),
31
+ outputTokens: parseTokenCount(match[3]),
32
+ cacheReadTokens: parseTokenCount(match[4]),
33
+ cacheWriteTokens: parseTokenCount(match[5]),
34
+ costUsd: match[6] ? Number(match[6].replace(/,/g, '')) : null
35
+ });
36
+ }
37
+
38
+ if (!cost && !apiDuration && !wallDuration && !codeChanges && !tokens && models.length === 0) {
39
+ throw new Error('Claude CLI returned an unrecognized /usage response');
40
+ }
41
+
42
+ const totalModelValue = key => models.length > 0
43
+ ? models.reduce((sum, model) => sum + Number(model[key] || 0), 0)
44
+ : null;
45
+
46
+ return {
47
+ totalCostUsd: cost ? Number(cost[1].replace(/,/g, '')) : null,
48
+ apiDuration: apiDuration ? apiDuration[1].trim() : null,
49
+ wallDuration: wallDuration ? wallDuration[1].trim() : null,
50
+ linesAdded: codeChanges ? Number(codeChanges[1].replace(/,/g, '')) : null,
51
+ linesRemoved: codeChanges ? Number(codeChanges[2].replace(/,/g, '')) : null,
52
+ inputTokens: tokens ? parseTokenCount(tokens[1]) : totalModelValue('inputTokens'),
53
+ outputTokens: tokens ? parseTokenCount(tokens[2]) : totalModelValue('outputTokens'),
54
+ cacheReadTokens: tokens ? parseTokenCount(tokens[3]) : totalModelValue('cacheReadTokens'),
55
+ cacheWriteTokens: tokens ? parseTokenCount(tokens[4]) : totalModelValue('cacheWriteTokens'),
56
+ models
57
+ };
58
+ }
59
+
60
+ function parseClaudeContextOutput(output) {
61
+ const text = stripTerminalFormatting(output);
62
+ const model = text.match(/^\*\*Model:\*\*\s*(.+?)\s*$/mi);
63
+ const tokens = text.match(/^\*\*Tokens:\*\*\s*([\d,.]+\s*[kmb]?)\s*\/\s*([\d,.]+\s*[kmb]?)\s*\(([\d.]+)%\)/mi);
64
+ if (!tokens) throw new Error('Claude CLI returned an unrecognized /context response');
65
+
66
+ const usedTokens = parseTokenCount(tokens[1]);
67
+ const maxTokens = parseTokenCount(tokens[2]);
68
+ const categories = [];
69
+ for (const line of text.split('\n')) {
70
+ const columns = line.split('|').slice(1, -1).map(value => value.trim());
71
+ if (columns.length < 3 || !columns[0] || /^category$/i.test(columns[0]) || /^-+$/.test(columns[0])) continue;
72
+ const categoryTokens = parseTokenCount(columns[1]);
73
+ if (categoryTokens == null || !/^<?[\d.]+%$/.test(columns[2])) continue;
74
+ categories.push({
75
+ label: columns[0].replace(/\*\*/g, ''),
76
+ tokens: categoryTokens,
77
+ percent: columns[2]
78
+ });
79
+ }
80
+
81
+ return {
82
+ model: model ? model[1].trim() : null,
83
+ usedTokens,
84
+ maxTokens,
85
+ usedPercent: Number(tokens[3]),
86
+ remainingTokens: usedTokens == null || maxTokens == null ? null : Math.max(0, maxTokens - usedTokens),
87
+ categories
88
+ };
89
+ }
90
+
91
+ module.exports = {
92
+ parseClaudeContextOutput,
93
+ parseClaudeUsageOutput,
94
+ parseTokenCount
95
+ };
@@ -1,11 +1,13 @@
1
1
  const { EventEmitter } = require('events');
2
2
  const crypto = require('crypto');
3
3
  const { normalizeEffort, normalizeModel, resolveClaudeModel } = require('./config');
4
+ const { parseClaudeContextOutput, parseClaudeUsageOutput } = require('./cli-usage');
4
5
 
5
6
  const PERMISSION_MODES = new Set(['default', 'acceptEdits', 'bypassPermissions', 'plan']);
6
7
  const EDIT_TOOLS = new Set(['Edit', 'MultiEdit', 'Write', 'NotebookEdit']);
7
8
  const EXIT_PLAN_TOOLS = new Set(['exit_plan_mode', 'ExitPlanMode']);
8
9
  const DENY_PERMISSION_MESSAGE = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
10
+ const LOCAL_COMMAND_TIMEOUT_MS = 30_000;
9
11
 
10
12
  function normalizePermissionMode(value) {
11
13
  const mode = String(value || 'default');
@@ -65,7 +67,15 @@ class AsyncMessageQueue {
65
67
  }
66
68
 
67
69
  class ClaudeStructuredSession extends EventEmitter {
68
- constructor({ id, tool, workingDir, name, logger, options = {} }) {
70
+ constructor({
71
+ id,
72
+ tool,
73
+ workingDir,
74
+ name,
75
+ logger,
76
+ options = {},
77
+ localCommandRunner = null
78
+ }) {
69
79
  super();
70
80
  this.id = id;
71
81
  this.tool = tool;
@@ -87,7 +97,12 @@ class ClaudeStructuredSession extends EventEmitter {
87
97
  this.inputQueue = null;
88
98
  this.query = null;
89
99
  this.runnerStarted = false;
100
+ this.runnerReadyPromise = null;
101
+ this.runnerInitializationAnnounced = false;
90
102
  this.abortRequested = false;
103
+ this.localCommandRunner = localCommandRunner;
104
+ this.localCommandChain = Promise.resolve();
105
+ this.pendingLocalCommand = null;
91
106
  this.permissionMode = normalizePermissionMode(options.permissionMode);
92
107
  this.model = normalizeModel(options.model);
93
108
  this.effort = normalizeEffort(options.effort);
@@ -97,8 +112,7 @@ class ClaudeStructuredSession extends EventEmitter {
97
112
  this.resumeSessionId = options.resume || null;
98
113
  this.claudeSessionId = options.resume || null;
99
114
  this.activeOptionSignature = null;
100
- this.contextRemaining = null;
101
- this.latestUsage = null;
115
+ this.turnQueue = [];
102
116
 
103
117
  // Compatibility with existing session-scoped Git/file APIs.
104
118
  this.ptyManager = {
@@ -146,8 +160,6 @@ class ClaudeStructuredSession extends EventEmitter {
146
160
  status: this.status,
147
161
  claudeSessionId: this.claudeSessionId || null,
148
162
  resumeSessionId: this.resumeSessionId || null,
149
- contextRemaining: this.contextRemaining,
150
- latestUsage: this.latestUsage,
151
163
  canAbort: this.status === 'thinking',
152
164
  pendingPermissionCount: this.pendingPermissions.size
153
165
  };
@@ -199,19 +211,29 @@ class ClaudeStructuredSession extends EventEmitter {
199
211
  return true;
200
212
  }
201
213
 
202
- sendUserMessage(text) {
214
+ sendUserMessage(text, attachments = []) {
203
215
  if (!this.running) return false;
204
216
  const prompt = String(text || '').trim();
205
- if (!prompt) return false;
217
+ const images = Array.isArray(attachments) ? attachments.filter(item => item?.data && item?.mediaType) : [];
218
+ if (!prompt && images.length === 0) return false;
206
219
  this.hasUnreadCompletion = false;
207
- this.appendMessage({ kind: 'user', text: prompt });
220
+ const turn = this.beginTurn(prompt, images.map(item => ({ name: item.name, size: item.size })));
221
+ this.setStatus('thinking');
222
+
223
+ const content = images.length > 0 ? [
224
+ ...(prompt ? [{ type: 'text', text: prompt }] : []),
225
+ ...images.map(item => ({
226
+ type: 'image',
227
+ source: { type: 'base64', media_type: item.mediaType, data: item.data }
228
+ }))
229
+ ] : prompt;
208
230
 
209
231
  const sdkMessage = {
210
232
  type: 'user',
211
233
  parent_tool_use_id: null,
212
234
  message: {
213
235
  role: 'user',
214
- content: prompt
236
+ content
215
237
  }
216
238
  };
217
239
 
@@ -223,6 +245,37 @@ class ClaudeStructuredSession extends EventEmitter {
223
245
  return true;
224
246
  }
225
247
 
248
+ beginTurn(prompt, attachments = []) {
249
+ const turn = { id: crypto.randomUUID(), startedAt: Date.now() };
250
+ this.turnQueue.push(turn);
251
+ this.appendMessage({ kind: 'turn-start', turnId: turn.id, createdAt: turn.startedAt });
252
+ this.appendMessage({ kind: 'user', text: prompt, attachments, turnId: turn.id, createdAt: turn.startedAt });
253
+ return turn;
254
+ }
255
+
256
+ currentTurn() {
257
+ return this.turnQueue[0] || null;
258
+ }
259
+
260
+ completeCurrentTurn(status = 'completed', reportedDurationMs = null) {
261
+ const turn = this.turnQueue.shift();
262
+ if (!turn) return null;
263
+ const completedAt = Date.now();
264
+ return this.appendMessage({
265
+ kind: 'turn-end',
266
+ turnId: turn.id,
267
+ turnStatus: status,
268
+ durationMs: Number.isFinite(Number(reportedDurationMs))
269
+ ? Math.max(0, Number(reportedDurationMs))
270
+ : Math.max(0, completedAt - turn.startedAt),
271
+ createdAt: completedAt
272
+ });
273
+ }
274
+
275
+ sealTurns(status = 'cancelled') {
276
+ while (this.turnQueue.length > 0) this.completeCurrentTurn(status);
277
+ }
278
+
226
279
  createMessageItem(message) {
227
280
  return {
228
281
  id: crypto.randomUUID(),
@@ -287,10 +340,14 @@ class ClaudeStructuredSession extends EventEmitter {
287
340
  this.inputQueue?.close();
288
341
  this.query?.close?.();
289
342
  this.abortController?.abort();
343
+ this.rejectPendingLocalCommand(new Error(reason));
290
344
  this.runnerStarted = false;
345
+ this.runnerReadyPromise = null;
346
+ this.runnerInitializationAnnounced = false;
291
347
  this.inputQueue = null;
292
348
  this.query = null;
293
349
  this.abortController = null;
350
+ this.sealTurns('cancelled');
294
351
  this.setStatus('idle');
295
352
  this.appendMessage({ kind: 'event', level: 'info', text: reason });
296
353
  this.emitEvent({ type: 'state', state: this.getControlState() });
@@ -300,14 +357,21 @@ class ClaudeStructuredSession extends EventEmitter {
300
357
  resetRunnerForNextTurn() {
301
358
  this.inputQueue?.close();
302
359
  this.query?.close?.();
360
+ this.rejectPendingLocalCommand(new Error('Claude CLI session was reset'));
303
361
  this.runnerStarted = false;
362
+ this.runnerReadyPromise = null;
363
+ this.runnerInitializationAnnounced = false;
304
364
  this.inputQueue = null;
305
365
  this.query = null;
306
366
  this.abortController = null;
367
+ this.turnQueue = [];
307
368
  }
308
369
 
309
370
  finishRunner() {
371
+ this.rejectPendingLocalCommand(new Error('Claude CLI session ended before returning the command output'));
310
372
  this.runnerStarted = false;
373
+ this.runnerReadyPromise = null;
374
+ this.runnerInitializationAnnounced = false;
311
375
  this.inputQueue?.close();
312
376
  this.inputQueue = null;
313
377
  this.query = null;
@@ -466,15 +530,27 @@ class ClaudeStructuredSession extends EventEmitter {
466
530
  this.completionReadInputSeq = this.inputSeq || 0;
467
531
  }
468
532
 
469
- async startRunner(initialMessage) {
533
+ startRunner(initialMessage = null) {
534
+ if (this.runnerStarted) {
535
+ if (initialMessage && this.inputQueue) this.inputQueue.push(initialMessage);
536
+ return this.runnerReadyPromise || Promise.resolve(this.query);
537
+ }
470
538
  this.runnerStarted = true;
539
+ this.runnerInitializationAnnounced = false;
471
540
  this.inputQueue = new AsyncMessageQueue();
472
- this.inputQueue.push(initialMessage);
541
+ if (initialMessage) this.inputQueue.push(initialMessage);
473
542
  this.abortController = new AbortController();
474
543
  this.abortRequested = false;
475
544
  this.activeOptionSignature = this.getOptionSignature();
476
- this.setStatus('thinking');
545
+ if (initialMessage) this.setStatus('thinking');
546
+
547
+ let resolveReady;
548
+ this.runnerReadyPromise = new Promise(resolve => { resolveReady = resolve; });
549
+ void this.runRunner(resolveReady);
550
+ return this.runnerReadyPromise;
551
+ }
477
552
 
553
+ async runRunner(resolveReady) {
478
554
  try {
479
555
  const sdk = await import('@anthropic-ai/claude-agent-sdk');
480
556
  const resolvedModel = resolveClaudeModel(this.model, process.env);
@@ -497,16 +573,25 @@ class ClaudeStructuredSession extends EventEmitter {
497
573
  prompt: this.inputQueue,
498
574
  options
499
575
  });
576
+ if (typeof this.query.initializationResult === 'function') {
577
+ await this.query.initializationResult();
578
+ }
579
+ resolveReady(this.query);
500
580
 
501
581
  for await (const message of this.query) {
502
582
  this.handleSdkMessage(message);
503
583
  }
504
584
  this.finishRunner();
585
+ this.sealTurns('completed');
505
586
  this.setStatus('idle');
506
587
  } catch (error) {
588
+ resolveReady(null);
589
+ const wasAborted = this.abortRequested;
590
+ this.inputQueue?.close();
591
+ this.query?.close?.();
592
+ this.finishRunner();
507
593
  if (!this.running) return;
508
- if (this.abortRequested) {
509
- this.finishRunner();
594
+ if (wasAborted) {
510
595
  this.setStatus('idle');
511
596
  return;
512
597
  }
@@ -515,10 +600,87 @@ class ClaudeStructuredSession extends EventEmitter {
515
600
  level: 'error',
516
601
  text: `Claude session error: ${error && error.message ? error.message : String(error)}`
517
602
  });
603
+ this.sealTurns('failed');
518
604
  this.setStatus('error');
519
605
  }
520
606
  }
521
607
 
608
+ async showUsage() {
609
+ try {
610
+ const output = await this.runLocalCommand('/usage');
611
+ const usage = { source: 'claude-cli-command', session: parseClaudeUsageOutput(output), fetchedAt: Date.now() };
612
+ this.appendMessage({ kind: 'usage', title: 'Claude usage', usage });
613
+ return true;
614
+ } catch (error) {
615
+ this.appendMessage({
616
+ kind: 'usage',
617
+ title: 'Claude usage',
618
+ error: error && error.message ? error.message : String(error)
619
+ });
620
+ return false;
621
+ }
622
+ }
623
+
624
+ async showContext() {
625
+ try {
626
+ const output = await this.runLocalCommand('/context');
627
+ this.appendMessage({ kind: 'context', title: 'Claude context', context: parseClaudeContextOutput(output) });
628
+ return true;
629
+ } catch (error) {
630
+ this.appendMessage({
631
+ kind: 'context',
632
+ title: 'Claude context',
633
+ error: error && error.message ? error.message : String(error)
634
+ });
635
+ return false;
636
+ }
637
+ }
638
+
639
+ runLocalCommand(command) {
640
+ const task = this.localCommandChain.then(() => this.executeLocalCommand(command));
641
+ this.localCommandChain = task.catch(() => {});
642
+ return task;
643
+ }
644
+
645
+ async executeLocalCommand(command) {
646
+ if (this.status === 'thinking') throw new Error('Wait for Claude to finish before running a local command');
647
+ if (this.localCommandRunner) return this.localCommandRunner(command);
648
+
649
+ const query = await this.startRunner();
650
+ if (!query || !this.inputQueue) throw new Error('Claude CLI session is not available');
651
+ if (this.pendingLocalCommand) throw new Error('Another Claude local command is already running');
652
+
653
+ return new Promise((resolve, reject) => {
654
+ const timeout = setTimeout(() => {
655
+ if (this.pendingLocalCommand?.command !== command) return;
656
+ this.pendingLocalCommand = null;
657
+ reject(new Error(`Claude ${command} command timed out`));
658
+ }, LOCAL_COMMAND_TIMEOUT_MS);
659
+ this.pendingLocalCommand = {
660
+ command,
661
+ resolve: output => {
662
+ clearTimeout(timeout);
663
+ this.pendingLocalCommand = null;
664
+ resolve(output);
665
+ },
666
+ reject: error => {
667
+ clearTimeout(timeout);
668
+ this.pendingLocalCommand = null;
669
+ reject(error);
670
+ }
671
+ };
672
+ this.inputQueue.push({
673
+ type: 'user',
674
+ parent_tool_use_id: null,
675
+ message: { role: 'user', content: command }
676
+ });
677
+ });
678
+ }
679
+
680
+ rejectPendingLocalCommand(error) {
681
+ this.pendingLocalCommand?.reject(error);
682
+ }
683
+
522
684
  getOptionSignature() {
523
685
  return JSON.stringify({
524
686
  permissionMode: this.permissionMode,
@@ -550,6 +712,7 @@ class ClaudeStructuredSession extends EventEmitter {
550
712
  canAllowEdits: EDIT_TOOLS.has(toolName) || EXIT_PLAN_TOOLS.has(toolName),
551
713
  canBypass: EXIT_PLAN_TOOLS.has(toolName),
552
714
  input,
715
+ toolUseId: options.toolUseID || null,
553
716
  createdAt: Date.now(),
554
717
  status: 'pending'
555
718
  };
@@ -569,28 +732,56 @@ class ClaudeStructuredSession extends EventEmitter {
569
732
  if (!message || typeof message !== 'object') return;
570
733
 
571
734
  if (message.type === 'system' && message.subtype === 'init') {
735
+ const signatureBeforeInit = this.getOptionSignature();
572
736
  this.claudeSessionId = message.session_id || this.claudeSessionId;
573
737
  this.resumeSessionId = this.claudeSessionId || this.resumeSessionId;
574
738
  if (message.model) this.model = String(message.model);
575
- this.appendMessage({
576
- kind: 'event',
577
- level: 'info',
578
- text: `Claude ready${message.model ? ` (${message.model})` : ''}`
579
- });
739
+ // Resolving runtime session/model values is not a user settings change
740
+ // and must not recycle the live runner after the turn.
741
+ if (this.activeOptionSignature === signatureBeforeInit) {
742
+ this.activeOptionSignature = this.getOptionSignature();
743
+ }
744
+ if (!this.runnerInitializationAnnounced) {
745
+ this.runnerInitializationAnnounced = true;
746
+ this.appendMessage({
747
+ kind: 'event',
748
+ level: 'info',
749
+ text: `Claude ready${message.model ? ` (${message.model})` : ''}`
750
+ });
751
+ }
580
752
  this.emitEvent({ type: 'state', state: this.getControlState() });
581
753
  return;
582
754
  }
583
755
 
756
+ if (message.type === 'system' && message.subtype === 'local_command_output') {
757
+ if (this.pendingLocalCommand) {
758
+ this.pendingLocalCommand.resolve(String(message.content || ''));
759
+ }
760
+ return;
761
+ }
762
+
763
+ if (this.pendingLocalCommand && message.type === 'assistant') return;
764
+
765
+ if (this.pendingLocalCommand && message.type === 'result') {
766
+ const output = typeof message.result === 'string' ? message.result : '';
767
+ if (message.is_error || !output) {
768
+ this.pendingLocalCommand.reject(new Error(output || `Claude ${this.pendingLocalCommand.command} command failed`));
769
+ } else {
770
+ this.pendingLocalCommand.resolve(output);
771
+ }
772
+ this.setStatus('idle');
773
+ return;
774
+ }
775
+
584
776
  if (message.type === 'assistant') {
585
- const usage = message.message && message.message.usage;
586
- if (usage) this.updateLatestUsageFromClaudeUsage(usage, message.message.model);
777
+ const turn = this.currentTurn();
587
778
  const content = message.message && message.message.content;
588
779
  const text = textFromContent(content).trim();
589
780
  const toolBlocks = Array.isArray(content)
590
781
  ? content.filter(item => item && item.type === 'tool_use')
591
782
  : [];
592
783
  if (text) {
593
- this.appendMessage({ kind: 'assistant', text, raw: message });
784
+ this.appendMessage({ kind: 'assistant', text, raw: message, turnId: turn?.id || null });
594
785
  }
595
786
  for (const block of toolBlocks) {
596
787
  this.appendMessage({
@@ -598,7 +789,9 @@ class ClaudeStructuredSession extends EventEmitter {
598
789
  name: block.name || 'tool',
599
790
  summary: this.summarizeToolInput(block.input),
600
791
  input: block.input,
601
- toolUseId: block.id
792
+ toolUseId: block.id,
793
+ turnId: turn?.id || null,
794
+ startedAtMs: Date.now()
602
795
  });
603
796
  }
604
797
  this.setStatus('thinking');
@@ -606,6 +799,7 @@ class ClaudeStructuredSession extends EventEmitter {
606
799
  }
607
800
 
608
801
  if (message.type === 'user') {
802
+ const turn = this.currentTurn();
609
803
  const content = message.message && message.message.content;
610
804
  if (Array.isArray(content)) {
611
805
  for (const item of content) {
@@ -614,7 +808,9 @@ class ClaudeStructuredSession extends EventEmitter {
614
808
  kind: 'tool-result',
615
809
  toolUseId: item.tool_use_id,
616
810
  text: textFromContent([item]).trim(),
617
- isError: Boolean(item.is_error)
811
+ isError: Boolean(item.is_error),
812
+ turnId: turn?.id || null,
813
+ completedAtMs: Date.now()
618
814
  });
619
815
  }
620
816
  }
@@ -623,8 +819,8 @@ class ClaudeStructuredSession extends EventEmitter {
623
819
  }
624
820
 
625
821
  if (message.type === 'result') {
626
- this.updateUsageFromResult(message);
627
- this.setStatus('idle');
822
+ this.completeCurrentTurn(message.is_error || message.subtype !== 'success' ? 'failed' : 'completed', message.duration_ms);
823
+ this.setStatus(this.turnQueue.length > 0 ? 'thinking' : 'idle');
628
824
  if (this.inputSeq > this.completionReadInputSeq) {
629
825
  this.hasUnreadCompletion = true;
630
826
  }
@@ -635,31 +831,6 @@ class ClaudeStructuredSession extends EventEmitter {
635
831
  }
636
832
  }
637
833
 
638
- updateUsageFromResult(message) {
639
- const usage = message && (message.usage || message.total_usage || message.result && message.result.usage);
640
- const context = usage && (usage.context_remaining || usage.contextRemaining || usage.remaining_context || usage.remainingContext);
641
- this.contextRemaining = typeof context === 'number' ? context : null;
642
- if (usage) this.updateLatestUsageFromClaudeUsage(usage, message.model || message.result && message.result.model);
643
- }
644
-
645
- updateLatestUsageFromClaudeUsage(usage, model = null) {
646
- const inputTokens = Number(usage.input_tokens || usage.inputTokens || 0);
647
- const outputTokens = Number(usage.output_tokens || usage.outputTokens || 0);
648
- const cacheCreation = Number(usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0);
649
- const cacheRead = Number(usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0);
650
- const contextSize = inputTokens + cacheCreation + cacheRead;
651
- this.latestUsage = {
652
- inputTokens,
653
- outputTokens,
654
- cacheCreation,
655
- cacheRead,
656
- totalTokens: inputTokens + outputTokens + cacheCreation + cacheRead,
657
- contextSize,
658
- model: model || null,
659
- updatedAt: Date.now()
660
- };
661
- }
662
-
663
834
  summarizeToolInput(input) {
664
835
  if (!input || typeof input !== 'object') return '';
665
836
  if (typeof input.command === 'string') return input.command;
@@ -673,6 +844,7 @@ class ClaudeStructuredSession extends EventEmitter {
673
844
  const item = this.createMessageItem(message);
674
845
  this.messages.push(item);
675
846
  this.emitEvent({ type: 'message', message: item });
847
+ return item;
676
848
  }
677
849
 
678
850
  setStatus(status) {