@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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "remix",
3
3
  "description": "Remix collaboration workflows for Claude Code",
4
- "version": "0.1.17",
4
+ "version": "0.1.19",
5
5
  "author": {
6
6
  "name": "Remix"
7
7
  },
package/README.md CHANGED
@@ -14,3 +14,38 @@ Claude Code plugin for Remix collaboration workflows.
14
14
  ```bash
15
15
  /reload-plugins
16
16
  ```
17
+
18
+ ### Subscription tier (plan) capture
19
+
20
+ When a Stop hook records a turn, the plugin attempts to resolve the user's
21
+ Claude Code subscription tier (e.g. `pro`, `max`, `team`, `enterprise`) and
22
+ attaches it to `agent.plan` on the recorded turn. The dashboard reads this
23
+ field to display "Charged on Max" alongside per-turn cost.
24
+
25
+ How it's resolved (no permission prompts):
26
+
27
+ - The plugin spawns `claude auth status --json` and parses
28
+ `subscriptionType` from the result. Because this command runs as the
29
+ `claude` binary itself, macOS treats it as the keychain entry's owner
30
+ and returns the data silently.
31
+ - The result is cached at
32
+ `${REMIX_COLLAB_STATE_ROOT:-~/.remix/collab-state}/claude-auth-cache.json`
33
+ for one hour (five minutes on failure), so most Stop hooks read the
34
+ cache file (~1 ms) rather than spawning `claude` (~150–500 ms).
35
+ - Failure modes (`claude` not on PATH, non-zero exit, malformed JSON,
36
+ API-key-only auth, logged-out account) all degrade to `agent.plan = null`
37
+ with no exception thrown.
38
+
39
+ Privacy posture:
40
+
41
+ - Only `subscriptionType` is captured. Email, organization, and account
42
+ IDs are intentionally NOT recorded, because `agent.plan` ships in the
43
+ per-turn `workspace_metadata` blob that's visible to every collaborator
44
+ who can view the project's timeline.
45
+
46
+ Configuration:
47
+
48
+ - `REMIX_CLAUDE_AUTH_TIMEOUT_MS` — override the spawn timeout (default
49
+ `5000`).
50
+ - `REMIX_COLLAB_STATE_ROOT` — override the cache directory (default
51
+ `~/.remix/collab-state`).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: remix-collab
3
- description: Specialized Claude agent for Remix repository collaboration, merge request review, sync, reconcile, and change-step recording workflows.
3
+ description: Specialized Claude agent for Remix repository collaboration, merge request review, sync, reconcile, and queued finalize-turn workflows.
4
4
  ---
5
5
 
6
6
  You are the Remix collaboration specialist.
@@ -15,10 +15,14 @@ Branch model:
15
15
  - If the current branch is unbound, initialize or provision that branch lane before continuing. Do not assume another branch's binding is safe to reuse.
16
16
  - If status reports a branch mismatch or missing branch binding, stop and resolve the branch state before recording or syncing.
17
17
 
18
+ Source-blind recording rule:
19
+
20
+ Any local content change since the last recorded turn — whether you typed it, the user typed it, a `git commit`, `git pull`, `git merge`, `git rebase`, or `git reset` produced it, or an IDE saved it — is recorded by exactly one `remix_collab_finalize_turn`. Do not pick a different command (`re-anchor`, `sync`, `reconcile`) based on what produced the change. Those commands are only correct when `remix_collab_status.recommendedAction` explicitly names them. `re-anchor` in particular is reserved for the narrow case where no local Remix baseline exists yet for this lane (status reports `re_anchor`); it is not a recovery for "the local content changed."
21
+
18
22
  Operating rules:
19
23
 
20
24
  1. Start with `remix_collab_status` for repo-bound collaboration tasks whenever the current state is unclear.
21
- 2. In a Remix-bound repo, Remix MCP tools are the required workflow layer for collaboration state and historical reasoning. Raw git mutation commands must not be used for ordinary bound-repo collaboration work. Raw git reads are only acceptable for exact repository facts such as specific commits, blame, ancestry, or raw patch details.
25
+ 2. In a Remix-bound repo, Remix MCP tools are the required workflow layer for collaboration state and historical reasoning. Raw git mutations are allowed but never substitute for `remix_collab_finalize_turn`; after raw git mutations, run `remix_collab_status` and follow its `recommendedAction`. Raw git reads are only acceptable for exact repository facts such as specific commits, blame, ancestry, or raw patch details.
22
26
  3. Use preview tools before apply tools whenever both exist.
23
27
  4. Treat reconcile as a last-resort, high-risk path.
24
28
  5. Prefer bounded merge request diffs first, then expand only when necessary.
@@ -32,19 +36,24 @@ Operating rules:
32
36
  9. Clearly explain local mutation risk before using tools that can modify the local repo.
33
37
  10. In a bound repo, Remix MCP tools are the required workflow layer for ordinary collaboration work.
34
38
  11. In a bound repo, exactly one final `remix_collab_finalize_turn` is required before the final user-facing response.
35
- 12. The final recording call must use the exact user prompt and your final assistant response.
36
- 13. Do not finish the turn before recording. Do not make additional repo mutations after the final turn-recording call unless you intend to record again.
37
- 14. Do not duplicate core business logic in reasoning. Use the MCP tools to inspect and execute the workflow.
39
+ 12. `remix_collab_finalize_turn` is queued only. A remote change step does not exist yet until the finalize queue drains successfully.
40
+ 13. The final recording call must use the exact user prompt and your final assistant response.
41
+ 14. If `remix_collab_status` reports `await_finalize`, use `remix_collab_drain_finalize_queue`, then re-check status before merge-related or recovery flows that depend on a remote recorded turn.
42
+ 15. Do not finish the turn before recording. Do not make additional repo mutations after the final turn-recording call unless you intend to record again.
43
+ 16. Do not duplicate core business logic in reasoning. Use the MCP tools to inspect and execute the workflow.
38
44
 
39
45
  When appropriate:
40
46
 
41
47
  - use `remix_collab_init` to bind the current repo
42
48
  - use `remix_collab_remix` to start from an existing app lineage
43
49
  - use `remix_collab_checkout` to continue work on an existing app id without creating a fork
50
+ - when the user asks for any URL, link, dashboard, or web address of a Remix-bound repo or app (e.g. "what's the URL of this repo on Remix", "the link to this app", "where can I see this on the dashboard"), use `remix_collab_status` and answer with `binding.dashboardUrl` (already in the canonical `https://dashboard.remix.one/apps/<appId>` form). Do NOT answer with `binding.remoteUrl` — that is the git remote, not the Remix dashboard. If `binding.dashboardUrl` is null but `binding.currentAppId` is set, fall back to `https://dashboard.remix.one/apps/<currentAppId>`. Do not require an additional tool call beyond `remix_collab_status`.
44
51
  - when helping the user choose an app, prefer scoped or membership-oriented discovery first: use `organizationId` / `projectId` when known, or use `ownership` with `accessScope="explicit_member"` when the user means “apps I can work on”
45
52
  - reserve `accessScope="all_readable"` for explicit public or broad readable discovery, not as the default work-oriented app picker
46
53
  - use memory summary/search/timeline tools before repo inspection when historical context or reasoning is needed
47
54
  - use the explicit change-step diff tool only after you already know which `changeStepId` matters
48
55
  - use `remix_collab_finalize_turn` as the final turn recorder
56
+ - treat `remix_collab_finalize_turn` as local capture plus queueing, not immediate remote materialization
57
+ - use `remix_collab_drain_finalize_queue` and `remix_collab_status` when the workflow is blocked on `await_finalize`
49
58
  - use `remix_collab_review_queue` for reviewable merge requests, `remix_collab_my_merge_requests` for authored requests, and `remix_collab_list_app_merge_requests` for app-scoped MR flows with required `queue` set to `app_reviewable`, `app_outgoing`, or `app_related_visible`
50
59
  - use merge request tools for review and approval flows
@@ -0,0 +1,31 @@
1
+ import { TurnUsage } from '@remixhq/core/collab';
2
+
3
+ type TranscriptEvent = Record<string, unknown>;
4
+ type ReadTranscriptResult = {
5
+ ok: true;
6
+ events: TranscriptEvent[];
7
+ } | {
8
+ ok: false;
9
+ reason: "transcript_not_found" | "transcript_unreadable";
10
+ };
11
+ declare function readAndParseTranscript(transcriptPath: string): Promise<ReadTranscriptResult>;
12
+
13
+ type SlicedTurn = {
14
+ sessionId: string;
15
+ promptId: string | null;
16
+ promptText: string | null;
17
+ assistantText: string | null;
18
+ occurredAt: string;
19
+ gitBranch: string | null;
20
+ cwd: string | null;
21
+ usage: TurnUsage;
22
+ };
23
+ type SliceTranscriptInput = {
24
+ events: TranscriptEvent[];
25
+ sessionId: string;
26
+ capturedAt: string;
27
+ extensions?: Record<string, unknown> | null;
28
+ };
29
+ declare function sliceTranscriptIntoTurns(input: SliceTranscriptInput): SlicedTurn[];
30
+
31
+ export { type ReadTranscriptResult, type SliceTranscriptInput, type SlicedTurn, type TranscriptEvent, readAndParseTranscript, sliceTranscriptIntoTurns };
@@ -0,0 +1,443 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/usage/claudeCodeTranscript.ts
4
+ import fs from "fs/promises";
5
+ async function readAndParseTranscript(transcriptPath) {
6
+ let raw;
7
+ try {
8
+ raw = await fs.readFile(transcriptPath, "utf8");
9
+ } catch (err) {
10
+ const code = err && typeof err === "object" && "code" in err ? err.code : null;
11
+ if (code === "ENOENT") {
12
+ return { ok: false, reason: "transcript_not_found" };
13
+ }
14
+ return { ok: false, reason: "transcript_unreadable" };
15
+ }
16
+ const events = [];
17
+ for (const line of raw.split("\n")) {
18
+ const trimmed = line.trim();
19
+ if (!trimmed) continue;
20
+ try {
21
+ const parsed = JSON.parse(trimmed);
22
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
23
+ events.push(parsed);
24
+ }
25
+ } catch {
26
+ }
27
+ }
28
+ return { ok: true, events };
29
+ }
30
+
31
+ // src/usage/claudeCodeUsageHarvester.ts
32
+ function parseTimestamp(value) {
33
+ if (typeof value !== "string") return null;
34
+ const ms = Date.parse(value);
35
+ return Number.isFinite(ms) ? ms : null;
36
+ }
37
+ function extractUserContentText(message) {
38
+ if (!message || typeof message !== "object") return null;
39
+ const content = message.content;
40
+ if (typeof content === "string") return content;
41
+ if (Array.isArray(content)) {
42
+ const textBlocks = content.filter((block) => Boolean(block) && typeof block === "object").filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text);
43
+ if (textBlocks.length === 0) return null;
44
+ return textBlocks.join("\n");
45
+ }
46
+ return null;
47
+ }
48
+ function isUserBoundary(event) {
49
+ return event.type === "user" && event.isMeta !== true && event.isSidechain !== true;
50
+ }
51
+ function extractAssistantContentText(turnEvents) {
52
+ const chunks = [];
53
+ for (const ev of turnEvents) {
54
+ if (ev.type !== "assistant") continue;
55
+ const text = extractUserContentText(ev.message);
56
+ if (text && text.length > 0) chunks.push(text);
57
+ }
58
+ if (chunks.length === 0) return null;
59
+ const deduped = [];
60
+ for (const chunk of chunks) {
61
+ if (deduped[deduped.length - 1] !== chunk) deduped.push(chunk);
62
+ }
63
+ return deduped.join("\n");
64
+ }
65
+ function asNumberOrNull(value) {
66
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
67
+ }
68
+ function asStringOrNull(value) {
69
+ return typeof value === "string" ? value : null;
70
+ }
71
+ function extractAssistantMessage(event) {
72
+ const message = event.message;
73
+ if (!message || typeof message !== "object") return null;
74
+ const msg = message;
75
+ if (msg.role !== "assistant" && msg.role !== void 0) {
76
+ }
77
+ const content = Array.isArray(msg.content) ? msg.content : [];
78
+ const usage = msg.usage && typeof msg.usage === "object" ? msg.usage : null;
79
+ return {
80
+ id: asStringOrNull(msg.id),
81
+ model: asStringOrNull(msg.model),
82
+ content,
83
+ usage
84
+ };
85
+ }
86
+ function usageIsComplete(usage) {
87
+ if (!usage) return false;
88
+ const hasInput = typeof usage.input_tokens === "number";
89
+ const hasOutput = typeof usage.output_tokens === "number";
90
+ const hasCacheRead = typeof usage.cache_read_input_tokens === "number";
91
+ return hasInput && hasOutput && hasCacheRead;
92
+ }
93
+ function buildModelCall(event, msg) {
94
+ const usage = msg.usage ?? {};
95
+ const cacheCreation = usage.cache_creation && typeof usage.cache_creation === "object" ? usage.cache_creation : null;
96
+ const has5m = cacheCreation && typeof cacheCreation.ephemeral_5m_input_tokens === "number";
97
+ const has1h = cacheCreation && typeof cacheCreation.ephemeral_1h_input_tokens === "number";
98
+ const cacheWrite5mTokens = has5m ? cacheCreation.ephemeral_5m_input_tokens : null;
99
+ const cacheWrite1hTokens = has1h ? cacheCreation.ephemeral_1h_input_tokens : null;
100
+ const splitAvailable = has5m || has1h;
101
+ const cacheWriteTokens = splitAvailable ? null : asNumberOrNull(usage.cache_creation_input_tokens);
102
+ return {
103
+ provider: "anthropic",
104
+ model: msg.model,
105
+ tier: asStringOrNull(usage.service_tier),
106
+ requestId: asStringOrNull(event.requestId),
107
+ timestamp: asStringOrNull(event.timestamp),
108
+ isSidechain: event.isSidechain === true,
109
+ inputTokens: asNumberOrNull(usage.input_tokens),
110
+ outputTokens: asNumberOrNull(usage.output_tokens),
111
+ cacheReadTokens: asNumberOrNull(usage.cache_read_input_tokens),
112
+ cacheWriteTokens,
113
+ cacheWrite5mTokens,
114
+ cacheWrite1hTokens,
115
+ reasoningTokens: null,
116
+ audioInputTokens: null,
117
+ imageInputTokens: null
118
+ };
119
+ }
120
+ function scanServerToolUses(content) {
121
+ const uses = [];
122
+ for (const block of content) {
123
+ if (!block || typeof block !== "object") continue;
124
+ const b = block;
125
+ const id = b.id;
126
+ if (typeof id !== "string" || !id.startsWith("srvtoolu_")) continue;
127
+ const name = typeof b.name === "string" ? b.name : "";
128
+ switch (name) {
129
+ case "web_search":
130
+ uses.push({ tool: "web_search", unit: "per_request", isKnown: true, id, source: "direct" });
131
+ break;
132
+ case "web_fetch":
133
+ uses.push({ tool: "web_fetch", unit: "per_request", isKnown: true, id, source: "direct" });
134
+ break;
135
+ case "code_execution":
136
+ uses.push({ tool: "code_execution", unit: "invocation", isKnown: true, id, source: "direct" });
137
+ break;
138
+ default:
139
+ uses.push({ tool: name || "unknown", unit: "invocation", isKnown: false, id, source: "direct" });
140
+ break;
141
+ }
142
+ }
143
+ return uses;
144
+ }
145
+ function buildClientToolNameMap(turnEvents) {
146
+ const map = /* @__PURE__ */ new Map();
147
+ for (const ev of turnEvents) {
148
+ if (ev.type !== "assistant") continue;
149
+ const msg = ev.message;
150
+ if (!msg || typeof msg !== "object") continue;
151
+ const content = msg.content;
152
+ if (!Array.isArray(content)) continue;
153
+ for (const block of content) {
154
+ if (!block || typeof block !== "object") continue;
155
+ const b = block;
156
+ if (b.type !== "tool_use") continue;
157
+ const id = b.id;
158
+ const name = b.name;
159
+ if (typeof id !== "string" || typeof name !== "string") continue;
160
+ if (!id.startsWith("toolu_")) continue;
161
+ map.set(id, name);
162
+ }
163
+ }
164
+ return map;
165
+ }
166
+ function scanEmbeddedServerToolUses(turnEvents, clientToolNames) {
167
+ const uses = [];
168
+ for (const ev of turnEvents) {
169
+ if (ev.type !== "user") continue;
170
+ const tur = ev.toolUseResult;
171
+ if (!tur || typeof tur !== "object") continue;
172
+ const results = tur.results;
173
+ if (!Array.isArray(results)) continue;
174
+ let parentToolName = "";
175
+ const userMsg = ev.message;
176
+ if (userMsg && typeof userMsg === "object") {
177
+ const userContent = userMsg.content;
178
+ if (Array.isArray(userContent)) {
179
+ for (const block of userContent) {
180
+ if (!block || typeof block !== "object") continue;
181
+ const b = block;
182
+ if (b.type !== "tool_result") continue;
183
+ const parentId = b.tool_use_id;
184
+ if (typeof parentId === "string") {
185
+ parentToolName = clientToolNames.get(parentId) ?? "";
186
+ break;
187
+ }
188
+ }
189
+ }
190
+ }
191
+ for (const entry of results) {
192
+ if (!entry || typeof entry !== "object") continue;
193
+ const srvId = entry.tool_use_id;
194
+ if (typeof srvId !== "string" || !srvId.startsWith("srvtoolu_")) continue;
195
+ if (parentToolName === "WebFetch") {
196
+ uses.push({ tool: "web_fetch", unit: "per_request", isKnown: true, id: srvId, source: "embedded" });
197
+ } else {
198
+ uses.push({ tool: "web_search", unit: "per_request", isKnown: true, id: srvId, source: "embedded" });
199
+ }
200
+ }
201
+ }
202
+ return uses;
203
+ }
204
+ function dedupeByServerToolId(records) {
205
+ const seen = /* @__PURE__ */ new Map();
206
+ for (const r of records) {
207
+ const existing = seen.get(r.id);
208
+ if (!existing) {
209
+ seen.set(r.id, r);
210
+ continue;
211
+ }
212
+ if (existing.source === "embedded" && r.source === "direct") {
213
+ seen.set(r.id, r);
214
+ }
215
+ }
216
+ return Array.from(seen.values());
217
+ }
218
+ function aggregateServerTools(uses) {
219
+ const map = /* @__PURE__ */ new Map();
220
+ let sawUnknown = false;
221
+ let sawEmbedded = false;
222
+ for (const use of uses) {
223
+ if (!use.isKnown) sawUnknown = true;
224
+ if (use.source === "embedded") sawEmbedded = true;
225
+ const key = `anthropic|${use.tool}|${use.unit}`;
226
+ const existing = map.get(key);
227
+ if (existing) {
228
+ existing.quantity += 1;
229
+ } else {
230
+ map.set(key, { provider: "anthropic", tool: use.tool, unit: use.unit, quantity: 1 });
231
+ }
232
+ }
233
+ return { serverTools: Array.from(map.values()), sawUnknown, sawEmbedded };
234
+ }
235
+ function sumCrossCheckCounts(usageBlocks) {
236
+ const totals = /* @__PURE__ */ new Map();
237
+ const keyFor = (raw) => {
238
+ if (raw === "web_search_requests") return "web_search";
239
+ if (raw === "web_fetch_requests") return "web_fetch";
240
+ return null;
241
+ };
242
+ for (const usage of usageBlocks) {
243
+ const stu = usage.server_tool_use;
244
+ if (!stu || typeof stu !== "object") continue;
245
+ for (const [rawKey, rawVal] of Object.entries(stu)) {
246
+ const mapped = keyFor(rawKey);
247
+ if (!mapped) continue;
248
+ if (typeof rawVal !== "number" || !Number.isFinite(rawVal)) continue;
249
+ totals.set(mapped, (totals.get(mapped) ?? 0) + rawVal);
250
+ }
251
+ }
252
+ return totals;
253
+ }
254
+ function primaryToolCounts(serverTools) {
255
+ const map = /* @__PURE__ */ new Map();
256
+ for (const entry of serverTools) {
257
+ map.set(entry.tool, (map.get(entry.tool) ?? 0) + entry.quantity);
258
+ }
259
+ return map;
260
+ }
261
+ function resolveVersion(events) {
262
+ for (const ev of events) {
263
+ if (typeof ev.version === "string" && ev.version.trim()) return ev.version.trim();
264
+ }
265
+ return null;
266
+ }
267
+ function buildTurnUsage(args) {
268
+ const assistantEvents = args.turnEvents.filter((ev) => ev.type === "assistant");
269
+ if (assistantEvents.length === 0) {
270
+ return { ok: false, reason: "no_messages_for_turn" };
271
+ }
272
+ const warnings = [...args.initialWarnings];
273
+ const calls = [];
274
+ const usageBlocks = [];
275
+ let anyIncomplete = false;
276
+ let anyComplete = false;
277
+ let sawLumpSumFallback = false;
278
+ const collectedServerToolUses = [];
279
+ const mainModels = /* @__PURE__ */ new Set();
280
+ const sidechainModels = /* @__PURE__ */ new Set();
281
+ for (const ev of assistantEvents) {
282
+ const msg = extractAssistantMessage(ev);
283
+ if (!msg) continue;
284
+ if (usageIsComplete(msg.usage)) {
285
+ anyComplete = true;
286
+ } else {
287
+ anyIncomplete = true;
288
+ }
289
+ if (msg.usage) usageBlocks.push(msg.usage);
290
+ const call = buildModelCall(ev, msg);
291
+ calls.push(call);
292
+ if (call.cacheWrite5mTokens === null && call.cacheWrite1hTokens === null && call.cacheWriteTokens !== null) {
293
+ sawLumpSumFallback = true;
294
+ }
295
+ collectedServerToolUses.push(...scanServerToolUses(msg.content));
296
+ if (call.model) {
297
+ if (call.isSidechain) sidechainModels.add(call.model);
298
+ else mainModels.add(call.model);
299
+ }
300
+ }
301
+ if (calls.length === 0) {
302
+ return { ok: false, reason: "no_messages_for_turn" };
303
+ }
304
+ if (sawLumpSumFallback) {
305
+ warnings.push({
306
+ code: "cache_split_unavailable",
307
+ message: "Assistant message reported lump-sum cache_creation_input_tokens without the 5m/1h split."
308
+ });
309
+ }
310
+ const clientToolNames = buildClientToolNameMap(args.turnEvents);
311
+ const embeddedUses = scanEmbeddedServerToolUses(args.turnEvents, clientToolNames);
312
+ const merged = dedupeByServerToolId([...collectedServerToolUses, ...embeddedUses]);
313
+ const { serverTools, sawUnknown, sawEmbedded } = aggregateServerTools(merged);
314
+ if (sawUnknown) {
315
+ warnings.push({
316
+ code: "unknown_server_tool",
317
+ message: "Encountered a server tool whose name is not in the known list (web_search, web_fetch, code_execution)."
318
+ });
319
+ }
320
+ const crossCheck = sumCrossCheckCounts(usageBlocks);
321
+ const primary = primaryToolCounts(serverTools);
322
+ const allTools = /* @__PURE__ */ new Set([...crossCheck.keys(), ...primary.keys()]);
323
+ for (const tool of allTools) {
324
+ const crossVal = crossCheck.get(tool) ?? 0;
325
+ const primVal = primary.get(tool) ?? 0;
326
+ if (crossVal === primVal) continue;
327
+ if (sawEmbedded && crossVal === 0) continue;
328
+ warnings.push({
329
+ code: "server_tool_count_mismatch",
330
+ message: `Server-tool ${tool} count mismatch: srvtoolu_ scan=${primVal}, usage.server_tool_use=${crossVal}. Trusting srvtoolu_ count.`
331
+ });
332
+ break;
333
+ }
334
+ const subagentMismatch = mainModels.size > 0 && sidechainModels.size > 0 && [...sidechainModels].some((m) => !mainModels.has(m));
335
+ if (subagentMismatch) {
336
+ warnings.push({
337
+ code: "subagent_model_differs",
338
+ message: "At least one sidechain ModelCall uses a model different from the main-chain model in this turn."
339
+ });
340
+ }
341
+ let confidence;
342
+ if (!anyComplete && anyIncomplete) confidence = "unknown";
343
+ else if (anyIncomplete) confidence = "partial";
344
+ else if (anyComplete) confidence = "exact";
345
+ else confidence = "unknown";
346
+ if (args.checkSubsequentEvent) {
347
+ const lastAssistantMs = (() => {
348
+ const msList = assistantEvents.map((ev) => parseTimestamp(ev.timestamp)).filter((ms) => ms !== null);
349
+ return msList.length ? Math.max(...msList) : null;
350
+ })();
351
+ const hasSubsequent = lastAssistantMs !== null && args.sessionEvents.some((ev) => {
352
+ const ms = parseTimestamp(ev.timestamp);
353
+ if (ms === null || ms <= lastAssistantMs) return false;
354
+ if (args.upperMs !== null && ms >= args.upperMs) return false;
355
+ return true;
356
+ });
357
+ if (!hasSubsequent && confidence === "exact") {
358
+ confidence = "partial";
359
+ warnings.push({
360
+ code: "transcript_truncated",
361
+ message: "Previous turn has no subsequent event after its last assistant message; transcript may be truncated."
362
+ });
363
+ }
364
+ }
365
+ const resolvedVersion = args.agent.version ?? resolveVersion(args.turnEvents) ?? resolveVersion(args.sessionEvents);
366
+ const usage = {
367
+ schemaVersion: 1,
368
+ capturedAt: args.capturedAt,
369
+ captureSource: args.captureSource,
370
+ confidence,
371
+ agent: {
372
+ name: args.agent.name,
373
+ version: resolvedVersion,
374
+ sessionId: args.agent.sessionId,
375
+ turnId: args.agent.turnId,
376
+ plan: args.agent.plan
377
+ },
378
+ calls,
379
+ serverTools,
380
+ warnings,
381
+ extensions: args.extensions
382
+ };
383
+ return { ok: true, usage };
384
+ }
385
+ function sliceTranscriptIntoTurns(input) {
386
+ const sessionEvents = input.events.filter((ev) => ev.sessionId === input.sessionId);
387
+ const boundaries = sessionEvents.filter(isUserBoundary);
388
+ const result = [];
389
+ for (let i = 0; i < boundaries.length; i++) {
390
+ const boundary = boundaries[i];
391
+ const nextBoundary = boundaries[i + 1] ?? null;
392
+ const boundaryMs = parseTimestamp(boundary.timestamp);
393
+ if (boundaryMs === null) continue;
394
+ const occurredAt = asStringOrNull(boundary.timestamp);
395
+ if (occurredAt === null) continue;
396
+ const upperMs = nextBoundary ? parseTimestamp(nextBoundary.timestamp) : null;
397
+ const turnEvents = sessionEvents.filter((ev) => {
398
+ const ms = parseTimestamp(ev.timestamp);
399
+ if (ms === null || ms <= boundaryMs) return false;
400
+ if (upperMs !== null && ms >= upperMs) return false;
401
+ return true;
402
+ });
403
+ const promptId = asStringOrNull(boundary.promptId);
404
+ const promptText = extractUserContentText(boundary.message);
405
+ const assistantText = extractAssistantContentText(turnEvents);
406
+ const gitBranch = asStringOrNull(boundary.gitBranch);
407
+ const cwd = asStringOrNull(boundary.cwd);
408
+ const built = buildTurnUsage({
409
+ sessionEvents,
410
+ turnEvents,
411
+ upperMs,
412
+ initialWarnings: [],
413
+ agent: {
414
+ name: "claude-code",
415
+ version: null,
416
+ sessionId: input.sessionId,
417
+ turnId: promptId,
418
+ plan: null
419
+ },
420
+ capturedAt: input.capturedAt,
421
+ captureSource: "historical_import",
422
+ extensions: input.extensions ?? null,
423
+ checkSubsequentEvent: false
424
+ });
425
+ if (!built.ok) continue;
426
+ result.push({
427
+ sessionId: input.sessionId,
428
+ promptId,
429
+ promptText,
430
+ assistantText,
431
+ occurredAt,
432
+ gitBranch,
433
+ cwd,
434
+ usage: built.usage
435
+ });
436
+ }
437
+ return result;
438
+ }
439
+ export {
440
+ readAndParseTranscript,
441
+ sliceTranscriptIntoTurns
442
+ };
443
+ //# sourceMappingURL=historical.js.map