@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.
- package/README.md +14 -1
- package/dist/index.js +73 -9
- 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 {
|
|
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
|
-
|
|
31278
|
-
|
|
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
|
|
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(
|
|
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({
|
|
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);
|