@osfactory/har 0.39.0 → 0.40.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.
Files changed (3) hide show
  1. package/README.md +14 -1
  2. package/dist/index.js +73 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="logo.png" alt="HAR logo" width="120">
2
+ <img src="assets/logo.png" alt="HAR logo" width="120">
3
3
  </p>
4
4
 
5
5
  # HAR — Harnesses for coding agents
@@ -68,6 +68,19 @@ Everything beyond install and first commands lives at **[https://harproject.dev/
68
68
 
69
69
  See [CONTRIBUTING.md](./CONTRIBUTING.md) for local setup, the dogfood harness loop, and architecture. Coding agents working on this repo should start with [AGENT.md](./AGENT.md). Maintainer release process: [RELEASING.md](./RELEASING.md).
70
70
 
71
+ ## Sponsors
72
+
73
+ HAR is sponsored by [Kerno](https://kerno.io) — runtime code and security review for coding agents.
74
+
75
+ <p align="center">
76
+ <a href="https://kerno.io">
77
+ <picture>
78
+ <source media="(prefers-color-scheme: dark)" srcset="assets/kerno-logo.svg">
79
+ <img src="assets/kerno-logo-dark.svg" alt="Kerno" height="36">
80
+ </picture>
81
+ </a>
82
+ </p>
83
+
71
84
  ## License
72
85
 
73
86
  Licensed under the [Apache License 2.0](./LICENSE).
package/dist/index.js CHANGED
@@ -28533,7 +28533,9 @@ function readPortalCredentials() {
28533
28533
  token: parsed.token,
28534
28534
  workspace: parsed.workspace,
28535
28535
  email: parsed.email,
28536
- createdAt: parsed.createdAt ?? ""
28536
+ createdAt: parsed.createdAt ?? "",
28537
+ refreshToken: parsed.refreshToken,
28538
+ expiresAt: parsed.expiresAt
28537
28539
  };
28538
28540
  } catch {
28539
28541
  return null;
@@ -28576,7 +28578,11 @@ function getPortalTarget() {
28576
28578
  }
28577
28579
  const stored = readPortalCredentials();
28578
28580
  if (stored) {
28579
- return { url: normalizeUrl(stored.portalUrl), token: stored.token };
28581
+ return {
28582
+ url: normalizeUrl(stored.portalUrl),
28583
+ token: stored.token,
28584
+ refreshToken: stored.refreshToken
28585
+ };
28580
28586
  }
28581
28587
  if (process.env.HAR_CLOUD_API_URL && process.env.HAR_CLOUD_API_KEY) {
28582
28588
  return {
@@ -30982,6 +30988,25 @@ var init_codex = __esm({
30982
30988
  });
30983
30989
 
30984
30990
  // src/core/usage-harvest/index.ts
30991
+ function sessionToolKey(sessionKey, agentTool) {
30992
+ return `${sessionKey}\0${agentTool}`;
30993
+ }
30994
+ function omitHarvestWhenOtelPresent(harvested, existing) {
30995
+ const otelKeys = new Set(
30996
+ existing.filter((row) => (row.sources ?? []).includes("otel")).map((row) => sessionToolKey(row.sessionKey, row.agentTool))
30997
+ );
30998
+ if (otelKeys.size === 0) return harvested;
30999
+ return harvested.filter((row) => !otelKeys.has(sessionToolKey(row.sessionKey, row.agentTool)));
31000
+ }
31001
+ function omitHarvestEventsWhenOtelPresent(harvested, existing) {
31002
+ const otelKeys = new Set(
31003
+ existing.filter((event) => event.source === "otel").map((event) => sessionToolKey(event.sessionKey, event.agentTool))
31004
+ );
31005
+ if (otelKeys.size === 0) return harvested;
31006
+ return harvested.filter(
31007
+ (event) => !otelKeys.has(sessionToolKey(event.sessionKey, event.agentTool))
31008
+ );
31009
+ }
30985
31010
  function harvestUsageForSlot(slot) {
30986
31011
  const out = [];
30987
31012
  const claude = harvestClaudeUsage(slot);
@@ -31274,9 +31299,8 @@ async function postJson(url, body, dryRun) {
31274
31299
  }
31275
31300
  return await response.json();
31276
31301
  }
31277
- async function postPortal(target, endpoint, body, dryRun) {
31278
- if (dryRun) return;
31279
- const response = await fetch(`${target.url}${endpoint}`, {
31302
+ function postPortalOnce(target, endpoint, body) {
31303
+ return fetch(`${target.url}${endpoint}`, {
31280
31304
  method: "POST",
31281
31305
  headers: {
31282
31306
  "Content-Type": "application/json",
@@ -31284,10 +31308,38 @@ async function postPortal(target, endpoint, body, dryRun) {
31284
31308
  },
31285
31309
  body: JSON.stringify(body)
31286
31310
  });
31311
+ }
31312
+ async function refreshPortalToken(target) {
31313
+ if (!target.refreshToken) return false;
31314
+ let response;
31315
+ try {
31316
+ response = await fetch(`${target.url}/api/cli/refresh`, {
31317
+ method: "POST",
31318
+ headers: { Authorization: `Bearer ${target.refreshToken}` }
31319
+ });
31320
+ } catch {
31321
+ return false;
31322
+ }
31323
+ if (!response.ok) return false;
31324
+ const data = await response.json().catch(() => null);
31325
+ if (!data?.token) return false;
31326
+ target.token = data.token;
31327
+ const stored = readPortalCredentials();
31328
+ if (stored) {
31329
+ writePortalCredentials({ ...stored, token: data.token, expiresAt: data.expiresAt });
31330
+ }
31331
+ return true;
31332
+ }
31333
+ async function postPortal(target, endpoint, body, dryRun) {
31334
+ if (dryRun) return;
31335
+ let response = await postPortalOnce(target, endpoint, body);
31336
+ if ((response.status === 401 || response.status === 403) && await refreshPortalToken(target)) {
31337
+ response = await postPortalOnce(target, endpoint, body);
31338
+ }
31287
31339
  if (!response.ok) {
31288
31340
  if (response.status === 401 || response.status === 403) {
31289
31341
  throw new Error(
31290
- `har-portal rejected the ingest token (HTTP ${response.status}) \u2014 check HAR_PORTAL_TOKEN.`
31342
+ `har-portal rejected the ingest token (HTTP ${response.status}) \u2014 run \`har control login\` (or check HAR_PORTAL_TOKEN).`
31291
31343
  );
31292
31344
  }
31293
31345
  const text = await response.text().catch(() => "");
@@ -31348,7 +31400,9 @@ async function collectPortalTelemetry(repoPath, slots, controlApiUrl, since) {
31348
31400
  }))
31349
31401
  );
31350
31402
  const persisted = await fetchPersistedPortalTelemetry(repoPath, controlApiUrl, { since });
31351
- const usage2 = mergePortalUsage(liveUsage, persisted.usage).map((row) => {
31403
+ const harvestedUsage = omitHarvestWhenOtelPresent(liveUsage, persisted.usage);
31404
+ const harvestedEvents = omitHarvestEventsWhenOtelPresent(liveEvents, persisted.events);
31405
+ const usage2 = mergePortalUsage(harvestedUsage, persisted.usage).map((row) => {
31352
31406
  const priced = enrichUsageWithPricing(row);
31353
31407
  const models = modelsFromBreakdown(
31354
31408
  priced.modelBreakdown
@@ -31359,7 +31413,7 @@ async function collectPortalTelemetry(repoPath, slots, controlApiUrl, since) {
31359
31413
  ...models.length > 0 ? { models } : {}
31360
31414
  };
31361
31415
  });
31362
- const events = dedupePortalEvents(liveEvents, persisted.events).map(
31416
+ const events = dedupePortalEvents(harvestedEvents, persisted.events).map(
31363
31417
  (event) => dropNullFields({ ...event })
31364
31418
  );
31365
31419
  return { usage: usage2, events, maxSyncedAt: persisted.maxSyncedAt };
@@ -81407,6 +81461,8 @@ async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
81407
81461
  const returnedState = requestUrl.searchParams.get("state");
81408
81462
  const workspace = requestUrl.searchParams.get("workspace") ?? void 0;
81409
81463
  const email2 = requestUrl.searchParams.get("email") ?? void 0;
81464
+ const refreshToken = requestUrl.searchParams.get("refreshToken") ?? void 0;
81465
+ const expiresAt = requestUrl.searchParams.get("expiresAt") ?? void 0;
81410
81466
  if (returnedState !== state || !token) {
81411
81467
  res.writeHead(400, { "Content-Type": "text/html" });
81412
81468
  res.end("<h1>Login failed</h1><p>You can close this tab and try again.</p>");
@@ -81417,7 +81473,15 @@ async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
81417
81473
  res.writeHead(200, { "Content-Type": "text/html" });
81418
81474
  res.end("<h1>Logged in</h1><p>You can close this tab and return to the terminal.</p>");
81419
81475
  cleanup();
81420
- resolve47({ portalUrl, token, workspace, email: email2, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
81476
+ resolve47({
81477
+ portalUrl,
81478
+ token,
81479
+ workspace,
81480
+ email: email2,
81481
+ refreshToken,
81482
+ expiresAt,
81483
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
81484
+ });
81421
81485
  });
81422
81486
  const cleanup = () => {
81423
81487
  clearTimeout(timer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osfactory/har",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "LLM-powered CLI for reproducible agent development environments",
5
5
  "keywords": [
6
6
  "har",