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/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-Cy1fCyBM.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-DV617-2k.css">
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.5.20",
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
- spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
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: "Claude Code + Cowork + Codex", 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 Mac.`, 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" }) } };
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");
@@ -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 Mac. 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.";
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 });
@@ -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 canonicalClaudeRoot = path.join(os.homedir(), ".claude", "projects");
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) {
@@ -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
- privacy: { canonicalDirectories: ["~/.claude/projects", "~/Library/Application Support/Claude/local-agent-mode-sessions", "~/.codex/sessions", "~/.codex/archived_sessions"], networkRequests: "only-after-final-donation-consent" },
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
- console.log(demo ? "Using synthetic demo sessions." : `Donation review reads selected sessions locally from ${path.join(os.homedir(), ".claude")}, ${path.join(os.homedir(), "Library", "Application Support", "Claude")}, and ${path.join(os.homedir(), ".codex")}.`);
186
- if (!process.argv.includes("--no-open") && process.env.NODE_ENV !== "test") spawn("open", [url], { stdio: "ignore", detached: true }).unref();
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
- async function listeningPids(port, runCommand) {
51
- try {
52
- const output = await runCommand("/usr/sbin/lsof", ["-nP", "-t", `-iTCP:${port}`, "-sTCP:LISTEN"]);
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
- export async function stopVerifiedStaleHelper(port, advertisedPid, { runCommand = run, kill = process.kill } = {}) {
58
- const listeners = await listeningPids(port, runCommand);
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
- let command;
63
- try { command = await runCommand("/bin/ps", ["-p", String(pid), "-o", "command="]); }
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
+ }