behavior-wrapped 0.5.8 → 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/assets/{index-DyBPWLnC.css → index-COnYUt6B.css} +1 -1
- package/dist/assets/index-DzSM-hPQ.js +12 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/cli.mjs +4 -3
- package/server/instrumental-workarounds.mjs +9 -7
- package/server/launcher.mjs +11 -0
- package/server/progress.mjs +21 -5
- package/server/workaround-evidence.mjs +125 -0
- package/dist/assets/index-D0gP-HqY.js +0 -11
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-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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
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
|
|
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("
|
|
229
|
+
progress.succeed("LLM Judge complete");
|
|
229
230
|
}
|
|
230
231
|
analyzed.phraseCard = phraseCard;
|
|
231
232
|
if (!localOnly) {
|
|
@@ -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 (
|
|
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);
|
package/server/launcher.mjs
CHANGED
|
@@ -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);
|
package/server/progress.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
77
|
+
if (interactive) clearLine();
|
|
78
|
+
output.write(`✓ ${summary} · ${elapsed}\n`);
|
|
63
79
|
},
|
|
64
80
|
stop() {
|
|
65
81
|
stopTimer();
|
|
66
|
-
if (interactive)
|
|
82
|
+
if (interactive) clearLine();
|
|
67
83
|
},
|
|
68
84
|
};
|
|
69
85
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { makeDonationPreview } from "./analysis.mjs";
|
|
2
|
+
import { redactText } from "./privacy.mjs";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_CONTEXT_TURNS = 2;
|
|
5
|
+
const MAX_OCCURRENCES = 100;
|
|
6
|
+
|
|
7
|
+
function finiteRecordIndex(value, length) {
|
|
8
|
+
return Number.isInteger(value) && value >= 0 && value < length ? value : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
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 [];
|
|
16
|
+
}
|
|
17
|
+
|
|
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
|
+
});
|
|
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;
|
|
90
|
+
const recordsById = new Map(sessionRecords.map((session) => [session.sessionId, session.records]));
|
|
91
|
+
const occurrences = (report?.workaroundReview?.occurrences || []).slice(0, MAX_OCCURRENCES).flatMap((occurrence, index) => {
|
|
92
|
+
const sessionId = occurrence?.location?.sessionId;
|
|
93
|
+
const records = recordsById.get(sessionId);
|
|
94
|
+
if (!records) return [];
|
|
95
|
+
const metadata = metadataById.get(sessionId);
|
|
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);
|
|
104
|
+
return [{
|
|
105
|
+
index: index + 1,
|
|
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),
|
|
115
|
+
contextTurns: boundedContext,
|
|
116
|
+
}];
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
format: "behavior-wrapped-workaround-evidence-v2",
|
|
120
|
+
localPrivate: true,
|
|
121
|
+
standardRedactionsApplied: true,
|
|
122
|
+
reportId: report?.id,
|
|
123
|
+
occurrences,
|
|
124
|
+
};
|
|
125
|
+
}
|