@osfactory/har 0.63.0 → 0.64.1

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 (2) hide show
  1. package/dist/index.js +262 -51
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -29230,9 +29230,6 @@ function resolvePortalTargetsForRepo(options) {
29230
29230
  const aliases = getRepoPortalTargetAliases(options.repoPath);
29231
29231
  const attached = aliases.map((alias) => getPortalTargetRecord(alias)).filter((entry) => entry !== null).map(recordToPortalTarget);
29232
29232
  if (attached.length > 0) return { source: "repo", targets: attached };
29233
- if (store.targets.length === 1) {
29234
- return { source: "single", targets: [recordToPortalTarget(store.targets[0])] };
29235
- }
29236
29233
  return null;
29237
29234
  }
29238
29235
  if (store.defaultTarget) {
@@ -32316,6 +32313,8 @@ function extractClaudeUsageFromRecords(records) {
32316
32313
  let tokensCacheCreation = 0;
32317
32314
  let costUsd = null;
32318
32315
  const modelBreakdown = {};
32316
+ const billed = /* @__PURE__ */ new Map();
32317
+ const unkeyed = [];
32319
32318
  for (const record2 of records) {
32320
32319
  if (!record2 || typeof record2 !== "object") continue;
32321
32320
  const payload = record2;
@@ -32330,29 +32329,34 @@ function extractClaudeUsageFromRecords(records) {
32330
32329
  const message = payload.message;
32331
32330
  const nestedUsage = payload.usage ?? message?.usage;
32332
32331
  if (nestedUsage && payload.type !== "result") {
32333
- const i = Number(nestedUsage.input_tokens ?? 0);
32334
- const o = Number(nestedUsage.output_tokens ?? 0);
32335
- const cr = Number(nestedUsage.cache_read_input_tokens ?? 0);
32336
- const cc = Number(nestedUsage.cache_creation_input_tokens ?? 0);
32337
- tokensInput += i;
32338
- tokensOutput += o;
32339
- tokensCacheRead += cr;
32340
- tokensCacheCreation += cc;
32341
32332
  const model = typeof message?.model === "string" ? message.model : void 0;
32342
- if (model && model !== "<synthetic>") {
32343
- const totals = modelBreakdown[model] ??= {
32344
- tokensInput: 0,
32345
- tokensOutput: 0,
32346
- tokensCacheRead: 0,
32347
- tokensCacheCreation: 0,
32348
- tokensTotal: 0
32349
- };
32350
- totals.tokensInput += i;
32351
- totals.tokensOutput += o;
32352
- totals.tokensCacheRead += cr;
32353
- totals.tokensCacheCreation += cc;
32354
- totals.tokensTotal += i + o + cr + cc;
32355
- }
32333
+ const entry = { usage: nestedUsage, model };
32334
+ if (typeof message?.id === "string" && message.id) billed.set(message.id, entry);
32335
+ else unkeyed.push(entry);
32336
+ }
32337
+ }
32338
+ for (const { usage: usage2, model } of [...billed.values(), ...unkeyed]) {
32339
+ const i = Number(usage2.input_tokens ?? 0);
32340
+ const o = Number(usage2.output_tokens ?? 0);
32341
+ const cr = Number(usage2.cache_read_input_tokens ?? 0);
32342
+ const cc = Number(usage2.cache_creation_input_tokens ?? 0);
32343
+ tokensInput += i;
32344
+ tokensOutput += o;
32345
+ tokensCacheRead += cr;
32346
+ tokensCacheCreation += cc;
32347
+ if (model && model !== "<synthetic>") {
32348
+ const totals = modelBreakdown[model] ??= {
32349
+ tokensInput: 0,
32350
+ tokensOutput: 0,
32351
+ tokensCacheRead: 0,
32352
+ tokensCacheCreation: 0,
32353
+ tokensTotal: 0
32354
+ };
32355
+ totals.tokensInput += i;
32356
+ totals.tokensOutput += o;
32357
+ totals.tokensCacheRead += cr;
32358
+ totals.tokensCacheCreation += cc;
32359
+ totals.tokensTotal += i + o + cr + cc;
32356
32360
  }
32357
32361
  }
32358
32362
  return { tokensInput, tokensOutput, tokensCacheRead, tokensCacheCreation, costUsd, modelBreakdown };
@@ -32391,8 +32395,9 @@ function findMatchingClaudeTranscripts(slot) {
32391
32395
  }
32392
32396
  }
32393
32397
  }
32394
- if (primaryHit) primary.push({ filePath, records, mtimeMs: stat.mtimeMs });
32395
- else if (fallbackHit) fallback.push({ filePath, records, mtimeMs: stat.mtimeMs });
32398
+ if (primaryHit) primary.push({ filePath, records, mtimeMs: stat.mtimeMs, primary: true });
32399
+ else if (fallbackHit)
32400
+ fallback.push({ filePath, records, mtimeMs: stat.mtimeMs, primary: false });
32396
32401
  }
32397
32402
  }
32398
32403
  primary.sort((a, b2) => b2.mtimeMs - a.mtimeMs);
@@ -32449,18 +32454,14 @@ function harvestClaudeUsage(slot) {
32449
32454
  suffix: slot.suffix,
32450
32455
  createdAt: slot.sessionCreatedAt
32451
32456
  });
32452
- let best = null;
32453
- for (const match of matches) {
32454
- const usage2 = extractClaudeUsageFromRecords(match.records);
32455
- if (usage2.tokensInput + usage2.tokensOutput + usage2.tokensCacheRead === 0 && (usage2.costUsd == null || usage2.costUsd === 0)) {
32456
- continue;
32457
- }
32458
- best = usage2;
32459
- break;
32457
+ const own = matches.filter((match) => match.primary);
32458
+ const usable = own.length > 0 ? own : matches;
32459
+ const usage2 = extractClaudeUsageFromRecords(usable.flatMap((match) => match.records));
32460
+ if (usage2.tokensInput + usage2.tokensOutput + usage2.tokensCacheRead === 0 && (usage2.costUsd == null || usage2.costUsd === 0)) {
32461
+ return null;
32460
32462
  }
32461
- if (!best) return null;
32462
32463
  const now = (/* @__PURE__ */ new Date()).toISOString();
32463
- const tokensTotal = best.tokensInput + best.tokensOutput + best.tokensCacheRead + best.tokensCacheCreation;
32464
+ const tokensTotal = usage2.tokensInput + usage2.tokensOutput + usage2.tokensCacheRead + usage2.tokensCacheCreation;
32464
32465
  return {
32465
32466
  sessionKey,
32466
32467
  agentId: slot.agentId,
@@ -32468,13 +32469,13 @@ function harvestClaudeUsage(slot) {
32468
32469
  workDir: slot.workDir,
32469
32470
  branch: slot.branch,
32470
32471
  suffix: slot.suffix,
32471
- tokensInput: best.tokensInput,
32472
- tokensOutput: best.tokensOutput,
32473
- tokensCacheRead: best.tokensCacheRead,
32474
- tokensCacheCreation: best.tokensCacheCreation,
32472
+ tokensInput: usage2.tokensInput,
32473
+ tokensOutput: usage2.tokensOutput,
32474
+ tokensCacheRead: usage2.tokensCacheRead,
32475
+ tokensCacheCreation: usage2.tokensCacheCreation,
32475
32476
  tokensTotal,
32476
- costUsd: best.costUsd,
32477
- ...Object.keys(best.modelBreakdown).length > 0 ? { modelBreakdown: best.modelBreakdown } : {},
32477
+ costUsd: usage2.costUsd,
32478
+ ...Object.keys(usage2.modelBreakdown).length > 0 ? { modelBreakdown: usage2.modelBreakdown } : {},
32478
32479
  sources: ["harvest"],
32479
32480
  firstSeenAt: slot.sessionCreatedAt ?? now,
32480
32481
  lastSeenAt: now
@@ -74842,7 +74843,30 @@ function openBrowser(url) {
74842
74843
  } catch {
74843
74844
  }
74844
74845
  }
74845
- async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
74846
+ var REPOS_QUERY_BUDGET = 1800;
74847
+ function encodeCliReposQuery(repos) {
74848
+ const unique = [...new Set(repos.filter((repo) => repo.trim().length > 0))];
74849
+ if (unique.length === 0) return null;
74850
+ const encoded = encodeURIComponent(JSON.stringify(unique));
74851
+ if (encoded.length > REPOS_QUERY_BUDGET) return null;
74852
+ return encoded;
74853
+ }
74854
+ function parseCliTargetsParam(raw) {
74855
+ if (!raw) return null;
74856
+ try {
74857
+ const parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
74858
+ if (!Array.isArray(parsed.targets)) return null;
74859
+ const targets = parsed.targets.filter(
74860
+ (entry) => typeof entry?.workspaceId === "string" && typeof entry?.token === "string" && Array.isArray(entry.repos)
74861
+ );
74862
+ return targets.length > 0 ? targets : null;
74863
+ } catch {
74864
+ return null;
74865
+ }
74866
+ }
74867
+ async function loginViaBrowser(portalUrl, openUrlOrOptions = openBrowser) {
74868
+ const options = typeof openUrlOrOptions === "function" ? { openUrl: openUrlOrOptions } : openUrlOrOptions;
74869
+ const openUrl = options.openUrl ?? openBrowser;
74846
74870
  const state = (0, import_crypto.randomUUID)();
74847
74871
  return new Promise((resolve53, reject) => {
74848
74872
  let timer;
@@ -74871,6 +74895,7 @@ async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
74871
74895
  res.writeHead(200, { "Content-Type": "text/html" });
74872
74896
  res.end("<h1>Logged in</h1><p>You can close this tab and return to the terminal.</p>");
74873
74897
  cleanup();
74898
+ const targets = parseCliTargetsParam(requestUrl.searchParams.get("targets"));
74874
74899
  resolve53({
74875
74900
  portalUrl,
74876
74901
  token,
@@ -74881,7 +74906,8 @@ async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
74881
74906
  email: email2,
74882
74907
  refreshToken,
74883
74908
  expiresAt,
74884
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
74909
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
74910
+ ...targets ? { targets } : {}
74885
74911
  });
74886
74912
  });
74887
74913
  const cleanup = () => {
@@ -74895,7 +74921,8 @@ async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
74895
74921
  server.listen(0, "127.0.0.1", () => {
74896
74922
  const { port } = server.address();
74897
74923
  const callback = `http://127.0.0.1:${port}${CALLBACK_PATH}`;
74898
- const loginUrl = `${portalUrl}/cli/login?callback=${encodeURIComponent(callback)}&state=${state}`;
74924
+ const reposQuery = encodeCliReposQuery(options.repos ?? []);
74925
+ const loginUrl = `${portalUrl}/cli/login?callback=${encodeURIComponent(callback)}&state=${state}${reposQuery ? `&repos=${reposQuery}` : ""}`;
74899
74926
  openUrl(loginUrl);
74900
74927
  process.stderr.write(`Opening ${loginUrl}
74901
74928
  If your browser did not open, paste that URL.
@@ -74908,6 +74935,59 @@ If your browser did not open, paste that URL.
74908
74935
  });
74909
74936
  }
74910
74937
 
74938
+ // src/core/portal-repo-map.ts
74939
+ init_control_repo_path();
74940
+ init_control_registry();
74941
+ init_portal_targets();
74942
+ function planRepoWorkspaceMap(opts) {
74943
+ const registered = (opts.registeredRepos ?? listRegisteredRepos()).map(
74944
+ (repoPath) => canonicalizeControlRepoPath(repoPath)
74945
+ );
74946
+ const current = opts.currentRepo ? canonicalizeControlRepoPath(opts.currentRepo) : null;
74947
+ const others = registered.filter((repoPath) => repoPath !== current);
74948
+ const connections = opts.connections ?? listPortalTargetRecords();
74949
+ if (others.length === 0 || connections.length === 0) return { kind: "none" };
74950
+ if (connections.length === 1) {
74951
+ const only = connections[0];
74952
+ return {
74953
+ kind: "attach-more",
74954
+ workspaceAlias: only.alias,
74955
+ workspaceLabel: displayPortalTargetLabel(only),
74956
+ repos: others
74957
+ };
74958
+ }
74959
+ return {
74960
+ kind: "map-orgs",
74961
+ currentAlias: opts.currentAlias,
74962
+ repos: others,
74963
+ connections: connections.map((entry) => ({
74964
+ alias: entry.alias,
74965
+ label: displayPortalTargetLabel(entry)
74966
+ }))
74967
+ };
74968
+ }
74969
+ function applyRepoWorkspaceMap(assignments) {
74970
+ const attached = [];
74971
+ for (const row of assignments) {
74972
+ if (!row.alias) continue;
74973
+ const canonical = canonicalizeControlRepoPath(row.repoPath);
74974
+ attachRepoPortalTarget(canonical, row.alias);
74975
+ recordRepoForControlSync(canonical);
74976
+ attached.push(canonical);
74977
+ }
74978
+ return attached;
74979
+ }
74980
+ function remainingUnassignedRepos(repos, assigned) {
74981
+ const taken = new Set(
74982
+ [...assigned].map((repoPath) => canonicalizeControlRepoPath(repoPath))
74983
+ );
74984
+ return repos.filter((repoPath) => !taken.has(canonicalizeControlRepoPath(repoPath)));
74985
+ }
74986
+ function remainingConnections(connections, usedAliases) {
74987
+ const used = /* @__PURE__ */ new Set([...usedAliases]);
74988
+ return connections.filter((entry) => !used.has(entry.alias));
74989
+ }
74990
+
74911
74991
  // src/core/portal-connect.ts
74912
74992
  init_portal_targets();
74913
74993
  function attachRepoIfPresent(alias, repoPath) {
@@ -74930,9 +75010,16 @@ async function runPortalConnect(input) {
74930
75010
  token: input.apiKey,
74931
75011
  setAsDefault: true
74932
75012
  });
74933
- return { record: record3, attachedRepo: attachRepoIfPresent(record3.alias, input.repoPath) };
75013
+ return {
75014
+ record: record3,
75015
+ attachedRepo: attachRepoIfPresent(record3.alias, input.repoPath),
75016
+ extraAttached: [],
75017
+ mappedInBrowser: false
75018
+ };
74934
75019
  }
74935
- const creds = await loginViaBrowser(input.portalUrl);
75020
+ const creds = await loginViaBrowser(input.portalUrl, {
75021
+ repos: listRegisteredReposForConnect(input.repoPath)
75022
+ });
74936
75023
  const record2 = upsertPortalTarget({
74937
75024
  alias: input.alias,
74938
75025
  portalUrl: input.portalUrl,
@@ -74946,7 +75033,35 @@ async function runPortalConnect(input) {
74946
75033
  email: creds.email,
74947
75034
  setAsDefault: true
74948
75035
  });
74949
- return { record: record2, attachedRepo: attachRepoIfPresent(record2.alias, input.repoPath) };
75036
+ const attachedRepo = attachRepoIfPresent(record2.alias, input.repoPath);
75037
+ if (!creds.targets || creds.targets.length === 0) {
75038
+ return { record: record2, attachedRepo, extraAttached: [], mappedInBrowser: false };
75039
+ }
75040
+ const extraAttached = [];
75041
+ for (const target of creds.targets) {
75042
+ const next = upsertPortalTarget({
75043
+ portalUrl: input.portalUrl,
75044
+ workspaceId: target.workspaceId,
75045
+ workspace: target.workspace,
75046
+ workspaceSlug: target.workspace,
75047
+ workspaceName: target.workspaceName,
75048
+ token: target.token,
75049
+ refreshToken: target.refreshToken,
75050
+ expiresAt: target.expiresAt,
75051
+ email: creds.email,
75052
+ setAsDefault: false
75053
+ });
75054
+ const attached = applyRepoWorkspaceMap(
75055
+ target.repos.map((repoPath) => ({ repoPath, alias: next.alias }))
75056
+ );
75057
+ extraAttached.push(...attached.filter((repoPath) => repoPath !== attachedRepo));
75058
+ }
75059
+ return { record: record2, attachedRepo, extraAttached, mappedInBrowser: true };
75060
+ }
75061
+ function listRegisteredReposForConnect(repoPath) {
75062
+ const resolved = repoPath ? path59.resolve(repoPath) : null;
75063
+ const current = resolved && fs49.existsSync(resolved) ? canonicalizeControlRepoPath(resolved) : null;
75064
+ return [.../* @__PURE__ */ new Set([...current ? [current] : [], ...listRegisteredRepos()])];
74950
75065
  }
74951
75066
 
74952
75067
  // src/cli/commands/hq.ts
@@ -74978,7 +75093,7 @@ var hqCommand = {
74978
75093
  alias: "y",
74979
75094
  type: "boolean",
74980
75095
  default: false,
74981
- describe: "Skip the destination prompt and use the resolved portal URL"
75096
+ describe: "Skip interactive prompts (portal URL and extra-repo mapping)"
74982
75097
  }),
74983
75098
  (argv) => handleHqConnect(
74984
75099
  argv
@@ -75036,6 +75151,90 @@ async function resolveConnectPortalUrl(explicit, skipPrompt) {
75036
75151
  ]);
75037
75152
  return resolvePortalUrl(url);
75038
75153
  }
75154
+ async function promptExtraRepoAttachments(opts) {
75155
+ const plan = planRepoWorkspaceMap(opts);
75156
+ if (plan.kind === "none") return [];
75157
+ const noun = plan.repos.length === 1 ? "repository" : "repositories";
75158
+ info(`${plan.repos.length} other registered ${noun}:`);
75159
+ for (const repoPath of plan.repos) info(` ${repoPath}`);
75160
+ if (plan.kind === "attach-more") {
75161
+ const { selected } = await esm_default13.prompt([
75162
+ {
75163
+ type: "checkbox",
75164
+ name: "selected",
75165
+ message: `Attach which of these ${plan.repos.length} ${noun} to ${plan.workspaceLabel}?`,
75166
+ choices: plan.repos.map((repoPath) => ({
75167
+ name: repoPath,
75168
+ value: repoPath,
75169
+ checked: true
75170
+ }))
75171
+ }
75172
+ ]);
75173
+ return applyRepoWorkspaceMap(
75174
+ selected.map((repoPath) => ({ repoPath, alias: plan.workspaceAlias }))
75175
+ );
75176
+ }
75177
+ const { sendAll } = await esm_default13.prompt([
75178
+ {
75179
+ type: "confirm",
75180
+ name: "sendAll",
75181
+ message: `Send all ${plan.repos.length} ${noun} to the same workspace?`,
75182
+ default: true
75183
+ }
75184
+ ]);
75185
+ if (sendAll) {
75186
+ const { alias } = await esm_default13.prompt([
75187
+ {
75188
+ type: "list",
75189
+ name: "alias",
75190
+ message: "Which workspace?",
75191
+ default: plan.currentAlias,
75192
+ choices: plan.connections.map((entry) => ({
75193
+ name: entry.label,
75194
+ value: entry.alias
75195
+ }))
75196
+ }
75197
+ ]);
75198
+ return applyRepoWorkspaceMap(plan.repos.map((repoPath) => ({ repoPath, alias })));
75199
+ }
75200
+ const attached = [];
75201
+ let remainingRepos = [...plan.repos];
75202
+ let remainingOrgs = [...plan.connections];
75203
+ while (remainingRepos.length > 0 && remainingOrgs.length > 0) {
75204
+ const left2 = remainingRepos.length === 1 ? "repository" : "repositories";
75205
+ info(`${remainingRepos.length} ${left2} left to assign.`);
75206
+ const { alias } = await esm_default13.prompt([
75207
+ {
75208
+ type: "list",
75209
+ name: "alias",
75210
+ message: "Workspace to assign repositories to",
75211
+ default: remainingOrgs.some((entry) => entry.alias === plan.currentAlias) ? plan.currentAlias : remainingOrgs[0]?.alias,
75212
+ choices: [
75213
+ ...remainingOrgs.map((entry) => ({
75214
+ name: entry.label,
75215
+ value: entry.alias
75216
+ })),
75217
+ { name: "Done \u2014 leave the rest unattached", value: "" }
75218
+ ]
75219
+ }
75220
+ ]);
75221
+ if (!alias) break;
75222
+ const { selected } = await esm_default13.prompt([
75223
+ {
75224
+ type: "checkbox",
75225
+ name: "selected",
75226
+ message: `Repositories for ${remainingOrgs.find((entry) => entry.alias === alias)?.label ?? alias}`,
75227
+ choices: remainingRepos.map((repoPath) => ({ name: repoPath, value: repoPath }))
75228
+ }
75229
+ ]);
75230
+ if (selected.length > 0) {
75231
+ attached.push(...applyRepoWorkspaceMap(selected.map((repoPath) => ({ repoPath, alias }))));
75232
+ }
75233
+ remainingRepos = remainingUnassignedRepos(remainingRepos, selected);
75234
+ remainingOrgs = remainingConnections(remainingOrgs, [alias]);
75235
+ }
75236
+ return attached;
75237
+ }
75039
75238
  async function handleHqConnect(argv) {
75040
75239
  if (argv.deprecated) {
75041
75240
  warn("har control login is deprecated \u2014 use har hq connect");
@@ -75046,7 +75245,7 @@ async function handleHqConnect(argv) {
75046
75245
  info(`Portal: ${portalUrl} (${PORTAL_SOURCE_LABEL[source]})`);
75047
75246
  }
75048
75247
  try {
75049
- const { record: record2, attachedRepo } = await runPortalConnect({
75248
+ const { record: record2, attachedRepo, extraAttached, mappedInBrowser } = await runPortalConnect({
75050
75249
  portalUrl,
75051
75250
  apiKey: argv.apiKey,
75052
75251
  repoPath: argv.repo ?? ".",
@@ -75074,6 +75273,18 @@ async function handleHqConnect(argv) {
75074
75273
  } else {
75075
75274
  info("No local repository attached (run this from a repo, or pass --repo).");
75076
75275
  }
75276
+ for (const repoPath of extraAttached) {
75277
+ success(`Attached ${repoPath}`);
75278
+ }
75279
+ if (!argv.json && !argv.yes && !mappedInBrowser && isInteractive()) {
75280
+ const extra = await promptExtraRepoAttachments({
75281
+ currentRepo: attachedRepo,
75282
+ currentAlias: record2.alias
75283
+ });
75284
+ for (const repoPath of extra) {
75285
+ success(`Attached ${repoPath}`);
75286
+ }
75287
+ }
75077
75288
  info("Sync: har control sync");
75078
75289
  } catch (err) {
75079
75290
  error(`Connect failed: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osfactory/har",
3
- "version": "0.63.0",
3
+ "version": "0.64.1",
4
4
  "description": "HAR: open agent harness CLI and MCP server for coding agents, isolated worktrees, and software-factory workflows",
5
5
  "keywords": [
6
6
  "har",