behavior-wrapped 0.11.0 → 0.12.0
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 +1 -1
- package/dist/assets/index-89_b8V4F.js +10 -0
- package/dist/assets/{index-CkqI9mbd.css → index-Dhat2zC7.css} +1 -1
- package/dist/index.html +2 -2
- package/docs/development.md +4 -2
- package/docs/privacy.md +4 -2
- package/docs/research-donations.md +4 -6
- package/package.json +3 -2
- package/server/cli.mjs +3 -3
- package/server/launcher.mjs +33 -72
- package/dist/assets/index-By4wq-jG.js +0 -11
- package/server/research-donation.mjs +0 -41
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-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-89_b8V4F.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Dhat2zC7.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/docs/development.md
CHANGED
|
@@ -7,11 +7,11 @@ npm install
|
|
|
7
7
|
npm run wrapped -- --demo
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
The normal CLI scans local session sources, creates a report, starts the
|
|
10
|
+
The normal CLI scans local session sources, creates a report, starts the localhost evidence helper, and opens the report. Add `--no-open` to leave the browser closed or `--days=N` to change the rolling window.
|
|
11
11
|
|
|
12
12
|
For fast formatting work, use `npm run wrapped -- --test`. Test mode skips consent and all LLM calls, uses deterministic local fallbacks, keeps the report on localhost, and does not publish it. `--no-llm` is an alias for `--test`.
|
|
13
13
|
|
|
14
|
-
Use `npm run dev` for hosted-page UI development. Run `npm start -- --no-open` to develop the loopback
|
|
14
|
+
Use `npm run dev` for hosted-page UI development. Run `npm start -- --no-open` to develop the loopback evidence and sharing helper directly. Before publishing, run:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
npm run check
|
|
@@ -32,3 +32,5 @@ npm run analyze:phrases -- ~/.claude/projects analysis-output/local-phrase-famil
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Use `--limit=100` to retain more than the default 50 families. The miner uses rare token-shingle postings, bounded token edit distance, and union-find clustering. Its versioned output contains aggregate phrases and benchmark data, never raw transcripts or tool output. The gitignored `analysis-output` directory is created with private file permissions.
|
|
35
|
+
|
|
36
|
+
Donation collection lives in the pinned `share-with-susan-calvin` dependency. Its public integration entry point starts the independent local review app. Tests use synthetic fixture roots and never transmit demo donations.
|
package/docs/privacy.md
CHANGED
|
@@ -28,9 +28,9 @@ Local-only reports omit AI-judged interaction tone, topics, and workarounds. The
|
|
|
28
28
|
|
|
29
29
|
Confirmed workaround cards can link to a localhost-only evidence page. Interaction-tone cards can likewise show the exact locally reconstructed yelling, thanking, and apology excerpts. These excerpts do not enter the public report, share-card export, or remote payload merely by opening the page.
|
|
30
30
|
|
|
31
|
-
Yelling and thanking occurrences include an optional **Is this inaccurate?** path. It resolves the occurrence to its original session on localhost and opens
|
|
31
|
+
Yelling and thanking occurrences include an optional **Is this inaccurate?** path. It resolves the occurrence to its original session on localhost and opens Share with Susan Calvin restricted to that one session. There, the user chooses a corrected label, customizes redactions, reviews the correction, then provides purpose-specific consent. Transcript messages are read-only except for supported redactions. Every message remains in its original sequence. Clicking the feedback link alone transmits nothing.
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Susan handles local encryption, private ciphertext storage, deletion receipts, and deletion controls for both classifier feedback and ordinary donations. Its encrypted contents include the reviewed session, original verdict, correction, judged excerpt, judge version, and optional note. Public reports, leaderboard payloads, share-card exports, and operational notifications never include this material.
|
|
34
34
|
|
|
35
35
|
## Boundaries
|
|
36
36
|
|
|
@@ -38,3 +38,5 @@ Accepted classifier feedback receives the same local encryption, private ciphert
|
|
|
38
38
|
- Hosted reports currently rely on unguessable URLs and installation-scoped management tokens rather than user accounts.
|
|
39
39
|
- Heuristic coverage can be incomplete; uncertain findings are labeled with confidence.
|
|
40
40
|
- The public data-use and storage policy is available at [susancalvin.org/data-policy](https://susancalvin.org/data-policy).
|
|
41
|
+
|
|
42
|
+
Ordinary donation invitations open Susan's own recent-session picker without transferring report data or selection. It obtains fresh consent, includes available message timestamps, and transmits only its reviewed snapshot. Existing Wrapped receipts can be managed with Susan's list/delete commands. Historical donations retain their original storage and consent, with deletion forwarded through the original URL.
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
# Research donations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Behavior Wrapped launches **Share with Susan Calvin** for optional donation. Ordinary sharing opens the standalone app's normal recent-session picker; the report and its selection are not transferred. Susan owns discovery, review, redaction, consent, encryption, uploads and deletion receipts. Nothing is donated until consent is given inside Susan.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
From a private yelling or thanking occurrence, **Correct with Susan Calvin** opens the original session only. Wrapped resolves the classification locally and passes the source reference and original judge context through process IPC. Susan collects and reviews the correction and optional explanation, applies the selected redactions, and requires consent for research and evaluating and improving Wrapped. No report ID, source path or local session ID enters the donation. A missing source produces an error rather than substituting a different session.
|
|
6
6
|
|
|
7
|
-
The
|
|
7
|
+
The local `/donate/:reportId` route is a compatibility bridge. It starts an independent Susan process on an available loopback port; Wrapped's evidence helper can expire without stopping review. Old report links and private evidence links retain their existing structure. A stopped helper can be restarted with `npx behavior-wrapped@latest open <report-id>`. Ordinary donations can also start directly with `npx share-with-susan-calvin@latest`.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
The receiving Worker accepts only encrypted protocol-2 envelopes. A private R2 bucket stores ciphertext, separating general donations and classifier-feedback donations by object prefix while applying the same access controls. A separate D1 database stores pseudonymous consent, size, count, encryption-key, and object-location metadata—never transcript text or classifier corrections. No automatic retention policy is currently configured. A locally retained deletion receipt lets the donor delete both records.
|
|
9
|
+
New donations use Susan's streamed encrypted receiver and grouped deletion receipts. Old uploads to `/v1/research-donations` return an upgrade instruction. Old deletion URLs continue forwarding to Susan, which owns the legacy storage bindings. Existing ciphertext and metadata remain in their original resources. `npx share-with-susan-calvin@latest list` finds receipts from both apps; its `delete` command dispatches by receipt origin and preserves credentials on failure.
|
|
12
10
|
|
|
13
11
|
## Maintainer operations
|
|
14
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "behavior-wrapped",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
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",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"vite": "^8.2.0"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
|
-
"@fontsource-variable/inter": "^5.3.0"
|
|
73
|
+
"@fontsource-variable/inter": "^5.3.0",
|
|
74
|
+
"share-with-susan-calvin": "0.6.0"
|
|
74
75
|
}
|
|
75
76
|
}
|
package/server/cli.mjs
CHANGED
|
@@ -118,7 +118,7 @@ async function ensureServer(demo = false) {
|
|
|
118
118
|
catch (error) { if (error?.code !== "ESRCH") throw error; }
|
|
119
119
|
}
|
|
120
120
|
const reason = childFailure ? `${childFailure.message}.` : `It did not become ready within ${HELPER_START_TIMEOUT_MS / 1_000} seconds and was stopped.`;
|
|
121
|
-
throw new Error(`Could not start the local
|
|
121
|
+
throw new Error(`Could not start the local helper on port ${port}. ${reason} Another application may already be using that port.`);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
function openUrl(url) {
|
|
@@ -148,9 +148,9 @@ async function createWrapped() {
|
|
|
148
148
|
console.log(`\n ${bright}behavior-wrapped${reset} ${muted}· the wrapped for your AI agents${reset}\n`);
|
|
149
149
|
const demo = process.argv.includes("--demo");
|
|
150
150
|
const testMode = process.argv.includes("--test") || process.argv.includes("--no-llm");
|
|
151
|
-
progress.start("Preparing local
|
|
151
|
+
progress.start("Preparing local evidence helper", `localhost:${port}`);
|
|
152
152
|
await ensureServer(demo);
|
|
153
|
-
progress.succeed("Local
|
|
153
|
+
progress.succeed("Local evidence helper ready");
|
|
154
154
|
const helperHeartbeat = setInterval(() => { void fetch(`${loopbackUrl}/api/health`).catch(() => {}); }, 30_000);
|
|
155
155
|
helperHeartbeat.unref();
|
|
156
156
|
const daysArgument = process.argv.find((argument) => argument.startsWith("--days="));
|
package/server/launcher.mjs
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { launchReview } from "share-with-susan-calvin/integration";
|
|
2
3
|
import http from "node:http";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { discoverAllSessionsAsync, readRecordsAsync, defaultDateRange, DEFAULT_WINDOW_DAYS } from "./discovery.mjs";
|
|
7
|
-
import {
|
|
8
|
-
import { deleteDonationReceipt, getOrCreateClientId, loadDonationReceipt, loadReport, saveDonationReceipt } from "./store.mjs";
|
|
9
|
-
import { deleteResearchDonation, RESEARCH_DONATION_URL, submitResearchDonation } from "./research-donation.mjs";
|
|
10
|
-
import { MAX_DONATION_BYTES } from "./research-donation-schema.mjs";
|
|
8
|
+
import { loadReport } from "./store.mjs";
|
|
11
9
|
import { APP_VERSION, LOCAL_DONATION_PROTOCOL } from "./runtime-version.mjs";
|
|
12
10
|
import { makeWorkaroundEvidencePreview } from "./workaround-evidence.mjs";
|
|
13
11
|
import { makeInteractionEvidencePreview } from "./interaction-evidence.mjs";
|
|
14
|
-
import { publicInteractionFeedback, resolveInteractionFeedback
|
|
15
|
-
import { donationSessionIntegrityError } from "./donation-session-integrity.mjs";
|
|
12
|
+
import { publicInteractionFeedback, resolveInteractionFeedback } from "./interaction-feedback.mjs";
|
|
16
13
|
import { createIdleShutdownController } from "./local-helper-runtime.mjs";
|
|
17
14
|
import { canonicalSessionDirectoryLabels, openExternalUrl, supportedAgentNames } from "./platform.mjs";
|
|
18
15
|
|
|
@@ -31,6 +28,7 @@ const configuredTestCatalogDelayMs = process.env.NODE_ENV === "test" ? Number(pr
|
|
|
31
28
|
const testCatalogDelayMs = Number.isFinite(configuredTestCatalogDelayMs) && configuredTestCatalogDelayMs > 0 ? configuredTestCatalogDelayMs : 0;
|
|
32
29
|
let catalog = null;
|
|
33
30
|
let catalogPromise = null;
|
|
31
|
+
const susanLaunches = new Map();
|
|
34
32
|
|
|
35
33
|
async function loadCatalog() {
|
|
36
34
|
if (testCatalogDelayMs) await new Promise((resolve) => setTimeout(resolve, testCatalogDelayMs));
|
|
@@ -177,71 +175,34 @@ const server = http.createServer(async (request, response) => {
|
|
|
177
175
|
const trusted = resolveInteractionFeedback(report, feedbackId, new Map(records.map((session) => [session.sessionId, session.records])));
|
|
178
176
|
return json(response, 200, { sessionIds: [reference.sessionId], feedback: publicInteractionFeedback(trusted), localPrivateSelection: true });
|
|
179
177
|
}
|
|
180
|
-
if (request.method === "POST" && url.pathname === "/api/
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const allowed = new Set(report.sessionIds || []);
|
|
207
|
-
const uniqueIds = new Set(suppliedIds);
|
|
208
|
-
if (!suppliedIds.length || uniqueIds.size !== suppliedIds.length || suppliedIds.some((id) => !allowed.has(id))) return json(response, 400, { error: "Donated sessions must come from this report." });
|
|
209
|
-
const feedbackReference = body.feedback ? resolveInteractionFeedback(report, body.feedback.feedbackId) : null;
|
|
210
|
-
if (body.feedback && (!feedbackReference || suppliedSessions.length !== 1 || suppliedIds[0] !== feedbackReference.sessionId)) return json(response, 400, { error: "Classifier feedback must contain only its original session." });
|
|
211
|
-
const availableCatalog = await catalogForRequest();
|
|
212
|
-
if (suppliedIds.some((id) => !availableCatalog.index.has(id))) return json(response, 404, { error: "A selected source session is no longer available on this device." });
|
|
213
|
-
const records = await chosenRecords(suppliedIds, {}, availableCatalog);
|
|
214
|
-
const sourceSessions = makeDonationPreview(records, new Map(), { unredacted: true }).sessions;
|
|
215
|
-
const integrityError = donationSessionIntegrityError(suppliedSessions, sourceSessions);
|
|
216
|
-
if (integrityError) return json(response, 400, { error: integrityError });
|
|
217
|
-
if (body.feedback) {
|
|
218
|
-
const trusted = resolveInteractionFeedback(report, body.feedback.feedbackId, new Map(records.map((session) => [session.sessionId, session.records])));
|
|
219
|
-
const classifierFeedback = sanitizeInteractionFeedbackSubmission(body.feedback, trusted);
|
|
220
|
-
if (!classifierFeedback) return json(response, 400, { error: "Choose a valid corrected classification before donating." });
|
|
221
|
-
donation = {
|
|
222
|
-
...donation,
|
|
223
|
-
purpose: "classifier_feedback",
|
|
224
|
-
classifierFeedback,
|
|
225
|
-
consent: { ...donation.consent, classifierFeedback: true },
|
|
226
|
-
};
|
|
227
|
-
} else donation = { ...donation, purpose: "general_research", classifierFeedback: undefined };
|
|
228
|
-
if (demo) return json(response, 201, { accepted: true, donation_id: "demo-not-transmitted", demo: true });
|
|
229
|
-
const result = await submitResearchDonation(donation, {
|
|
230
|
-
clientId: getOrCreateClientId(),
|
|
231
|
-
endpoint: process.env.BEHAVIOR_WRAPPED_DONATION_URL || RESEARCH_DONATION_URL,
|
|
232
|
-
});
|
|
233
|
-
saveDonationReceipt(result);
|
|
234
|
-
return json(response, 201, { accepted: true, donation_id: result.donation_id, encrypted: true });
|
|
235
|
-
}
|
|
236
|
-
const donationMatch = url.pathname.match(/^\/api\/research-donations\/([0-9a-f-]{36})$/);
|
|
237
|
-
if (request.method === "DELETE" && donationMatch) {
|
|
238
|
-
const receipt = loadDonationReceipt(donationMatch[1]);
|
|
239
|
-
if (!receipt) return json(response, 404, { error: "Local deletion receipt not found." });
|
|
240
|
-
if (demo) { deleteDonationReceipt(donationMatch[1]); return json(response, 200, { deleted: true, demo: true }); }
|
|
241
|
-
const result = await deleteResearchDonation(receipt.donationId, receipt.deletionToken, { endpoint: process.env.BEHAVIOR_WRAPPED_DONATION_URL || RESEARCH_DONATION_URL });
|
|
242
|
-
deleteDonationReceipt(donationMatch[1]);
|
|
243
|
-
return json(response, 200, result);
|
|
178
|
+
if (request.method === "POST" && url.pathname === "/api/share-with-susan") {
|
|
179
|
+
if (!new Set([`http://127.0.0.1:${port}`, `http://localhost:${port}`]).has(request.headers.origin || "")) return json(response, 403, { error: "Open sharing from the local app." });
|
|
180
|
+
const body = await readBody(request, 2_000);
|
|
181
|
+
let feedback = null;
|
|
182
|
+
if (body.feedbackId) {
|
|
183
|
+
const report = loadReport(body.reportId);
|
|
184
|
+
const reference = resolveInteractionFeedback(report, body.feedbackId);
|
|
185
|
+
if (!reference) return json(response, 404, { error: "That classification is no longer available. Reopen the local evidence page." });
|
|
186
|
+
const available = await catalogForRequest();
|
|
187
|
+
if (!available.index.has(reference.sessionId)) return json(response, 404, { error: "The original session is no longer available on this device." });
|
|
188
|
+
const records = await chosenRecords([reference.sessionId], {}, available);
|
|
189
|
+
feedback = resolveInteractionFeedback(report, body.feedbackId, new Map(records.map(session => [session.sessionId, session.records])));
|
|
190
|
+
}
|
|
191
|
+
const key = feedback ? `${body.reportId}:${body.feedbackId}` : "general";
|
|
192
|
+
let pending = susanLaunches.get(key);
|
|
193
|
+
if (pending) {
|
|
194
|
+
const previous = await pending;
|
|
195
|
+
const alive = await fetch(`${previous.url}/api/health`, { signal: AbortSignal.timeout(500) }).then(async result => result.ok && (await result.json()).app === "share-with-susan-calvin").catch(() => false);
|
|
196
|
+
if (!alive) { susanLaunches.delete(key); pending = null; }
|
|
197
|
+
}
|
|
198
|
+
if (!pending) {
|
|
199
|
+
pending = launchReview({ demo, feedback, ...(demo ? { demoRoots: { claudeRoot: fixtureRoot, coworkRoot: coworkFixtureRoot, codexRoots: [codexFixtureRoot] } } : {}) });
|
|
200
|
+
susanLaunches.set(key, pending);
|
|
201
|
+
pending.catch(() => { if (susanLaunches.get(key) === pending) susanLaunches.delete(key); });
|
|
202
|
+
}
|
|
203
|
+
return json(response, 200, await pending);
|
|
244
204
|
}
|
|
205
|
+
if (request.method === "POST" && ["/api/donation-preview", "/api/research-donations"].includes(url.pathname)) return json(response, 410, { error: "Donation review has moved. Run npx behavior-wrapped@latest or npx share-with-susan-calvin@latest." });
|
|
245
206
|
if (request.method !== "GET" && request.method !== "HEAD") return json(response, 405, { error: "Method not allowed" });
|
|
246
207
|
const requested = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
|
|
247
208
|
let file = path.resolve(dist, requested);
|
|
@@ -262,8 +223,8 @@ const idleShutdown = createIdleShutdownController({
|
|
|
262
223
|
|
|
263
224
|
server.listen(port, "127.0.0.1", () => {
|
|
264
225
|
const url = `http://localhost:${port}`;
|
|
265
|
-
console.log(`Behavior Wrapped
|
|
226
|
+
console.log(`Behavior Wrapped local helper is ready at ${url}`);
|
|
266
227
|
const sessionDirectories = canonicalSessionDirectoryLabels().join(", ");
|
|
267
|
-
console.log(demo ? "Using synthetic demo sessions." : `
|
|
228
|
+
console.log(demo ? "Using synthetic demo sessions." : `Private evidence reads selected sessions locally from ${sessionDirectories}.`);
|
|
268
229
|
if (!process.argv.includes("--no-open") && process.env.NODE_ENV !== "test") openExternalUrl(url);
|
|
269
230
|
});
|