@osfactory/har 0.22.0 → 0.24.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 +1 -1
  2. package/dist/index.js +305 -74
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -69,7 +69,7 @@ cd MyApp && har env init --profile ios
69
69
  har env add-stage rocketsim # optional: UI flow validation on the simulator
70
70
  ```
71
71
 
72
- After init, paste the printed adaptation prompt into your coding agent to tailor scripts to your stack. For built-in Claude adaptation: `har env init --auto` (requires `ANTHROPIC_API_KEY`).
72
+ After init, paste the adaptation prompt into your coding agent (interactive terminals offer a clipboard copy; also saved to `.har/ADAPT-PROMPT.md`) to tailor scripts to your stack. For built-in Claude adaptation: `har env init --auto` (requires `ANTHROPIC_API_KEY`).
73
73
 
74
74
  Optional stage templates depend on the profile: `har env add-stage playwright` for browser E2E on web apps, `har env add-stage rocketsim` for simulator user-flow checks on iOS.
75
75
 
package/dist/index.js CHANGED
@@ -6449,6 +6449,7 @@ function readPortalCredentials() {
6449
6449
  portalUrl: parsed.portalUrl,
6450
6450
  token: parsed.token,
6451
6451
  workspace: parsed.workspace,
6452
+ email: parsed.email,
6452
6453
  createdAt: parsed.createdAt ?? ""
6453
6454
  };
6454
6455
  } catch {
@@ -6677,22 +6678,22 @@ exec "${otelHookCommand}" "$@"
6677
6678
  }
6678
6679
  return wrapperPath;
6679
6680
  }
6680
- function commandExists(command2) {
6681
- const result = (0, import_child_process9.spawnSync)("sh", ["-c", `command -v ${JSON.stringify(command2)}`], {
6681
+ function commandExists2(command2) {
6682
+ const result = (0, import_child_process10.spawnSync)("sh", ["-c", `command -v ${JSON.stringify(command2)}`], {
6682
6683
  encoding: "utf8"
6683
6684
  });
6684
6685
  return result.status === 0 && Boolean(result.stdout?.trim());
6685
6686
  }
6686
6687
  function resolveOtelHookCommand() {
6687
- if (commandExists("otel-hook")) {
6688
- const which = (0, import_child_process9.spawnSync)("sh", ["-c", "command -v otel-hook"], { encoding: "utf8" });
6688
+ if (commandExists2("otel-hook")) {
6689
+ const which = (0, import_child_process10.spawnSync)("sh", ["-c", "command -v otel-hook"], { encoding: "utf8" });
6689
6690
  return which.stdout?.trim() || "otel-hook";
6690
6691
  }
6691
6692
  return null;
6692
6693
  }
6693
6694
  function tryInstallPackage() {
6694
- if (commandExists("uv")) {
6695
- const result = (0, import_child_process9.spawnSync)(
6695
+ if (commandExists2("uv")) {
6696
+ const result = (0, import_child_process10.spawnSync)(
6696
6697
  "uv",
6697
6698
  ["tool", "install", "--force", OTEL_HOOKS_PACKAGE],
6698
6699
  { encoding: "utf8", timeout: 18e4 }
@@ -6701,8 +6702,8 @@ function tryInstallPackage() {
6701
6702
  return { ok: true, detail: `uv tool install ${OTEL_HOOKS_PACKAGE}` };
6702
6703
  }
6703
6704
  }
6704
- if (commandExists("pipx")) {
6705
- const result = (0, import_child_process9.spawnSync)("pipx", ["install", "--force", OTEL_HOOKS_PACKAGE], {
6705
+ if (commandExists2("pipx")) {
6706
+ const result = (0, import_child_process10.spawnSync)("pipx", ["install", "--force", OTEL_HOOKS_PACKAGE], {
6706
6707
  encoding: "utf8",
6707
6708
  timeout: 18e4
6708
6709
  });
@@ -6714,9 +6715,9 @@ function tryInstallPackage() {
6714
6715
  detail: `pipx install failed: ${(result.stderr || result.stdout || "").trim().slice(0, 400)}`
6715
6716
  };
6716
6717
  }
6717
- if (commandExists("pip3") || commandExists("pip")) {
6718
- const pip = commandExists("pip3") ? "pip3" : "pip";
6719
- const result = (0, import_child_process9.spawnSync)(
6718
+ if (commandExists2("pip3") || commandExists2("pip")) {
6719
+ const pip = commandExists2("pip3") ? "pip3" : "pip";
6720
+ const result = (0, import_child_process10.spawnSync)(
6720
6721
  pip,
6721
6722
  ["install", "--user", OTEL_HOOKS_PACKAGE],
6722
6723
  { encoding: "utf8", timeout: 18e4 }
@@ -6735,7 +6736,7 @@ function tryInstallPackage() {
6735
6736
  };
6736
6737
  }
6737
6738
  function runSetupAgent(agent, otelHookCommand, hooksHome) {
6738
- const result = (0, import_child_process9.spawnSync)(otelHookCommand, ["setup", "--agent", agent], {
6739
+ const result = (0, import_child_process10.spawnSync)(otelHookCommand, ["setup", "--agent", agent], {
6739
6740
  encoding: "utf8",
6740
6741
  env: {
6741
6742
  ...process.env,
@@ -6885,11 +6886,11 @@ function ensureOtelHooks(options) {
6885
6886
  function disableOtelHooksExport() {
6886
6887
  return writeOtelHooksConfig(buildOtelHooksConfig({ enabled: false }));
6887
6888
  }
6888
- var import_child_process9, fs41, os13, path43, OTEL_HOOKS_PACKAGE, OTEL_HOOKS_AGENTS;
6889
+ var import_child_process10, fs41, os13, path43, OTEL_HOOKS_PACKAGE, OTEL_HOOKS_AGENTS;
6889
6890
  var init_otel_hooks = __esm({
6890
6891
  "src/core/otel-hooks.ts"() {
6891
6892
  "use strict";
6892
- import_child_process9 = require("child_process");
6893
+ import_child_process10 = require("child_process");
6893
6894
  fs41 = __toESM(require("fs"));
6894
6895
  os13 = __toESM(require("os"));
6895
6896
  path43 = __toESM(require("path"));
@@ -49738,6 +49739,81 @@ function addCustomStage(repoPath, options) {
49738
49739
  // src/harness/adaptation-prompt.ts
49739
49740
  var fs22 = __toESM(require("fs"));
49740
49741
  var path21 = __toESM(require("path"));
49742
+ var readline4 = __toESM(require("readline"));
49743
+
49744
+ // src/utils/clipboard.ts
49745
+ var import_child_process2 = require("child_process");
49746
+ function commandExists(command2) {
49747
+ const probe = process.platform === "win32" ? (0, import_child_process2.spawnSync)("where", [command2], { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] }) : (0, import_child_process2.spawnSync)("which", [command2], { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] });
49748
+ return probe.status === 0;
49749
+ }
49750
+ function tryPipeCommand(command2, args, text) {
49751
+ const result = (0, import_child_process2.spawnSync)(command2, args, {
49752
+ input: text,
49753
+ encoding: "utf8",
49754
+ stdio: ["pipe", "ignore", "ignore"]
49755
+ });
49756
+ return result.status === 0 && !result.error;
49757
+ }
49758
+ function writeOsc52Clipboard(text, stream = process.stderr) {
49759
+ if (!("isTTY" in stream) || !stream.isTTY) return false;
49760
+ if (Buffer.byteLength(text, "utf8") > 2e5) return false;
49761
+ const b64 = Buffer.from(text, "utf8").toString("base64");
49762
+ stream.write(`\x1B]52;c;${b64}\x07`);
49763
+ return true;
49764
+ }
49765
+ function copyViaPlatformTools(text) {
49766
+ if (process.platform === "darwin" && commandExists("pbcopy")) {
49767
+ if (tryPipeCommand("pbcopy", [], text)) return { ok: true, method: "pbcopy" };
49768
+ }
49769
+ if (process.platform === "win32") {
49770
+ if (commandExists("powershell")) {
49771
+ const result = (0, import_child_process2.spawnSync)(
49772
+ "powershell",
49773
+ [
49774
+ "-NoProfile",
49775
+ "-NonInteractive",
49776
+ "-Command",
49777
+ "$OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::new(); Set-Clipboard -Value ([Console]::In.ReadToEnd())"
49778
+ ],
49779
+ {
49780
+ input: text,
49781
+ encoding: "utf8",
49782
+ stdio: ["pipe", "ignore", "ignore"]
49783
+ }
49784
+ );
49785
+ if (result.status === 0 && !result.error) return { ok: true, method: "powershell" };
49786
+ }
49787
+ if (commandExists("clip") && tryPipeCommand("clip", [], text)) {
49788
+ return { ok: true, method: "clip" };
49789
+ }
49790
+ }
49791
+ if (process.platform === "linux" || process.platform === "freebsd" || process.platform === "openbsd") {
49792
+ if (commandExists("wl-copy") && tryPipeCommand("wl-copy", [], text)) {
49793
+ return { ok: true, method: "wl-copy" };
49794
+ }
49795
+ if (commandExists("xclip") && tryPipeCommand("xclip", ["-selection", "clipboard"], text)) {
49796
+ return { ok: true, method: "xclip" };
49797
+ }
49798
+ if (commandExists("xsel") && tryPipeCommand("xsel", ["--clipboard", "--input"], text)) {
49799
+ return { ok: true, method: "xsel" };
49800
+ }
49801
+ }
49802
+ return {
49803
+ ok: false,
49804
+ detail: "No system clipboard command available (pbcopy, wl-copy/xclip/xsel, or clip)"
49805
+ };
49806
+ }
49807
+ function copyTextToClipboard(text) {
49808
+ const platform = copyViaPlatformTools(text);
49809
+ if (platform.ok) return platform;
49810
+ if (writeOsc52Clipboard(text)) {
49811
+ return { ok: true, method: "osc52" };
49812
+ }
49813
+ return platform;
49814
+ }
49815
+
49816
+ // src/harness/adaptation-prompt.ts
49741
49817
  var ADAPTATION_PROMPT_FILE = "ADAPT-PROMPT.md";
49742
49818
  var PROFILE_HINTS = {
49743
49819
  default: "Web app profile (SaaS/full-stack) \u2014 Docker Compose for shared infra (HARNESS_INFRA_SERVICES), PM2 for the primary application only, git worktree per agent slot by default. Launch provisions toolchain via harness.env (HARNESS_ECOSYSTEM, HARNESS_INSTALL_CMD) and writes paths to .env.agent.<id>. Identify the primary app agents modify; run supporting services shared.",
@@ -49788,11 +49864,47 @@ function printAdaptationPrompt(content) {
49788
49864
  `);
49789
49865
  process.stderr.write("\n");
49790
49866
  }
49867
+ function askYesNo3(question) {
49868
+ const rl = readline4.createInterface({ input: process.stdin, output: process.stderr });
49869
+ return new Promise((resolve38) => {
49870
+ process.stderr.write(`${question} `);
49871
+ rl.once("line", (answer) => {
49872
+ rl.close();
49873
+ const trimmed = answer.trim();
49874
+ resolve38(trimmed === "" || /^y(es)?$/i.test(trimmed));
49875
+ });
49876
+ });
49877
+ }
49878
+ async function offerAdaptationPromptClipboard(content, options = {}) {
49879
+ const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
49880
+ if (!interactive && !options.autoYes) {
49881
+ info("Prompt also saved to .har/ADAPT-PROMPT.md (open or copy from there)");
49882
+ return false;
49883
+ }
49884
+ let shouldCopy = options.autoYes === true;
49885
+ if (!shouldCopy && interactive) {
49886
+ shouldCopy = await askYesNo3("Copy adaptation prompt to clipboard for your coding agent? [Y/n]");
49887
+ }
49888
+ if (!shouldCopy) {
49889
+ info("Skipped clipboard copy \u2014 prompt is in .har/ADAPT-PROMPT.md");
49890
+ return false;
49891
+ }
49892
+ const result = copyTextToClipboard(content);
49893
+ if (result.ok) {
49894
+ success(
49895
+ result.method === "osc52" ? "Copied adaptation prompt to clipboard (terminal OSC 52) \u2014 paste into your coding agent" : "Copied adaptation prompt to clipboard \u2014 paste into your coding agent"
49896
+ );
49897
+ return true;
49898
+ }
49899
+ warn(`Could not copy to clipboard: ${result.detail}`);
49900
+ info("Open .har/ADAPT-PROMPT.md and paste that into your coding agent instead");
49901
+ return false;
49902
+ }
49791
49903
 
49792
49904
  // src/harness/cursor-rule.ts
49793
49905
  var fs23 = __toESM(require("fs"));
49794
49906
  var path22 = __toESM(require("path"));
49795
- var readline4 = __toESM(require("readline"));
49907
+ var readline5 = __toESM(require("readline"));
49796
49908
  var CURSOR_RULE_RELATIVE_PATH = ".cursor/rules/har-workflow.mdc";
49797
49909
  function getCursorRulePath(repoPath) {
49798
49910
  return path22.join(repoPath, CURSOR_RULE_RELATIVE_PATH);
@@ -49842,10 +49954,10 @@ async function handleCursorRule(options) {
49842
49954
  }
49843
49955
  async function promptScaffoldCursorRule(exists) {
49844
49956
  const action = exists ? "Update" : "Create";
49845
- return askYesNo3(`${action} ${CURSOR_RULE_RELATIVE_PATH}? [Y/n]`);
49957
+ return askYesNo4(`${action} ${CURSOR_RULE_RELATIVE_PATH}? [Y/n]`);
49846
49958
  }
49847
- async function askYesNo3(question) {
49848
- const rl = readline4.createInterface({ input: process.stdin, output: process.stderr });
49959
+ async function askYesNo4(question) {
49960
+ const rl = readline5.createInterface({ input: process.stdin, output: process.stderr });
49849
49961
  return new Promise((resolve38) => {
49850
49962
  process.stderr.write(`${question} `);
49851
49963
  rl.once("line", (answer) => {
@@ -50283,22 +50395,25 @@ function listRegisteredRepos() {
50283
50395
  return kept;
50284
50396
  }
50285
50397
 
50398
+ // src/core/control-sync.ts
50399
+ init_portal_credentials();
50400
+
50286
50401
  // src/core/slot-status.ts
50287
50402
  var fs33 = __toESM(require("fs"));
50288
50403
  var os8 = __toESM(require("os"));
50289
50404
  var path33 = __toESM(require("path"));
50290
- var import_child_process7 = require("child_process");
50405
+ var import_child_process8 = require("child_process");
50291
50406
 
50292
50407
  // src/core/runs.ts
50293
50408
  var crypto2 = __toESM(require("crypto"));
50294
- var import_child_process2 = require("child_process");
50409
+ var import_child_process3 = require("child_process");
50295
50410
  var fs29 = __toESM(require("fs"));
50296
50411
  var os6 = __toESM(require("os"));
50297
50412
  var path28 = __toESM(require("path"));
50298
50413
  var RUNS_DIR = "runs";
50299
50414
  function gitCommonDir(cwd) {
50300
50415
  try {
50301
- const out = (0, import_child_process2.execSync)("git rev-parse --git-common-dir", {
50416
+ const out = (0, import_child_process3.execSync)("git rev-parse --git-common-dir", {
50302
50417
  cwd,
50303
50418
  encoding: "utf8",
50304
50419
  stdio: ["pipe", "pipe", "ignore"]
@@ -50315,7 +50430,7 @@ function sameGitCheckout(a2, b2) {
50315
50430
  }
50316
50431
  function gitPrefix(cwd) {
50317
50432
  try {
50318
- return (0, import_child_process2.execSync)("git rev-parse --show-prefix", {
50433
+ return (0, import_child_process3.execSync)("git rev-parse --show-prefix", {
50319
50434
  cwd,
50320
50435
  encoding: "utf8",
50321
50436
  stdio: ["pipe", "pipe", "ignore"]
@@ -50484,13 +50599,13 @@ function listRuns(repoPath, filter2 = {}) {
50484
50599
  var path32 = __toESM(require("path"));
50485
50600
 
50486
50601
  // src/core/control-port.ts
50487
- var import_child_process4 = require("child_process");
50602
+ var import_child_process5 = require("child_process");
50488
50603
  var fs31 = __toESM(require("fs"));
50489
50604
  var path30 = __toESM(require("path"));
50490
50605
  init_control_config();
50491
50606
 
50492
50607
  // src/core/slot-ports.ts
50493
- var import_child_process3 = require("child_process");
50608
+ var import_child_process4 = require("child_process");
50494
50609
  var fs30 = __toESM(require("fs"));
50495
50610
  var path29 = __toESM(require("path"));
50496
50611
  function portStep(env3) {
@@ -50504,7 +50619,7 @@ function slotPortLaneEnd(defaultPort, step) {
50504
50619
  }
50505
50620
  function isPortInUse(port, host = "127.0.0.1") {
50506
50621
  try {
50507
- (0, import_child_process3.execSync)(`bash -c 'exec 3<>/dev/tcp/${host}/${port}'`, { stdio: "ignore" });
50622
+ (0, import_child_process4.execSync)(`bash -c 'exec 3<>/dev/tcp/${host}/${port}'`, { stdio: "ignore" });
50508
50623
  return true;
50509
50624
  } catch {
50510
50625
  return false;
@@ -50575,7 +50690,7 @@ function controlContainerOnPort(containers, port) {
50575
50690
  }
50576
50691
  function listDockerContainers() {
50577
50692
  try {
50578
- const raw = (0, import_child_process4.execSync)("docker ps --format {{.Names}}\\t{{.Ports}}", {
50693
+ const raw = (0, import_child_process5.execSync)("docker ps --format {{.Names}}\\t{{.Ports}}", {
50579
50694
  encoding: "utf8",
50580
50695
  stdio: ["pipe", "pipe", "ignore"],
50581
50696
  timeout: 3e3
@@ -50643,10 +50758,10 @@ function inspectControlUpReadiness(repoPath) {
50643
50758
  var fs32 = __toESM(require("fs"));
50644
50759
  var os7 = __toESM(require("os"));
50645
50760
  var path31 = __toESM(require("path"));
50646
- var import_child_process5 = require("child_process");
50761
+ var import_child_process6 = require("child_process");
50647
50762
  function runGit(cwd, args) {
50648
50763
  try {
50649
- return (0, import_child_process5.execSync)(`git ${args}`, {
50764
+ return (0, import_child_process6.execSync)(`git ${args}`, {
50650
50765
  cwd,
50651
50766
  encoding: "utf8",
50652
50767
  stdio: ["pipe", "pipe", "ignore"]
@@ -50812,10 +50927,10 @@ function checkLaunchGuard(repoPath, agentId, options = {}) {
50812
50927
  }
50813
50928
 
50814
50929
  // src/core/slot-preflight.ts
50815
- var import_child_process6 = require("child_process");
50930
+ var import_child_process7 = require("child_process");
50816
50931
  function listPm2Processes() {
50817
50932
  try {
50818
- const raw = (0, import_child_process6.execSync)("npx --yes pm2 jlist", {
50933
+ const raw = (0, import_child_process7.execSync)("npx --yes pm2 jlist", {
50819
50934
  encoding: "utf8",
50820
50935
  stdio: ["pipe", "pipe", "ignore"],
50821
50936
  timeout: 3e3
@@ -51039,7 +51154,7 @@ function formatPreflightReport(agentId, readiness) {
51039
51154
  var BYPASS_WARNING_MS = 15 * 60 * 1e3;
51040
51155
  function readGitRemote(repoPath) {
51041
51156
  try {
51042
- const remote = (0, import_child_process7.execSync)("git remote get-url origin", {
51157
+ const remote = (0, import_child_process8.execSync)("git remote get-url origin", {
51043
51158
  cwd: repoPath,
51044
51159
  encoding: "utf8",
51045
51160
  stdio: ["pipe", "pipe", "ignore"]
@@ -51051,7 +51166,7 @@ function readGitRemote(repoPath) {
51051
51166
  }
51052
51167
  function runGit2(cwd, args) {
51053
51168
  try {
51054
- return (0, import_child_process7.execSync)(`git ${args}`, {
51169
+ return (0, import_child_process8.execSync)(`git ${args}`, {
51055
51170
  cwd,
51056
51171
  encoding: "utf8",
51057
51172
  stdio: ["pipe", "pipe", "ignore"]
@@ -51127,7 +51242,7 @@ function lastBuildPass(run2) {
51127
51242
  }
51128
51243
  function listPm2Processes2() {
51129
51244
  try {
51130
- const raw = (0, import_child_process7.execSync)("npx --yes pm2 jlist", {
51245
+ const raw = (0, import_child_process8.execSync)("npx --yes pm2 jlist", {
51131
51246
  encoding: "utf8",
51132
51247
  stdio: ["pipe", "pipe", "ignore"],
51133
51248
  timeout: 3e3
@@ -51722,6 +51837,7 @@ function extractClaudeUsageFromRecords(records) {
51722
51837
  let tokensCacheRead = 0;
51723
51838
  let tokensCacheCreation = 0;
51724
51839
  let costUsd = null;
51840
+ const modelBreakdown = {};
51725
51841
  for (const record2 of records) {
51726
51842
  if (!record2 || typeof record2 !== "object") continue;
51727
51843
  const payload = record2;
@@ -51733,15 +51849,35 @@ function extractClaudeUsageFromRecords(records) {
51733
51849
  tokensCacheCreation = Number(usage2.cache_creation_input_tokens ?? 0);
51734
51850
  if (payload.total_cost_usd != null) costUsd = Number(payload.total_cost_usd);
51735
51851
  }
51736
- const nestedUsage = payload.usage ?? payload.message?.usage;
51852
+ const message = payload.message;
51853
+ const nestedUsage = payload.usage ?? message?.usage;
51737
51854
  if (nestedUsage && payload.type !== "result") {
51738
- tokensInput += Number(nestedUsage.input_tokens ?? 0);
51739
- tokensOutput += Number(nestedUsage.output_tokens ?? 0);
51740
- tokensCacheRead += Number(nestedUsage.cache_read_input_tokens ?? 0);
51741
- tokensCacheCreation += Number(nestedUsage.cache_creation_input_tokens ?? 0);
51855
+ const i2 = Number(nestedUsage.input_tokens ?? 0);
51856
+ const o2 = Number(nestedUsage.output_tokens ?? 0);
51857
+ const cr2 = Number(nestedUsage.cache_read_input_tokens ?? 0);
51858
+ const cc = Number(nestedUsage.cache_creation_input_tokens ?? 0);
51859
+ tokensInput += i2;
51860
+ tokensOutput += o2;
51861
+ tokensCacheRead += cr2;
51862
+ tokensCacheCreation += cc;
51863
+ const model = typeof message?.model === "string" ? message.model : void 0;
51864
+ if (model && model !== "<synthetic>") {
51865
+ const totals = modelBreakdown[model] ??= {
51866
+ tokensInput: 0,
51867
+ tokensOutput: 0,
51868
+ tokensCacheRead: 0,
51869
+ tokensCacheCreation: 0,
51870
+ tokensTotal: 0
51871
+ };
51872
+ totals.tokensInput += i2;
51873
+ totals.tokensOutput += o2;
51874
+ totals.tokensCacheRead += cr2;
51875
+ totals.tokensCacheCreation += cc;
51876
+ totals.tokensTotal += i2 + o2 + cr2 + cc;
51877
+ }
51742
51878
  }
51743
51879
  }
51744
- return { tokensInput, tokensOutput, tokensCacheRead, tokensCacheCreation, costUsd };
51880
+ return { tokensInput, tokensOutput, tokensCacheRead, tokensCacheCreation, costUsd, modelBreakdown };
51745
51881
  }
51746
51882
  function findMatchingClaudeTranscripts(slot) {
51747
51883
  const targets = [slot.workDir, slot.worktreePath].filter(Boolean);
@@ -51848,6 +51984,7 @@ function harvestClaudeUsage(slot) {
51848
51984
  tokensCacheCreation: best.tokensCacheCreation,
51849
51985
  tokensTotal,
51850
51986
  costUsd: best.costUsd,
51987
+ ...Object.keys(best.modelBreakdown).length > 0 ? { modelBreakdown: best.modelBreakdown } : {},
51851
51988
  sources: ["harvest"],
51852
51989
  firstSeenAt: slot.sessionCreatedAt ?? now,
51853
51990
  lastSeenAt: now
@@ -52009,9 +52146,9 @@ async function postJson(url, body, dryRun) {
52009
52146
  }
52010
52147
  return await response.json();
52011
52148
  }
52012
- async function postPortalSync(target, body, dryRun) {
52149
+ async function postPortal(target, endpoint, body, dryRun) {
52013
52150
  if (dryRun) return;
52014
- const response = await fetch(`${target.url}/api/sync`, {
52151
+ const response = await fetch(`${target.url}${endpoint}`, {
52015
52152
  method: "POST",
52016
52153
  headers: {
52017
52154
  "Content-Type": "application/json",
@@ -52029,21 +52166,98 @@ async function postPortalSync(target, body, dryRun) {
52029
52166
  throw new Error(`har-portal sync failed: HTTP ${response.status}${text ? ` \u2014 ${text}` : ""}`);
52030
52167
  }
52031
52168
  }
52032
- function buildPortalSyncBody(repoPath) {
52169
+ function resolvePortalUserEmail() {
52170
+ return readPortalCredentials()?.email || void 0;
52171
+ }
52172
+ function dropNullFields(record2) {
52173
+ return Object.fromEntries(Object.entries(record2).filter(([, value]) => value !== null));
52174
+ }
52175
+ function modelsFromBreakdown(breakdown) {
52176
+ if (!breakdown) return [];
52177
+ return Object.entries(breakdown).map(([model, totals]) => ({
52178
+ model,
52179
+ ...totals && typeof totals === "object" ? totals : {}
52180
+ }));
52181
+ }
52182
+ function collectPortalTelemetry(repoPath, slots) {
52183
+ if (!isTelemetryEnabled()) return { usage: [], events: [] };
52184
+ try {
52185
+ const userEmail = resolvePortalUserEmail();
52186
+ const usage2 = slots.flatMap(
52187
+ (slot) => harvestUsageForSlot({
52188
+ agentId: slot.agentId,
52189
+ workDir: slot.workDir,
52190
+ worktreePath: slot.worktreePath,
52191
+ branch: slot.branch,
52192
+ suffix: slot.suffix,
52193
+ sessionCreatedAt: slot.sessionCreatedAt,
52194
+ repoPath
52195
+ }).map((row) => {
52196
+ const models = modelsFromBreakdown(
52197
+ row.modelBreakdown
52198
+ );
52199
+ return {
52200
+ ...row,
52201
+ workUnitId: slot.workUnitId ?? row.workUnitId,
52202
+ attemptId: slot.attemptId ?? row.attemptId,
52203
+ sessionKey: row.sessionKey || buildSessionKey({
52204
+ branch: slot.branch,
52205
+ agentId: slot.agentId,
52206
+ suffix: slot.suffix,
52207
+ createdAt: slot.sessionCreatedAt
52208
+ }),
52209
+ ...userEmail ? { userEmail } : {},
52210
+ ...models.length > 0 ? { models } : {}
52211
+ };
52212
+ })
52213
+ );
52214
+ const events = slots.flatMap(
52215
+ (slot) => harvestEventsForSlot({
52216
+ agentId: slot.agentId,
52217
+ workDir: slot.workDir,
52218
+ worktreePath: slot.worktreePath,
52219
+ branch: slot.branch,
52220
+ suffix: slot.suffix,
52221
+ sessionCreatedAt: slot.sessionCreatedAt,
52222
+ repoPath
52223
+ }).map(
52224
+ (event) => dropNullFields({
52225
+ ...event,
52226
+ workUnitId: slot.workUnitId ?? event.workUnitId,
52227
+ attemptId: slot.attemptId ?? event.attemptId
52228
+ })
52229
+ )
52230
+ );
52231
+ return { usage: usage2, events };
52232
+ } catch {
52233
+ return { usage: [], events: [] };
52234
+ }
52235
+ }
52236
+ function buildPortalPayload(repoPath) {
52033
52237
  const runs = listRuns(repoPath);
52034
52238
  const status = collectEnvironmentStatus(repoPath);
52035
52239
  const manifest = readManifest(repoPath);
52036
52240
  const stagesRegistry = readStageRegistry(repoPath);
52037
- const validations = listValidations(resolveHarnessRoot(repoPath));
52038
- return {
52241
+ const harnessRoot = resolveHarnessRoot(repoPath);
52242
+ const validations = listValidations(harnessRoot);
52243
+ const workUnits = listWorkUnits(harnessRoot);
52244
+ const attempts = listWorkAttempts(harnessRoot);
52245
+ const validationBindings = listValidationBindings(harnessRoot);
52246
+ const { usage: usage2, events } = collectPortalTelemetry(repoPath, status.slots);
52247
+ const syncBody = {
52039
52248
  path: repoPath,
52040
52249
  ...manifest ? { manifest } : {},
52041
52250
  ...stagesRegistry ? { stagesRegistry } : {},
52042
52251
  runs,
52043
52252
  slots: status.slots,
52044
52253
  generatedAt: status.generatedAt,
52045
- ...validations.length > 0 ? { validations } : {}
52254
+ ...validations.length > 0 ? { validations } : {},
52255
+ ...workUnits.length > 0 ? { workUnits } : {},
52256
+ ...attempts.length > 0 ? { attempts } : {},
52257
+ ...validationBindings.length > 0 ? { validationBindings } : {},
52258
+ ...usage2.length > 0 ? { usage: usage2 } : {}
52046
52259
  };
52260
+ return { syncBody, events };
52047
52261
  }
52048
52262
  async function isControlApiReachable(apiUrl = getControlApiUrl()) {
52049
52263
  try {
@@ -52161,7 +52375,16 @@ async function syncRepoWithControl(options) {
52161
52375
  }
52162
52376
  const portal = getPortalTarget();
52163
52377
  if (portal) {
52164
- await postPortalSync(portal, buildPortalSyncBody(repoPath), options.dryRun);
52378
+ const { syncBody, events } = buildPortalPayload(repoPath);
52379
+ await postPortal(portal, "/api/sync", syncBody, options.dryRun);
52380
+ if (events.length > 0) {
52381
+ await postPortal(
52382
+ portal,
52383
+ "/api/otel",
52384
+ { path: repoPath, events, spans: [] },
52385
+ options.dryRun
52386
+ );
52387
+ }
52165
52388
  return;
52166
52389
  }
52167
52390
  const registerResult = await registerRepoWithControl({ ...options, repoPath });
@@ -52322,7 +52545,7 @@ function checkLaunchGuard2(repoPath, agentId, options = {}) {
52322
52545
  init_control_config();
52323
52546
 
52324
52547
  // src/core/control-lifecycle.ts
52325
- var import_child_process8 = require("child_process");
52548
+ var import_child_process9 = require("child_process");
52326
52549
  var path42 = __toESM(require("path"));
52327
52550
 
52328
52551
  // src/core/control-image.ts
@@ -52373,7 +52596,7 @@ function buildDockerRunArgs(options) {
52373
52596
  return args;
52374
52597
  }
52375
52598
  function runDocker(args) {
52376
- const result = (0, import_child_process8.spawnSync)("docker", args, {
52599
+ const result = (0, import_child_process9.spawnSync)("docker", args, {
52377
52600
  stdio: "inherit",
52378
52601
  env: process.env
52379
52602
  });
@@ -52383,7 +52606,7 @@ function runDocker(args) {
52383
52606
  return result.status ?? 1;
52384
52607
  }
52385
52608
  function controlContainerExists(name = CONTROL_CONTAINER_NAME) {
52386
- const result = (0, import_child_process8.spawnSync)("docker", ["ps", "-aq", "-f", `name=^${name}$`], {
52609
+ const result = (0, import_child_process9.spawnSync)("docker", ["ps", "-aq", "-f", `name=^${name}$`], {
52387
52610
  encoding: "utf8",
52388
52611
  stdio: ["pipe", "pipe", "ignore"]
52389
52612
  });
@@ -52994,7 +53217,7 @@ var getEnvironmentStatus = defaultRunService.getEnvironmentStatus.bind(defaultRu
52994
53217
  var getEnvironmentLogs = defaultRunService.getEnvironmentLogs.bind(defaultRunService);
52995
53218
 
52996
53219
  // src/core/commit-gate-onboarding.ts
52997
- var readline5 = __toESM(require("readline"));
53220
+ var readline6 = __toESM(require("readline"));
52998
53221
 
52999
53222
  // src/core/hooks.ts
53000
53223
  var fs42 = __toESM(require("fs"));
@@ -53262,8 +53485,8 @@ async function recordCommitAssociation(cwd) {
53262
53485
  }
53263
53486
 
53264
53487
  // src/core/commit-gate-onboarding.ts
53265
- function askYesNo4(question) {
53266
- const rl = readline5.createInterface({ input: process.stdin, output: process.stderr });
53488
+ function askYesNo5(question) {
53489
+ const rl = readline6.createInterface({ input: process.stdin, output: process.stderr });
53267
53490
  return new Promise((resolve38) => {
53268
53491
  process.stderr.write(`${question} `);
53269
53492
  rl.once("line", (answer) => {
@@ -53294,7 +53517,7 @@ async function handleCommitGateOnboarding(options) {
53294
53517
  const status = getHooksStatus(options.repoPath);
53295
53518
  let shouldInstall = status.preCommitInstalled || status.postCommitInstalled || options.install === "always" || options.autoYes === true;
53296
53519
  if (!shouldInstall && options.install === "prompt" && process.stdin.isTTY && process.stdout.isTTY) {
53297
- shouldInstall = await askYesNo4("Install the HAR commit gate for this repository? [Y/n]");
53520
+ shouldInstall = await askYesNo5("Install the HAR commit gate for this repository? [Y/n]");
53298
53521
  }
53299
53522
  if (!shouldInstall) {
53300
53523
  info("Commit gate is not installed; run `har hooks install` when ready.");
@@ -53664,7 +53887,7 @@ async function handleInit(argv) {
53664
53887
  if (argv.auto) {
53665
53888
  await handleAgentMdProposal(repoPath, argv.yes);
53666
53889
  } else {
53667
- emitManualAdaptationPrompt(repoPath, "init", argv.profile);
53890
+ await emitManualAdaptationPrompt(repoPath, "init", argv.profile, void 0, argv.yes);
53668
53891
  }
53669
53892
  divider();
53670
53893
  success("Harness initialized!");
@@ -53736,7 +53959,13 @@ async function handleMaintain(argv) {
53736
53959
  if (!result.validation.pass) {
53737
53960
  warn("Harness has validation errors \u2014 fix them before running --finalize.");
53738
53961
  }
53739
- emitManualAdaptationPrompt(repoPath, "maintain", "default", result.bundle?.report);
53962
+ await emitManualAdaptationPrompt(
53963
+ repoPath,
53964
+ "maintain",
53965
+ "default",
53966
+ result.bundle?.report,
53967
+ argv.yes
53968
+ );
53740
53969
  info("After your coding agent finishes adapting, record it with: har env maintain --finalize");
53741
53970
  }
53742
53971
  divider();
@@ -53788,7 +54017,7 @@ function resolveOnboardingOptions(argv) {
53788
54017
  }
53789
54018
  };
53790
54019
  }
53791
- function emitManualAdaptationPrompt(repoPath, mode, profile = "default", bundleReport) {
54020
+ async function emitManualAdaptationPrompt(repoPath, mode, profile = "default", bundleReport, autoYes = false) {
53792
54021
  const prompt2 = mode === "init" ? buildInitAdaptationPrompt(repoPath, profile) : buildMaintainAdaptationPrompt(repoPath, bundleReport);
53793
54022
  writeAdaptationPrompt(repoPath, prompt2);
53794
54023
  process.stderr.write("\n");
@@ -53804,6 +54033,7 @@ function emitManualAdaptationPrompt(repoPath, mode, profile = "default", bundleR
53804
54033
  info(" Reference templates are in .har/maintain/templates/");
53805
54034
  }
53806
54035
  printAdaptationPrompt(prompt2);
54036
+ await offerAdaptationPromptClipboard(prompt2, { autoYes });
53807
54037
  }
53808
54038
  async function handleAgentMdProposal(repoPath, autoYes) {
53809
54039
  if (autoYes) {
@@ -53923,8 +54153,8 @@ async function handleLaunch(argv) {
53923
54153
  if (!confirmReplace && process.stdin.isTTY && process.stdout.isTTY) {
53924
54154
  warn("Occupied slot \u2014 review before replacing:");
53925
54155
  console.error(guard.reason ?? "");
53926
- const readline9 = await import("readline");
53927
- const rl = readline9.createInterface({ input: process.stdin, output: process.stderr });
54156
+ const readline10 = await import("readline");
54157
+ const rl = readline10.createInterface({ input: process.stdin, output: process.stderr });
53928
54158
  const answer = await new Promise((resolve38) => {
53929
54159
  rl.question("Replace this slot? [y/N] ", resolve38);
53930
54160
  });
@@ -53940,8 +54170,8 @@ async function handleLaunch(argv) {
53940
54170
  if (!guardAfterConfirm.allowed && guardAfterConfirm.blocked && guardAfterConfirm.slot?.dirty) {
53941
54171
  if (!force && process.stdin.isTTY && process.stdout.isTTY) {
53942
54172
  warn("Worktree has uncommitted changes.");
53943
- const readline9 = await import("readline");
53944
- const rl = readline9.createInterface({ input: process.stdin, output: process.stderr });
54173
+ const readline10 = await import("readline");
54174
+ const rl = readline10.createInterface({ input: process.stdin, output: process.stderr });
53945
54175
  const answer = await new Promise((resolve38) => {
53946
54176
  rl.question("Discard uncommitted changes? [y/N] ", resolve38);
53947
54177
  });
@@ -54146,7 +54376,7 @@ function printNextSteps(auto2) {
54146
54376
  console.error("");
54147
54377
  console.error(" Read: .har/README.md");
54148
54378
  if (!auto2) {
54149
- console.error(" Adapt: paste prompt above into your coding agent");
54379
+ console.error(" Adapt: paste clipboard / prompt above into your coding agent");
54150
54380
  console.error(" Prompt file: .har/ADAPT-PROMPT.md");
54151
54381
  } else {
54152
54382
  console.error(" Agent guide: AGENT.md (repo root, if applied)");
@@ -54167,13 +54397,13 @@ init_portal_credentials();
54167
54397
  // src/core/portal-login.ts
54168
54398
  var http = __toESM(require("http"));
54169
54399
  var import_crypto = require("crypto");
54170
- var import_child_process10 = require("child_process");
54400
+ var import_child_process11 = require("child_process");
54171
54401
  var CALLBACK_PATH = "/callback";
54172
54402
  var LOGIN_TIMEOUT_MS = 3 * 6e4;
54173
54403
  function openBrowser(url) {
54174
54404
  const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
54175
54405
  try {
54176
- const child = (0, import_child_process10.spawn)(cmd, args, { stdio: "ignore", detached: true });
54406
+ const child = (0, import_child_process11.spawn)(cmd, args, { stdio: "ignore", detached: true });
54177
54407
  child.on("error", () => {
54178
54408
  });
54179
54409
  child.unref();
@@ -54194,6 +54424,7 @@ async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
54194
54424
  const token = requestUrl.searchParams.get("token");
54195
54425
  const returnedState = requestUrl.searchParams.get("state");
54196
54426
  const workspace = requestUrl.searchParams.get("workspace") ?? void 0;
54427
+ const email2 = requestUrl.searchParams.get("email") ?? void 0;
54197
54428
  if (returnedState !== state || !token) {
54198
54429
  res.writeHead(400, { "Content-Type": "text/html" });
54199
54430
  res.end("<h1>Login failed</h1><p>You can close this tab and try again.</p>");
@@ -54204,7 +54435,7 @@ async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
54204
54435
  res.writeHead(200, { "Content-Type": "text/html" });
54205
54436
  res.end("<h1>Logged in</h1><p>You can close this tab and return to the terminal.</p>");
54206
54437
  cleanup();
54207
- resolve38({ portalUrl, token, workspace, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
54438
+ resolve38({ portalUrl, token, workspace, email: email2, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
54208
54439
  });
54209
54440
  const cleanup = () => {
54210
54441
  clearTimeout(timer);
@@ -54287,10 +54518,10 @@ function listUnregisterWorktreeCandidates(repoPath) {
54287
54518
  }
54288
54519
  return [...byPath.values()].sort((a2, b2) => a2.agentId - b2.agentId);
54289
54520
  }
54290
- async function askYesNo5(question) {
54521
+ async function askYesNo6(question) {
54291
54522
  if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
54292
- const readline9 = await import("readline");
54293
- const rl = readline9.createInterface({ input: process.stdin, output: process.stderr });
54523
+ const readline10 = await import("readline");
54524
+ const rl = readline10.createInterface({ input: process.stdin, output: process.stderr });
54294
54525
  const answer = await new Promise((resolve38) => {
54295
54526
  rl.question(question, resolve38);
54296
54527
  });
@@ -54304,7 +54535,7 @@ async function confirmUnregister(options) {
54304
54535
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
54305
54536
  throw new Error("Non-interactive terminal \u2014 pass --yes (and optionally --delete-worktrees)");
54306
54537
  }
54307
- const proceed = await askYesNo5("Unregister this repository from Mission Control? [y/N] ");
54538
+ const proceed = await askYesNo6("Unregister this repository from Mission Control? [y/N] ");
54308
54539
  if (!proceed) return { proceed: false, deleteWorktrees: false };
54309
54540
  const existing = options.worktrees.filter((w2) => w2.exists);
54310
54541
  if (existing.length === 0) {
@@ -54316,7 +54547,7 @@ async function confirmUnregister(options) {
54316
54547
  process.stderr.write(` agent-${wt2.agentId}: ${wt2.worktreePath}${dirty}
54317
54548
  `);
54318
54549
  }
54319
- const deleteWorktrees = await askYesNo5(
54550
+ const deleteWorktrees = await askYesNo6(
54320
54551
  `Also delete ${existing.length} session worktree(s) on disk? [y/N] `
54321
54552
  );
54322
54553
  return { proceed: true, deleteWorktrees };
@@ -62766,7 +62997,7 @@ function getStore() {
62766
62997
  }
62767
62998
  return store;
62768
62999
  }
62769
- function readline6() {
63000
+ function readline7() {
62770
63001
  return getStore().rl;
62771
63002
  }
62772
63003
  function withUpdates(fn) {
@@ -62811,7 +63042,7 @@ var effectScheduler = {
62811
63042
  const { index } = store;
62812
63043
  store.hooksEffect.push(() => {
62813
63044
  store.hooksCleanup[index]?.();
62814
- const cleanFn = cb(readline6());
63045
+ const cleanFn = cb(readline7());
62815
63046
  if (cleanFn != null && typeof cleanFn !== "function") {
62816
63047
  throw new ValidationError("useEffect return value must be a cleanup function or nothing.");
62817
63048
  }
@@ -63287,7 +63518,7 @@ function breakLines(content, width) {
63287
63518
  return content.split("\n").flatMap((line) => (0, import_wrap_ansi.default)(line, width, { trim: false, hard: true }).split("\n").map((str) => str.trimEnd())).join("\n");
63288
63519
  }
63289
63520
  function readlineWidth() {
63290
- return (0, import_cli_width.default)({ defaultWidth: 80, output: readline6().output });
63521
+ return (0, import_cli_width.default)({ defaultWidth: 80, output: readline7().output });
63291
63522
  }
63292
63523
 
63293
63524
  // node_modules/@inquirer/core/dist/esm/lib/pagination/lines.mjs
@@ -63379,7 +63610,7 @@ function usePagination({ items, active, renderItem, pageSize, loop = true }) {
63379
63610
  }
63380
63611
 
63381
63612
  // node_modules/@inquirer/core/dist/esm/lib/create-prompt.mjs
63382
- var readline7 = __toESM(require("node:readline"), 1);
63613
+ var readline8 = __toESM(require("node:readline"), 1);
63383
63614
  var import_node_async_hooks3 = require("node:async_hooks");
63384
63615
  var import_mute_stream = __toESM(require_lib3(), 1);
63385
63616
 
@@ -63722,7 +63953,7 @@ function createPrompt(view) {
63722
63953
  const cleanups = /* @__PURE__ */ new Set();
63723
63954
  const output = new import_mute_stream.default();
63724
63955
  output.pipe(context.output ?? process.stdout);
63725
- const rl = readline7.createInterface({
63956
+ const rl = readline8.createInterface({
63726
63957
  terminal: true,
63727
63958
  input,
63728
63959
  output
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osfactory/har",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "LLM-powered CLI for reproducible agent development environments",
5
5
  "keywords": [
6
6
  "har",