@remixhq/claude-plugin 0.1.17 → 0.1.19

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 @@
1
+ {"version":3,"sources":["../src/usage/claudeCodeTranscript.ts","../src/usage/claudeCodeUsageHarvester.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\n\nexport type TranscriptEvent = Record<string, unknown>;\n\nexport type ReadTranscriptResult =\n | { ok: true; events: TranscriptEvent[] }\n | { ok: false; reason: \"transcript_not_found\" | \"transcript_unreadable\" };\n\nexport async function readAndParseTranscript(transcriptPath: string): Promise<ReadTranscriptResult> {\n let raw: string;\n try {\n raw = await fs.readFile(transcriptPath, \"utf8\");\n } catch (err) {\n const code = err && typeof err === \"object\" && \"code\" in err ? (err as { code?: unknown }).code : null;\n if (code === \"ENOENT\") {\n return { ok: false, reason: \"transcript_not_found\" };\n }\n return { ok: false, reason: \"transcript_unreadable\" };\n }\n\n const events: TranscriptEvent[] = [];\n for (const line of raw.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const parsed = JSON.parse(trimmed);\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n events.push(parsed as TranscriptEvent);\n }\n } catch {\n // skip malformed lines\n }\n }\n return { ok: true, events };\n}\n","// Field-name map verified against real Claude Code transcripts under\n// ~/.claude/projects/**/*.jsonl (CC version 2.1.114):\n// event.sessionId, event.type, event.isMeta, event.isSidechain,\n// event.uuid, event.timestamp, event.requestId (assistant only, Anthropic req_...),\n// event.version (Claude Code version, e.g. \"2.1.114\"),\n// event.message.{id, model, role, content, usage}\n// usage.{input_tokens, output_tokens, cache_read_input_tokens,\n// cache_creation_input_tokens, cache_creation.{ephemeral_5m_input_tokens,\n// ephemeral_1h_input_tokens}, server_tool_use.{web_search_requests,\n// web_fetch_requests, ...}, service_tier}\n//\n// Prompt mutation: for plain typed prompts, event.message.content is the\n// literal string the user submitted (verified). For slash commands it becomes\n// \"<command-name>/foo</command-name>...\" wrapping. The boundary walker matches\n// on exact string equality first; a timestamp-tolerance fallback covers the\n// slash-command case without needing a special path.\n//\n// Sidechain marker: boolean event.isSidechain at event top level. Task-spawned\n// subagent assistant messages carry isSidechain: true and may use a different\n// model — billed separately.\n\nimport type {\n ModelCall,\n ServerToolUsage,\n TurnUsage,\n TurnUsageCaptureSource,\n} from \"@remixhq/core/collab\";\nimport type { TranscriptEvent } from \"./claudeCodeTranscript.js\";\n\nexport const HARVESTER_WARNING_CODES = [\n \"cache_split_unavailable\",\n \"unknown_server_tool\",\n \"transcript_truncated\",\n \"subagent_model_differs\",\n \"server_tool_count_mismatch\",\n] as const;\nexport type HarvesterWarningCode = (typeof HARVESTER_WARNING_CODES)[number];\n\nexport type HarvestWarning = { code: HarvesterWarningCode; message: string };\n\nexport type HarvestUsageInput = {\n events: TranscriptEvent[];\n sessionId: string;\n promptText: string;\n submittedAt: string;\n checkSubsequentEvent?: boolean;\n // Exclusive upper bound on transcript events considered part of this turn.\n // Used by the previous-turn harvest to avoid bleeding into the current\n // turn's user message + assistant response when prompts repeat (e.g. user\n // says \"hi\" twice in a row). Defaults to no upper bound (current-turn\n // harvest behavior).\n nextBoundaryAt?: string | null;\n agent: {\n name: \"claude-code\";\n version: string | null;\n sessionId: string | null;\n turnId: string | null;\n plan: string | null;\n };\n capturedAt: string;\n extensions: Record<string, unknown> | null;\n};\n\nexport type HarvestUsageFailureReason = \"no_user_boundary_found\" | \"no_messages_for_turn\";\n\nexport type HarvestUsageResult =\n | {\n ok: true;\n usage: TurnUsage;\n // Timestamp of the user-event boundary that was matched in the\n // transcript (NOT the hook's submittedAt). Callers use this as the\n // upper bound for a subsequent previous-turn harvest so the cutoff\n // sits exactly on this turn's user message rather than on the hook's\n // (slightly later) fire time.\n boundaryAt: string | null;\n }\n | { ok: false; reason: HarvestUsageFailureReason };\n\nconst BOUNDARY_TIMESTAMP_TOLERANCE_MS = 500;\n\nfunction parseTimestamp(value: unknown): number | null {\n if (typeof value !== \"string\") return null;\n const ms = Date.parse(value);\n return Number.isFinite(ms) ? ms : null;\n}\n\nfunction extractUserContentText(message: unknown): string | null {\n if (!message || typeof message !== \"object\") return null;\n const content = (message as { content?: unknown }).content;\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n const textBlocks = content\n .filter((block): block is { type?: string; text?: string } => Boolean(block) && typeof block === \"object\")\n .filter((block) => block.type === \"text\" && typeof block.text === \"string\")\n .map((block) => block.text as string);\n if (textBlocks.length === 0) return null;\n return textBlocks.join(\"\\n\");\n }\n return null;\n}\n\nfunction isUserBoundary(event: TranscriptEvent): boolean {\n return (\n event.type === \"user\" &&\n event.isMeta !== true &&\n event.isSidechain !== true\n );\n}\n\n// Concatenate the text content of every assistant event inside a single\n// turn into one string. Mirrors `extractUserContentText` but iterates\n// over a list (turns can have many assistant events: streaming chunks,\n// tool-use cycles, sidechain delegated agents, etc).\n//\n// We DELIBERATELY include sidechain assistant events here because they\n// represent real model output the user paid for and would expect to see\n// in the dashboard. The slicer's \"no sidechain user boundary\" rule\n// (isUserBoundary) is about turn segmentation, not about hiding the\n// model's response text.\n//\n// We DELIBERATELY drop tool_use / tool_result / thinking blocks: this\n// field is meant to render in a \"what did the agent say\" pane, not to\n// reproduce the entire structured tool log. Tool calls are surfaced\n// elsewhere (turnUsage in workspace_metadata).\nfunction extractAssistantContentText(turnEvents: TranscriptEvent[]): string | null {\n const chunks: string[] = [];\n for (const ev of turnEvents) {\n if (ev.type !== \"assistant\") continue;\n const text = extractUserContentText(ev.message);\n if (text && text.length > 0) chunks.push(text);\n }\n if (chunks.length === 0) return null;\n // Strip duplicate adjacent chunks that some Claude transcripts emit\n // (streaming-restart artefacts where the assistant resumes mid-text\n // and the harvester sees the prefix twice). Cheap to do here, hard\n // to undo at read time.\n const deduped: string[] = [];\n for (const chunk of chunks) {\n if (deduped[deduped.length - 1] !== chunk) deduped.push(chunk);\n }\n return deduped.join(\"\\n\");\n}\n\nfunction findBoundary(\n sessionEvents: TranscriptEvent[],\n promptText: string,\n submittedAt: string,\n upperMs: number | null,\n): { boundary: TranscriptEvent | null; usedFallback: boolean } {\n // Exclude any event at or after the upper bound. Used by the previous-turn\n // harvest so a repeated prompt text can't lock onto the current turn's user\n // message.\n const isWithinUpperBound = (ev: TranscriptEvent): boolean => {\n if (upperMs === null) return true;\n const ms = parseTimestamp(ev.timestamp);\n return ms !== null && ms < upperMs;\n };\n\n let contentMatch: TranscriptEvent | null = null;\n for (let i = sessionEvents.length - 1; i >= 0; i--) {\n const ev = sessionEvents[i];\n if (!isUserBoundary(ev)) continue;\n if (!isWithinUpperBound(ev)) continue;\n const text = extractUserContentText(ev.message);\n if (text !== null && text === promptText) {\n contentMatch = ev;\n break;\n }\n }\n if (contentMatch) return { boundary: contentMatch, usedFallback: false };\n\n const submittedMs = parseTimestamp(submittedAt);\n if (submittedMs === null) return { boundary: null, usedFallback: false };\n\n for (let i = sessionEvents.length - 1; i >= 0; i--) {\n const ev = sessionEvents[i];\n if (!isUserBoundary(ev)) continue;\n if (!isWithinUpperBound(ev)) continue;\n const ms = parseTimestamp(ev.timestamp);\n if (ms === null) continue;\n if (ms >= submittedMs - BOUNDARY_TIMESTAMP_TOLERANCE_MS) {\n return { boundary: ev, usedFallback: true };\n }\n }\n return { boundary: null, usedFallback: false };\n}\n\nfunction asNumberOrNull(value: unknown): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\nfunction asStringOrNull(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\ntype AssistantMessage = {\n id: string | null;\n model: string | null;\n content: unknown[];\n usage: Record<string, unknown> | null;\n};\n\nfunction extractAssistantMessage(event: TranscriptEvent): AssistantMessage | null {\n const message = event.message;\n if (!message || typeof message !== \"object\") return null;\n const msg = message as Record<string, unknown>;\n if (msg.role !== \"assistant\" && msg.role !== undefined) {\n // some transcripts may omit role on assistant events; keep permissive\n }\n const content = Array.isArray(msg.content) ? (msg.content as unknown[]) : [];\n const usage = msg.usage && typeof msg.usage === \"object\" ? (msg.usage as Record<string, unknown>) : null;\n return {\n id: asStringOrNull(msg.id),\n model: asStringOrNull(msg.model),\n content,\n usage,\n };\n}\n\nfunction usageIsComplete(usage: Record<string, unknown> | null): boolean {\n if (!usage) return false;\n const hasInput = typeof usage.input_tokens === \"number\";\n const hasOutput = typeof usage.output_tokens === \"number\";\n const hasCacheRead = typeof usage.cache_read_input_tokens === \"number\";\n return hasInput && hasOutput && hasCacheRead;\n}\n\nfunction buildModelCall(event: TranscriptEvent, msg: AssistantMessage): ModelCall {\n const usage = msg.usage ?? {};\n const cacheCreation =\n usage.cache_creation && typeof usage.cache_creation === \"object\"\n ? (usage.cache_creation as Record<string, unknown>)\n : null;\n const has5m = cacheCreation && typeof cacheCreation.ephemeral_5m_input_tokens === \"number\";\n const has1h = cacheCreation && typeof cacheCreation.ephemeral_1h_input_tokens === \"number\";\n const cacheWrite5mTokens = has5m ? (cacheCreation!.ephemeral_5m_input_tokens as number) : null;\n const cacheWrite1hTokens = has1h ? (cacheCreation!.ephemeral_1h_input_tokens as number) : null;\n const splitAvailable = has5m || has1h;\n const cacheWriteTokens = splitAvailable ? null : asNumberOrNull(usage.cache_creation_input_tokens);\n\n return {\n provider: \"anthropic\",\n model: msg.model,\n tier: asStringOrNull(usage.service_tier),\n requestId: asStringOrNull(event.requestId),\n timestamp: asStringOrNull(event.timestamp),\n isSidechain: event.isSidechain === true,\n inputTokens: asNumberOrNull(usage.input_tokens),\n outputTokens: asNumberOrNull(usage.output_tokens),\n cacheReadTokens: asNumberOrNull(usage.cache_read_input_tokens),\n cacheWriteTokens,\n cacheWrite5mTokens,\n cacheWrite1hTokens,\n reasoningTokens: null,\n audioInputTokens: null,\n imageInputTokens: null,\n };\n}\n\ntype ServerToolUseRecord = {\n tool: string;\n unit: string;\n isKnown: boolean;\n // Stable identifier so we can dedupe across \"direct\" (raw API server_tool_use\n // blocks in assistant content) and \"embedded\" (Claude Code's WebSearch\n // wrapper, where the underlying srvtoolu_* lives in tool_result.toolUseResult)\n // discovery paths.\n id: string;\n source: \"direct\" | \"embedded\";\n};\n\nfunction scanServerToolUses(content: unknown[]): ServerToolUseRecord[] {\n const uses: ServerToolUseRecord[] = [];\n for (const block of content) {\n if (!block || typeof block !== \"object\") continue;\n const b = block as Record<string, unknown>;\n const id = b.id;\n if (typeof id !== \"string\" || !id.startsWith(\"srvtoolu_\")) continue;\n const name = typeof b.name === \"string\" ? b.name : \"\";\n // Units MUST match the values stored in the backend's `server_tool_pricing`\n // table — the rate lookup is exact-match on (provider, tool, unit). The\n // seed uses `per_request` for billable web tools (anthropic web_search +\n // web_fetch are $0.01/req); emitting plain `request` here used to silently\n // zero out server_tool_cost_usd because the lookup found no row.\n switch (name) {\n case \"web_search\":\n uses.push({ tool: \"web_search\", unit: \"per_request\", isKnown: true, id, source: \"direct\" });\n break;\n case \"web_fetch\":\n uses.push({ tool: \"web_fetch\", unit: \"per_request\", isKnown: true, id, source: \"direct\" });\n break;\n case \"code_execution\":\n uses.push({ tool: \"code_execution\", unit: \"invocation\", isKnown: true, id, source: \"direct\" });\n break;\n default:\n uses.push({ tool: name || \"unknown\", unit: \"invocation\", isKnown: false, id, source: \"direct\" });\n break;\n }\n }\n return uses;\n}\n\n// Claude Code's `WebSearch` / `WebFetch` tools never appear as `server_tool_use`\n// blocks on the assistant message — they show up as ordinary `tool_use` blocks\n// with `toolu_*` IDs and a capitalized `name` (\"WebSearch\"). The actual server\n// tool that Anthropic bills (with the `srvtoolu_*` ID) is invoked from a\n// secondary Haiku sub-call (see leaked source analysis at\n// trevorfox.com/2026/04/how-claude-code-search-actually-works/), and Claude\n// Code mirrors the underlying `srvtoolu_*` ID into the matching tool_result\n// user message under `event.toolUseResult.results[].tool_use_id`.\n//\n// Because the Haiku sub-call is its own API request, the main turn's\n// `usage.server_tool_use.web_search_requests` is always 0 — so the existing\n// \"direct\" scan misses these completely. We recover them here by walking\n// tool_result events for embedded srvtoolu_ IDs.\nfunction buildClientToolNameMap(turnEvents: TranscriptEvent[]): Map<string, string> {\n const map = new Map<string, string>();\n for (const ev of turnEvents) {\n if (ev.type !== \"assistant\") continue;\n const msg = ev.message;\n if (!msg || typeof msg !== \"object\") continue;\n const content = (msg as { content?: unknown }).content;\n if (!Array.isArray(content)) continue;\n for (const block of content) {\n if (!block || typeof block !== \"object\") continue;\n const b = block as Record<string, unknown>;\n if (b.type !== \"tool_use\") continue;\n const id = b.id;\n const name = b.name;\n if (typeof id !== \"string\" || typeof name !== \"string\") continue;\n if (!id.startsWith(\"toolu_\")) continue;\n map.set(id, name);\n }\n }\n return map;\n}\n\nfunction scanEmbeddedServerToolUses(\n turnEvents: TranscriptEvent[],\n clientToolNames: Map<string, string>,\n): ServerToolUseRecord[] {\n const uses: ServerToolUseRecord[] = [];\n for (const ev of turnEvents) {\n if (ev.type !== \"user\") continue;\n const tur = (ev as Record<string, unknown>).toolUseResult;\n if (!tur || typeof tur !== \"object\") continue;\n const results = (tur as Record<string, unknown>).results;\n if (!Array.isArray(results)) continue;\n\n // The parent client-side tool's ID (toolu_*) lives on the tool_result\n // content block inside the user message. We use it to attribute the\n // embedded srvtoolu_ to the correct logical tool.\n let parentToolName = \"\";\n const userMsg = ev.message;\n if (userMsg && typeof userMsg === \"object\") {\n const userContent = (userMsg as { content?: unknown }).content;\n if (Array.isArray(userContent)) {\n for (const block of userContent) {\n if (!block || typeof block !== \"object\") continue;\n const b = block as Record<string, unknown>;\n if (b.type !== \"tool_result\") continue;\n const parentId = b.tool_use_id;\n if (typeof parentId === \"string\") {\n parentToolName = clientToolNames.get(parentId) ?? \"\";\n break;\n }\n }\n }\n }\n\n for (const entry of results) {\n if (!entry || typeof entry !== \"object\") continue;\n const srvId = (entry as Record<string, unknown>).tool_use_id;\n if (typeof srvId !== \"string\" || !srvId.startsWith(\"srvtoolu_\")) continue;\n // Today, Claude Code only delegates `WebSearch` to Anthropic's\n // server-side `web_search` tool. WebFetch summarizes via Haiku without\n // emitting a srvtoolu_. If a future Claude Code release adds another\n // wrapper we'll see it via the parent tool name.\n if (parentToolName === \"WebFetch\") {\n uses.push({ tool: \"web_fetch\", unit: \"per_request\", isKnown: true, id: srvId, source: \"embedded\" });\n } else {\n uses.push({ tool: \"web_search\", unit: \"per_request\", isKnown: true, id: srvId, source: \"embedded\" });\n }\n }\n }\n return uses;\n}\n\nfunction dedupeByServerToolId(records: ServerToolUseRecord[]): ServerToolUseRecord[] {\n const seen = new Map<string, ServerToolUseRecord>();\n for (const r of records) {\n const existing = seen.get(r.id);\n if (!existing) {\n seen.set(r.id, r);\n continue;\n }\n // Direct path is preferred (it carries the canonical name from Anthropic's\n // own server_tool_use block). Embedded only wins if direct is absent.\n if (existing.source === \"embedded\" && r.source === \"direct\") {\n seen.set(r.id, r);\n }\n }\n return Array.from(seen.values());\n}\n\nfunction aggregateServerTools(\n uses: ServerToolUseRecord[],\n): { serverTools: ServerToolUsage[]; sawUnknown: boolean; sawEmbedded: boolean } {\n const map = new Map<string, ServerToolUsage>();\n let sawUnknown = false;\n let sawEmbedded = false;\n for (const use of uses) {\n if (!use.isKnown) sawUnknown = true;\n if (use.source === \"embedded\") sawEmbedded = true;\n const key = `anthropic|${use.tool}|${use.unit}`;\n const existing = map.get(key);\n if (existing) {\n existing.quantity += 1;\n } else {\n map.set(key, { provider: \"anthropic\", tool: use.tool, unit: use.unit, quantity: 1 });\n }\n }\n return { serverTools: Array.from(map.values()), sawUnknown, sawEmbedded };\n}\n\nfunction sumCrossCheckCounts(usageBlocks: Record<string, unknown>[]): Map<string, number> {\n const totals = new Map<string, number>();\n const keyFor = (raw: string): string | null => {\n if (raw === \"web_search_requests\") return \"web_search\";\n if (raw === \"web_fetch_requests\") return \"web_fetch\";\n return null;\n };\n for (const usage of usageBlocks) {\n const stu = usage.server_tool_use;\n if (!stu || typeof stu !== \"object\") continue;\n for (const [rawKey, rawVal] of Object.entries(stu as Record<string, unknown>)) {\n const mapped = keyFor(rawKey);\n if (!mapped) continue;\n if (typeof rawVal !== \"number\" || !Number.isFinite(rawVal)) continue;\n totals.set(mapped, (totals.get(mapped) ?? 0) + rawVal);\n }\n }\n return totals;\n}\n\nfunction primaryToolCounts(serverTools: ServerToolUsage[]): Map<string, number> {\n const map = new Map<string, number>();\n for (const entry of serverTools) {\n map.set(entry.tool, (map.get(entry.tool) ?? 0) + entry.quantity);\n }\n return map;\n}\n\nfunction resolveVersion(events: TranscriptEvent[]): string | null {\n for (const ev of events) {\n if (typeof ev.version === \"string\" && ev.version.trim()) return ev.version.trim();\n }\n return null;\n}\n\n// Shared core: takes a pre-computed turn slice and assembles the TurnUsage\n// payload. Used by both the live hook path (`harvestClaudeCodeUsage`, which\n// finds the slice via prompt-text matching) and the historical importer\n// (`sliceTranscriptIntoTurns`, which walks every user boundary in the\n// transcript). Both paths MUST go through this function so the\n// workspace_metadata.turnUsage shape is byte-identical for the same source\n// events; the pricing pipeline reads that shape and silent shape drift\n// between live and historical would corrupt cost numbers without surfacing.\nfunction buildTurnUsage(args: {\n sessionEvents: TranscriptEvent[];\n turnEvents: TranscriptEvent[];\n upperMs: number | null;\n initialWarnings: HarvestWarning[];\n agent: TurnUsage[\"agent\"];\n capturedAt: string;\n captureSource: TurnUsageCaptureSource;\n extensions: Record<string, unknown> | null;\n checkSubsequentEvent: boolean;\n}): { ok: true; usage: TurnUsage } | { ok: false; reason: HarvestUsageFailureReason } {\n const assistantEvents = args.turnEvents.filter((ev) => ev.type === \"assistant\");\n if (assistantEvents.length === 0) {\n return { ok: false, reason: \"no_messages_for_turn\" };\n }\n\n const warnings: HarvestWarning[] = [...args.initialWarnings];\n\n const calls: ModelCall[] = [];\n const usageBlocks: Record<string, unknown>[] = [];\n let anyIncomplete = false;\n let anyComplete = false;\n let sawLumpSumFallback = false;\n const collectedServerToolUses: ServerToolUseRecord[] = [];\n\n const mainModels = new Set<string>();\n const sidechainModels = new Set<string>();\n\n for (const ev of assistantEvents) {\n const msg = extractAssistantMessage(ev);\n if (!msg) continue;\n if (usageIsComplete(msg.usage)) {\n anyComplete = true;\n } else {\n anyIncomplete = true;\n }\n if (msg.usage) usageBlocks.push(msg.usage);\n\n const call = buildModelCall(ev, msg);\n calls.push(call);\n\n if (call.cacheWrite5mTokens === null && call.cacheWrite1hTokens === null && call.cacheWriteTokens !== null) {\n sawLumpSumFallback = true;\n }\n\n collectedServerToolUses.push(...scanServerToolUses(msg.content));\n\n if (call.model) {\n if (call.isSidechain) sidechainModels.add(call.model);\n else mainModels.add(call.model);\n }\n }\n\n if (calls.length === 0) {\n return { ok: false, reason: \"no_messages_for_turn\" };\n }\n\n if (sawLumpSumFallback) {\n warnings.push({\n code: \"cache_split_unavailable\",\n message: \"Assistant message reported lump-sum cache_creation_input_tokens without the 5m/1h split.\",\n });\n }\n\n // Pull in server-tool usage from Claude Code's WebSearch/WebFetch wrappers,\n // where the billable srvtoolu_ ID lives in the tool_result event rather than\n // on the assistant message. Dedupe by srvtoolu_ ID so a future Claude Code\n // version that surfaces the ID in BOTH places won't double-count.\n const clientToolNames = buildClientToolNameMap(args.turnEvents);\n const embeddedUses = scanEmbeddedServerToolUses(args.turnEvents, clientToolNames);\n const merged = dedupeByServerToolId([...collectedServerToolUses, ...embeddedUses]);\n const { serverTools, sawUnknown, sawEmbedded } = aggregateServerTools(merged);\n if (sawUnknown) {\n warnings.push({\n code: \"unknown_server_tool\",\n message: \"Encountered a server tool whose name is not in the known list (web_search, web_fetch, code_execution).\",\n });\n }\n\n const crossCheck = sumCrossCheckCounts(usageBlocks);\n const primary = primaryToolCounts(serverTools);\n const allTools = new Set<string>([...crossCheck.keys(), ...primary.keys()]);\n for (const tool of allTools) {\n const crossVal = crossCheck.get(tool) ?? 0;\n const primVal = primary.get(tool) ?? 0;\n if (crossVal === primVal) continue;\n if (sawEmbedded && crossVal === 0) continue;\n warnings.push({\n code: \"server_tool_count_mismatch\",\n message: `Server-tool ${tool} count mismatch: srvtoolu_ scan=${primVal}, usage.server_tool_use=${crossVal}. Trusting srvtoolu_ count.`,\n });\n break;\n }\n\n const subagentMismatch =\n mainModels.size > 0 &&\n sidechainModels.size > 0 &&\n [...sidechainModels].some((m) => !mainModels.has(m));\n if (subagentMismatch) {\n warnings.push({\n code: \"subagent_model_differs\",\n message: \"At least one sidechain ModelCall uses a model different from the main-chain model in this turn.\",\n });\n }\n\n let confidence: TurnUsage[\"confidence\"];\n if (!anyComplete && anyIncomplete) confidence = \"unknown\";\n else if (anyIncomplete) confidence = \"partial\";\n else if (anyComplete) confidence = \"exact\";\n else confidence = \"unknown\";\n\n if (args.checkSubsequentEvent) {\n const lastAssistantMs = (() => {\n const msList = assistantEvents\n .map((ev) => parseTimestamp(ev.timestamp))\n .filter((ms): ms is number => ms !== null);\n return msList.length ? Math.max(...msList) : null;\n })();\n const hasSubsequent =\n lastAssistantMs !== null &&\n args.sessionEvents.some((ev) => {\n const ms = parseTimestamp(ev.timestamp);\n if (ms === null || ms <= lastAssistantMs) return false;\n if (args.upperMs !== null && ms >= args.upperMs) return false;\n return true;\n });\n if (!hasSubsequent && confidence === \"exact\") {\n confidence = \"partial\";\n warnings.push({\n code: \"transcript_truncated\",\n message: \"Previous turn has no subsequent event after its last assistant message; transcript may be truncated.\",\n });\n }\n }\n\n const resolvedVersion =\n args.agent.version ?? resolveVersion(args.turnEvents) ?? resolveVersion(args.sessionEvents);\n\n const usage: TurnUsage = {\n schemaVersion: 1,\n capturedAt: args.capturedAt,\n captureSource: args.captureSource,\n confidence,\n agent: {\n name: args.agent.name,\n version: resolvedVersion,\n sessionId: args.agent.sessionId,\n turnId: args.agent.turnId,\n plan: args.agent.plan,\n },\n calls,\n serverTools,\n warnings,\n extensions: args.extensions,\n };\n return { ok: true, usage };\n}\n\nexport function harvestClaudeCodeUsage(input: HarvestUsageInput): HarvestUsageResult {\n const sessionEvents = input.events.filter((ev) => ev.sessionId === input.sessionId);\n const upperMs = input.nextBoundaryAt ? parseTimestamp(input.nextBoundaryAt) : null;\n const { boundary, usedFallback } = findBoundary(\n sessionEvents,\n input.promptText,\n input.submittedAt,\n upperMs,\n );\n if (!boundary) {\n return { ok: false, reason: \"no_user_boundary_found\" };\n }\n const boundaryMs = parseTimestamp(boundary.timestamp);\n if (boundaryMs === null) {\n return { ok: false, reason: \"no_user_boundary_found\" };\n }\n\n const turnEvents = sessionEvents.filter((ev) => {\n const ms = parseTimestamp(ev.timestamp);\n if (ms === null || ms <= boundaryMs) return false;\n if (upperMs !== null && ms >= upperMs) return false;\n return true;\n });\n\n const initialWarnings: HarvestWarning[] = [];\n if (usedFallback) {\n initialWarnings.push({\n code: \"transcript_truncated\",\n message: \"Prompt-text equality match failed; used timestamp-tolerance fallback to locate the user boundary.\",\n });\n }\n\n const built = buildTurnUsage({\n sessionEvents,\n turnEvents,\n upperMs,\n initialWarnings,\n agent: input.agent,\n capturedAt: input.capturedAt,\n captureSource: \"hook\",\n extensions: input.extensions,\n checkSubsequentEvent: input.checkSubsequentEvent === true,\n });\n\n if (!built.ok) return built;\n return {\n ok: true,\n usage: built.usage,\n boundaryAt: typeof boundary.timestamp === \"string\" ? boundary.timestamp : null,\n };\n}\n\n// =============================================================================\n// Historical-import slicer\n// =============================================================================\n// Walks every user-boundary event in a session's transcript and produces one\n// SlicedTurn per boundary, calling buildTurnUsage with captureSource =\n// \"historical_import\". This is the read-side primitive used by\n// `remix history import`.\n//\n// Sidechain policy (matches live hook behavior, enforced by isUserBoundary):\n// * Sidechain user events (isSidechain: true) DO NOT start a new turn.\n// * Sidechain assistant events ARE captured into the parent turn's\n// `calls` array with isSidechain: true preserved (handled inside\n// buildTurnUsage via buildModelCall).\n//\n// `gitBranch`, `cwd`, and `promptId` are read from the user-boundary event\n// directly (verified present on real Claude transcripts, CC version 2.1.116).\n// They're returned alongside the TurnUsage so the importer can compute the\n// dedup key, the repo fingerprint, and the workspace_metadata.branch.\n\nexport type SlicedTurn = {\n sessionId: string;\n promptId: string | null;\n promptText: string | null;\n // Concatenated text of every assistant message inside this turn\n // (post-prompt, pre-next-prompt). Tool-use / tool-result / thinking\n // blocks are excluded — see extractAssistantContentText for the\n // reasoning. The CLI runner gates whether this gets uploaded with\n // the same opt-in as promptText (--include-prompt-text), since the\n // sensitivity is identical: this is the user's literal AI dialogue.\n assistantText: string | null;\n occurredAt: string;\n gitBranch: string | null;\n cwd: string | null;\n usage: TurnUsage;\n};\n\nexport type SliceTranscriptInput = {\n events: TranscriptEvent[];\n sessionId: string;\n capturedAt: string;\n extensions?: Record<string, unknown> | null;\n};\n\nexport function sliceTranscriptIntoTurns(input: SliceTranscriptInput): SlicedTurn[] {\n const sessionEvents = input.events.filter((ev) => ev.sessionId === input.sessionId);\n const boundaries = sessionEvents.filter(isUserBoundary);\n const result: SlicedTurn[] = [];\n\n for (let i = 0; i < boundaries.length; i++) {\n const boundary = boundaries[i];\n const nextBoundary = boundaries[i + 1] ?? null;\n const boundaryMs = parseTimestamp(boundary.timestamp);\n if (boundaryMs === null) continue;\n const occurredAt = asStringOrNull(boundary.timestamp);\n if (occurredAt === null) continue;\n\n const upperMs = nextBoundary ? parseTimestamp(nextBoundary.timestamp) : null;\n\n const turnEvents = sessionEvents.filter((ev) => {\n const ms = parseTimestamp(ev.timestamp);\n if (ms === null || ms <= boundaryMs) return false;\n if (upperMs !== null && ms >= upperMs) return false;\n return true;\n });\n\n const promptId = asStringOrNull((boundary as Record<string, unknown>).promptId);\n const promptText = extractUserContentText(boundary.message);\n const assistantText = extractAssistantContentText(turnEvents);\n const gitBranch = asStringOrNull((boundary as Record<string, unknown>).gitBranch);\n const cwd = asStringOrNull((boundary as Record<string, unknown>).cwd);\n\n const built = buildTurnUsage({\n sessionEvents,\n turnEvents,\n upperMs,\n initialWarnings: [],\n agent: {\n name: \"claude-code\",\n version: null,\n sessionId: input.sessionId,\n turnId: promptId,\n plan: null,\n },\n capturedAt: input.capturedAt,\n captureSource: \"historical_import\",\n extensions: input.extensions ?? null,\n checkSubsequentEvent: false,\n });\n\n if (!built.ok) continue;\n\n result.push({\n sessionId: input.sessionId,\n promptId,\n promptText,\n assistantText,\n occurredAt,\n gitBranch,\n cwd,\n usage: built.usage,\n });\n }\n\n return result;\n}\n"],"mappings":"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","names":[]}
@@ -685,6 +685,12 @@ function normalizeStringArray(value) {
685
685
  )
686
686
  );
687
687
  }
688
+ function normalizeManualRecordingScope(value) {
689
+ if (value === "full_turn") {
690
+ return "full_turn";
691
+ }
692
+ return null;
693
+ }
688
694
  function normalizeTouchedRepo(value, repoRoot) {
689
695
  if (!value || typeof value !== "object") return null;
690
696
  const parsed = value;
@@ -703,7 +709,7 @@ function normalizeTouchedRepo(value, repoRoot) {
703
709
  manuallyRecorded: Boolean(parsed.manuallyRecorded),
704
710
  manuallyRecordedAt: normalizeString(parsed.manuallyRecordedAt),
705
711
  manuallyRecordedByTool: normalizeString(parsed.manuallyRecordedByTool),
706
- manualRecordingScope: parsed.manualRecordingScope === "change_step" || parsed.manualRecordingScope === "full_turn" ? parsed.manualRecordingScope : null,
712
+ manualRecordingScope: normalizeManualRecordingScope(parsed.manualRecordingScope),
707
713
  manualRemoteChangeRecordedAt: normalizeString(parsed.manualRemoteChangeRecordedAt),
708
714
  stopAttempted: Boolean(parsed.stopAttempted),
709
715
  stopRecorded: Boolean(parsed.stopRecorded),
@@ -855,7 +861,7 @@ async function markPendingTurnConsultedMemory(sessionId) {
855
861
  // package.json
856
862
  var package_default = {
857
863
  name: "@remixhq/claude-plugin",
858
- version: "0.1.17",
864
+ version: "0.1.19",
859
865
  description: "Claude Code plugin for Remix collaboration workflows",
860
866
  homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
861
867
  license: "MIT",
@@ -878,16 +884,27 @@ var package_default = {
878
884
  "hooks",
879
885
  "agents"
880
886
  ],
887
+ exports: {
888
+ ".": {
889
+ types: "./dist/index.d.ts",
890
+ import: "./dist/index.js"
891
+ },
892
+ "./historical": {
893
+ types: "./dist/historical.d.ts",
894
+ import: "./dist/historical.js"
895
+ }
896
+ },
881
897
  scripts: {
882
898
  build: "tsup",
883
899
  postbuild: `node -e "const fs=require('node:fs'); for (const p of ['dist/mcp-server.cjs','dist/hook-pre-git.cjs','dist/hook-user-prompt.cjs','dist/hook-post-collab.cjs','dist/hook-stop-collab.cjs']) fs.chmodSync(p, 0o755);"`,
884
900
  dev: "tsx src/mcp-server.ts",
885
901
  typecheck: "tsc -p tsconfig.json --noEmit",
902
+ test: "node --import tsx --test src/**/*.test.ts",
886
903
  prepack: "npm run build"
887
904
  },
888
905
  dependencies: {
889
- "@remixhq/core": "^0.1.12",
890
- "@remixhq/mcp": "^0.1.12"
906
+ "@remixhq/core": "^0.1.14",
907
+ "@remixhq/mcp": "^0.1.14"
891
908
  },
892
909
  devDependencies: {
893
910
  "@types/node": "^25.4.0",
@@ -7751,7 +7768,7 @@ var {
7751
7768
  getCancelSignal: getCancelSignal2
7752
7769
  } = getIpcExport();
7753
7770
 
7754
- // node_modules/@remixhq/core/dist/chunk-RREREIGW.js
7771
+ // node_modules/@remixhq/core/dist/chunk-WT6VRLXU.js
7755
7772
  async function runGit(args, cwd) {
7756
7773
  const res = await execa("git", args, { cwd, stderr: "ignore" });
7757
7774
  return String(res.stdout || "").trim();
@@ -7765,7 +7782,7 @@ async function getCurrentBranch(cwd) {
7765
7782
  }
7766
7783
  }
7767
7784
 
7768
- // node_modules/@remixhq/core/dist/chunk-IXWQWFYT.js
7785
+ // node_modules/@remixhq/core/dist/chunk-YCFLOHJV.js
7769
7786
  var import_promises14 = __toESM(require("fs/promises"), 1);
7770
7787
  var import_path = __toESM(require("path"), 1);
7771
7788
  var import_promises15 = __toESM(require("fs/promises"), 1);
@@ -8028,11 +8045,6 @@ function extractToolInput(payload) {
8028
8045
  function extractToolResponse(payload) {
8029
8046
  return getNestedRecord(payload.tool_response) ?? getNestedRecord(payload.toolResponse);
8030
8047
  }
8031
- function extractToolStructuredData(payload) {
8032
- const toolResponse = extractToolResponse(payload);
8033
- const structuredContent = getNestedRecord(toolResponse?.structuredContent) ?? getNestedRecord(payload.structuredContent);
8034
- return getNestedRecord(toolResponse?.data) ?? getNestedRecord(structuredContent?.data) ?? structuredContent;
8035
- }
8036
8048
  function extractToolName(payload) {
8037
8049
  return extractString(payload, ["tool_name", "toolName"]);
8038
8050
  }
@@ -8046,26 +8058,6 @@ function normalizeHookToolName(toolName) {
8046
8058
  }
8047
8059
  return trimmed;
8048
8060
  }
8049
- function extractAssistantResponse(payload) {
8050
- const candidateKeys = [
8051
- "last_assistant_message",
8052
- "lastAssistantMessage",
8053
- "assistant_response",
8054
- "assistantResponse",
8055
- "assistant_message",
8056
- "assistantMessage",
8057
- "response",
8058
- "message"
8059
- ];
8060
- return extractString(payload, candidateKeys) ?? extractString(extractToolResponse(payload) ?? {}, candidateKeys) ?? extractString(extractToolStructuredData(payload) ?? {}, candidateKeys) ?? extractString(extractToolInput(payload), candidateKeys);
8061
- }
8062
- function extractFinalizeTurnMode(payload) {
8063
- const mode = extractString(extractToolStructuredData(payload) ?? {}, ["mode"]) ?? extractString(extractToolResponse(payload) ?? {}, ["mode"]) ?? extractString(payload, ["mode"]);
8064
- if (mode === "changed_turn" || mode === "no_diff_turn") {
8065
- return mode;
8066
- }
8067
- return null;
8068
- }
8069
8061
  function extractString(input, keys) {
8070
8062
  for (const key of keys) {
8071
8063
  const value = input[key];
@@ -8117,9 +8109,6 @@ function didToolSucceed(payload) {
8117
8109
  const hookEventName = extractString(payload, ["hook_event_name", "hookEventName"]);
8118
8110
  return hookEventName === "PostToolUse";
8119
8111
  }
8120
- function isRemoteChangeRecordedButLocalSyncFailed(payload) {
8121
- return extractToolErrorMessage(payload) === "Change step succeeded remotely, but automatic local sync failed.";
8122
- }
8123
8112
  function collectStringPathValue(value) {
8124
8113
  if (typeof value === "string" && value.trim()) return [value.trim()];
8125
8114
  if (Array.isArray(value)) {
@@ -8184,7 +8173,7 @@ async function resolveBoundRepoFromToolCwd(payload) {
8184
8173
 
8185
8174
  // src/hook-post-collab.ts
8186
8175
  function isRepoMutationToolName(toolName) {
8187
- return /remix_collab_(add|add_change_step|sync_apply|approve_and_sync_target|sync_upstream|reconcile_apply)$/i.test(toolName);
8176
+ return /remix_collab_(sync_apply|approve_and_sync_target|sync_upstream|reconcile_apply)$/i.test(toolName);
8188
8177
  }
8189
8178
  function isMemoryToolName(toolName) {
8190
8179
  return /remix_collab_memory_(summary|search|timeline|change_step_diff)$/i.test(toolName);
@@ -8198,31 +8187,12 @@ function isStructuredLocalReadToolName(toolName) {
8198
8187
  function isShellToolName(toolName) {
8199
8188
  return /^Bash$/i.test(toolName);
8200
8189
  }
8201
- function isRemoteChangeRecordingToolName(toolName) {
8202
- return /remix_collab_(add|add_change_step)$/i.test(toolName);
8203
- }
8204
- function hasManualFullTurnPayload(payload) {
8205
- const toolInput = extractToolInput(payload);
8206
- return Boolean(extractString(toolInput, ["prompt"]) && extractAssistantResponse(payload));
8207
- }
8208
- function getManualRecordingScope(payload, toolName) {
8190
+ function getManualRecordingScope(toolName) {
8209
8191
  if (/remix_collab_finalize_turn$/i.test(toolName)) {
8210
8192
  return "full_turn";
8211
8193
  }
8212
- if (/remix_collab_(add|add_change_step)$/i.test(toolName)) {
8213
- return hasManualFullTurnPayload(payload) ? "full_turn" : "change_step";
8214
- }
8215
- if (/remix_collab_(record_turn|record_no_diff_turn)$/i.test(toolName)) {
8216
- return "full_turn";
8217
- }
8218
8194
  return null;
8219
8195
  }
8220
- function didFinalizeTurnRecordRemoteChange(payload, toolName) {
8221
- if (!/remix_collab_finalize_turn$/i.test(toolName)) {
8222
- return false;
8223
- }
8224
- return extractFinalizeTurnMode(payload) === "changed_turn";
8225
- }
8226
8196
  function isLikelyMutatingShellCommand(command) {
8227
8197
  const normalized = command.trim().toLowerCase();
8228
8198
  if (!normalized) return false;
@@ -8288,7 +8258,6 @@ async function runHookPostCollab(payload) {
8288
8258
  return;
8289
8259
  }
8290
8260
  const toolSucceeded = didToolSucceed(payload);
8291
- const remoteChangeRecordedButSyncFailed = (isRemoteChangeRecordingToolName(toolName) || /remix_collab_finalize_turn$/i.test(toolName)) && isRemoteChangeRecordedButLocalSyncFailed(payload);
8292
8261
  await appendHookDiagnosticsEvent({
8293
8262
  hook: "PostToolUse",
8294
8263
  sessionId,
@@ -8297,7 +8266,6 @@ async function runHookPostCollab(payload) {
8297
8266
  toolName,
8298
8267
  fields: {
8299
8268
  toolSucceeded,
8300
- remoteChangeRecordedButSyncFailed,
8301
8269
  isMemoryTool: isMemoryToolName(toolName),
8302
8270
  isRepoMutationTool: isRepoMutationToolName(toolName),
8303
8271
  isShellTool: isShellToolName(toolName),
@@ -8305,7 +8273,7 @@ async function runHookPostCollab(payload) {
8305
8273
  isStructuredLocalReadTool: isStructuredLocalReadToolName(toolName)
8306
8274
  }
8307
8275
  });
8308
- if (!toolSucceeded && !remoteChangeRecordedButSyncFailed) {
8276
+ if (!toolSucceeded) {
8309
8277
  await appendHookDiagnosticsEvent({
8310
8278
  hook: "PostToolUse",
8311
8279
  sessionId,
@@ -8326,7 +8294,7 @@ async function runHookPostCollab(payload) {
8326
8294
  toolName
8327
8295
  });
8328
8296
  }
8329
- const manualRecordingScope = getManualRecordingScope(payload, toolName);
8297
+ const manualRecordingScope = getManualRecordingScope(toolName);
8330
8298
  if (isRepoMutationToolName(toolName) || manualRecordingScope) {
8331
8299
  const targetRepo = await resolveBoundRepoFromToolCwd(payload);
8332
8300
  if (targetRepo) {
@@ -8345,7 +8313,7 @@ async function runHookPostCollab(payload) {
8345
8313
  await markTouchedRepoManuallyRecorded(sessionId, targetRepo.repoRoot, {
8346
8314
  toolName,
8347
8315
  scope: manualRecordingScope,
8348
- remoteChangeRecorded: toolSucceeded ? isRemoteChangeRecordingToolName(toolName) || didFinalizeTurnRecordRemoteChange(payload, toolName) : remoteChangeRecordedButSyncFailed
8316
+ remoteChangeRecorded: false
8349
8317
  });
8350
8318
  }
8351
8319
  await appendHookDiagnosticsEvent({
@@ -8357,8 +8325,7 @@ async function runHookPostCollab(payload) {
8357
8325
  repoRoot: targetRepo.repoRoot,
8358
8326
  fields: {
8359
8327
  hasObservedWrite: isRepoMutationToolName(toolName),
8360
- manualRecordingScope,
8361
- remoteChangeRecordedButSyncFailed
8328
+ manualRecordingScope
8362
8329
  }
8363
8330
  });
8364
8331
  } else {