behavior-wrapped 0.2.11 → 0.2.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/README.md +4 -4
- package/dist/assets/index-CRu8P31z.css +1 -0
- package/dist/assets/index-StWaoDNG.js +11 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/scripts/compare-free-judges.mjs +162 -0
- package/scripts/review-interaction-tone.mjs +1 -1
- package/server/analysis.mjs +42 -9
- package/server/cli.mjs +5 -4
- package/server/consent.mjs +2 -2
- package/server/frustration-card.mjs +2 -1
- package/server/instrumental-workarounds.mjs +2 -1
- package/server/interaction-tone.mjs +2 -1
- package/server/launcher.mjs +1 -1
- package/server/phrase-card.mjs +6 -5
- package/server/privacy.mjs +9 -2
- package/server/public-report-schema.mjs +2 -2
- package/server/session-topics.mjs +2 -1
- package/dist/assets/index-0bjvY6uC.js +0 -11
- package/dist/assets/index-Cjdrcfkw.css +0 -1
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-StWaoDNG.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CRu8P31z.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));
|
|
@@ -25,7 +25,7 @@ function section(title, matches) {
|
|
|
25
25
|
|
|
26
26
|
const markdown = `# Private interaction-tone review
|
|
27
27
|
|
|
28
|
-
Generated locally from ${sessions.length} sessions in the latest 30-day window.
|
|
28
|
+
Generated locally from ${sessions.length} sessions in the latest 30-day window. GPT-5.6 Luna classified ${result.candidateMessages} occurrences represented by ${candidates.length} redacted, deduplicated candidates. Repeated identical excerpts appear once with an occurrence count.
|
|
29
29
|
|
|
30
30
|
This file is private, gitignored, and may contain excerpts from your session history.
|
|
31
31
|
|
package/server/analysis.mjs
CHANGED
|
@@ -450,26 +450,59 @@ export function analyzeSessions(sessionRecords) {
|
|
|
450
450
|
return { stats, findings: analyzeBehavior(sessionRecords) };
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
+
const donationRedactionRules = [
|
|
454
|
+
{ kind: "code", label: "Code block", pattern: /```[\s\S]*?```/g, replacement: "[CODE REMOVED]" },
|
|
455
|
+
{ kind: "inline-code", label: "Inline code", pattern: /`[^`\n]+`/g, replacement: "[INLINE CODE REMOVED]" },
|
|
456
|
+
{ kind: "url", label: "URL", pattern: /https?:\/\/\S+/g, replacement: "[URL REMOVED]" },
|
|
457
|
+
{ kind: "path", label: "Filesystem path", pattern: /(?:[A-Za-z]:\\|\/(?:Users|home|private|tmp|var|opt)\/)[^\s,;:)]+/g, replacement: "[PATH REMOVED]" },
|
|
458
|
+
];
|
|
459
|
+
|
|
453
460
|
function donationText(value) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
.
|
|
458
|
-
.
|
|
461
|
+
const detections = [];
|
|
462
|
+
let text = String(value || "");
|
|
463
|
+
for (const rule of donationRedactionRules) text = text.replace(rule.pattern, (match, offset, source) => {
|
|
464
|
+
detections.push({ ...rule, pattern: undefined, value: match, length: match.length, context: { before: source.slice(Math.max(0, offset - 80), offset), match, after: source.slice(offset + match.length, offset + match.length + 80) } });
|
|
465
|
+
return rule.replacement;
|
|
466
|
+
});
|
|
467
|
+
return { text, detections };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function donationRedactionInventory(detections) {
|
|
471
|
+
const categories = new Map();
|
|
472
|
+
for (const detection of detections) {
|
|
473
|
+
const kind = String(detection.kind || detection.replacement).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
474
|
+
const item = categories.get(detection.replacement) || { kind, label: detection.label, replacement: detection.replacement, count: 0, matches: new Map() };
|
|
475
|
+
item.count++;
|
|
476
|
+
const value = String(detection.value || "");
|
|
477
|
+
const displayValue = value.length > 500 ? `${value.slice(0, 500)}…` : value;
|
|
478
|
+
const match = item.matches.get(value) || { value: displayValue, truncated: displayValue !== value, length: detection.length || value.length, count: 0, contexts: [] };
|
|
479
|
+
match.count++;
|
|
480
|
+
if (match.contexts.length < 6 && detection.context) match.contexts.push({
|
|
481
|
+
before: String(detection.context.before || "").replace(/\s+/g, " "),
|
|
482
|
+
match: value.length > 180 ? `${value.slice(0, 180)}…` : value,
|
|
483
|
+
after: String(detection.context.after || "").replace(/\s+/g, " "),
|
|
484
|
+
});
|
|
485
|
+
item.matches.set(value, match);
|
|
486
|
+
categories.set(detection.replacement, item);
|
|
487
|
+
}
|
|
488
|
+
return [...categories.values()].map((item) => ({ ...item, matches: [...item.matches.values()].sort((left, right) => right.count - left.count || left.value.localeCompare(right.value)) })).sort((left, right) => right.count - left.count || left.label.localeCompare(right.label));
|
|
459
489
|
}
|
|
460
490
|
|
|
461
491
|
export function makeDonationPreview(sessionRecords, metadataById) {
|
|
462
|
-
|
|
492
|
+
const detections = [];
|
|
463
493
|
const sessions = sessionRecords.map(({ sessionId, records }) => {
|
|
464
494
|
const messages = records.flatMap((record) => {
|
|
465
495
|
if (record.type !== "user" && record.type !== "assistant") return [];
|
|
466
496
|
const value = visibleText(record);
|
|
467
497
|
if (!value) return [];
|
|
468
|
-
const
|
|
469
|
-
|
|
498
|
+
const prepared = donationText(value);
|
|
499
|
+
const redacted = redactText(prepared.text);
|
|
500
|
+
detections.push(...prepared.detections, ...redacted.detections);
|
|
470
501
|
return [{ role: record.type, timestamp: record.timestamp || null, text: redacted.text }];
|
|
471
502
|
});
|
|
472
503
|
return { sessionId, label: metadataById.get(sessionId)?.label || `Session ${sessionId.slice(0, 6)}`, messages };
|
|
473
504
|
});
|
|
474
|
-
|
|
505
|
+
const redactions = donationRedactionInventory(detections);
|
|
506
|
+
const detectionCount = detections.length;
|
|
507
|
+
return { format: "behavior-wrapped-donation-preview-v1", createdLocally: true, detectionCount, redactions, sessions };
|
|
475
508
|
}
|
package/server/cli.mjs
CHANGED
|
@@ -20,7 +20,8 @@ const root = path.dirname(here);
|
|
|
20
20
|
const fixtureRoot = path.join(root, "fixtures", "projects");
|
|
21
21
|
const codexFixtureRoot = path.join(root, "fixtures", "codex-sessions");
|
|
22
22
|
const port = Number(process.env.BEHAVIOR_WRAPPED_PORT || 4317);
|
|
23
|
-
const baseUrl = `http://
|
|
23
|
+
const baseUrl = `http://localhost:${port}`;
|
|
24
|
+
const loopbackUrl = `http://127.0.0.1:${port}`;
|
|
24
25
|
const command = process.argv[2];
|
|
25
26
|
const verbose = process.argv.includes("--verbose") || process.argv.includes("--debug") || process.env.BEHAVIOR_WRAPPED_DEBUG === "1";
|
|
26
27
|
const muted = "\x1b[2m"; const bright = "\x1b[1m"; const lime = "\x1b[38;2;201;242;75m"; const purple = "\x1b[38;2;141;92;255m"; const reset = "\x1b[0m";
|
|
@@ -68,7 +69,7 @@ function printJudgeDebug(label, error) {
|
|
|
68
69
|
|
|
69
70
|
async function serverReady(expectedDemo = false) {
|
|
70
71
|
try {
|
|
71
|
-
const response = await fetch(`${
|
|
72
|
+
const response = await fetch(`${loopbackUrl}/api/health`);
|
|
72
73
|
const body = await response.json();
|
|
73
74
|
return response.ok && body.app === "behavior-wrapped" && Boolean(body.demo) === expectedDemo;
|
|
74
75
|
} catch { return false; }
|
|
@@ -225,7 +226,7 @@ async function createWrapped() {
|
|
|
225
226
|
const id = createReportId();
|
|
226
227
|
const safeFindings = analyzed.findings.map(({ evidence, method, ...finding }) => finding);
|
|
227
228
|
const hasPrivateWorkaroundEvidence = Boolean(analyzed.workaroundReview?.occurrences?.length || analyzed.workaroundReview?.borderline?.length);
|
|
228
|
-
const report = { id, createdAt: new Date().toISOString(), rangeLabel: formatRange(chosenSessions), source: "Claude Code + Codex", stats: analyzed.stats, findings: safeFindings, phraseCard: analyzed.phraseCard, interactionCard: analyzed.interactionCard, workaroundCard: analyzed.workaroundCard, workaroundReview: analyzed.workaroundReview, sessionIds: chosenSessions.map((session) => session.id), donationHelperUrl: `${baseUrl}/donate/${id}`, privacy: { shareSafe: !hasPrivateWorkaroundEvidence, containsTranscriptText: hasPrivateWorkaroundEvidence, externalTransmission: !testMode, ...(testMode ? { transmittedData: "None; test mode stays local.", externalRecipient: "None" } : { transmittedData: "redacted phrase, interaction-tone, and session-topic candidates; locally redacted context windows around explicit blockers for workaround discovery; aggregate report statistics; and a random client ID only", externalRecipient: "Behavior Wrapped relay, OpenRouter,
|
|
229
|
+
const report = { id, createdAt: new Date().toISOString(), rangeLabel: formatRange(chosenSessions), source: "Claude Code + Codex", stats: analyzed.stats, findings: safeFindings, phraseCard: analyzed.phraseCard, interactionCard: analyzed.interactionCard, workaroundCard: analyzed.workaroundCard, workaroundReview: analyzed.workaroundReview, sessionIds: chosenSessions.map((session) => session.id), donationHelperUrl: `${baseUrl}/donate/${id}`, privacy: { shareSafe: !hasPrivateWorkaroundEvidence, containsTranscriptText: hasPrivateWorkaroundEvidence, externalTransmission: !testMode, ...(testMode ? { transmittedData: "None; test mode stays local.", externalRecipient: "None" } : { transmittedData: "redacted phrase, interaction-tone, and session-topic candidates; locally redacted context windows around explicit blockers for workaround discovery; aggregate report statistics; and a random client ID only", externalRecipient: "Behavior Wrapped relay, OpenRouter, a zero-data-retention GPT-5.6 Luna provider, and public report hosting" }) } };
|
|
229
230
|
let publicUrl = null;
|
|
230
231
|
if (!testMode) {
|
|
231
232
|
progress.start("Publishing share-safe Wrapped", "strict aggregate-only schema");
|
|
@@ -241,7 +242,7 @@ async function createWrapped() {
|
|
|
241
242
|
}
|
|
242
243
|
}
|
|
243
244
|
saveReport(report);
|
|
244
|
-
progress.start("Starting local donation helper", `
|
|
245
|
+
progress.start("Starting local donation helper", `localhost:${port}`);
|
|
245
246
|
await ensureServer(demo);
|
|
246
247
|
progress.succeed("Local donation helper ready");
|
|
247
248
|
const localUrl = `${baseUrl}/w/${id}`;
|
package/server/consent.mjs
CHANGED
|
@@ -5,12 +5,12 @@ const lime = "\x1b[38;2;201;242;75m";
|
|
|
5
5
|
const purple = "\x1b[38;2;141;92;255m";
|
|
6
6
|
const reset = "\x1b[0m";
|
|
7
7
|
|
|
8
|
-
export const remoteAnalysisConsentText = "Behavior Wrapped will send redacted excerpts from your session history to
|
|
8
|
+
export const remoteAnalysisConsentText = "Behavior Wrapped will send redacted excerpts from your session history to GPT-5.6 Luna via OpenRouter using zero-data-retention providers for analysis. OK to proceed?";
|
|
9
9
|
|
|
10
10
|
export async function requestRemoteAnalysisConsent({ input = process.stdin, output = process.stdout } = {}) {
|
|
11
11
|
const prompt = createInterface({ input, output });
|
|
12
12
|
try {
|
|
13
|
-
const question = `${lime}◇${reset} Behavior Wrapped will send redacted excerpts from your session history to ${purple}${bright}
|
|
13
|
+
const question = `${lime}◇${reset} Behavior Wrapped will send redacted excerpts from your session history to ${purple}${bright}GPT-5.6 Luna${reset} via OpenRouter using zero-data-retention providers for analysis. OK to proceed? ${bright}(Y/n)${reset} `;
|
|
14
14
|
const answer = await prompt.question(question);
|
|
15
15
|
return /^(?:|y|yes)$/i.test(answer.trim());
|
|
16
16
|
} finally {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { redactAggregateText } from "./privacy.mjs";
|
|
2
|
-
import { extractCandidateId, OPENROUTER_MODEL, PHRASE_JUDGE_NAME } from "./phrase-card.mjs";
|
|
2
|
+
import { extractCandidateId, OPENROUTER_MODEL, OPENROUTER_PROVIDER_PREFERENCES, PHRASE_JUDGE_NAME } from "./phrase-card.mjs";
|
|
3
3
|
|
|
4
4
|
export const FRUSTRATION_JUDGE_RELAY_URL = "https://agent-behavior-wrapped-judge.haoxingdu.workers.dev/v1/frustration-quote";
|
|
5
5
|
const MAX_CANDIDATES = 40;
|
|
@@ -97,6 +97,7 @@ function buildOpenRouterQuoteRequest(candidates, { model, prompt, schemaName, pr
|
|
|
97
97
|
const payload = JSON.stringify(candidates);
|
|
98
98
|
return {
|
|
99
99
|
model,
|
|
100
|
+
provider: OPENROUTER_PROVIDER_PREFERENCES,
|
|
100
101
|
temperature: 0,
|
|
101
102
|
reasoning: { effort: "none", exclude: true },
|
|
102
103
|
max_tokens: 32,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { displayModelName } from "./model-names.mjs";
|
|
2
2
|
import { redactAggregateText } from "./privacy.mjs";
|
|
3
|
-
import { OPENROUTER_MODEL } from "./phrase-card.mjs";
|
|
3
|
+
import { OPENROUTER_MODEL, OPENROUTER_PROVIDER_PREFERENCES } from "./phrase-card.mjs";
|
|
4
4
|
import { judgeError, judgeRequestDetails, judgeResponseDetails } from "./judge-debug.mjs";
|
|
5
5
|
import { semanticActions, semanticMethods, semanticToolUse } from "./tool-semantics.mjs";
|
|
6
6
|
|
|
@@ -287,6 +287,7 @@ export function buildOpenRouterWorkaroundRequest(chunks, model = OPENROUTER_MODE
|
|
|
287
287
|
const blockerOrder = blockerIds.map((id, index) => `${index + 1}. ${id}`).join("\n");
|
|
288
288
|
return {
|
|
289
289
|
model,
|
|
290
|
+
provider: OPENROUTER_PROVIDER_PREFERENCES,
|
|
290
291
|
temperature: 0,
|
|
291
292
|
reasoning: { effort: reasoningEffort, exclude: true },
|
|
292
293
|
max_tokens: 8192,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { redactAggregateText } from "./privacy.mjs";
|
|
2
|
-
import { OPENROUTER_MODEL, PHRASE_JUDGE_NAME } from "./phrase-card.mjs";
|
|
2
|
+
import { OPENROUTER_MODEL, OPENROUTER_PROVIDER_PREFERENCES, PHRASE_JUDGE_NAME } from "./phrase-card.mjs";
|
|
3
3
|
import { judgeError, judgeRequestDetails, judgeResponseDetails } from "./judge-debug.mjs";
|
|
4
4
|
|
|
5
5
|
export const INTERACTION_TONE_RELAY_URL = "https://agent-behavior-wrapped-judge.haoxingdu.workers.dev/v1/interaction-tone";
|
|
@@ -106,6 +106,7 @@ export function buildOpenRouterInteractionToneRequest(candidates, model = OPENRO
|
|
|
106
106
|
const ids = candidates.map((candidate) => candidate.candidate_id);
|
|
107
107
|
return {
|
|
108
108
|
model,
|
|
109
|
+
provider: OPENROUTER_PROVIDER_PREFERENCES,
|
|
109
110
|
temperature: 0,
|
|
110
111
|
seed: 1729,
|
|
111
112
|
reasoning: { effort: "none", exclude: true },
|
package/server/launcher.mjs
CHANGED
|
@@ -144,7 +144,7 @@ const server = http.createServer(async (request, response) => {
|
|
|
144
144
|
});
|
|
145
145
|
|
|
146
146
|
server.listen(port, "127.0.0.1", () => {
|
|
147
|
-
const url = `http://
|
|
147
|
+
const url = `http://localhost:${port}`;
|
|
148
148
|
console.log(`Behavior Wrapped donation helper is ready at ${url}`);
|
|
149
149
|
console.log(demo ? "Using synthetic demo sessions." : `Donation review reads selected sessions locally from ${path.join(os.homedir(), ".claude")} and ${path.join(os.homedir(), ".codex")}.`);
|
|
150
150
|
if (!process.argv.includes("--no-open") && process.env.NODE_ENV !== "test") spawn("open", [url], { stdio: "ignore", detached: true }).unref();
|
package/server/phrase-card.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { redactAggregateText } from "./privacy.mjs";
|
|
2
2
|
import { judgeError, judgeRequestDetails, judgeResponseDetails } from "./judge-debug.mjs";
|
|
3
3
|
|
|
4
|
-
export const OPENROUTER_MODEL = "
|
|
5
|
-
export const PHRASE_JUDGE_NAME = "
|
|
4
|
+
export const OPENROUTER_MODEL = "openai/gpt-5.6-luna";
|
|
5
|
+
export const PHRASE_JUDGE_NAME = "GPT-5.6 Luna";
|
|
6
|
+
export const OPENROUTER_PROVIDER_PREFERENCES = Object.freeze({ data_collection: "deny", zdr: true });
|
|
6
7
|
export const PHRASE_JUDGE_RELAY_URL = "https://agent-behavior-wrapped-judge.haoxingdu.workers.dev/v1/phrase-card";
|
|
7
8
|
|
|
8
9
|
const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
|
|
@@ -153,10 +154,10 @@ export function buildOpenRouterJudgeRequest(candidates, model = OPENROUTER_MODEL
|
|
|
153
154
|
assertSafePayload(payload);
|
|
154
155
|
return {
|
|
155
156
|
model,
|
|
157
|
+
provider: OPENROUTER_PROVIDER_PREFERENCES,
|
|
156
158
|
temperature: 0,
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
// of unnecessary tokens before returning the candidate ID.
|
|
159
|
+
// These are bounded classification tasks, so additional reasoning would add
|
|
160
|
+
// latency and cost without changing the requested output contract.
|
|
160
161
|
reasoning: { effort: "none", exclude: true },
|
|
161
162
|
max_tokens: PHRASE_JUDGE_MAX_TOKENS,
|
|
162
163
|
messages: [
|
package/server/privacy.mjs
CHANGED
|
@@ -37,8 +37,15 @@ export function redactText(input, manualTerms = []) {
|
|
|
37
37
|
let text = String(input ?? "");
|
|
38
38
|
const detections = [];
|
|
39
39
|
for (const [pattern, replacement] of [...SECRET_PATTERNS, ...PII_PATTERNS]) {
|
|
40
|
-
text = text.replace(pattern, (match,
|
|
41
|
-
detections.push({
|
|
40
|
+
text = text.replace(pattern, (match, offset, source) => {
|
|
41
|
+
detections.push({
|
|
42
|
+
kind: replacement.slice(1, -1),
|
|
43
|
+
label: replacement.slice(1, -1).toLowerCase().replace(/\b\w/g, (letter) => letter.toUpperCase()),
|
|
44
|
+
value: match,
|
|
45
|
+
replacement,
|
|
46
|
+
length: match.length,
|
|
47
|
+
context: { before: source.slice(Math.max(0, offset - 80), offset), match, after: source.slice(offset + match.length, offset + match.length + 80) },
|
|
48
|
+
});
|
|
42
49
|
return replacement;
|
|
43
50
|
});
|
|
44
51
|
}
|
|
@@ -68,8 +68,8 @@ export function sanitizePublicReport(value) {
|
|
|
68
68
|
models: safeWorkaroundModels,
|
|
69
69
|
...(value.workaroundCard.count > 0 && safeWorkaroundExample ? { example: safeWorkaroundExample } : {}),
|
|
70
70
|
} : null;
|
|
71
|
-
const defaultDonationHelperUrl = `http://
|
|
72
|
-
const donationHelperUrl = new RegExp(`^http://
|
|
71
|
+
const defaultDonationHelperUrl = `http://localhost:4317/donate/${value.id}`;
|
|
72
|
+
const donationHelperUrl = new RegExp(`^http://localhost:[0-9]{2,5}/donate/${value.id}$`).test(value.donationHelperUrl || "") ? value.donationHelperUrl : defaultDonationHelperUrl;
|
|
73
73
|
return {
|
|
74
74
|
id: value.id,
|
|
75
75
|
createdAt: /^\d{4}-\d{2}-\d{2}T/.test(value.createdAt || "") ? value.createdAt : new Date().toISOString(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { redactAggregateText } from "./privacy.mjs";
|
|
2
|
-
import { OPENROUTER_MODEL, PHRASE_JUDGE_NAME } from "./phrase-card.mjs";
|
|
2
|
+
import { OPENROUTER_MODEL, OPENROUTER_PROVIDER_PREFERENCES, PHRASE_JUDGE_NAME } from "./phrase-card.mjs";
|
|
3
3
|
import { judgeError, judgeRequestDetails, judgeResponseDetails } from "./judge-debug.mjs";
|
|
4
4
|
|
|
5
5
|
export const SESSION_TOPIC_RELAY_URL = "https://agent-behavior-wrapped-judge.haoxingdu.workers.dev/v1/session-topics";
|
|
@@ -115,6 +115,7 @@ export function buildOpenRouterSessionTopicRequest(candidates, model = OPENROUTE
|
|
|
115
115
|
const ids = candidates.map((candidate) => candidate.candidate_id);
|
|
116
116
|
return {
|
|
117
117
|
model,
|
|
118
|
+
provider: OPENROUTER_PROVIDER_PREFERENCES,
|
|
118
119
|
temperature: 0,
|
|
119
120
|
reasoning: { effort: "none", exclude: true },
|
|
120
121
|
max_tokens: Math.min(8192, Math.max(512, candidates.length * 32)),
|