behavior-wrapped 0.8.9 → 0.8.11

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="A local-first behavior report for you and your AI agents." />
8
8
  <title>Behavior Wrapped</title>
9
- <script type="module" crossorigin src="/assets/index-CD48EtI2.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-RUb7reCR.css">
9
+ <script type="module" crossorigin src="/assets/index-CFnO0oEF.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-Ctd7RCMR.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.8.9",
3
+ "version": "0.8.11",
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",
@@ -62,7 +62,7 @@ function proseText(value) {
62
62
  .replace(/`[^`\n]+`/g, " ")
63
63
  .replace(/^\s*>.*$/gm, " ")
64
64
  .replace(/https?:\/\/\S+/g, " ")
65
- .replace(/\b(?:[A-Za-z]:)?[/.~][^\s]+/g, " ")
65
+ .replace(/(?:^|\s)(?:[A-Za-z]:)?[/.~][^\s]+/g, " ")
66
66
  .replace(/<[^>]+>/g, " ")
67
67
  .replace(/\s+/g, " ")
68
68
  .trim();
@@ -87,9 +87,10 @@ function repeatedInstructions(sessionRecords) {
87
87
  for (let sessionIndex = 0; sessionIndex < sessionRecords.length; sessionIndex++) {
88
88
  for (const record of sessionRecords[sessionIndex].records) {
89
89
  if (record.type !== "user" || record.isMeta) continue;
90
- const cleaned = redactAggregateText(proseText(visibleText(record)));
90
+ const cleaned = redactAggregateText(proseText(localOpeningPrompt(visibleText(record))));
91
91
  if (!cleaned || /\[(?:REDACTED|REMOVED)[^\]]*\]/i.test(cleaned)) continue;
92
92
  for (const rawClause of cleaned.split(/(?<=[.!?])\s+|[;\n]+/)) {
93
+ if (/\*\*[^*\n]{2,80}:\*\*/.test(rawClause)) continue;
93
94
  const instruction = rawClause.replace(/^[-*\d.)\s]+/, "").replace(/\s+/g, " ").trim();
94
95
  const words = instruction.match(/\p{L}+(?:['’]\p{L}+)?/gu) || [];
95
96
  if (words.length < 3 || words.length > 18 || instruction.length > 160 || !instructionOpeningPattern.test(instruction)) continue;
@@ -106,7 +107,7 @@ function repeatedInstructions(sessionRecords) {
106
107
  return [...instructions.values()]
107
108
  .filter((item) => item.occurrences >= 2)
108
109
  .sort((left, right) => right.sessions.size - left.sessions.size || right.occurrences - left.occurrences || left.instruction.localeCompare(right.instruction))
109
- .slice(0, 4)
110
+ .slice(0, 2)
110
111
  .map((item) => ({ instruction: item.instruction, occurrences: item.occurrences, distinctSessions: item.sessions.size }));
111
112
  }
112
113
 
@@ -453,6 +454,8 @@ export function analyzeSessions(sessionRecords) {
453
454
  };
454
455
  for (const [recordIndex, record] of records.entries()) {
455
456
  const text = visibleText(record);
457
+ const userProse = record.type === "user" ? proseText(localOpeningPrompt(text)) : "";
458
+ const assistantResponseProse = record.type === "assistant" ? proseText(text) : "";
456
459
  const declaredModel = record?.message?.model || record?.model;
457
460
  if (typeof declaredModel === "string" && declaredModel) currentModel = declaredModel;
458
461
  if (record.type === "system" && record.subtype === "turn_duration") {
@@ -462,10 +465,10 @@ export function analyzeSessions(sessionRecords) {
462
465
  longestUninterruptedRun = { durationMs: Math.round(durationMs), agent, agentName: definition?.name || "Agent" };
463
466
  }
464
467
  }
465
- if (record.type === "user" && !record.isMeta && text) {
468
+ if (record.type === "user" && !record.isMeta && userProse) {
466
469
  finishResponse();
467
470
  hasCurrentPrompt = true;
468
- userInputWords += wordCount(text);
471
+ userInputWords += wordCount(userProse);
469
472
  userInputCount++;
470
473
  sessionTurns++;
471
474
  if (isFrustratedMessage(text)) frustratedMessages++;
@@ -477,8 +480,8 @@ export function analyzeSessions(sessionRecords) {
477
480
  location: { sessionId, recordIndex, timestamp: record.timestamp || null },
478
481
  });
479
482
  }
480
- } else if (record.type === "assistant" && hasCurrentPrompt && text) {
481
- currentResponseWords += wordCount(text);
483
+ } else if (record.type === "assistant" && hasCurrentPrompt && assistantResponseProse) {
484
+ currentResponseWords += wordCount(assistantResponseProse);
482
485
  }
483
486
  if (record.type === "assistant" && text) {
484
487
  assistantProse.push(text);
@@ -554,6 +557,7 @@ export function analyzeSessions(sessionRecords) {
554
557
  agentUserWordRatio: userInputWords ? Number((agentResponseWords / userInputWords).toFixed(2)) : null,
555
558
  averageAgentResponseWords: agentResponseCount ? Math.round(agentResponseWords / agentResponseCount) : 0,
556
559
  averageUserInputWords: userInputCount ? Math.round(userInputWords / userInputCount) : 0,
560
+ wordCountMethod: "Counts conversational prose only, excluding injected workspace instructions, code, URLs, paths, and markup.",
557
561
  longestSessionTurns: Math.max(0, ...sessionTurnCounts),
558
562
  sessionTurnCounts: [...sessionTurnCounts].sort((left, right) => left - right),
559
563
  sessionTurnMethod: "Counts each visible, non-meta user message as one turn.",
@@ -74,7 +74,7 @@ function isSshIdentity(match, offset, source) {
74
74
  return localPart === "git" || /ssh:\/\/$/i.test(before) || after === ":";
75
75
  }
76
76
 
77
- const NON_PERSON_WORDS = new Set("agent assistant user system model tool team claude zulip github person someone anyone everyone nobody only the this that new latest direct explicit online private prior session status handoff instruction instructions message messages ping pings task work context state directory window".split(" "));
77
+ const NON_PERSON_WORDS = new Set("agent assistant user system model tool team claude zulip github person someone anyone everyone nobody being only the this that new latest direct explicit online private prior session status handoff instruction instructions message messages ping pings task work context state directory window".split(" "));
78
78
 
79
79
  function replaceLikelyPersonNames(value) {
80
80
  return String(value)
@@ -108,7 +108,7 @@ export function redactText(input, manualTerms = [], { disabledKinds = [], disabl
108
108
  if (!labeledCredentialValue(match)) return match;
109
109
  const replacement = "[REDACTED CREDENTIAL]";
110
110
  const enabled = isEnabled("credential", match);
111
- detections.push(detectionDetails({ kind: "credential", label: "Credential", value: match, replacement, offset, source, enabled }));
111
+ detections.push(detectionDetails({ kind: "credential", label: "API keys and secrets", value: match, replacement, offset, source, enabled }));
112
112
  return enabled ? replacement : protect(match);
113
113
  });
114
114
  for (const [pattern, replacement] of [...SECRET_PATTERNS, ...(includeHeuristicSecrets ? HEURISTIC_SECRET_PATTERNS : []), ...PII_PATTERNS]) {
@@ -117,7 +117,9 @@ export function redactText(input, manualTerms = [], { disabledKinds = [], disabl
117
117
  const enabled = isEnabled(kind, match);
118
118
  detections.push(detectionDetails({
119
119
  kind,
120
- label: replacement.slice(1, -1).toLowerCase().replace(/\b\w/g, (letter) => letter.toUpperCase()),
120
+ label: /(?:SECRET|KEY|TOKEN|CREDENTIAL|HIGH-ENTROPY)/.test(replacement)
121
+ ? "API keys and secrets"
122
+ : replacement.slice(1, -1).toLowerCase().replace(/\b\w/g, (letter) => letter.toUpperCase()),
121
123
  value: match,
122
124
  replacement,
123
125
  offset,
@@ -38,7 +38,7 @@ function safeStockPhrases(value) {
38
38
 
39
39
  function safeRepeatedInstructions(value) {
40
40
  if (!Array.isArray(value)) return [];
41
- return value.slice(0, 4).flatMap((item) => {
41
+ return value.slice(0, 2).flatMap((item) => {
42
42
  const instruction = safeText(item?.instruction, 160).trim();
43
43
  const occurrences = Math.round(safeNumber(item?.occurrences, 1_000_000));
44
44
  const distinctSessions = Math.round(safeNumber(item?.distinctSessions, 1_000_000));