behavior-wrapped 0.5.20 → 0.6.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 +6 -1
- package/dist/assets/{index-Cy1fCyBM.js → index-DO52v2Jm.js} +2 -2
- package/dist/assets/{index-DV617-2k.css → index-a017khge.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +3 -2
- package/server/cli.mjs +8 -2
- package/server/consent.mjs +1 -1
- package/server/discovery.mjs +6 -7
- package/server/launcher.mjs +7 -5
- package/server/local-helper-runtime.mjs +25 -10
- package/server/platform.mjs +39 -0
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-DO52v2Jm.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-a017khge.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.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"wrapped"
|
|
24
24
|
],
|
|
25
25
|
"os": [
|
|
26
|
-
"darwin"
|
|
26
|
+
"darwin",
|
|
27
|
+
"linux"
|
|
27
28
|
],
|
|
28
29
|
"files": [
|
|
29
30
|
"dist/",
|
package/server/cli.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { judgeErrorDetails } from "./judge-debug.mjs";
|
|
|
16
16
|
import { createCliProgress } from "./progress.mjs";
|
|
17
17
|
import { helperHealthMatches, stopVerifiedStaleHelper } from "./local-helper-runtime.mjs";
|
|
18
18
|
import { APP_VERSION, LOCAL_DONATION_PROTOCOL } from "./runtime-version.mjs";
|
|
19
|
+
import { openExternalUrl } from "./platform.mjs";
|
|
19
20
|
|
|
20
21
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
21
22
|
const root = path.dirname(here);
|
|
@@ -56,6 +57,11 @@ function formatRange(sessions) {
|
|
|
56
57
|
return `${format.format(values[0])} – ${format.format(values.at(-1))}, ${year}`;
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
function formatAgentSource(sessions) {
|
|
61
|
+
const present = new Set(sessions.map((session) => session.agent));
|
|
62
|
+
return [["claude", "Claude Code"], ["cowork", "Cowork"], ["codex", "Codex"]].filter(([agent]) => present.has(agent)).map(([, name]) => name).join(" + ");
|
|
63
|
+
}
|
|
64
|
+
|
|
59
65
|
async function optionalAnalysis(promise, label, warnings) {
|
|
60
66
|
try { return await promise; }
|
|
61
67
|
catch (error) {
|
|
@@ -101,7 +107,7 @@ async function ensureServer(demo = false) {
|
|
|
101
107
|
|
|
102
108
|
function openUrl(url) {
|
|
103
109
|
if (process.argv.includes("--no-open")) return;
|
|
104
|
-
|
|
110
|
+
openExternalUrl(url);
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
function printList() {
|
|
@@ -252,7 +258,7 @@ async function createWrapped() {
|
|
|
252
258
|
const id = createReportId();
|
|
253
259
|
const safeFindings = analyzed.findings.map(({ evidence, method, ...finding }) => finding);
|
|
254
260
|
const hasPrivateWorkaroundEvidence = Boolean(analyzed.workaroundReview?.occurrences?.length || analyzed.workaroundReview?.borderline?.length);
|
|
255
|
-
const report = { id, createdAt: new Date().toISOString(), rangeLabel: formatRange(chosenSessions), source:
|
|
261
|
+
const report = { id, createdAt: new Date().toISOString(), rangeLabel: formatRange(chosenSessions), source: formatAgentSource(chosenSessions), stats: analyzed.stats, findings: safeFindings, phraseCard: analyzed.phraseCard, interactionCard: analyzed.interactionCard, workaroundCard: analyzed.workaroundCard, workaroundReview: analyzed.workaroundReview, sessionSummaries: analyzed.sessionSummaries || [], sessionIds: chosenSessions.map((session) => session.id), donationHelperUrl: `${baseUrl}/donate/${id}`, privacy: { shareSafe: !hasPrivateWorkaroundEvidence, containsTranscriptText: hasPrivateWorkaroundEvidence, externalTransmission: !localOnly, analysisMode: localOnly ? "local-only" : "remote", leaderboardParticipation: localOnly ? "excluded" : "included-by-default", ...(localOnly ? { transmittedData: `None; ${testMode ? "test" : "local-only"} mode stays on this device.`, 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" }) } };
|
|
256
262
|
let publicUrl = null;
|
|
257
263
|
if (!localOnly) {
|
|
258
264
|
progress.start("Publishing share-safe Wrapped", "strict aggregate-only schema");
|
package/server/consent.mjs
CHANGED
|
@@ -6,7 +6,7 @@ const purple = "\x1b[38;2;141;92;255m";
|
|
|
6
6
|
const reset = "\x1b[0m";
|
|
7
7
|
|
|
8
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
|
-
export const localOnlyAnalysisText = "Local-only analysis keeps all session data on this
|
|
9
|
+
export const localOnlyAnalysisText = "Local-only analysis keeps all session data on this device. It uses deterministic statistics and a locally counted favorite phrase, but omits AI-judged interaction tone, usage topics, and instrumental workarounds. Those omissions leave leaderboard plots 2 and 3 incomplete, so the report will not be published or included in the leaderboard.";
|
|
10
10
|
|
|
11
11
|
export async function requestAnalysisMode({ input = process.stdin, output = process.stdout } = {}) {
|
|
12
12
|
const prompt = createInterface({ input, output });
|
package/server/discovery.mjs
CHANGED
|
@@ -4,10 +4,9 @@ import os from "node:os";
|
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import readline from "node:readline";
|
|
6
6
|
import { semanticToolUse } from "./tool-semantics.mjs";
|
|
7
|
+
import { canonicalSessionRoots } from "./platform.mjs";
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
-
const canonicalCodexRoots = [path.join(os.homedir(), ".codex", "sessions"), path.join(os.homedir(), ".codex", "archived_sessions")];
|
|
10
|
-
const canonicalCoworkRoot = path.join(os.homedir(), "Library", "Application Support", "Claude", "local-agent-mode-sessions");
|
|
9
|
+
const { claudeRoot: canonicalClaudeRoot, codexRoots: canonicalCodexRoots, coworkRoot: canonicalCoworkRoot } = canonicalSessionRoots();
|
|
11
10
|
const DEFAULT_WINDOW_DAYS = 30;
|
|
12
11
|
const metadataCacheFile = path.join(process.env.BEHAVIOR_WRAPPED_STORE_ROOT || path.join(os.homedir(), ".agent-behavior-wrapped"), "session-index-v1.json");
|
|
13
12
|
|
|
@@ -247,7 +246,7 @@ async function readCoworkSessionMetadataAsync(file, cache) {
|
|
|
247
246
|
}
|
|
248
247
|
|
|
249
248
|
function recursiveJsonl(root) {
|
|
250
|
-
if (!fs.existsSync(root)) return [];
|
|
249
|
+
if (!root || !fs.existsSync(root)) return [];
|
|
251
250
|
const files = [];
|
|
252
251
|
const stack = [root];
|
|
253
252
|
while (stack.length) {
|
|
@@ -306,7 +305,7 @@ function coworkAuditFiles(root) {
|
|
|
306
305
|
}
|
|
307
306
|
|
|
308
307
|
export function discoverCoworkSessions(root = canonicalCoworkRoot) {
|
|
309
|
-
if (!fs.existsSync(root)) return finishCatalog([], false);
|
|
308
|
+
if (!root || !fs.existsSync(root)) return finishCatalog([], false);
|
|
310
309
|
const sessions = [];
|
|
311
310
|
for (const file of coworkAuditFiles(root)) {
|
|
312
311
|
try { sessions.push(readCoworkSessionMetadata(file)); } catch { /* Skip unreadable sessions. */ }
|
|
@@ -336,14 +335,14 @@ export async function discoverAllSessionsAsync(options = {}) {
|
|
|
336
335
|
}
|
|
337
336
|
}
|
|
338
337
|
}
|
|
339
|
-
if (fs.existsSync(coworkRoot)) for (const file of coworkAuditFiles(coworkRoot)) {
|
|
338
|
+
if (coworkRoot && fs.existsSync(coworkRoot)) for (const file of coworkAuditFiles(coworkRoot)) {
|
|
340
339
|
try { sessions.push(await readCoworkSessionMetadataAsync(file, cache)); } catch { /* Skip unreadable sessions. */ }
|
|
341
340
|
}
|
|
342
341
|
for (const root of codexRoots) for (const file of recursiveJsonl(root)) {
|
|
343
342
|
try { sessions.push(await readSessionMetadata(file, "codex", "Codex project", cache)); } catch { /* Skip unreadable sessions. */ }
|
|
344
343
|
}
|
|
345
344
|
if (persistCache) writeMetadataCache(cache);
|
|
346
|
-
return finishCatalog(sessions, fs.existsSync(claudeRoot) || fs.existsSync(coworkRoot) || codexRoots.some((root) => fs.existsSync(root)));
|
|
345
|
+
return finishCatalog(sessions, fs.existsSync(claudeRoot) || Boolean(coworkRoot && fs.existsSync(coworkRoot)) || codexRoots.some((root) => fs.existsSync(root)));
|
|
347
346
|
}
|
|
348
347
|
|
|
349
348
|
function isoDay(value) {
|
package/server/launcher.mjs
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import os from "node:os";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
8
6
|
import { discoverAllSessionsAsync, readRecordsAsync, defaultDateRange, DEFAULT_WINDOW_DAYS } from "./discovery.mjs";
|
|
9
7
|
import { makeDonationPreview } from "./analysis.mjs";
|
|
10
8
|
import { deleteDonationReceipt, getOrCreateClientId, loadDonationReceipt, loadReport, saveDonationReceipt } from "./store.mjs";
|
|
@@ -12,6 +10,7 @@ import { deleteResearchDonation, RESEARCH_DONATION_URL, submitResearchDonation }
|
|
|
12
10
|
import { APP_VERSION, LOCAL_DONATION_PROTOCOL } from "./runtime-version.mjs";
|
|
13
11
|
import { makeWorkaroundEvidencePreview } from "./workaround-evidence.mjs";
|
|
14
12
|
import { createIdleShutdownController } from "./local-helper-runtime.mjs";
|
|
13
|
+
import { canonicalSessionDirectoryLabels, openExternalUrl, supportedAgentNames } from "./platform.mjs";
|
|
15
14
|
|
|
16
15
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
17
16
|
const root = path.dirname(here);
|
|
@@ -80,13 +79,15 @@ async function chosenRecords(ids, options = {}) {
|
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
function publicCatalog() {
|
|
82
|
+
const agentNames = supportedAgentNames(process.platform, { includeCowork: demo || process.platform === "darwin" });
|
|
83
83
|
return {
|
|
84
84
|
rootAvailable: catalog.rootAvailable,
|
|
85
85
|
demo,
|
|
86
86
|
projects: catalog.projects,
|
|
87
87
|
sessions: catalog.sessions.map((session, index) => ({ ...session, label: session.label || `Session ${index + 1}` })),
|
|
88
88
|
defaultRange: defaultDateRange(catalog.sessions, { days: DEFAULT_WINDOW_DAYS, anchorLatest: demo }),
|
|
89
|
-
|
|
89
|
+
agentNames,
|
|
90
|
+
privacy: { canonicalDirectories: canonicalSessionDirectoryLabels(), networkRequests: "only-after-final-donation-consent" },
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
|
|
@@ -182,6 +183,7 @@ const idleShutdown = createIdleShutdownController({
|
|
|
182
183
|
server.listen(port, "127.0.0.1", () => {
|
|
183
184
|
const url = `http://localhost:${port}`;
|
|
184
185
|
console.log(`Behavior Wrapped donation helper is ready at ${url}`);
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
const sessionDirectories = canonicalSessionDirectoryLabels().join(", ");
|
|
187
|
+
console.log(demo ? "Using synthetic demo sessions." : `Donation review reads selected sessions locally from ${sessionDirectories}.`);
|
|
188
|
+
if (!process.argv.includes("--no-open") && process.env.NODE_ENV !== "test") openExternalUrl(url);
|
|
187
189
|
});
|
|
@@ -47,21 +47,36 @@ export function isVerifiedLauncherCommand(command, port) {
|
|
|
47
47
|
&& new RegExp(`(?:^|\\s)--port=${escapedPort}(?:\\s|$)`).test(command || "");
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return String(output).split(/\s+/).filter((value) => /^\d+$/.test(value)).map(Number);
|
|
54
|
-
} catch { return []; }
|
|
50
|
+
function processTools(platform) {
|
|
51
|
+
if (platform === "darwin") return { lsof: ["/usr/sbin/lsof", "lsof"], ps: ["/bin/ps", "ps"] };
|
|
52
|
+
return { lsof: ["lsof", "/usr/bin/lsof"], ps: ["ps", "/bin/ps"] };
|
|
55
53
|
}
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
const
|
|
55
|
+
async function listeningPids(port, runCommand, platform) {
|
|
56
|
+
for (const file of processTools(platform).lsof) {
|
|
57
|
+
try {
|
|
58
|
+
const output = await runCommand(file, ["-nP", "-t", `-iTCP:${port}`, "-sTCP:LISTEN"]);
|
|
59
|
+
return String(output).split(/\s+/).filter((value) => /^\d+$/.test(value)).map(Number);
|
|
60
|
+
} catch { /* Try the next standard location. */ }
|
|
61
|
+
}
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function processCommand(pid, runCommand, platform) {
|
|
66
|
+
for (const file of processTools(platform).ps) {
|
|
67
|
+
try { return await runCommand(file, ["-p", String(pid), "-o", "command="]); }
|
|
68
|
+
catch { /* Try the next standard location. */ }
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function stopVerifiedStaleHelper(port, advertisedPid, { runCommand = run, kill = process.kill, platform = process.platform } = {}) {
|
|
74
|
+
const listeners = await listeningPids(port, runCommand, platform);
|
|
59
75
|
const candidates = Number.isInteger(advertisedPid) && advertisedPid > 1 ? listeners.filter((pid) => pid === advertisedPid) : listeners;
|
|
60
76
|
let stopped = false;
|
|
61
77
|
for (const pid of candidates) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
catch { continue; }
|
|
78
|
+
const command = await processCommand(pid, runCommand, platform);
|
|
79
|
+
if (command === null) continue;
|
|
65
80
|
if (!isVerifiedLauncherCommand(String(command).trim(), port)) continue;
|
|
66
81
|
try { kill(pid, "SIGTERM"); stopped = true; }
|
|
67
82
|
catch (error) { if (error?.code !== "ESRCH") throw error; }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export function canonicalSessionRoots({ home = os.homedir(), platform = process.platform } = {}) {
|
|
6
|
+
return {
|
|
7
|
+
claudeRoot: path.join(home, ".claude", "projects"),
|
|
8
|
+
codexRoots: [path.join(home, ".codex", "sessions"), path.join(home, ".codex", "archived_sessions")],
|
|
9
|
+
coworkRoot: platform === "darwin" ? path.join(home, "Library", "Application Support", "Claude", "local-agent-mode-sessions") : null,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function canonicalSessionDirectoryLabels(platform = process.platform) {
|
|
14
|
+
return [
|
|
15
|
+
"~/.claude/projects",
|
|
16
|
+
...(platform === "darwin" ? ["~/Library/Application Support/Claude/local-agent-mode-sessions"] : []),
|
|
17
|
+
"~/.codex/sessions",
|
|
18
|
+
"~/.codex/archived_sessions",
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function supportedAgentNames(platform = process.platform, { includeCowork = platform === "darwin" } = {}) {
|
|
23
|
+
return ["Claude Code", ...(includeCowork ? ["Cowork"] : []), "Codex"];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function browserOpenCommand(url, platform = process.platform) {
|
|
27
|
+
if (platform === "darwin") return { file: "open", args: [url] };
|
|
28
|
+
if (platform === "linux") return { file: "xdg-open", args: [url] };
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function openExternalUrl(url, { platform = process.platform, spawnImpl = spawn } = {}) {
|
|
33
|
+
const command = browserOpenCommand(url, platform);
|
|
34
|
+
if (!command) return false;
|
|
35
|
+
const child = spawnImpl(command.file, command.args, { detached: true, stdio: "ignore" });
|
|
36
|
+
child.on?.("error", () => {});
|
|
37
|
+
child.unref?.();
|
|
38
|
+
return true;
|
|
39
|
+
}
|