behavior-wrapped 0.5.16 → 0.5.18

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-DIrSEDB2.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-ChdYH7Sm.css">
9
+ <script type="module" crossorigin src="/assets/index-Cz-I5Bz_.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-CQfAT656.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.16",
3
+ "version": "0.5.18",
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",
package/server/cli.mjs CHANGED
@@ -129,6 +129,8 @@ async function createWrapped() {
129
129
  progress.start("Preparing local donation helper", `localhost:${port}`);
130
130
  await ensureServer(demo);
131
131
  progress.succeed("Local donation helper ready");
132
+ const helperHeartbeat = setInterval(() => { void fetch(`${loopbackUrl}/api/health`).catch(() => {}); }, 30_000);
133
+ helperHeartbeat.unref();
132
134
  const daysArgument = process.argv.find((argument) => argument.startsWith("--days="));
133
135
  const windowDays = daysArgument ? Number(daysArgument.split("=")[1]) : DEFAULT_WINDOW_DAYS;
134
136
  if (!Number.isInteger(windowDays) || windowDays < 1 || windowDays > 3650) throw new Error("--days must be a whole number from 1 to 3650.");
@@ -11,6 +11,7 @@ import { deleteDonationReceipt, getOrCreateClientId, loadDonationReceipt, loadRe
11
11
  import { deleteResearchDonation, RESEARCH_DONATION_URL, submitResearchDonation } from "./research-donation.mjs";
12
12
  import { APP_VERSION, LOCAL_DONATION_PROTOCOL } from "./runtime-version.mjs";
13
13
  import { makeWorkaroundEvidencePreview } from "./workaround-evidence.mjs";
14
+ import { createIdleShutdownController } from "./local-helper-runtime.mjs";
14
15
 
15
16
  const here = path.dirname(fileURLToPath(import.meta.url));
16
17
  const root = path.dirname(here);
@@ -21,6 +22,8 @@ const coworkFixtureRoot = path.join(root, "fixtures", "cowork-sessions");
21
22
  const demo = process.argv.includes("--demo");
22
23
  const portArg = process.argv.find((arg) => arg.startsWith("--port="));
23
24
  const port = Number(portArg?.split("=")[1] || 4317);
25
+ const configuredIdleMs = Number(process.env.BEHAVIOR_WRAPPED_HELPER_IDLE_MS);
26
+ const helperIdleMs = Number.isFinite(configuredIdleMs) && configuredIdleMs >= 100 ? configuredIdleMs : 5 * 60 * 1_000;
24
27
  let catalog = await loadCatalog();
25
28
 
26
29
  async function loadCatalog() {
@@ -91,6 +94,7 @@ const mime = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; cha
91
94
  const server = http.createServer(async (request, response) => {
92
95
  try {
93
96
  if (!new Set([`127.0.0.1:${port}`, `localhost:${port}`]).has(request.headers.host || "")) return json(response, 403, { error: "Local access only" });
97
+ idleShutdown.touch();
94
98
  const url = new URL(request.url || "/", `http://${request.headers.host}`);
95
99
  if (request.method === "GET" && url.pathname === "/api/health") return json(response, 200, { app: "behavior-wrapped", version: APP_VERSION, local: true, purpose: "research-donation", donationProtocol: LOCAL_DONATION_PROTOCOL, pid: process.pid, demo });
96
100
  if (request.method === "GET" && url.pathname === "/api/discover") {
@@ -169,6 +173,12 @@ const server = http.createServer(async (request, response) => {
169
173
  }
170
174
  });
171
175
 
176
+ const idleShutdown = createIdleShutdownController({
177
+ enabled: process.env.BEHAVIOR_WRAPPED_DAEMON === "1",
178
+ idleMs: helperIdleMs,
179
+ onIdle: () => server.close(() => process.exit(0)),
180
+ });
181
+
172
182
  server.listen(port, "127.0.0.1", () => {
173
183
  const url = `http://localhost:${port}`;
174
184
  console.log(`Behavior Wrapped donation helper is ready at ${url}`);
@@ -9,6 +9,38 @@ export function helperHealthMatches(value, { version, protocol, demo }) {
9
9
  && value.version === version && value.donationProtocol === protocol && Boolean(value.demo) === Boolean(demo));
10
10
  }
11
11
 
12
+ export function createIdleShutdownController({
13
+ enabled,
14
+ idleMs,
15
+ onIdle,
16
+ setTimeoutImpl = setTimeout,
17
+ clearTimeoutImpl = clearTimeout,
18
+ }) {
19
+ let timer = null;
20
+ let stopped = false;
21
+
22
+ function schedule() {
23
+ if (!enabled || stopped) return;
24
+ if (timer) clearTimeoutImpl(timer);
25
+ timer = setTimeoutImpl(() => {
26
+ timer = null;
27
+ stopped = true;
28
+ onIdle();
29
+ }, idleMs);
30
+ timer?.unref?.();
31
+ }
32
+
33
+ function touch() { schedule(); }
34
+ function stop() {
35
+ stopped = true;
36
+ if (timer) clearTimeoutImpl(timer);
37
+ timer = null;
38
+ }
39
+
40
+ schedule();
41
+ return { touch, stop };
42
+ }
43
+
12
44
  export function isVerifiedLauncherCommand(command, port) {
13
45
  const escapedPort = String(port).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14
46
  return new RegExp(`(?:/(?:agent-)?behavior-wrapped/|(?:^|\\s))server/launcher\\.mjs(?:\\s|$)`, "i").test(command || "")