behavior-wrapped 0.5.9 → 0.5.10

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-BeJIqYle.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-DokkhBtC.css">
9
+ <script type="module" crossorigin src="/assets/index-DzSM-hPQ.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-COnYUt6B.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.9",
3
+ "version": "0.5.10",
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",
package/server/cli.mjs CHANGED
@@ -181,8 +181,9 @@ async function createWrapped() {
181
181
  progress.succeed(`${testMode ? "Local test fallbacks" : "Local-only analysis"} ready; no LLM calls made`);
182
182
  } else {
183
183
  const judgeStates = { phrase: "waiting", tone: "waiting", topics: "waiting", workarounds: "waiting" };
184
+ const judgeLabels = { phrase: "favorite phrase", tone: "agent interaction", topics: "topics", workarounds: "workarounds" };
184
185
  const judgeStatus = (state) => state === "waiting" ? "queued" : state === "working" ? "…" : state === "done" ? "✓" : state === "failed" ? "skipped" : state;
185
- const judgeSummary = () => Object.entries(judgeStates).map(([name, state]) => `${name} ${judgeStatus(state)}`).join(" · ");
186
+ const judgeSummary = () => Object.entries(judgeStates).map(([name, state]) => `${judgeLabels[name]} ${judgeStatus(state)}`).join(" · ");
186
187
  const trackJudge = (name, promise) => {
187
188
  judgeStates[name] = "working";
188
189
  progress.update(judgeSummary());
@@ -196,7 +197,7 @@ async function createWrapped() {
196
197
  throw error;
197
198
  });
198
199
  };
199
- progress.start("Running privacy-safe AI analysis", judgeSummary());
200
+ progress.start("Running redacted excerpts through LLM Judge", judgeSummary(), { separateDetail: true });
200
201
  const phraseCardPromise = process.env.BEHAVIOR_WRAPPED_DIRECT_OPENROUTER === "1"
201
202
  ? judgePhraseCard(candidates, process.env.OPENROUTER_API_KEY)
202
203
  : judgePhraseCardViaRelay(candidates, { endpoint: process.env.BEHAVIOR_WRAPPED_JUDGE_URL || PHRASE_JUDGE_RELAY_URL, clientId: getOrCreateClientId() });
@@ -225,7 +226,7 @@ async function createWrapped() {
225
226
  optionalAnalysis(trackJudge("topics", sessionTopicsPromise), "usage-topic card", analysisWarnings),
226
227
  optionalAnalysis(trackJudge("workarounds", workaroundsPromise), "instrumental-workaround card", analysisWarnings),
227
228
  ]);
228
- progress.succeed("Privacy-safe AI analysis complete");
229
+ progress.succeed("LLM Judge complete");
229
230
  }
230
231
  analyzed.phraseCard = phraseCard;
231
232
  if (!localOnly) {
@@ -19,6 +19,7 @@ export function createCliProgress({
19
19
  let label = "";
20
20
  let detail = "";
21
21
  let startedAt = 0;
22
+ let separateDetail = false;
22
23
 
23
24
  function line(symbol) {
24
25
  const extra = detail ? ` · ${detail}` : "";
@@ -27,7 +28,19 @@ export function createCliProgress({
27
28
 
28
29
  function render() {
29
30
  if (!timer) return;
30
- output.write(`\r\x1b[2K${line(spinnerFrames[frame++ % spinnerFrames.length])}`);
31
+ clearLine();
32
+ output.write(separateDetail
33
+ ? `${spinnerFrames[frame++ % spinnerFrames.length]} ${detail}`
34
+ : line(spinnerFrames[frame++ % spinnerFrames.length]));
35
+ }
36
+
37
+ function clearLine() {
38
+ if (typeof output.clearLine === "function" && typeof output.cursorTo === "function") {
39
+ output.clearLine(0);
40
+ output.cursorTo(0);
41
+ return;
42
+ }
43
+ output.write("\r\x1b[2K");
31
44
  }
32
45
 
33
46
  function stopTimer() {
@@ -37,16 +50,18 @@ export function createCliProgress({
37
50
  }
38
51
 
39
52
  return {
40
- start(nextLabel, nextDetail = "") {
53
+ start(nextLabel, nextDetail = "", options = {}) {
41
54
  stopTimer();
42
55
  label = nextLabel;
43
56
  detail = nextDetail;
57
+ separateDetail = Boolean(options.separateDetail && detail);
44
58
  startedAt = now();
45
59
  frame = 0;
46
60
  if (!interactive) {
47
- output.write(`◇ ${label}${detail ? ` · ${detail}` : ""}\n`);
61
+ output.write(separateDetail ? `◇ ${label}\n◇ ${detail}\n` : `◇ ${label}${detail ? ` · ${detail}` : ""}\n`);
48
62
  return;
49
63
  }
64
+ if (separateDetail) output.write(`◇ ${label}\n`);
50
65
  timer = setIntervalImpl(render, intervalMs);
51
66
  timer?.unref?.();
52
67
  render();
@@ -59,11 +74,12 @@ export function createCliProgress({
59
74
  succeed(summary = label) {
60
75
  const elapsed = elapsedLabel(now() - startedAt);
61
76
  stopTimer();
62
- output.write(interactive ? `\r\x1b[2K✓ ${summary} · ${elapsed}\n` : `✓ ${summary} · ${elapsed}\n`);
77
+ if (interactive) clearLine();
78
+ output.write(`✓ ${summary} · ${elapsed}\n`);
63
79
  },
64
80
  stop() {
65
81
  stopTimer();
66
- if (interactive) output.write("\r\x1b[2K");
82
+ if (interactive) clearLine();
67
83
  },
68
84
  };
69
85
  }
@@ -1,4 +1,5 @@
1
1
  import { makeDonationPreview } from "./analysis.mjs";
2
+ import { redactText } from "./privacy.mjs";
2
3
 
3
4
  const DEFAULT_CONTEXT_TURNS = 2;
4
5
  const MAX_OCCURRENCES = 100;
@@ -7,34 +8,80 @@ function finiteRecordIndex(value, length) {
7
8
  return Number.isInteger(value) && value >= 0 && value < length ? value : null;
8
9
  }
9
10
 
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] : []);
11
+ function contentBlocks(record) {
12
+ const content = record?.message?.content ?? record?.content;
13
+ if (Array.isArray(content)) return content;
14
+ if (typeof content === "string") return [{ type: "text", text: content }];
15
+ return [];
18
16
  }
19
17
 
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);
18
+ function visibleText(record) {
19
+ return contentBlocks(record).filter((block) => block?.type === "text").map((block) => block.text || "").join("\n").trim();
20
+ }
21
+
22
+ function toolResultText(record) {
23
+ return contentBlocks(record).filter((block) => block?.type === "tool_result").map((block) => {
24
+ const content = typeof block.content === "string"
25
+ ? block.content
26
+ : Array.isArray(block.content)
27
+ ? block.content.map((part) => typeof part === "string" ? part : part?.text || "").filter(Boolean).join("\n")
28
+ : "";
29
+ return [block.error_summary, content].filter(Boolean).join("\n");
30
+ }).filter(Boolean).join("\n").trim();
31
+ }
32
+
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();
38
+ }
39
+
40
+ function locallyRedacted(value) {
41
+ return redactText(String(value || ""), [], { includeHeuristicSecrets: false }).text.trim();
42
+ }
43
+
44
+ function matchingEvidenceText(occurrence, kind) {
45
+ return String((occurrence?.evidence || []).find((event) => event?.kind === kind)?.text || "").trim();
46
+ }
47
+
48
+ function fallbackRecordIndex(occurrence, records, kind) {
49
+ const kinds = new Set(Array.isArray(kind) ? kind : kind ? [kind] : []);
50
+ const timestamps = new Set((occurrence?.evidence || []).filter((event) => !kinds.size || kinds.has(event?.kind)).map((event) => event?.timestamp).filter(Boolean));
51
+ const index = records.findIndex((record) => timestamps.has(record?.timestamp));
52
+ return index >= 0 ? index : null;
53
+ }
54
+
55
+ function occurrenceRecordIndex(occurrence, records, field, kind) {
56
+ return finiteRecordIndex(occurrence?.location?.[field], records.length) ?? fallbackRecordIndex(occurrence, records, kind);
57
+ }
58
+
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
+ });
31
78
  }
32
79
 
33
80
  function safeFallbackMessages(occurrence) {
34
81
  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 }] : [];
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" }] : [];
38
85
  });
39
86
  }
40
87
 
@@ -45,26 +92,31 @@ export function makeWorkaroundEvidencePreview(report, sessionRecords, metadataBy
45
92
  const sessionId = occurrence?.location?.sessionId;
46
93
  const records = recordsById.get(sessionId);
47
94
  if (!records) return [];
48
- const excerpt = excerptRecords(records, occurrence, boundedContext);
49
- const preview = excerpt.length ? makeDonationPreview([{ sessionId, records: excerpt }], metadataById).sessions[0] : null;
50
95
  const metadata = metadataById.get(sessionId);
51
- const messages = preview?.messages?.length ? preview.messages : safeFallbackMessages(occurrence);
96
+ const donationSession = makeDonationPreview([{ sessionId, records }], metadataById).sessions[0];
97
+ const blockerIndex = occurrenceRecordIndex(occurrence, records, "blockerRecordIndex", "tool_result");
98
+ const alternativeIndex = occurrenceRecordIndex(occurrence, records, "alternativeRecordIndex", ["assistant_text", "tool_use"]);
99
+ const blockerRecord = blockerIndex === null ? null : records[blockerIndex];
100
+ 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);
52
104
  return [{
53
105
  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,
106
+ session: {
107
+ label: metadata?.label || `Session ${index + 1}`,
108
+ agentName: metadata?.agentName || "AI agent",
109
+ startedAt: metadata?.startedAt || records.find((record) => record?.timestamp)?.timestamp || null,
110
+ openingMessage: donationSession?.summary || "Opening message unavailable",
111
+ },
112
+ workaroundAction: { text: workaroundText, timestamp: alternativeRecord?.timestamp || null },
113
+ blocker: { text: blockerText, timestamp: blockerRecord?.timestamp || null },
114
+ context: context.length ? context : safeFallbackMessages(occurrence),
62
115
  contextTurns: boundedContext,
63
- reconstructedFromTranscript: Boolean(preview?.messages?.length),
64
116
  }];
65
117
  });
66
118
  return {
67
- format: "behavior-wrapped-workaround-evidence-v1",
119
+ format: "behavior-wrapped-workaround-evidence-v2",
68
120
  localPrivate: true,
69
121
  standardRedactionsApplied: true,
70
122
  reportId: report?.id,