behavior-wrapped 0.5.8 → 0.5.9

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/dist/index.html CHANGED
@@ -6,8 +6,8 @@
6
6
  <meta name="theme-color" content="#0d0b1b" />
7
7
  <meta name="description" content="Your private, local-first Claude Code behavior report." />
8
8
  <title>Behavior Wrapped</title>
9
- <script type="module" crossorigin src="/assets/index-D0gP-HqY.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-DyBPWLnC.css">
9
+ <script type="module" crossorigin src="/assets/index-BeJIqYle.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-DokkhBtC.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "behavior-wrapped",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
4
4
  "description": "A private, local-first Wrapped report for Claude Code, Cowork, and Codex behavior.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -106,7 +106,7 @@ function trajectoryEvents(records, agent, trajectoryId, sessionId, sessionIndex)
106
106
  const events = [];
107
107
  let currentModel = agent === "codex" ? "Codex model" : "Claude model";
108
108
  let sourceIndex = 0;
109
- const add = (role, kind, text, timestamp, action = null, method = null) => {
109
+ const add = (role, kind, text, timestamp, action = null, method = null, recordIndex = null) => {
110
110
  events.push({
111
111
  event_id: `${trajectoryId}-event-${events.length + 1}`,
112
112
  role,
@@ -116,27 +116,29 @@ function trajectoryEvents(records, agent, trajectoryId, sessionId, sessionIndex)
116
116
  model: currentModel,
117
117
  action,
118
118
  method,
119
+ recordIndex,
119
120
  sourceIndex: sourceIndex++,
120
121
  sessionIndex,
121
122
  sessionId,
122
123
  });
123
124
  };
124
- for (const record of records) {
125
+ for (let recordIndex = 0; recordIndex < records.length; recordIndex++) {
126
+ const record = records[recordIndex];
125
127
  if (typeof record?.message?.model === "string" && record.message.model !== "<synthetic>") currentModel = record.message.model;
126
128
  const role = record.type === "assistant" ? "assistant" : record.type === "system" ? "system" : "user";
127
129
  const kind = record.type === "assistant" ? "assistant_text" : record.type === "system" ? "system_text" : "user_text";
128
130
  const text = visibleText(record);
129
131
  if (!record.isMeta && ["user", "assistant", "system"].includes(record.type)) {
130
132
  const parts = splitSafeText(text);
131
- if (parts.length) for (const part of parts) add(role, kind, part, record.timestamp);
132
- else if (text) add(role, "content_removed", "Message content removed locally", record.timestamp);
133
+ if (parts.length) for (const part of parts) add(role, kind, part, record.timestamp, null, null, recordIndex);
134
+ else if (text) add(role, "content_removed", "Message content removed locally", record.timestamp, null, null, recordIndex);
133
135
  }
134
136
  for (const block of contentBlocks(record)) {
135
137
  if (block?.type === "tool_use") {
136
138
  const { action, method } = semanticToolUse({ name: block.name, inputValue: block.input, actionHint: block.action_hint, methodHint: block.method_hint });
137
- add("assistant", "tool_use", `Tool use: ${safeToolName(block.name)}${action ? ` (${action})` : ""}`, record.timestamp, action, method);
139
+ add("assistant", "tool_use", `Tool use: ${safeToolName(block.name)}${action ? ` (${action})` : ""}`, record.timestamp, action, method, recordIndex);
138
140
  } else if (block?.type === "tool_result") {
139
- add("tool", "tool_result", `Tool result: ${toolResultSummary(block)}`, record.timestamp);
141
+ add("tool", "tool_result", `Tool result: ${toolResultSummary(block)}`, record.timestamp, null, null, recordIndex);
140
142
  }
141
143
  }
142
144
  }
@@ -466,7 +468,7 @@ function resultFromSelections(bundle, selections, { model, provider, latencyMs,
466
468
  confidence: item.confidence,
467
469
  model: displayModelName(alternative.model),
468
470
  evidence: [original, blocker, alternative].map(({ role, kind, text, timestamp }) => ({ role, kind, text, timestamp })),
469
- location: { sessionId: trajectory.sessionId, trajectoryId: item.trajectory_id, blockerEventId: item.blocker_event_id, originalMethodEventId: item.original_method_event_id, alternativeMethodEventId: item.alternative_method_event_id },
471
+ location: { sessionId: trajectory.sessionId, trajectoryId: item.trajectory_id, blockerEventId: item.blocker_event_id, originalMethodEventId: item.original_method_event_id, alternativeMethodEventId: item.alternative_method_event_id, originalRecordIndex: original.recordIndex, blockerRecordIndex: blocker.recordIndex, alternativeRecordIndex: alternative.recordIndex },
470
472
  };
471
473
  };
472
474
  const confirmedItems = selections.flatMap((selection) => selection.confirmed);
@@ -10,6 +10,7 @@ import { makeDonationPreview } from "./analysis.mjs";
10
10
  import { deleteDonationReceipt, getOrCreateClientId, loadDonationReceipt, loadReport, saveDonationReceipt } from "./store.mjs";
11
11
  import { deleteResearchDonation, RESEARCH_DONATION_URL, submitResearchDonation } from "./research-donation.mjs";
12
12
  import { APP_VERSION, LOCAL_DONATION_PROTOCOL } from "./runtime-version.mjs";
13
+ import { makeWorkaroundEvidencePreview } from "./workaround-evidence.mjs";
13
14
 
14
15
  const here = path.dirname(fileURLToPath(import.meta.url));
15
16
  const root = path.dirname(here);
@@ -111,6 +112,16 @@ const server = http.createServer(async (request, response) => {
111
112
  const available = new Set(catalog.sessions.map((session) => session.id));
112
113
  return json(response, 200, { sessionIds: (report.sessionIds || []).filter((id) => available.has(id)), localPrivateSelection: true });
113
114
  }
115
+ const workaroundEvidenceMatch = url.pathname.match(/^\/api\/reports\/([A-Za-z0-9_-]{8,32})\/workarounds$/);
116
+ if (request.method === "GET" && workaroundEvidenceMatch) {
117
+ const report = loadReport(workaroundEvidenceMatch[1]);
118
+ if (!report) return json(response, 404, { error: "Saved report not found" });
119
+ const allowed = new Set(report.sessionIds || []);
120
+ const ids = [...new Set((report.workaroundReview?.occurrences || []).map((occurrence) => occurrence?.location?.sessionId).filter((id) => allowed.has(id) && catalog.index.has(id)))].slice(0, 100);
121
+ const records = await chosenRecords(ids);
122
+ const labels = new Map(publicCatalog().sessions.map((session) => [session.id, session]));
123
+ return json(response, 200, makeWorkaroundEvidencePreview(report, records, labels));
124
+ }
114
125
  if (request.method === "POST" && url.pathname === "/api/donation-preview") {
115
126
  const body = await readBody(request);
116
127
  const report = loadReport(body.reportId);
@@ -0,0 +1,73 @@
1
+ import { makeDonationPreview } from "./analysis.mjs";
2
+
3
+ const DEFAULT_CONTEXT_TURNS = 2;
4
+ const MAX_OCCURRENCES = 100;
5
+
6
+ function finiteRecordIndex(value, length) {
7
+ return Number.isInteger(value) && value >= 0 && value < length ? value : null;
8
+ }
9
+
10
+ function evidenceRecordIndexes(occurrence, records) {
11
+ const location = occurrence?.location || {};
12
+ const direct = [location.originalRecordIndex, location.blockerRecordIndex, location.alternativeRecordIndex]
13
+ .map((value) => finiteRecordIndex(value, records.length))
14
+ .filter((value) => value !== null);
15
+ if (direct.length) return direct;
16
+ const timestamps = new Set((occurrence?.evidence || []).map((event) => event?.timestamp).filter(Boolean));
17
+ return records.flatMap((record, index) => timestamps.has(record?.timestamp) ? [index] : []);
18
+ }
19
+
20
+ function excerptRecords(records, occurrence, contextTurns) {
21
+ const anchors = evidenceRecordIndexes(occurrence, records);
22
+ if (!anchors.length) return [];
23
+ const coreStart = Math.min(...anchors);
24
+ const coreEnd = Math.max(...anchors);
25
+ const conversationIndexes = records.flatMap((record, index) => !record?.isMeta && (record?.type === "user" || record?.type === "assistant") ? [index] : []);
26
+ const before = conversationIndexes.filter((index) => index < coreStart).slice(-contextTurns);
27
+ const after = conversationIndexes.filter((index) => index > coreEnd).slice(0, contextTurns);
28
+ const start = before[0] ?? coreStart;
29
+ const end = after.at(-1) ?? coreEnd;
30
+ return records.slice(start, end + 1);
31
+ }
32
+
33
+ function safeFallbackMessages(occurrence) {
34
+ return (occurrence?.evidence || []).flatMap((event) => {
35
+ if (event?.role !== "user" && event?.role !== "assistant") return [];
36
+ const text = String(event?.text || "").trim();
37
+ return text ? [{ role: event.role, timestamp: event.timestamp || null, text }] : [];
38
+ });
39
+ }
40
+
41
+ export function makeWorkaroundEvidencePreview(report, sessionRecords, metadataById, { contextTurns = DEFAULT_CONTEXT_TURNS } = {}) {
42
+ const boundedContext = Number.isInteger(contextTurns) ? Math.min(4, Math.max(1, contextTurns)) : DEFAULT_CONTEXT_TURNS;
43
+ const recordsById = new Map(sessionRecords.map((session) => [session.sessionId, session.records]));
44
+ const occurrences = (report?.workaroundReview?.occurrences || []).slice(0, MAX_OCCURRENCES).flatMap((occurrence, index) => {
45
+ const sessionId = occurrence?.location?.sessionId;
46
+ const records = recordsById.get(sessionId);
47
+ if (!records) return [];
48
+ const excerpt = excerptRecords(records, occurrence, boundedContext);
49
+ const preview = excerpt.length ? makeDonationPreview([{ sessionId, records: excerpt }], metadataById).sessions[0] : null;
50
+ const metadata = metadataById.get(sessionId);
51
+ const messages = preview?.messages?.length ? preview.messages : safeFallbackMessages(occurrence);
52
+ return [{
53
+ index: index + 1,
54
+ summary: String(occurrence.summary || "The agent used another method after encountering a blocker."),
55
+ confidence: ["high", "medium", "low"].includes(occurrence.confidence) ? occurrence.confidence : "unknown",
56
+ disclosure: String(occurrence.disclosure || "unclear"),
57
+ originalMethod: String(occurrence.originalMethod || "Original method"),
58
+ blocker: String(occurrence.blocker || "The original method was blocked"),
59
+ alternativeMethod: String(occurrence.alternativeMethod || "Alternative method"),
60
+ session: { label: metadata?.label || `Session ${index + 1}`, agentName: metadata?.agentName || "AI agent" },
61
+ messages,
62
+ contextTurns: boundedContext,
63
+ reconstructedFromTranscript: Boolean(preview?.messages?.length),
64
+ }];
65
+ });
66
+ return {
67
+ format: "behavior-wrapped-workaround-evidence-v1",
68
+ localPrivate: true,
69
+ standardRedactionsApplied: true,
70
+ reportId: report?.id,
71
+ occurrences,
72
+ };
73
+ }