@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +35 -0
- package/agents/remix-collab.md +14 -5
- package/dist/historical.d.ts +31 -0
- package/dist/historical.js +443 -0
- package/dist/historical.js.map +1 -0
- package/dist/hook-post-collab.cjs +29 -62
- package/dist/hook-post-collab.cjs.map +1 -1
- package/dist/hook-pre-git.cjs +19 -13
- package/dist/hook-pre-git.cjs.map +1 -1
- package/dist/hook-stop-collab.cjs +3592 -1810
- package/dist/hook-stop-collab.cjs.map +1 -1
- package/dist/hook-user-prompt.cjs +7656 -74
- package/dist/hook-user-prompt.cjs.map +1 -1
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +40997 -37432
- package/dist/mcp-server.cjs.map +1 -1
- package/package.json +14 -3
- package/skills/identity-and-scope-routing/SKILL.md +1 -0
- package/skills/init-or-remix/SKILL.md +12 -2
- package/skills/review-merge-request/SKILL.md +5 -3
- package/skills/safe-collab-workflow/SKILL.md +43 -11
- package/skills/submit-change-step/SKILL.md +75 -16
- package/skills/sync-and-reconcile/SKILL.md +3 -2
|
@@ -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
|
|
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.
|
|
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.
|
|
890
|
-
"@remixhq/mcp": "^0.1.
|
|
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-
|
|
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-
|
|
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_(
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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 {
|