@osfactory/har 0.39.0 → 0.41.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 +212 -32
  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);
@@ -31043,6 +31068,27 @@ function writePortalWatermark(repoPath, portalUrl, lastSyncedAt) {
31043
31068
  }
31044
31069
  writeState(state);
31045
31070
  }
31071
+ function readRunsWatermarkEntry(repoPath, target) {
31072
+ const canonical = canonicalizeControlRepoPath(repoPath);
31073
+ const entry = readState().states.find(
31074
+ (state) => state.repoPath === canonical && state.portalUrl === target
31075
+ );
31076
+ return entry ? { lastSyncedAt: entry.lastSyncedAt, repoId: entry.repoId } : null;
31077
+ }
31078
+ function writeRunsWatermark(repoPath, target, repoId, lastSyncedAt) {
31079
+ const canonical = canonicalizeControlRepoPath(repoPath);
31080
+ const state = readState();
31081
+ const existing = state.states.find(
31082
+ (entry) => entry.repoPath === canonical && entry.portalUrl === target
31083
+ );
31084
+ if (existing) {
31085
+ existing.lastSyncedAt = lastSyncedAt;
31086
+ existing.repoId = repoId;
31087
+ } else {
31088
+ state.states.push({ repoPath: canonical, portalUrl: target, lastSyncedAt, repoId });
31089
+ }
31090
+ writeState(state);
31091
+ }
31046
31092
  function selectSince(rows, since, getTimestamp) {
31047
31093
  let maxSyncedAt = null;
31048
31094
  const selected = [];
@@ -31250,6 +31296,7 @@ var init_portal_usage_merge = __esm({
31250
31296
  // src/core/control-sync.ts
31251
31297
  var control_sync_exports = {};
31252
31298
  __export(control_sync_exports, {
31299
+ chunkBySerializedSize: () => chunkBySerializedSize,
31253
31300
  discoverHarRepos: () => discoverHarRepos,
31254
31301
  ensureRepoRegisteredWithControl: () => ensureRepoRegisteredWithControl,
31255
31302
  isControlApiReachable: () => isControlApiReachable,
@@ -31261,38 +31308,133 @@ __export(control_sync_exports, {
31261
31308
  syncRunWithControlAsync: () => syncRunWithControlAsync,
31262
31309
  waitForControlApi: () => waitForControlApi
31263
31310
  });
31311
+ function maxSyncBatchBytes() {
31312
+ const raw = Number(process.env.HAR_SYNC_MAX_BATCH_BYTES);
31313
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_MAX_BATCH_BYTES;
31314
+ }
31315
+ function chunkBySerializedSize(items, maxBytes) {
31316
+ const batches = [];
31317
+ let current = [];
31318
+ let currentBytes = 0;
31319
+ for (const item of items) {
31320
+ const itemBytes = Buffer.byteLength(JSON.stringify(item), "utf8");
31321
+ if (current.length > 0 && currentBytes + itemBytes > maxBytes) {
31322
+ batches.push(current);
31323
+ current = [];
31324
+ currentBytes = 0;
31325
+ }
31326
+ current.push(item);
31327
+ currentBytes += itemBytes;
31328
+ }
31329
+ if (current.length > 0) batches.push(current);
31330
+ return batches;
31331
+ }
31332
+ function runTimestamp(run2) {
31333
+ return run2.finishedAt ?? run2.startedAt;
31334
+ }
31335
+ function rewindSince(since) {
31336
+ if (!since) return null;
31337
+ const raw = Number(process.env.HAR_SYNC_OVERLAP_MS);
31338
+ const overlap = Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_SYNC_OVERLAP_MS;
31339
+ return new Date(new Date(since).getTime() - overlap).toISOString();
31340
+ }
31341
+ function runsWatermarkTarget(target) {
31342
+ return `runs:${target}`;
31343
+ }
31344
+ async function syncRunBatches(newRuns, repoPath, runsTarget, getRepoId, dryRun, send) {
31345
+ const ordered = [...newRuns].sort((a2, b3) => runTimestamp(a2).localeCompare(runTimestamp(b3)));
31346
+ const batches = chunkBySerializedSize(ordered, maxSyncBatchBytes());
31347
+ const toSend = batches.length > 0 ? batches : [[]];
31348
+ for (let i2 = 0; i2 < toSend.length; i2++) {
31349
+ const batch = toSend[i2];
31350
+ await send(batch, i2);
31351
+ if (!dryRun && batch.length > 0) {
31352
+ writeRunsWatermark(repoPath, runsTarget, getRepoId(), runTimestamp(batch[batch.length - 1]));
31353
+ }
31354
+ }
31355
+ }
31356
+ function describeFetchFailure(url, bodyBytes, err) {
31357
+ const base = err instanceof Error ? err.message : String(err);
31358
+ const cause = err.cause;
31359
+ const code = cause?.code ? ` [${cause.code}]` : "";
31360
+ const detail = cause?.message && cause.message !== base ? `: ${cause.message}` : "";
31361
+ const size = bodyBytes > 0 ? ` (request body ${(bodyBytes / 1024 / 1024).toFixed(2)} MB)` : "";
31362
+ return `${base}${code}${detail}${size} \u2014 POST ${url}`;
31363
+ }
31364
+ async function postForResponse(url, headers, payload) {
31365
+ let lastErr;
31366
+ for (let attempt = 1; attempt <= MAX_FETCH_ATTEMPTS; attempt++) {
31367
+ try {
31368
+ return await fetch(url, { method: "POST", headers, body: payload });
31369
+ } catch (err) {
31370
+ lastErr = err;
31371
+ if (attempt < MAX_FETCH_ATTEMPTS) {
31372
+ await new Promise((resolve47) => setTimeout(resolve47, attempt * 100));
31373
+ }
31374
+ }
31375
+ }
31376
+ throw new Error(describeFetchFailure(url, Buffer.byteLength(payload, "utf8"), lastErr));
31377
+ }
31264
31378
  async function postJson(url, body, dryRun) {
31265
31379
  if (dryRun) return null;
31266
- const response = await fetch(url, {
31267
- method: "POST",
31268
- headers: { "Content-Type": "application/json" },
31269
- body: JSON.stringify(body)
31270
- });
31380
+ const response = await postForResponse(
31381
+ url,
31382
+ { "Content-Type": "application/json" },
31383
+ JSON.stringify(body)
31384
+ );
31271
31385
  if (!response.ok) {
31272
31386
  const text = await response.text().catch(() => "");
31273
31387
  throw new Error(`Control API ${response.status}: ${text || response.statusText}`);
31274
31388
  }
31275
31389
  return await response.json();
31276
31390
  }
31277
- async function postPortal(target, endpoint, body, dryRun) {
31278
- if (dryRun) return;
31279
- const response = await fetch(`${target.url}${endpoint}`, {
31280
- method: "POST",
31281
- headers: {
31391
+ function postPortalOnce(target, endpoint, body) {
31392
+ return postForResponse(
31393
+ `${target.url}${endpoint}`,
31394
+ {
31282
31395
  "Content-Type": "application/json",
31283
31396
  Authorization: `Bearer ${target.token}`
31284
31397
  },
31285
- body: JSON.stringify(body)
31286
- });
31398
+ JSON.stringify(body)
31399
+ );
31400
+ }
31401
+ async function refreshPortalToken(target) {
31402
+ if (!target.refreshToken) return false;
31403
+ let response;
31404
+ try {
31405
+ response = await fetch(`${target.url}/api/cli/refresh`, {
31406
+ method: "POST",
31407
+ headers: { Authorization: `Bearer ${target.refreshToken}` }
31408
+ });
31409
+ } catch {
31410
+ return false;
31411
+ }
31412
+ if (!response.ok) return false;
31413
+ const data = await response.json().catch(() => null);
31414
+ if (!data?.token) return false;
31415
+ target.token = data.token;
31416
+ const stored = readPortalCredentials();
31417
+ if (stored) {
31418
+ writePortalCredentials({ ...stored, token: data.token, expiresAt: data.expiresAt });
31419
+ }
31420
+ return true;
31421
+ }
31422
+ async function postPortal(target, endpoint, body, dryRun) {
31423
+ if (dryRun) return null;
31424
+ let response = await postPortalOnce(target, endpoint, body);
31425
+ if ((response.status === 401 || response.status === 403) && await refreshPortalToken(target)) {
31426
+ response = await postPortalOnce(target, endpoint, body);
31427
+ }
31287
31428
  if (!response.ok) {
31288
31429
  if (response.status === 401 || response.status === 403) {
31289
31430
  throw new Error(
31290
- `har-portal rejected the ingest token (HTTP ${response.status}) \u2014 check HAR_PORTAL_TOKEN.`
31431
+ `har-portal rejected the ingest token (HTTP ${response.status}) \u2014 run \`har control login\` (or check HAR_PORTAL_TOKEN).`
31291
31432
  );
31292
31433
  }
31293
31434
  const text = await response.text().catch(() => "");
31294
31435
  throw new Error(`har-portal sync failed: HTTP ${response.status}${text ? ` \u2014 ${text}` : ""}`);
31295
31436
  }
31437
+ return await response.json().catch(() => null);
31296
31438
  }
31297
31439
  function resolvePortalUserEmail() {
31298
31440
  return readPortalCredentials()?.email || void 0;
@@ -31348,7 +31490,9 @@ async function collectPortalTelemetry(repoPath, slots, controlApiUrl, since) {
31348
31490
  }))
31349
31491
  );
31350
31492
  const persisted = await fetchPersistedPortalTelemetry(repoPath, controlApiUrl, { since });
31351
- const usage2 = mergePortalUsage(liveUsage, persisted.usage).map((row) => {
31493
+ const harvestedUsage = omitHarvestWhenOtelPresent(liveUsage, persisted.usage);
31494
+ const harvestedEvents = omitHarvestEventsWhenOtelPresent(liveEvents, persisted.events);
31495
+ const usage2 = mergePortalUsage(harvestedUsage, persisted.usage).map((row) => {
31352
31496
  const priced = enrichUsageWithPricing(row);
31353
31497
  const models = modelsFromBreakdown(
31354
31498
  priced.modelBreakdown
@@ -31359,7 +31503,7 @@ async function collectPortalTelemetry(repoPath, slots, controlApiUrl, since) {
31359
31503
  ...models.length > 0 ? { models } : {}
31360
31504
  };
31361
31505
  });
31362
- const events = dedupePortalEvents(liveEvents, persisted.events).map(
31506
+ const events = dedupePortalEvents(harvestedEvents, persisted.events).map(
31363
31507
  (event) => dropNullFields({ ...event })
31364
31508
  );
31365
31509
  return { usage: usage2, events, maxSyncedAt: persisted.maxSyncedAt };
@@ -31387,7 +31531,6 @@ async function buildPortalPayload(repoPath, controlApiUrl, since) {
31387
31531
  path: repoPath,
31388
31532
  ...manifest ? { manifest } : {},
31389
31533
  ...stagesRegistry ? { stagesRegistry } : {},
31390
- runs,
31391
31534
  slots: status.slots,
31392
31535
  generatedAt: status.generatedAt,
31393
31536
  ...validations.length > 0 ? { validations } : {},
@@ -31396,7 +31539,7 @@ async function buildPortalPayload(repoPath, controlApiUrl, since) {
31396
31539
  ...validationBindings.length > 0 ? { validationBindings } : {},
31397
31540
  ...usage2.length > 0 ? { usage: usage2 } : {}
31398
31541
  };
31399
- return { syncBody, events, maxSyncedAt };
31542
+ return { syncBody, runs, events, maxSyncedAt };
31400
31543
  }
31401
31544
  async function isControlApiReachable(apiUrl = getControlApiUrl()) {
31402
31545
  try {
@@ -31434,7 +31577,7 @@ async function syncReposWithControl(options) {
31434
31577
  try {
31435
31578
  await withTimeout(
31436
31579
  syncRepoWithControl({ repoPath, apiUrl, dryRun: options.dryRun, cloud: options.cloud, full: options.full }),
31437
- SYNC_TIMEOUT_MS,
31580
+ EXPLICIT_SYNC_TIMEOUT_MS,
31438
31581
  "control sync"
31439
31582
  );
31440
31583
  synced++;
@@ -31497,7 +31640,7 @@ async function ensureRepoRegisteredWithControl(repoPath, apiUrl) {
31497
31640
  }
31498
31641
  async function syncRepoWithLocalControl(options) {
31499
31642
  const apiUrl = options.apiUrl ?? getControlApiUrl();
31500
- const { repoPath, dryRun } = options;
31643
+ const { repoPath, dryRun, full } = options;
31501
31644
  const registerResult = await registerRepoWithControl({ ...options, repoPath });
31502
31645
  const repoId = registerResult?.id;
31503
31646
  if (!repoId && !dryRun) {
@@ -31508,11 +31651,11 @@ async function syncRepoWithLocalControl(options) {
31508
31651
  if (!existing) {
31509
31652
  return;
31510
31653
  }
31511
- await syncRepoRunsAndSlots(apiUrl, existing.id, repoPath, dryRun);
31654
+ await syncRepoRunsAndSlots(apiUrl, existing.id, repoPath, dryRun, full);
31512
31655
  return;
31513
31656
  }
31514
31657
  if (repoId) {
31515
- await syncRepoRunsAndSlots(apiUrl, repoId, repoPath, dryRun);
31658
+ await syncRepoRunsAndSlots(apiUrl, repoId, repoPath, dryRun, full);
31516
31659
  }
31517
31660
  }
31518
31661
  async function syncRepoWithPortal(options, controlApiUrl) {
@@ -31520,10 +31663,28 @@ async function syncRepoWithPortal(options, controlApiUrl) {
31520
31663
  if (!portal) return;
31521
31664
  const { repoPath, dryRun, full } = options;
31522
31665
  const since = full ? null : readPortalWatermark(repoPath, portal.url);
31523
- const { syncBody, events, maxSyncedAt } = await buildPortalPayload(repoPath, controlApiUrl, since);
31524
- await postPortal(portal, "/api/sync", syncBody, dryRun);
31525
- if (events.length > 0) {
31526
- await postPortal(portal, "/api/otel", { path: repoPath, events, spans: [] }, dryRun);
31666
+ const runsTarget = runsWatermarkTarget(portal.url);
31667
+ const stored = full ? null : readRunsWatermarkEntry(repoPath, runsTarget);
31668
+ const runsSince = rewindSince(stored?.lastSyncedAt ?? null);
31669
+ const { syncBody, runs, events, maxSyncedAt } = await buildPortalPayload(
31670
+ repoPath,
31671
+ controlApiUrl,
31672
+ since
31673
+ );
31674
+ const { selected: newRuns } = selectSince(runs, runsSince, runTimestamp);
31675
+ let repoId;
31676
+ await syncRunBatches(newRuns, repoPath, runsTarget, () => repoId, dryRun, async (batch, i2) => {
31677
+ const body = i2 === 0 ? { ...syncBody, runs: batch } : { path: repoPath, runs: batch };
31678
+ const res = await postPortal(portal, "/api/sync", body, dryRun);
31679
+ if (i2 === 0 && typeof res?.repositoryId === "string") repoId = res.repositoryId;
31680
+ });
31681
+ if (!full && !dryRun && stored?.repoId && repoId && stored.repoId !== repoId) {
31682
+ await syncRunBatches(listRuns(repoPath), repoPath, runsTarget, () => repoId, dryRun, async (batch) => {
31683
+ await postPortal(portal, "/api/sync", { path: repoPath, runs: batch }, dryRun);
31684
+ });
31685
+ }
31686
+ for (const batch of chunkBySerializedSize(events, maxSyncBatchBytes())) {
31687
+ await postPortal(portal, "/api/otel", { path: repoPath, events: batch, spans: [] }, dryRun);
31527
31688
  }
31528
31689
  if (!dryRun && maxSyncedAt) {
31529
31690
  writePortalWatermark(repoPath, portal.url, maxSyncedAt);
@@ -31564,10 +31725,15 @@ async function syncRepoWithControl(options) {
31564
31725
  await syncRepoWithLocalControl({ ...options, repoPath, apiUrl });
31565
31726
  await syncRepoWithPortal({ ...options, repoPath }, apiUrl);
31566
31727
  }
31567
- async function syncRepoRunsAndSlots(apiUrl, repoId, repoPath, dryRun) {
31728
+ async function syncRepoRunsAndSlots(apiUrl, repoId, repoPath, dryRun, full) {
31729
+ const runsTarget = runsWatermarkTarget(apiUrl);
31730
+ const stored = full ? null : readRunsWatermarkEntry(repoPath, runsTarget);
31731
+ const runsSince = rewindSince(stored?.repoId === repoId ? stored?.lastSyncedAt ?? null : null);
31568
31732
  const runs = listRuns(repoPath);
31569
- const runsBody = SyncRunsInputSchema.parse({ runs });
31570
- await postJson(`${apiUrl}/api/repos/${repoId}/runs`, runsBody, dryRun);
31733
+ const { selected: newRuns } = selectSince(runs, runsSince, runTimestamp);
31734
+ await syncRunBatches(newRuns, repoPath, runsTarget, () => repoId, dryRun, async (batch) => {
31735
+ await postJson(`${apiUrl}/api/repos/${repoId}/runs`, SyncRunsInputSchema.parse({ runs: batch }), dryRun);
31736
+ });
31571
31737
  const status = collectEnvironmentStatus(repoPath);
31572
31738
  const slotsBody = SyncSlotsInputSchema.parse({
31573
31739
  slots: status.slots,
@@ -31722,7 +31888,7 @@ async function syncRunWithControlAsync(repoPath, run2) {
31722
31888
  `);
31723
31889
  }
31724
31890
  }
31725
- var path27, SYNC_TIMEOUT_MS;
31891
+ var path27, DEFAULT_MAX_BATCH_BYTES, DEFAULT_SYNC_OVERLAP_MS, MAX_FETCH_ATTEMPTS, SYNC_TIMEOUT_MS, EXPLICIT_SYNC_TIMEOUT_MS;
31726
31892
  var init_control_sync = __esm({
31727
31893
  "src/core/control-sync.ts"() {
31728
31894
  "use strict";
@@ -31746,7 +31912,11 @@ var init_control_sync = __esm({
31746
31912
  init_portal_usage_merge();
31747
31913
  init_schema2();
31748
31914
  init_portal_watermark();
31915
+ DEFAULT_MAX_BATCH_BYTES = 4 * 1024 * 1024;
31916
+ DEFAULT_SYNC_OVERLAP_MS = 6e4;
31917
+ MAX_FETCH_ATTEMPTS = 3;
31749
31918
  SYNC_TIMEOUT_MS = 15e3;
31919
+ EXPLICIT_SYNC_TIMEOUT_MS = 12e4;
31750
31920
  }
31751
31921
  });
31752
31922
 
@@ -81407,6 +81577,8 @@ async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
81407
81577
  const returnedState = requestUrl.searchParams.get("state");
81408
81578
  const workspace = requestUrl.searchParams.get("workspace") ?? void 0;
81409
81579
  const email2 = requestUrl.searchParams.get("email") ?? void 0;
81580
+ const refreshToken = requestUrl.searchParams.get("refreshToken") ?? void 0;
81581
+ const expiresAt = requestUrl.searchParams.get("expiresAt") ?? void 0;
81410
81582
  if (returnedState !== state || !token) {
81411
81583
  res.writeHead(400, { "Content-Type": "text/html" });
81412
81584
  res.end("<h1>Login failed</h1><p>You can close this tab and try again.</p>");
@@ -81417,7 +81589,15 @@ async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
81417
81589
  res.writeHead(200, { "Content-Type": "text/html" });
81418
81590
  res.end("<h1>Logged in</h1><p>You can close this tab and return to the terminal.</p>");
81419
81591
  cleanup();
81420
- resolve47({ portalUrl, token, workspace, email: email2, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
81592
+ resolve47({
81593
+ portalUrl,
81594
+ token,
81595
+ workspace,
81596
+ email: email2,
81597
+ refreshToken,
81598
+ expiresAt,
81599
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
81600
+ });
81421
81601
  });
81422
81602
  const cleanup = () => {
81423
81603
  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.41.0",
4
4
  "description": "LLM-powered CLI for reproducible agent development environments",
5
5
  "keywords": [
6
6
  "har",