@link-assistant/hive-mind 1.52.0 → 1.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,10 +13,12 @@ import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs
13
13
  import { createInteractiveHandler } from './interactive-mode.lib.mjs';
14
14
  import { initProgressMonitoring } from './solve.progress-monitoring.lib.mjs';
15
15
  import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
16
+ import Decimal from 'decimal.js-light';
16
17
  import { displayBudgetStats, createEmptySubSessionUsage, accumulateModelUsage, displayModelUsage, displayCostComparison, mergeResultModelUsage, createSubAgentCallEntry, accumulateSubAgentUsage } from './claude.budget-stats.lib.mjs';
17
18
  import { buildClaudeResumeCommand } from './claude.command-builder.lib.mjs';
18
19
  import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; // see issue #1141
19
20
  import { CLAUDE_MODELS as availableModels } from './models/index.mjs'; // Issue #1221
21
+ import { buildMcpConfigWithoutPlaywright } from './playwright-mcp.lib.mjs';
20
22
  export { availableModels }; // Re-export for backward compatibility
21
23
  const showResumeCommand = async (sessionId, tempDir, claudePath, model, log) => {
22
24
  if (!sessionId || !tempDir) return;
@@ -429,51 +431,48 @@ export const checkModelVisionCapability = async modelId => {
429
431
  return false;
430
432
  }
431
433
  };
432
- /** Calculate USD cost for a model's usage with detailed breakdown */
434
+ /** Calculate USD cost for a model's usage with detailed breakdown (Issue #1600: uses Decimal for precision) */
433
435
  export const calculateModelCost = (usage, modelInfo, includeBreakdown = false) => {
434
436
  if (!modelInfo || !modelInfo.cost) {
435
437
  return includeBreakdown ? { total: 0, breakdown: null } : 0;
436
438
  }
437
439
  const cost = modelInfo.cost;
440
+ const million = new Decimal(1000000);
438
441
  const breakdown = {
439
442
  input: { tokens: 0, costPerMillion: 0, cost: 0 },
440
443
  cacheWrite: { tokens: 0, costPerMillion: 0, cost: 0 },
441
444
  cacheRead: { tokens: 0, costPerMillion: 0, cost: 0 },
442
445
  output: { tokens: 0, costPerMillion: 0, cost: 0 },
443
446
  };
444
- // Input tokens cost (per million tokens)
445
447
  if (usage.inputTokens && cost.input) {
446
448
  breakdown.input = {
447
449
  tokens: usage.inputTokens,
448
450
  costPerMillion: cost.input,
449
- cost: (usage.inputTokens / 1000000) * cost.input,
451
+ cost: new Decimal(usage.inputTokens).div(million).mul(new Decimal(cost.input)).toNumber(),
450
452
  };
451
453
  }
452
- // Cache creation tokens cost
453
454
  if (usage.cacheCreationTokens && cost.cache_write) {
454
455
  breakdown.cacheWrite = {
455
456
  tokens: usage.cacheCreationTokens,
456
457
  costPerMillion: cost.cache_write,
457
- cost: (usage.cacheCreationTokens / 1000000) * cost.cache_write,
458
+ cost: new Decimal(usage.cacheCreationTokens).div(million).mul(new Decimal(cost.cache_write)).toNumber(),
458
459
  };
459
460
  }
460
- // Cache read tokens cost
461
461
  if (usage.cacheReadTokens && cost.cache_read) {
462
462
  breakdown.cacheRead = {
463
463
  tokens: usage.cacheReadTokens,
464
464
  costPerMillion: cost.cache_read,
465
- cost: (usage.cacheReadTokens / 1000000) * cost.cache_read,
465
+ cost: new Decimal(usage.cacheReadTokens).div(million).mul(new Decimal(cost.cache_read)).toNumber(),
466
466
  };
467
467
  }
468
- // Output tokens cost
469
468
  if (usage.outputTokens && cost.output) {
470
469
  breakdown.output = {
471
470
  tokens: usage.outputTokens,
472
471
  costPerMillion: cost.output,
473
- cost: (usage.outputTokens / 1000000) * cost.output,
472
+ cost: new Decimal(usage.outputTokens).div(million).mul(new Decimal(cost.output)).toNumber(),
474
473
  };
475
474
  }
476
- const totalCost = breakdown.input.cost + breakdown.cacheWrite.cost + breakdown.cacheRead.cost + breakdown.output.cost;
475
+ const totalCost = new Decimal(breakdown.input.cost).plus(breakdown.cacheWrite.cost).plus(breakdown.cacheRead.cost).plus(breakdown.output.cost).toNumber();
477
476
  if (includeBreakdown) {
478
477
  return {
479
478
  total: totalCost,
@@ -619,7 +618,7 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
619
618
  let totalCacheCreationTokens = 0;
620
619
  let totalCacheReadTokens = 0;
621
620
  let totalOutputTokens = 0;
622
- let totalCostUSD = 0;
621
+ let totalCostDecimal = new Decimal(0);
623
622
  let hasCostData = false;
624
623
  for (const usage of Object.values(modelUsage)) {
625
624
  totalInputTokens += usage.inputTokens;
@@ -627,7 +626,7 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
627
626
  totalCacheReadTokens += usage.cacheReadTokens;
628
627
  totalOutputTokens += usage.outputTokens;
629
628
  if (usage.costUSD !== null) {
630
- totalCostUSD += usage.costUSD;
629
+ totalCostDecimal = totalCostDecimal.plus(new Decimal(usage.costUSD));
631
630
  hasCostData = true;
632
631
  }
633
632
  }
@@ -642,7 +641,7 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
642
641
  cacheReadTokens: totalCacheReadTokens,
643
642
  outputTokens: totalOutputTokens,
644
643
  totalTokens,
645
- totalCostUSD: hasCostData ? totalCostUSD : null,
644
+ totalCostUSD: hasCostData ? totalCostDecimal.toNumber() : null,
646
645
  // Issue #1501: Peak context usage (max single-request fill) and dedup stats
647
646
  peakContextUsage: globalPeakContext,
648
647
  duplicateEntriesSkipped: duplicateCount,
@@ -774,6 +773,14 @@ export const executeClaudeCommand = async params => {
774
773
  await log(`šŸ”„ Resuming from session: ${argv.resume}`);
775
774
  claudeArgs = `--resume ${argv.resume} ${claudeArgs}`;
776
775
  }
776
+ let mcpConfigPath = null;
777
+ if (argv.playwrightMcp === false) {
778
+ mcpConfigPath = await buildMcpConfigWithoutPlaywright(log);
779
+ if (mcpConfigPath) {
780
+ claudeArgs += ` --strict-mcp-config --mcp-config "${mcpConfigPath}"`;
781
+ await log('šŸŽ­ Playwright MCP physically disabled for this session via --strict-mcp-config', { verbose: true });
782
+ }
783
+ }
777
784
  claudeArgs += ` -p "${escapedPrompt}" --append-system-prompt "${escapedSystemPrompt}"`;
778
785
  const fullCommand = `(cd "${tempDir}" && ${claudePath} ${claudeArgs} | jq -c .)`;
779
786
  await log(`\n${formatAligned('šŸ“', 'Raw command:', '')}`);
@@ -797,11 +804,12 @@ export const executeClaudeCommand = async params => {
797
804
  if (!isNewVersion && thinkLevel) await log(`šŸ“Š Thinking level (via keywords): ${thinkLevel}`, { verbose: true });
798
805
  }
799
806
  const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
807
+ const mcpDisableArgs = mcpConfigPath ? ['--strict-mcp-config', '--mcp-config', mcpConfigPath] : [];
800
808
  if (argv.resume) {
801
809
  const simpleEscapedPrompt = prompt.replace(/"/g, '\\"');
802
- execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
810
+ execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
803
811
  } else {
804
- execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} --append-system-prompt "${simpleEscapedSystem}"`;
812
+ execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} --append-system-prompt "${simpleEscapedSystem}"`;
805
813
  }
806
814
  await log(`${formatAligned('šŸ“‹', 'Command details:', '')}`);
807
815
  await log(formatAligned('šŸ“‚', 'Working directory:', tempDir, 2));
@@ -268,6 +268,7 @@ GitHub CLI command patterns.
268
268
  Playwright MCP usage (browser automation via mcp__playwright__* tools).
269
269
  - When you develop frontend web applications (HTML, CSS, JavaScript, React, Vue, Angular, etc.), use Playwright MCP tools to test the UI in a real browser.
270
270
  - When WebFetch tool fails to retrieve expected content (e.g., returns empty content, JavaScript-rendered pages, or login-protected pages), use Playwright MCP tools (browser_navigate, browser_snapshot) as a fallback for web browsing.
271
+ - When WebSearch tool fails or returns insufficient results, use Playwright MCP tools (browser_navigate, browser_snapshot) as a fallback for internet search.
271
272
  - When you need to interact with dynamic web pages that require JavaScript execution, use Playwright MCP tools.
272
273
  - When you need to visually verify how a web page looks or take screenshots, use browser_take_screenshot from Playwright MCP.
273
274
  - When you need to fill forms, click buttons, or perform user interactions on web pages, use Playwright MCP tools (browser_click, browser_type, browser_fill_form).
package/src/codex.lib.mjs CHANGED
@@ -21,8 +21,9 @@ import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
21
21
  import { mapModelToId, resolveCodexReasoningEffort } from './codex.options.lib.mjs';
22
22
  import { createInteractiveHandler } from './interactive-mode.lib.mjs';
23
23
  import { initProgressMonitoring } from './solve.progress-monitoring.lib.mjs';
24
+ import { getCodexPlaywrightMcpDisableConfigArgs } from './playwright-mcp.lib.mjs';
24
25
 
25
- const CODEX_USAGE_FIELD_NAMES = ['input_tokens', 'cached_input_tokens', 'output_tokens'];
26
+ const CODEX_USAGE_FIELD_NAMES = ['input_tokens', 'cached_input_tokens', 'output_tokens', 'cache_write_tokens', 'cache_creation_input_tokens', 'reasoning_tokens', 'input_tokens_details.cached_tokens', 'input_tokens_details.cache_read_tokens', 'input_tokens_details.cache_write_tokens', 'input_tokens_details.cache_creation_tokens', 'input_tokens_details.cache_creation_input_tokens', 'output_tokens_details.reasoning_tokens'];
26
27
  const getCodexExecEnv = (verbose = false) => (verbose ? { ...process.env, RUST_LOG: 'debug' } : { ...process.env });
27
28
  const CODEX_MODEL_DIAGNOSTIC_PATHS = [
28
29
  ['model', data => data?.model],
@@ -32,6 +33,40 @@ const CODEX_MODEL_DIAGNOSTIC_PATHS = [
32
33
  ['message.model', data => data?.message?.model],
33
34
  ];
34
35
 
36
+ const createCodexTokenFieldAvailability = () => ({
37
+ inputTokens: false,
38
+ outputTokens: false,
39
+ reasoningTokens: false,
40
+ cacheReadTokens: false,
41
+ cacheWriteTokens: false,
42
+ });
43
+
44
+ const hasOwnPath = (object, pathName) => {
45
+ let cursor = object;
46
+ for (const part of pathName.split('.')) {
47
+ if (!cursor || typeof cursor !== 'object' || !Object.hasOwn(cursor, part)) return false;
48
+ cursor = cursor[part];
49
+ }
50
+ return true;
51
+ };
52
+
53
+ const getPathValue = (object, pathName) => pathName.split('.').reduce((cursor, part) => cursor?.[part], object);
54
+
55
+ const getFirstObservedNumber = (object, pathNames) => {
56
+ for (const pathName of pathNames) {
57
+ if (!hasOwnPath(object, pathName)) continue;
58
+ const value = getPathValue(object, pathName);
59
+ return Number.isFinite(value) ? value : 0;
60
+ }
61
+ return 0;
62
+ };
63
+
64
+ const hasAnyObservedPath = (object, pathNames) => pathNames.some(pathName => hasOwnPath(object, pathName));
65
+
66
+ const CODEX_CACHE_READ_USAGE_PATHS = ['cached_input_tokens', 'input_tokens_details.cached_tokens', 'input_tokens_details.cache_read_tokens'];
67
+ const CODEX_CACHE_WRITE_USAGE_PATHS = ['cache_write_tokens', 'cache_creation_input_tokens', 'input_tokens_details.cache_write_tokens', 'input_tokens_details.cache_creation_tokens', 'input_tokens_details.cache_creation_input_tokens'];
68
+ const CODEX_REASONING_USAGE_PATHS = ['reasoning_tokens', 'output_tokens_details.reasoning_tokens'];
69
+
35
70
  export const createCodexTokenUsage = requestedModelId => ({
36
71
  inputTokens: 0,
37
72
  outputTokens: 0,
@@ -42,6 +77,7 @@ export const createCodexTokenUsage = requestedModelId => ({
42
77
  stepCount: 0,
43
78
  requestedModelId: requestedModelId || null,
44
79
  respondedModelId: requestedModelId || null,
80
+ tokenFieldAvailability: createCodexTokenFieldAvailability(),
45
81
  });
46
82
 
47
83
  const createEmptyCodexItemUsage = () => ({
@@ -162,6 +198,7 @@ export const parseCodexExecJsonOutput = (output, state = {}, requestedModelId =
162
198
  observedModelDiagnosticPaths: state.observedModelDiagnosticPaths || [],
163
199
  };
164
200
 
201
+ nextState.tokenUsage.tokenFieldAvailability ||= createCodexTokenFieldAvailability();
165
202
  const observedModelPaths = new Set(nextState.observedModelDiagnosticPaths);
166
203
 
167
204
  for (const rawLine of output.split('\n')) {
@@ -205,17 +242,28 @@ export const parseCodexExecJsonOutput = (output, state = {}, requestedModelId =
205
242
  }
206
243
 
207
244
  if (eventType === 'turn.completed' && data.usage && typeof data.usage === 'object') {
208
- const inputTokens = Number.isFinite(data.usage.input_tokens) ? data.usage.input_tokens : 0;
209
- const cachedInputTokens = Number.isFinite(data.usage.cached_input_tokens) ? data.usage.cached_input_tokens : 0;
210
- const outputTokens = Number.isFinite(data.usage.output_tokens) ? data.usage.output_tokens : 0;
245
+ const inputTokens = getFirstObservedNumber(data.usage, ['input_tokens']);
246
+ const cachedInputTokens = getFirstObservedNumber(data.usage, CODEX_CACHE_READ_USAGE_PATHS);
247
+ const cacheWriteTokens = getFirstObservedNumber(data.usage, CODEX_CACHE_WRITE_USAGE_PATHS);
248
+ const outputTokens = getFirstObservedNumber(data.usage, ['output_tokens']);
249
+ const reasoningTokens = getFirstObservedNumber(data.usage, CODEX_REASONING_USAGE_PATHS);
250
+
251
+ if (hasOwnPath(data.usage, 'input_tokens')) nextState.tokenUsage.tokenFieldAvailability.inputTokens = true;
252
+ if (hasAnyObservedPath(data.usage, CODEX_CACHE_READ_USAGE_PATHS)) nextState.tokenUsage.tokenFieldAvailability.cacheReadTokens = true;
253
+ if (hasAnyObservedPath(data.usage, CODEX_CACHE_WRITE_USAGE_PATHS)) nextState.tokenUsage.tokenFieldAvailability.cacheWriteTokens = true;
254
+ if (hasOwnPath(data.usage, 'output_tokens')) nextState.tokenUsage.tokenFieldAvailability.outputTokens = true;
255
+ if (hasAnyObservedPath(data.usage, CODEX_REASONING_USAGE_PATHS)) nextState.tokenUsage.tokenFieldAvailability.reasoningTokens = true;
256
+
211
257
  const nonCachedInputTokens = Math.max(0, inputTokens - cachedInputTokens);
212
258
  nextState.tokenUsage.inputTokens += nonCachedInputTokens;
213
259
  nextState.tokenUsage.cacheReadTokens += cachedInputTokens;
260
+ nextState.tokenUsage.cacheWriteTokens += cacheWriteTokens;
214
261
  nextState.tokenUsage.outputTokens += outputTokens;
262
+ nextState.tokenUsage.reasoningTokens += reasoningTokens;
215
263
  nextState.tokenUsage.totalTokens = nextState.tokenUsage.inputTokens + nextState.tokenUsage.cacheReadTokens + nextState.tokenUsage.outputTokens + nextState.tokenUsage.cacheWriteTokens;
216
264
  nextState.tokenUsage.stepCount += 1;
217
265
 
218
- const usageFieldSet = CODEX_USAGE_FIELD_NAMES.filter(fieldName => Object.hasOwn(data.usage, fieldName));
266
+ const usageFieldSet = CODEX_USAGE_FIELD_NAMES.filter(fieldName => hasOwnPath(data.usage, fieldName));
219
267
  if (usageFieldSet.length > 0) nextState.observedUsageFieldSets.push(usageFieldSet);
220
268
  }
221
269
 
@@ -537,6 +585,10 @@ export const executeCodexCommand = async params => {
537
585
  } else {
538
586
  codexArgs += ` --model ${shellQuote(mappedModel)}`;
539
587
  }
588
+ const codexPlaywrightMcpDisableConfigArgs = argv.playwrightMcp === false ? await getCodexPlaywrightMcpDisableConfigArgs(log) : [];
589
+ for (const arg of codexPlaywrightMcpDisableConfigArgs) {
590
+ codexArgs += ` ${shellQuote(arg)}`;
591
+ }
540
592
  codexArgs += ` --json --skip-git-repo-check -o ${shellQuote(lastMessageFile)} -c ${shellQuote(`model_reasoning_effort=${reasoningEffort}`)} -c ${shellQuote('model_reasoning_summary=auto')} --dangerously-bypass-approvals-and-sandbox`;
541
593
 
542
594
  const fullCommand = `(cd ${shellQuote(tempDir)} && cat ${shellQuote(promptFile)} | ${codexPath} ${codexArgs})`;
@@ -260,6 +260,7 @@ GitHub CLI command patterns.
260
260
  Playwright MCP usage (browser automation via MCP tools).
261
261
  - When you develop frontend web applications or debug UI issues, use Playwright MCP tools to test the UI in a real browser.
262
262
  - When simple fetch-based browsing is insufficient for dynamic pages, use Playwright MCP browser automation as a fallback.
263
+ - When WebSearch tool fails or returns insufficient results, use Playwright MCP browser automation as a fallback for internet search.
263
264
  - When reproducing or verifying UI bugs, take before/after screenshots and close the browser when finished.`
264
265
  : ''
265
266
  }${
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ import Decimal from 'decimal.js-light';
4
+
5
+ const formatTokenCount = value => (Number.isFinite(value) ? value : 0).toLocaleString();
6
+
7
+ const isObservedTokenField = (usage, fieldName) => {
8
+ const value = usage?.[fieldName];
9
+ if (Number.isFinite(value) && value > 0) return true;
10
+ if (usage?.tokenFieldAvailability?.[fieldName] === true) return true;
11
+ if (Array.isArray(usage?.availableTokenFields) && usage.availableTokenFields.includes(fieldName)) return true;
12
+ return false;
13
+ };
14
+
15
+ const buildTokenUsageString = tokenUsage => {
16
+ const parts = [`${formatTokenCount(tokenUsage.inputTokens)} input`, `${formatTokenCount(tokenUsage.outputTokens)} output`];
17
+ if (isObservedTokenField(tokenUsage, 'reasoningTokens')) parts.push(`${formatTokenCount(tokenUsage.reasoningTokens)} reasoning`);
18
+ if (isObservedTokenField(tokenUsage, 'cacheReadTokens')) parts.push(`${formatTokenCount(tokenUsage.cacheReadTokens)} cache read`);
19
+ if (isObservedTokenField(tokenUsage, 'cacheWriteTokens')) parts.push(`${formatTokenCount(tokenUsage.cacheWriteTokens)} cache write`);
20
+ return `\n- Token usage: ${parts.join(', ')}`;
21
+ };
22
+
23
+ /** Build cost estimation string for log comments (Issue #1250, Issue #1557, Issue #1600: Decimal precision) */
24
+ export const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) => {
25
+ const hasPublic = totalCostUSD !== null && totalCostUSD !== undefined;
26
+ const hasAnthropic = anthropicTotalCostUSD !== null && anthropicTotalCostUSD !== undefined;
27
+ const hasPricing = pricingInfo && (pricingInfo.modelName || pricingInfo.tokenUsage || pricingInfo.isFreeModel || pricingInfo.isOpencodeFreeModel);
28
+ const hasOpencodeCost = pricingInfo?.opencodeCost !== null && pricingInfo?.opencodeCost !== undefined;
29
+ if (!hasPublic && !hasAnthropic && !hasPricing && !hasOpencodeCost) return '';
30
+ const publicDec = hasPublic ? new Decimal(totalCostUSD) : null;
31
+ const anthropicDec = hasAnthropic ? new Decimal(anthropicTotalCostUSD) : null;
32
+ if (publicDec && anthropicDec && publicDec.toFixed(6) === anthropicDec.toFixed(6)) return `\n\n### šŸ’° Cost: **$${anthropicDec.toFixed(6)}**`;
33
+ let costInfo = '\n\n### šŸ’° **Cost estimation:**';
34
+ if (pricingInfo?.modelName) {
35
+ costInfo += `\n- Model: ${pricingInfo.modelName}`;
36
+ if (pricingInfo.provider) costInfo += `\n- Provider: ${pricingInfo.provider}`;
37
+ }
38
+ if (hasPublic) {
39
+ if (pricingInfo?.isFreeModel && publicDec.eq(0) && !pricingInfo?.baseModelName) {
40
+ costInfo += '\n- Public pricing estimate: $0.00 (Free model)';
41
+ } else {
42
+ let pricingRef = '';
43
+ if (pricingInfo?.baseModelName && pricingInfo?.originalProvider) {
44
+ pricingRef = ` (based on ${pricingInfo.originalProvider} ${pricingInfo.baseModelName} prices)`;
45
+ } else if (pricingInfo?.originalProvider) {
46
+ pricingRef = ` (based on ${pricingInfo.originalProvider} prices)`;
47
+ }
48
+ costInfo += `\n- Public pricing estimate: $${publicDec.toFixed(6)}${pricingRef}`;
49
+ }
50
+ } else if (hasPricing) {
51
+ costInfo += '\n- Public pricing estimate: unknown';
52
+ }
53
+ if (hasOpencodeCost) {
54
+ if (pricingInfo.isOpencodeFreeModel) {
55
+ costInfo += '\n- Calculated by OpenCode Zen: $0.00 (Free model)';
56
+ } else {
57
+ costInfo += `\n- Calculated by OpenCode Zen: $${new Decimal(pricingInfo.opencodeCost).toFixed(6)}`;
58
+ }
59
+ }
60
+ if (pricingInfo?.tokenUsage) costInfo += buildTokenUsageString(pricingInfo.tokenUsage);
61
+ if (hasAnthropic) {
62
+ costInfo += `\n- Calculated by Anthropic: $${anthropicDec.toFixed(6)}`;
63
+ if (hasPublic) {
64
+ const diff = anthropicDec.minus(publicDec);
65
+ const pct = publicDec.gt(0) ? diff.div(publicDec).mul(100) : new Decimal(0);
66
+ costInfo += `\n- Difference: $${diff.toFixed(6)} (${pct.gt(0) ? '+' : ''}${pct.toFixed(2)}%)`;
67
+ }
68
+ }
69
+ return costInfo;
70
+ };
@@ -14,60 +14,8 @@ import { formatResetTimeWithRelative } from './usage-limit.lib.mjs'; // See: htt
14
14
  import { getToolDisplayName, getModelInfoForComment } from './models/index.mjs';
15
15
  export { getToolDisplayName }; // Re-export for use by other modules
16
16
  import { buildBudgetStatsString } from './claude.budget-stats.lib.mjs';
17
-
18
- /** Build cost estimation string for log comments (Issue #1250, Issue #1557) */
19
- const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) => {
20
- const hasPublic = totalCostUSD !== null && totalCostUSD !== undefined;
21
- const hasAnthropic = anthropicTotalCostUSD !== null && anthropicTotalCostUSD !== undefined;
22
- const hasPricing = pricingInfo && (pricingInfo.modelName || pricingInfo.tokenUsage || pricingInfo.isFreeModel || pricingInfo.isOpencodeFreeModel);
23
- const hasOpencodeCost = pricingInfo?.opencodeCost !== null && pricingInfo?.opencodeCost !== undefined;
24
- if (!hasPublic && !hasAnthropic && !hasPricing && !hasOpencodeCost) return '';
25
- // Issue #1557: Simplified display when public and Anthropic costs match
26
- if (hasPublic && hasAnthropic && totalCostUSD.toFixed(6) === anthropicTotalCostUSD.toFixed(6)) return `\n\n### šŸ’° Cost: **$${anthropicTotalCostUSD.toFixed(6)}**`;
27
- let costInfo = '\n\n### šŸ’° **Cost estimation:**';
28
- if (pricingInfo?.modelName) {
29
- costInfo += `\n- Model: ${pricingInfo.modelName}`;
30
- if (pricingInfo.provider) costInfo += `\n- Provider: ${pricingInfo.provider}`;
31
- }
32
- if (hasPublic) {
33
- if (pricingInfo?.isFreeModel && totalCostUSD === 0 && !pricingInfo?.baseModelName) {
34
- costInfo += '\n- Public pricing estimate: $0.00 (Free model)';
35
- } else {
36
- let pricingRef = '';
37
- if (pricingInfo?.baseModelName && pricingInfo?.originalProvider) {
38
- pricingRef = ` (based on ${pricingInfo.originalProvider} ${pricingInfo.baseModelName} prices)`;
39
- } else if (pricingInfo?.originalProvider) {
40
- pricingRef = ` (based on ${pricingInfo.originalProvider} prices)`;
41
- }
42
- costInfo += `\n- Public pricing estimate: $${totalCostUSD.toFixed(6)}${pricingRef}`;
43
- }
44
- } else if (hasPricing) {
45
- costInfo += '\n- Public pricing estimate: unknown';
46
- }
47
- if (hasOpencodeCost) {
48
- if (pricingInfo.isOpencodeFreeModel) {
49
- costInfo += '\n- Calculated by OpenCode Zen: $0.00 (Free model)';
50
- } else {
51
- costInfo += `\n- Calculated by OpenCode Zen: $${pricingInfo.opencodeCost.toFixed(6)}`;
52
- }
53
- }
54
- if (pricingInfo?.tokenUsage) {
55
- const u = pricingInfo.tokenUsage;
56
- let tokenInfo = `\n- Token usage: ${u.inputTokens?.toLocaleString() || 0} input, ${u.outputTokens?.toLocaleString() || 0} output`;
57
- if (u.reasoningTokens > 0) tokenInfo += `, ${u.reasoningTokens.toLocaleString()} reasoning`;
58
- if (u.cacheReadTokens > 0 || u.cacheWriteTokens > 0) tokenInfo += `, ${u.cacheReadTokens?.toLocaleString() || 0} cache read, ${u.cacheWriteTokens?.toLocaleString() || 0} cache write`;
59
- costInfo += tokenInfo;
60
- }
61
- if (hasAnthropic) {
62
- costInfo += `\n- Calculated by Anthropic: $${anthropicTotalCostUSD.toFixed(6)}`;
63
- if (hasPublic) {
64
- const diff = anthropicTotalCostUSD - totalCostUSD;
65
- const pct = totalCostUSD > 0 ? (diff / totalCostUSD) * 100 : 0;
66
- costInfo += `\n- Difference: $${diff.toFixed(6)} (${pct > 0 ? '+' : ''}${pct.toFixed(2)}%)`;
67
- }
68
- }
69
- return costInfo;
70
- };
17
+ import { buildCostInfoString } from './github-cost-info.lib.mjs';
18
+ export { buildCostInfoString };
71
19
  export const maskGitHubToken = maskToken; // Alias for backward compatibility
72
20
  export const escapeCodeBlocksInLog = logContent => logContent.replace(/```/g, '\\`\\`\\`'); // Escape ``` in logs
73
21
  export const checkFileInBranch = async (owner, repo, fileName, branchName) => {
@@ -19,6 +19,11 @@ import { timeouts } from './config.lib.mjs';
19
19
  import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
20
20
  import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
21
21
  import { opencodeModels, defaultModels } from './models/index.mjs';
22
+ import { checkPlaywrightMcpPackageAvailability, getOpenCodePlaywrightMcpDisableEnv } from './playwright-mcp.lib.mjs';
23
+ import { createAgentTokenUsage, accumulateAgentStepFinishUsage, parseAgentTokenUsage as parseOpenCodeTokenUsage } from './agent-token-usage.lib.mjs';
24
+ import { calculateAgentPricing } from './agent.lib.mjs';
25
+
26
+ export { parseOpenCodeTokenUsage };
22
27
 
23
28
  // Model mapping to translate aliases to full model IDs for OpenCode
24
29
  // Issue #1473: Uses centralized opencodeModels from models/index.mjs (single source of truth)
@@ -97,6 +102,9 @@ export const handleOpenCodeRuntimeSwitch = async () => {
97
102
  await log('ā„¹ļø OpenCode runtime handling not required for this operation');
98
103
  };
99
104
 
105
+ /** Check if Playwright MCP is available for OpenCode @returns {Promise<boolean>} */
106
+ export const checkPlaywrightMcpAvailability = checkPlaywrightMcpPackageAvailability;
107
+
100
108
  // Main function to execute OpenCode with prompts and settings
101
109
  export const executeOpenCode = async params => {
102
110
  const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, formatAligned, getResourceSnapshot, opencodePath = 'opencode', $ } = params;
@@ -237,11 +245,20 @@ export const executeOpenCodeCommand = async params => {
237
245
  await log(` Memory: ${resourcesBefore.memory.split('\n')[1]}`, { verbose: true });
238
246
  await log(` Load: ${resourcesBefore.load}`, { verbose: true });
239
247
 
248
+ const opencodeEnv = { ...process.env };
249
+
250
+ // Apply Playwright MCP session state before launching OpenCode.
251
+ if (argv.playwrightMcp === false) {
252
+ Object.assign(opencodeEnv, await getOpenCodePlaywrightMcpDisableEnv({ env: opencodeEnv, cwd: tempDir, log }));
253
+ await log('šŸŽ­ Playwright MCP physically disabled for this OpenCode session via --no-playwright-mcp', { verbose: true });
254
+ }
255
+
240
256
  // Build OpenCode command
241
257
  let execCommand;
242
258
 
243
259
  // Map model alias to full ID
244
260
  const mappedModel = mapModelToId(argv.model);
261
+ const streamingTokenUsage = createAgentTokenUsage();
245
262
 
246
263
  // Build opencode command arguments
247
264
  let opencodeArgs = `run --format json --model ${mappedModel}`;
@@ -268,17 +285,28 @@ export const executeOpenCodeCommand = async params => {
268
285
  await log(`${fullCommand}`);
269
286
  await log('');
270
287
 
288
+ const buildPricingInfo = async () => {
289
+ const tokenUsage = streamingTokenUsage;
290
+ if (tokenUsage.stepCount === 0) {
291
+ return { tokenUsage, pricingInfo: null, publicPricingEstimate: null };
292
+ }
293
+ const pricingInfo = await calculateAgentPricing(mappedModel, tokenUsage);
294
+ return { tokenUsage, pricingInfo, publicPricingEstimate: pricingInfo?.totalCostUSD ?? null };
295
+ };
296
+
271
297
  try {
272
298
  // Pipe the prompt file to opencode via stdin
273
299
  if (argv.resume) {
274
300
  execCommand = $({
275
301
  cwd: tempDir,
276
302
  mirror: false,
303
+ env: opencodeEnv,
277
304
  })`cat ${promptFile} | ${opencodePath} run --format json --resume ${argv.resume} --model ${mappedModel}`;
278
305
  } else {
279
306
  execCommand = $({
280
307
  cwd: tempDir,
281
308
  mirror: false,
309
+ env: opencodeEnv,
282
310
  })`cat ${promptFile} | ${opencodePath} run --format json --model ${mappedModel}`;
283
311
  }
284
312
 
@@ -313,6 +341,7 @@ export const executeOpenCodeCommand = async params => {
313
341
  for (const line of lines) {
314
342
  if (!line.trim()) continue;
315
343
  const data = sanitizeObjectStrings(JSON.parse(line));
344
+ accumulateAgentStepFinishUsage(streamingTokenUsage, data);
316
345
  // Track text content for result summary
317
346
  // OpenCode outputs text via 'text', 'assistant', 'message', or 'result' type events
318
347
  if (data.type === 'text' && data.text) {
@@ -355,6 +384,7 @@ export const executeOpenCodeCommand = async params => {
355
384
  for (const line of lines) {
356
385
  if (!line.trim()) continue;
357
386
  const data = sanitizeObjectStrings(JSON.parse(line));
387
+ accumulateAgentStepFinishUsage(streamingTokenUsage, data);
358
388
  if (data.type === 'text' && data.text) {
359
389
  lastTextContent = data.text;
360
390
  } else if (data.type === 'assistant' && data.message?.content) {
@@ -427,12 +457,14 @@ export const executeOpenCodeCommand = async params => {
427
457
  await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true });
428
458
  await log(` Load: ${resourcesAfter.load}`, { verbose: true });
429
459
 
460
+ const pricingResult = await buildPricingInfo();
430
461
  return {
431
462
  success: false,
432
463
  sessionId,
433
464
  limitReached: false,
434
465
  limitResetTime: null,
435
466
  permissionPromptDetected: true,
467
+ ...pricingResult,
436
468
  resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
437
469
  };
438
470
  }
@@ -466,17 +498,41 @@ export const executeOpenCodeCommand = async params => {
466
498
  await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true });
467
499
  await log(` Load: ${resourcesAfter.load}`, { verbose: true });
468
500
 
501
+ const pricingResult = await buildPricingInfo();
469
502
  return {
470
503
  success: false,
471
504
  sessionId,
472
505
  limitReached,
473
506
  limitResetTime,
507
+ ...pricingResult,
474
508
  resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
475
509
  };
476
510
  }
477
511
 
478
512
  await log('\n\nāœ… OpenCode command completed');
479
513
 
514
+ const pricingResult = await buildPricingInfo();
515
+ if (pricingResult.tokenUsage.stepCount > 0) {
516
+ await log('\nšŸ’° Token Usage Summary:');
517
+ await log(` šŸ“Š ${pricingResult.pricingInfo?.modelName || mappedModel} (${pricingResult.tokenUsage.stepCount} steps):`);
518
+ await log(` Input tokens: ${pricingResult.tokenUsage.inputTokens.toLocaleString()}`);
519
+ await log(` Output tokens: ${pricingResult.tokenUsage.outputTokens.toLocaleString()}`);
520
+ if (pricingResult.tokenUsage.reasoningTokens > 0 || pricingResult.tokenUsage.tokenFieldAvailability?.reasoningTokens) {
521
+ await log(` Reasoning tokens: ${pricingResult.tokenUsage.reasoningTokens.toLocaleString()}`);
522
+ }
523
+ if (pricingResult.tokenUsage.cacheReadTokens > 0 || pricingResult.tokenUsage.tokenFieldAvailability?.cacheReadTokens) {
524
+ await log(` Cache read: ${pricingResult.tokenUsage.cacheReadTokens.toLocaleString()}`);
525
+ }
526
+ if (pricingResult.tokenUsage.cacheWriteTokens > 0 || pricingResult.tokenUsage.tokenFieldAvailability?.cacheWriteTokens) {
527
+ await log(` Cache write: ${pricingResult.tokenUsage.cacheWriteTokens.toLocaleString()}`);
528
+ }
529
+ if (pricingResult.pricingInfo?.totalCostUSD !== null && pricingResult.pricingInfo?.totalCostUSD !== undefined) {
530
+ await log(` Public pricing estimate: $${pricingResult.pricingInfo.totalCostUSD.toFixed(6)}`);
531
+ } else {
532
+ await log(' Cost: Not available (could not fetch pricing)');
533
+ }
534
+ }
535
+
480
536
  // Issue #1263: Log if result summary was captured
481
537
  if (lastTextContent) {
482
538
  await log('šŸ“ Captured result summary from OpenCode output', { verbose: true });
@@ -487,6 +543,7 @@ export const executeOpenCodeCommand = async params => {
487
543
  sessionId,
488
544
  limitReached,
489
545
  limitResetTime,
546
+ ...pricingResult,
490
547
  resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
491
548
  };
492
549
  } catch (error) {
@@ -510,6 +567,9 @@ export const executeOpenCodeCommand = async params => {
510
567
  sessionId: null,
511
568
  limitReached: false,
512
569
  limitResetTime: null,
570
+ tokenUsage: streamingTokenUsage.stepCount > 0 ? streamingTokenUsage : null,
571
+ pricingInfo: null,
572
+ publicPricingEstimate: null,
513
573
  resultSummary: null, // Issue #1263: No result summary available on error
514
574
  };
515
575
  }
@@ -607,7 +667,9 @@ export const checkForUncommittedChanges = async (tempDir, owner, repo, branchNam
607
667
  export default {
608
668
  validateOpenCodeConnection,
609
669
  handleOpenCodeRuntimeSwitch,
670
+ checkPlaywrightMcpAvailability,
610
671
  executeOpenCode,
611
672
  executeOpenCodeCommand,
612
673
  checkForUncommittedChanges,
674
+ parseOpenCodeTokenUsage,
613
675
  };
@@ -226,6 +226,24 @@ GitHub CLI command patterns.
226
226
  - When adding issue comment, use gh issue comment NUMBER --body "text" --repo OWNER/REPO.
227
227
  - When viewing PR details, use gh pr view NUMBER --repo OWNER/REPO.
228
228
  - When filtering with jq, use gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate --jq 'reverse | .[0:5]'.${
229
+ argv && argv.promptPlaywrightMcp
230
+ ? `
231
+
232
+ Playwright MCP usage (browser automation via MCP tools).
233
+ - When you develop frontend web applications (HTML, CSS, JavaScript, React, Vue, Angular, etc.), use Playwright MCP tools to test the UI in a real browser.
234
+ - When WebFetch tool fails to retrieve expected content (e.g., returns empty content, JavaScript-rendered pages, or login-protected pages), use Playwright MCP tools (browser_navigate, browser_snapshot) as a fallback for web browsing.
235
+ - When WebSearch tool fails or returns insufficient results, use Playwright MCP tools (browser_navigate, browser_snapshot) as a fallback for internet search.
236
+ - When you need to interact with dynamic web pages that require JavaScript execution, use Playwright MCP tools.
237
+ - When you need to visually verify how a web page looks or take screenshots, use browser_take_screenshot from Playwright MCP.
238
+ - When you need to fill forms, click buttons, or perform user interactions on web pages, use Playwright MCP tools (browser_click, browser_type, browser_fill_form).
239
+ - When you need to test responsive design or different viewport sizes, use browser_resize from Playwright MCP.
240
+ - When you finish using the browser, close it with browser_close to free resources.
241
+ - When reproducing UI bugs, use browser_take_screenshot to capture the problem state before implementing any fix.
242
+ - When fixing UI bugs, take before/after screenshots to provide visual evidence of the fix for human verification.
243
+ - When creating UI tests, save baseline screenshots to the repository for visual regression testing.
244
+ - When verifying UI fixes, compare screenshots to ensure the fix does not introduce unintended visual changes.`
245
+ : ''
246
+ }${
229
247
  modelSupportsVision
230
248
  ? `
231
249