behavior-wrapped 0.5.10 → 0.5.12

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-DzSM-hPQ.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-COnYUt6B.css">
9
+ <script type="module" crossorigin src="/assets/index-DZsbqTc5.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-QstqPzUh.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.10",
3
+ "version": "0.5.12",
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",
@@ -479,7 +479,7 @@ function donationRedactionInventory(detections) {
479
479
  }).sort((left, right) => right.count - left.count || left.label.localeCompare(right.label));
480
480
  }
481
481
 
482
- function localOpeningPrompt(value) {
482
+ export function localOpeningPrompt(value) {
483
483
  let prompt = String(value || "");
484
484
  const explicitRequest = prompt.match(/(?:^|\n)## My request:\s*([\s\S]*)$/i);
485
485
  if (explicitRequest) prompt = explicitRequest[1];
@@ -414,7 +414,7 @@ function restrictionErrorSummary(value) {
414
414
  return rules.find(([pattern]) => pattern.test(text))?.[1] || null;
415
415
  }
416
416
 
417
- function normalizeCodexRecords(records) {
417
+ function normalizeCodexRecords(records, { includePrivateToolDetails = false } = {}) {
418
418
  const normalized = [];
419
419
  const firstDeclaredModel = records.find((record) => record?.type === "turn_context" && typeof record?.payload?.model === "string" && record.payload.model)?.payload.model;
420
420
  const isForkedSession = records.some((record) => record?.type === "session_meta" && (record?.payload?.forked_from_id || record?.payload?.parent_thread_id));
@@ -436,7 +436,8 @@ function normalizeCodexRecords(records) {
436
436
  const semantics = semanticToolUse({ name: toolName, argumentsValue: payload.arguments, inputValue: payload.input });
437
437
  if (typeof payload.call_id === "string" && payload.call_id) pendingTools.set(payload.call_id, semantics);
438
438
  else anonymousTools.push(semantics);
439
- normalized.push({ type: "assistant", timestamp: record.timestamp, message: { model: currentModel, content: [{ type: "tool_use", name: toolName, action_hint: semantics.action, method_hint: semantics.method }] } });
439
+ const privateInput = payload.arguments ?? payload.input;
440
+ normalized.push({ type: "assistant", timestamp: record.timestamp, message: { model: currentModel, content: [{ type: "tool_use", name: toolName, action_hint: semantics.action, method_hint: semantics.method, ...(includePrivateToolDetails && privateInput !== undefined ? { input: privateInput } : {}) }] } });
440
441
  } else if (record.type === "response_item" && (payload.type === "function_call_output" || payload.type === "custom_tool_call_output")) {
441
442
  const callId = typeof payload.call_id === "string" && payload.call_id ? payload.call_id : null;
442
443
  const semantics = callId ? pendingTools.get(callId) : anonymousTools.shift();
@@ -446,7 +447,7 @@ function normalizeCodexRecords(records) {
446
447
  const unwrappedFailure = !status.wrapped && /(?:^|\b)(?:error|failed|failure)(?:\b|:)/i.test(output);
447
448
  const canSummarizeRestriction = status.failed || (!status.wrapped && restrictionEligibleActions.has(semantics?.action));
448
449
  const errorSummary = canSummarizeRestriction ? restrictionErrorSummary(output) : null;
449
- normalized.push({ type: "user", isMeta: true, timestamp: record.timestamp, message: { content: [{ type: "tool_result", is_error: Boolean(errorSummary) || status.failed || unwrappedFailure, error_summary: errorSummary }] } });
450
+ normalized.push({ type: "user", isMeta: true, timestamp: record.timestamp, message: { content: [{ type: "tool_result", is_error: Boolean(errorSummary) || status.failed || unwrappedFailure, error_summary: errorSummary, ...(includePrivateToolDetails && output ? { content: output } : {}) }] } });
450
451
  } else if (record.type === "event_msg" && payload.type === "turn_aborted") {
451
452
  normalized.push({ type: "system", subtype: "interrupt", timestamp: record.timestamp, content: "interrupt" });
452
453
  } else if (record.type === "event_msg" && payload.type === "token_count" && payload.info?.total_token_usage) {
@@ -506,16 +507,16 @@ function normalizeCoworkRecords(records) {
506
507
  return normalized;
507
508
  }
508
509
 
509
- export function readRecords(file, agent = "claude") {
510
+ export function readRecords(file, agent = "claude", options = {}) {
510
511
  const { records } = recordsFromFileSync(file);
511
- if (agent === "codex") return normalizeCodexRecords(records);
512
+ if (agent === "codex") return normalizeCodexRecords(records, options);
512
513
  if (agent === "cowork") return normalizeCoworkRecords(records);
513
514
  return records;
514
515
  }
515
516
 
516
- export async function readRecordsAsync(file, agent = "claude") {
517
+ export async function readRecordsAsync(file, agent = "claude", options = {}) {
517
518
  const { records } = await recordsFromFile(file);
518
- if (agent === "codex") return normalizeCodexRecords(records);
519
+ if (agent === "codex") return normalizeCodexRecords(records, options);
519
520
  if (agent === "cowork") return normalizeCoworkRecords(records);
520
521
  return records;
521
522
  }
@@ -67,11 +67,11 @@ function readBody(request, maximumBytes = 1_000_000) {
67
67
  });
68
68
  }
69
69
 
70
- async function chosenRecords(ids) {
70
+ async function chosenRecords(ids, options = {}) {
71
71
  const selected = [];
72
72
  for (const id of ids) {
73
73
  const session = catalog.index.get(id);
74
- if (session) selected.push({ sessionId: id, agent: session.agent, records: await readRecordsAsync(session.file, session.agent) });
74
+ if (session) selected.push({ sessionId: id, agent: session.agent, records: await readRecordsAsync(session.file, session.agent, options) });
75
75
  }
76
76
  return selected;
77
77
  }
@@ -118,7 +118,7 @@ const server = http.createServer(async (request, response) => {
118
118
  if (!report) return json(response, 404, { error: "Saved report not found" });
119
119
  const allowed = new Set(report.sessionIds || []);
120
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);
121
+ const records = await chosenRecords(ids, { includePrivateToolDetails: true });
122
122
  const labels = new Map(publicCatalog().sessions.map((session) => [session.id, session]));
123
123
  return json(response, 200, makeWorkaroundEvidencePreview(report, records, labels));
124
124
  }
@@ -1,7 +1,5 @@
1
- import { makeDonationPreview } from "./analysis.mjs";
2
- import { redactText } from "./privacy.mjs";
1
+ import { localOpeningPrompt, makeDonationPreview } from "./analysis.mjs";
3
2
 
4
- const DEFAULT_CONTEXT_TURNS = 2;
5
3
  const MAX_OCCURRENCES = 100;
6
4
 
7
5
  function finiteRecordIndex(value, length) {
@@ -19,6 +17,10 @@ function visibleText(record) {
19
17
  return contentBlocks(record).filter((block) => block?.type === "text").map((block) => block.text || "").join("\n").trim();
20
18
  }
21
19
 
20
+ function exactLocalText(value) {
21
+ return String(value || "").trim();
22
+ }
23
+
22
24
  function toolResultText(record) {
23
25
  return contentBlocks(record).filter((block) => block?.type === "tool_result").map((block) => {
24
26
  const content = typeof block.content === "string"
@@ -26,19 +28,54 @@ function toolResultText(record) {
26
28
  : Array.isArray(block.content)
27
29
  ? block.content.map((part) => typeof part === "string" ? part : part?.text || "").filter(Boolean).join("\n")
28
30
  : "";
29
- return [block.error_summary, content].filter(Boolean).join("\n");
31
+ return content || block.error_summary || "";
30
32
  }).filter(Boolean).join("\n").trim();
31
33
  }
32
34
 
33
- function toolUseText(record) {
34
- return contentBlocks(record).filter((block) => block?.type === "tool_use").map((block) => {
35
- const input = block.input && typeof block.input === "object" ? JSON.stringify(block.input, null, 2) : String(block.input || "").trim();
36
- return [`${String(block.name || "Tool")} tool call`, input].filter(Boolean).join("\n");
37
- }).filter(Boolean).join("\n\n").trim();
35
+ function parsedJson(value) {
36
+ try { return JSON.parse(value); } catch { return null; }
37
+ }
38
+
39
+ function exactCommandFromWrapper(value) {
40
+ const match = String(value).match(/(?:\b(?:cmd|command)|"(?:cmd|command)")\s*:\s*("(?:\\.|[^"\\])*")/s);
41
+ return match ? parsedJson(match[1]) : null;
42
+ }
43
+
44
+ function formattedToolInput(value) {
45
+ if (value && typeof value === "object") {
46
+ const direct = value.cmd ?? value.command;
47
+ return typeof direct === "string" && direct.trim() ? direct.trim() : JSON.stringify(value, null, 2);
48
+ }
49
+ const text = String(value || "").trim();
50
+ if (!text) return "";
51
+ const decoded = parsedJson(text);
52
+ if (decoded && typeof decoded === "object") return formattedToolInput(decoded);
53
+ return exactCommandFromWrapper(text) || text;
54
+ }
55
+
56
+ function toolAction(record, fallbackText) {
57
+ const block = contentBlocks(record).find((item) => item?.type === "tool_use");
58
+ if (block) return {
59
+ toolName: String(block.name || "Tool"),
60
+ details: exactLocalText(formattedToolInput(block.input) || fallbackText),
61
+ timestamp: record?.timestamp || null,
62
+ };
63
+ return {
64
+ toolName: "Agent message",
65
+ details: exactLocalText(visibleText(record) || fallbackText),
66
+ timestamp: record?.timestamp || null,
67
+ };
38
68
  }
39
69
 
40
- function locallyRedacted(value) {
41
- return redactText(String(value || ""), [], { includeHeuristicSecrets: false }).text.trim();
70
+ function isUserTurn(record) {
71
+ return record?.type === "user" && !record?.isMeta && Boolean(localOpeningPrompt(visibleText(record)));
72
+ }
73
+
74
+ function turnsBetweenOpeningAndAction(records, actionIndex) {
75
+ if (actionIndex === null) return 0;
76
+ const openingIndex = records.findIndex((record) => record?.type === "user" && !record?.isMeta && localOpeningPrompt(visibleText(record)));
77
+ if (openingIndex < 0 || actionIndex <= openingIndex) return 0;
78
+ return records.slice(openingIndex + 1, actionIndex).filter(isUserTurn).length;
42
79
  }
43
80
 
44
81
  function matchingEvidenceText(occurrence, kind) {
@@ -56,69 +93,45 @@ function occurrenceRecordIndex(occurrence, records, field, kind) {
56
93
  return finiteRecordIndex(occurrence?.location?.[field], records.length) ?? fallbackRecordIndex(occurrence, records, kind);
57
94
  }
58
95
 
59
- function transcriptMessage(record, kind = "context") {
60
- const text = locallyRedacted(visibleText(record));
61
- if (!text) return null;
62
- return { role: record.type, timestamp: record.timestamp || null, text, kind };
63
- }
64
-
65
- function contextAroundBlocker(records, blockerIndex, alternativeIndex, contextTurns, blockerText) {
66
- if (blockerIndex === null) return [];
67
- const conversationIndexes = records.flatMap((record, index) => !record?.isMeta && (record?.type === "user" || record?.type === "assistant") && visibleText(record) ? [index] : []);
68
- const selectedIndexes = [
69
- ...conversationIndexes.filter((index) => index < blockerIndex).slice(-contextTurns),
70
- blockerIndex,
71
- ...conversationIndexes.filter((index) => index > blockerIndex).slice(0, contextTurns),
72
- ];
73
- return selectedIndexes.flatMap((recordIndex) => {
74
- if (recordIndex === blockerIndex) return [{ role: "tool", timestamp: records[recordIndex]?.timestamp || null, text: blockerText, kind: "blocker" }];
75
- const message = transcriptMessage(records[recordIndex], recordIndex === alternativeIndex ? "workaround" : "context");
76
- return message ? [message] : [];
77
- });
78
- }
79
-
80
- function safeFallbackMessages(occurrence) {
81
- return (occurrence?.evidence || []).flatMap((event) => {
82
- if (!["user", "assistant", "tool"].includes(event?.role)) return [];
83
- const text = locallyRedacted(event?.text);
84
- return text ? [{ role: event.role, timestamp: event.timestamp || null, text, kind: event.kind === "tool_result" ? "blocker" : "context" }] : [];
85
- });
86
- }
87
-
88
- export function makeWorkaroundEvidencePreview(report, sessionRecords, metadataById, { contextTurns = DEFAULT_CONTEXT_TURNS } = {}) {
89
- const boundedContext = Number.isInteger(contextTurns) ? Math.min(4, Math.max(1, contextTurns)) : DEFAULT_CONTEXT_TURNS;
96
+ export function makeWorkaroundEvidencePreview(report, sessionRecords, metadataById) {
90
97
  const recordsById = new Map(sessionRecords.map((session) => [session.sessionId, session.records]));
91
98
  const occurrences = (report?.workaroundReview?.occurrences || []).slice(0, MAX_OCCURRENCES).flatMap((occurrence, index) => {
92
99
  const sessionId = occurrence?.location?.sessionId;
93
100
  const records = recordsById.get(sessionId);
94
101
  if (!records) return [];
95
102
  const metadata = metadataById.get(sessionId);
96
- const donationSession = makeDonationPreview([{ sessionId, records }], metadataById).sessions[0];
103
+ const donationSession = makeDonationPreview([{ sessionId, records }], metadataById, { unredacted: true }).sessions[0];
104
+ const fullOpeningMessage = donationSession?.messages?.filter((message) => message.role === "user").map((message) => exactLocalText(localOpeningPrompt(message.text))).find(Boolean)
105
+ || donationSession?.summary
106
+ || "Opening message unavailable";
107
+ const originalIndex = occurrenceRecordIndex(occurrence, records, "originalRecordIndex", "tool_use");
97
108
  const blockerIndex = occurrenceRecordIndex(occurrence, records, "blockerRecordIndex", "tool_result");
98
109
  const alternativeIndex = occurrenceRecordIndex(occurrence, records, "alternativeRecordIndex", ["assistant_text", "tool_use"]);
110
+ const originalRecord = originalIndex === null ? null : records[originalIndex];
99
111
  const blockerRecord = blockerIndex === null ? null : records[blockerIndex];
100
112
  const alternativeRecord = alternativeIndex === null ? null : records[alternativeIndex];
101
- const blockerText = locallyRedacted(toolResultText(blockerRecord) || matchingEvidenceText(occurrence, "tool_result") || occurrence.blocker || "The original method was blocked.");
102
- const workaroundText = locallyRedacted(visibleText(alternativeRecord) || occurrence.alternativeMethod || toolUseText(alternativeRecord) || matchingEvidenceText(occurrence, "assistant_text") || "The agent tried another method.");
103
- const context = contextAroundBlocker(records, blockerIndex, alternativeIndex, boundedContext, blockerText);
113
+ const blockerText = exactLocalText(toolResultText(blockerRecord) || matchingEvidenceText(occurrence, "tool_result") || occurrence.blocker || "The original method was blocked.");
104
114
  return [{
105
115
  index: index + 1,
106
116
  session: {
107
117
  label: metadata?.label || `Session ${index + 1}`,
108
118
  agentName: metadata?.agentName || "AI agent",
109
119
  startedAt: metadata?.startedAt || records.find((record) => record?.timestamp)?.timestamp || null,
110
- openingMessage: donationSession?.summary || "Opening message unavailable",
120
+ openingMessage: {
121
+ preview: donationSession?.summary || fullOpeningMessage,
122
+ full: fullOpeningMessage,
123
+ },
124
+ turnsBeforeWorkaround: turnsBetweenOpeningAndAction(records, originalIndex),
111
125
  },
112
- workaroundAction: { text: workaroundText, timestamp: alternativeRecord?.timestamp || null },
126
+ originalAction: toolAction(originalRecord, occurrence.originalMethod || matchingEvidenceText(occurrence, "tool_use") || "Original tool call unavailable"),
113
127
  blocker: { text: blockerText, timestamp: blockerRecord?.timestamp || null },
114
- context: context.length ? context : safeFallbackMessages(occurrence),
115
- contextTurns: boundedContext,
128
+ workaroundAction: toolAction(alternativeRecord, occurrence.alternativeMethod || matchingEvidenceText(occurrence, "assistant_text") || "Workaround action unavailable"),
116
129
  }];
117
130
  });
118
131
  return {
119
- format: "behavior-wrapped-workaround-evidence-v2",
132
+ format: "behavior-wrapped-workaround-evidence-v4",
120
133
  localPrivate: true,
121
- standardRedactionsApplied: true,
134
+ standardRedactionsApplied: false,
122
135
  reportId: report?.id,
123
136
  occurrences,
124
137
  };