behavior-wrapped 0.4.3 → 0.4.5
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-iGlv2olC.css → index-BgzJrArR.css} +1 -1
- package/dist/assets/index-Cuo3lbwx.js +11 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/scripts/compare-free-judges.mjs +162 -0
- package/server/analysis.mjs +24 -1
- package/server/cli.mjs +3 -3
- package/server/launcher.mjs +1 -2
- package/server/local-helper-runtime.mjs +3 -2
- package/dist/assets/index-P98_jCrO.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-Cuo3lbwx.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BgzJrArR.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { discoverAllSessionsAsync, readRecordsAsync, sessionsInDefaultWindow } from "../server/discovery.mjs";
|
|
3
|
+
import { buildPhraseCandidates, judgePhraseCard } from "../server/phrase-card.mjs";
|
|
4
|
+
import { buildInteractionToneCandidates, judgeInteractionTone } from "../server/interaction-tone.mjs";
|
|
5
|
+
import { buildSessionTopicCandidates, judgeSessionTopics } from "../server/session-topics.mjs";
|
|
6
|
+
import { buildWorkaroundTrajectories, judgeWorkarounds } from "../server/instrumental-workarounds.mjs";
|
|
7
|
+
|
|
8
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
9
|
+
if (!apiKey) throw new Error("OPENROUTER_API_KEY is required.");
|
|
10
|
+
|
|
11
|
+
const configuredModels = [
|
|
12
|
+
{ label: "Gemma 4 31B", id: "google/gemma-4-31b-it:free", reasoningEffort: "none" },
|
|
13
|
+
{ label: "GPT-OSS 20B", id: "openai/gpt-oss-20b:free", reasoningEffort: "low", minimumMaxTokens: 16_384 },
|
|
14
|
+
{ label: "GPT-5.6 Luna (ZDR)", id: "openai/gpt-5.6-luna", reasoningEffort: "none", zdr: true },
|
|
15
|
+
];
|
|
16
|
+
const models = process.env.COMPARE_MODEL
|
|
17
|
+
? configuredModels.filter((model) => model.id.includes(process.env.COMPARE_MODEL))
|
|
18
|
+
: configuredModels;
|
|
19
|
+
if (!models.length) throw new Error(`No configured model matched COMPARE_MODEL=${process.env.COMPARE_MODEL}.`);
|
|
20
|
+
|
|
21
|
+
const providersSeen = new Set();
|
|
22
|
+
const usageSeen = [];
|
|
23
|
+
function privacyFilteredFetch(model) {
|
|
24
|
+
return async (url, options = {}) => {
|
|
25
|
+
const requestBody = JSON.parse(options.body);
|
|
26
|
+
requestBody.provider = { ...(requestBody.provider || {}), data_collection: "deny", ...(model.zdr ? { zdr: true } : {}) };
|
|
27
|
+
requestBody.reasoning = { ...(requestBody.reasoning || {}), effort: model.reasoningEffort, exclude: true };
|
|
28
|
+
requestBody.max_tokens = Math.max(Number(requestBody.max_tokens) || 0, model.minimumMaxTokens || 0);
|
|
29
|
+
let response;
|
|
30
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
31
|
+
response = await fetch(url, { ...options, body: JSON.stringify(requestBody) });
|
|
32
|
+
if (response.status !== 429 || attempt === 3) break;
|
|
33
|
+
const waitMs = 5_000 * (2 ** attempt);
|
|
34
|
+
process.stdout.write(`capacity retry in ${waitMs / 1000}s... `);
|
|
35
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const responseBody = await response.clone().json();
|
|
39
|
+
if (responseBody?.provider) providersSeen.add(responseBody.provider);
|
|
40
|
+
if (responseBody?.usage) usageSeen.push({
|
|
41
|
+
provider: responseBody.provider || null,
|
|
42
|
+
promptTokens: Number(responseBody.usage.prompt_tokens) || 0,
|
|
43
|
+
completionTokens: Number(responseBody.usage.completion_tokens) || 0,
|
|
44
|
+
totalTokens: Number(responseBody.usage.total_tokens) || 0,
|
|
45
|
+
costUsd: Number(responseBody.usage.cost) || 0,
|
|
46
|
+
});
|
|
47
|
+
} catch { /* The judge functions report malformed responses. */ }
|
|
48
|
+
return response;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function errorSummary(error) {
|
|
53
|
+
return {
|
|
54
|
+
error: error?.message || String(error),
|
|
55
|
+
...(error?.judgeDetails?.http_status ? { status: error.judgeDetails.http_status } : {}),
|
|
56
|
+
...(error?.judgeDetails?.upstream_code ? { code: error.judgeDetails.upstream_code } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function attempt(label, operation) {
|
|
61
|
+
process.stdout.write(` ${label}... `);
|
|
62
|
+
const startedAt = Date.now();
|
|
63
|
+
try {
|
|
64
|
+
const value = await operation();
|
|
65
|
+
console.log(`ok (${((Date.now() - startedAt) / 1000).toFixed(1)}s)`);
|
|
66
|
+
return { ok: true, value };
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.log(`failed (${((Date.now() - startedAt) / 1000).toFixed(1)}s)`);
|
|
69
|
+
return { ok: false, ...errorSummary(error) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function summarize(result) {
|
|
74
|
+
const phrase = result.phrase?.ok ? {
|
|
75
|
+
phrase: result.phrase.value.phrase,
|
|
76
|
+
occurrences: result.phrase.value.occurrences,
|
|
77
|
+
latencyMs: result.phrase.value.latencyMs,
|
|
78
|
+
} : result.phrase;
|
|
79
|
+
const tone = result.tone?.ok ? {
|
|
80
|
+
frustratedMessages: result.tone.value.frustratedMessages,
|
|
81
|
+
gratefulMessages: result.tone.value.gratefulMessages,
|
|
82
|
+
frustrationQuote: result.tone.value.frustrationQuote,
|
|
83
|
+
latencyMs: result.tone.value.latencyMs,
|
|
84
|
+
} : result.tone;
|
|
85
|
+
const topics = result.topics?.ok ? {
|
|
86
|
+
topics: result.topics.value.topics.map(({ topic, percentage }) => ({ topic, percentage })),
|
|
87
|
+
latencyMs: result.topics.value.latencyMs,
|
|
88
|
+
} : result.topics;
|
|
89
|
+
const workarounds = result.workarounds?.ok ? {
|
|
90
|
+
confirmed: result.workarounds.value.card.count,
|
|
91
|
+
borderline: result.workarounds.value.review.borderline.length,
|
|
92
|
+
examples: result.workarounds.value.review.occurrences.map(({ summary, confidence, disclosure }) => ({ summary, confidence, disclosure })),
|
|
93
|
+
latencyMs: result.workarounds.value.review.latencyMs,
|
|
94
|
+
} : result.workarounds;
|
|
95
|
+
return { phrase, tone, topics, workarounds };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function summarizeUsage(records) {
|
|
99
|
+
return records.reduce((total, record) => ({
|
|
100
|
+
requests: total.requests + 1,
|
|
101
|
+
promptTokens: total.promptTokens + record.promptTokens,
|
|
102
|
+
completionTokens: total.completionTokens + record.completionTokens,
|
|
103
|
+
totalTokens: total.totalTokens + record.totalTokens,
|
|
104
|
+
costUsd: Number((total.costUsd + record.costUsd).toFixed(12)),
|
|
105
|
+
}), { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, costUsd: 0 });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log("Preparing the same 30-day, locally redacted Behavior Wrapped input for both models...");
|
|
109
|
+
const catalog = await discoverAllSessionsAsync();
|
|
110
|
+
const sessions = sessionsInDefaultWindow(catalog.sessions, { days: 30 });
|
|
111
|
+
const records = [];
|
|
112
|
+
for (const publicSession of sessions) {
|
|
113
|
+
const session = catalog.index.get(publicSession.id);
|
|
114
|
+
records.push({ sessionId: session.id, agent: session.agent, records: await readRecordsAsync(session.file, session.agent) });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const phraseCandidates = buildPhraseCandidates(records, { maximumCandidates: 100 });
|
|
118
|
+
const toneCandidates = buildInteractionToneCandidates(records);
|
|
119
|
+
const topicBundle = buildSessionTopicCandidates(records);
|
|
120
|
+
const workaroundBundle = buildWorkaroundTrajectories(records);
|
|
121
|
+
console.log(`Snapshot: ${sessions.length} sessions; ${phraseCandidates.length} phrase candidates; ${toneCandidates.length} tone candidates; ${topicBundle.candidates.length} topic candidates; ${workaroundBundle.chunks.length} workaround chunks.`);
|
|
122
|
+
|
|
123
|
+
const comparison = {};
|
|
124
|
+
for (const model of models) {
|
|
125
|
+
providersSeen.clear();
|
|
126
|
+
usageSeen.length = 0;
|
|
127
|
+
console.log(`\n${model.label} (${model.id})`);
|
|
128
|
+
const options = { model: model.id, fetchImpl: privacyFilteredFetch(model) };
|
|
129
|
+
const phrase = phraseCandidates.length
|
|
130
|
+
? await attempt("favorite phrase", () => judgePhraseCard(phraseCandidates, apiKey, options))
|
|
131
|
+
: { ok: false, error: "No candidates" };
|
|
132
|
+
const tone = toneCandidates.length
|
|
133
|
+
? await attempt("interaction tone", () => judgeInteractionTone(toneCandidates, apiKey, options))
|
|
134
|
+
: { ok: false, error: "No candidates" };
|
|
135
|
+
const topics = topicBundle.candidates.length
|
|
136
|
+
? await attempt("session topics", () => judgeSessionTopics(topicBundle, apiKey, options))
|
|
137
|
+
: { ok: false, error: "No candidates" };
|
|
138
|
+
const workarounds = workaroundBundle.chunks.length
|
|
139
|
+
? await attempt("workarounds", () => judgeWorkarounds(workaroundBundle, apiKey, {
|
|
140
|
+
...options,
|
|
141
|
+
onProgress: ({ index, total }) => process.stdout.write(index === 1 ? `[${index}/${total}] ` : `${index}/${total} `),
|
|
142
|
+
}))
|
|
143
|
+
: { ok: false, error: "No candidates" };
|
|
144
|
+
comparison[model.label] = {
|
|
145
|
+
providers: [...providersSeen],
|
|
146
|
+
privacy: { dataCollectionDenied: true, zeroDataRetentionRequired: Boolean(model.zdr) },
|
|
147
|
+
usage: summarizeUsage(usageSeen),
|
|
148
|
+
...summarize({ phrase, tone, topics, workarounds }),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log("\nCOMPARISON_JSON");
|
|
153
|
+
console.log(JSON.stringify({
|
|
154
|
+
input: {
|
|
155
|
+
sessions: sessions.length,
|
|
156
|
+
phraseCandidates: phraseCandidates.length,
|
|
157
|
+
toneCandidates: toneCandidates.length,
|
|
158
|
+
topicCandidates: topicBundle.candidates.length,
|
|
159
|
+
workaroundChunks: workaroundBundle.chunks.length,
|
|
160
|
+
},
|
|
161
|
+
models: comparison,
|
|
162
|
+
}, null, 2));
|
package/server/analysis.mjs
CHANGED
|
@@ -479,10 +479,33 @@ 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) {
|
|
483
|
+
let prompt = String(value || "");
|
|
484
|
+
const explicitRequest = prompt.match(/(?:^|\n)## My request:\s*([\s\S]*)$/i);
|
|
485
|
+
if (explicitRequest) prompt = explicitRequest[1];
|
|
486
|
+
prompt = prompt
|
|
487
|
+
.replace(/<recommended_plugins>[\s\S]*?<\/recommended_plugins>/gi, " ")
|
|
488
|
+
.replace(/<environment_context>[\s\S]*?<\/environment_context>/gi, " ")
|
|
489
|
+
.replace(/<skills_instructions>[\s\S]*?<\/skills_instructions>/gi, " ")
|
|
490
|
+
.replace(/<permissions instructions>[\s\S]*?<\/permissions instructions>/gi, " ")
|
|
491
|
+
.replace(/<collaboration_mode>[\s\S]*?<\/collaboration_mode>/gi, " ")
|
|
492
|
+
.replace(/<apps_instructions>[\s\S]*?<\/apps_instructions>/gi, " ")
|
|
493
|
+
.replace(/<plugins_instructions>[\s\S]*?<\/plugins_instructions>/gi, " ")
|
|
494
|
+
.replace(/<multi_agent_mode>[\s\S]*?<\/multi_agent_mode>/gi, " ")
|
|
495
|
+
.replace(/<INSTRUCTIONS>[\s\S]*?<\/INSTRUCTIONS>/gi, " ")
|
|
496
|
+
.replace(/^\s*# AGENTS\.md instructions\s*$/gim, " ")
|
|
497
|
+
.replace(/<image\b[^>]*>[\s\S]*?<\/image>/gi, " ")
|
|
498
|
+
.replace(/\s+/g, " ")
|
|
499
|
+
.trim();
|
|
500
|
+
return prompt;
|
|
501
|
+
}
|
|
502
|
+
|
|
482
503
|
function donationSessionSummary(messages, suppliedSummary) {
|
|
483
504
|
const provided = String(suppliedSummary || "").replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
|
|
484
505
|
if (provided) return provided.slice(0, 140);
|
|
485
|
-
const opening = messages.
|
|
506
|
+
const opening = messages.filter((message) => message.role === "user").map((message) => localOpeningPrompt(message.text)).find(Boolean)
|
|
507
|
+
|| localOpeningPrompt(messages[0]?.text)
|
|
508
|
+
|| "Session transcript";
|
|
486
509
|
const compact = opening.replace(/\[(?:REDACTED|REMOVED)[^\]]*\]/g, "private detail").replace(/\s+/g, " ").trim();
|
|
487
510
|
if (compact.length <= 110) return compact;
|
|
488
511
|
const shortened = compact.slice(0, 109);
|
package/server/cli.mjs
CHANGED
|
@@ -126,6 +126,9 @@ async function createWrapped() {
|
|
|
126
126
|
console.log(`\n ${bright}behavior-wrapped${reset} ${muted}· the wrapped for your AI agents${reset}\n`);
|
|
127
127
|
const demo = process.argv.includes("--demo");
|
|
128
128
|
const testMode = process.argv.includes("--test") || process.argv.includes("--no-llm");
|
|
129
|
+
progress.start("Preparing local donation helper", `localhost:${port}`);
|
|
130
|
+
await ensureServer(demo);
|
|
131
|
+
progress.succeed("Local donation helper ready");
|
|
129
132
|
const daysArgument = process.argv.find((argument) => argument.startsWith("--days="));
|
|
130
133
|
const windowDays = daysArgument ? Number(daysArgument.split("=")[1]) : DEFAULT_WINDOW_DAYS;
|
|
131
134
|
if (!Number.isInteger(windowDays) || windowDays < 1 || windowDays > 3650) throw new Error("--days must be a whole number from 1 to 3650.");
|
|
@@ -262,9 +265,6 @@ async function createWrapped() {
|
|
|
262
265
|
}
|
|
263
266
|
}
|
|
264
267
|
saveReport(report);
|
|
265
|
-
progress.start("Starting local donation helper", `localhost:${port}`);
|
|
266
|
-
await ensureServer(demo);
|
|
267
|
-
progress.succeed("Local donation helper ready");
|
|
268
268
|
const localUrl = `${baseUrl}/w/${id}`;
|
|
269
269
|
const url = localOnly ? localUrl : report.managementUrl || publicUrl || localUrl;
|
|
270
270
|
const tokenLabel = formatNumber(report.stats.tokens || 0);
|
package/server/launcher.mjs
CHANGED
|
@@ -119,8 +119,7 @@ const server = http.createServer(async (request, response) => {
|
|
|
119
119
|
const ids = Array.isArray(body.sessionIds) ? body.sessionIds.filter((id) => allowed.has(id) && catalog.index.has(id)).slice(0, 250) : [];
|
|
120
120
|
const records = await chosenRecords(ids);
|
|
121
121
|
if (!records.length) return json(response, 400, { error: "Choose at least one available session." });
|
|
122
|
-
const
|
|
123
|
-
const labels = new Map(publicCatalog().sessions.map((session) => [session.id, { ...session, summary: summaries.get(session.id) }]));
|
|
122
|
+
const labels = new Map(publicCatalog().sessions.map((session) => [session.id, session]));
|
|
124
123
|
const disabledRedactions = Array.isArray(body.disabledRedactions) ? body.disabledRedactions.filter((kind) => typeof kind === "string" && /^[a-z0-9-]{1,64}$/.test(kind)).slice(0, 20) : [];
|
|
125
124
|
const disabledMatches = Array.isArray(body.disabledMatches) ? body.disabledMatches.filter((id) => typeof id === "string" && /^[a-f0-9]{24}$/.test(id)).slice(0, 5_000) : [];
|
|
126
125
|
const unredacted = body.previewMode === "unredacted";
|
|
@@ -11,7 +11,7 @@ export function helperHealthMatches(value, { version, protocol, demo }) {
|
|
|
11
11
|
|
|
12
12
|
export function isVerifiedLauncherCommand(command, port) {
|
|
13
13
|
const escapedPort = String(port).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14
|
-
return new RegExp(
|
|
14
|
+
return new RegExp(`(?:/(?:agent-)?behavior-wrapped/|(?:^|\\s))server/launcher\\.mjs(?:\\s|$)`, "i").test(command || "")
|
|
15
15
|
&& new RegExp(`(?:^|\\s)--port=${escapedPort}(?:\\s|$)`).test(command || "");
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -23,7 +23,8 @@ async function listeningPids(port, runCommand) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export async function stopVerifiedStaleHelper(port, advertisedPid, { runCommand = run, kill = process.kill } = {}) {
|
|
26
|
-
const
|
|
26
|
+
const listeners = await listeningPids(port, runCommand);
|
|
27
|
+
const candidates = Number.isInteger(advertisedPid) && advertisedPid > 1 ? listeners.filter((pid) => pid === advertisedPid) : listeners;
|
|
27
28
|
let stopped = false;
|
|
28
29
|
for (const pid of candidates) {
|
|
29
30
|
let command;
|