@harnessforce/cli 0.1.1 → 0.3.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 +63 -39
  2. package/dist/bin.js +1648 -1190
  3. package/package.json +9 -9
package/dist/bin.js CHANGED
@@ -1,18 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import { constants, homedir, tmpdir } from "node:os";
4
- import { createInterface } from "node:readline/promises";
5
4
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
6
5
  import { basename, dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
7
- import { createHash, randomBytes, randomUUID } from "node:crypto";
8
- import { access, chmod, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
6
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
7
+ import { access, chmod, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
9
8
  import { execFile, spawn } from "node:child_process";
10
9
  import { constants as constants$1, createReadStream } from "node:fs";
11
10
  import { createServer } from "node:http";
12
- import { ATTR, INTERVENTION_KINDS, LOOP_KINDS, PROPOSAL_KINDS, isToken, normalizeRepository } from "@harnessforce/semconv";
13
- import { createInterface as createInterface$1 } from "node:readline";
11
+ import { ATTR, INTERVENTION_KINDS, LOOP_KINDS, MAX_WORK_LOCATIONS, PROPOSAL_KINDS, isToken, normalizeRepository } from "@harnessforce/semconv";
12
+ import { createInterface } from "node:readline";
14
13
  import { isDeepStrictEqual } from "node:util";
15
- //#region ../agent-core/src/config/canonical.ts
14
+ // ../agent-core/src/config/canonical.ts
16
15
  const byCodeUnit$1 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
17
16
  function canonicalJson(value) {
18
17
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
@@ -29,11 +28,9 @@ function sortComponents(components) {
29
28
  return [...components].sort((a, b) => byCodeUnit$1(a.kind, b.kind) || byCodeUnit$1(a.source, b.source) || byCodeUnit$1(a.id, b.id));
30
29
  }
31
30
  const snapshotId = (components) => hashValue(sortComponents(components));
32
- //#endregion
33
- //#region ../agent-core/src/object.ts
31
+ // ../agent-core/src/object.ts
34
32
  const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
35
- //#endregion
36
- //#region ../agent-core/src/config/files.ts
33
+ // ../agent-core/src/config/files.ts
37
34
  const MAX_DEPTH = 8;
38
35
  const MAX_FILE_BYTES = 1048576;
39
36
  var CollectionExpired = class extends Error {};
@@ -94,8 +91,7 @@ async function listDirectories(dir, guard) {
94
91
  for (const entry of await entries(dir, guard)) if (await kindOf(dir, entry, guard) === "directory") names.push(entry.name);
95
92
  return names;
96
93
  }
97
- //#endregion
98
- //#region ../agent-core/src/managed.ts
94
+ // ../agent-core/src/managed.ts
99
95
  const MANAGED_DIRS = {
100
96
  darwin: "/Library/Application Support/ClaudeCode",
101
97
  win32: "C:\\Program Files\\ClaudeCode"
@@ -114,8 +110,7 @@ async function readManagedEnv(managedDir, names) {
114
110
  }
115
111
  return values;
116
112
  }
117
- //#endregion
118
- //#region ../agent-core/src/process/lookup.ts
113
+ // ../agent-core/src/process/lookup.ts
119
114
  const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
120
115
  const NPM_SHIM_MAX_BYTES = 65536;
121
116
  const SHEBANG_MAX_BYTES = 256;
@@ -296,8 +291,7 @@ async function commandLineFor(file, args, context, { quoteArgs = true } = {}) {
296
291
  verbatim: true
297
292
  };
298
293
  }
299
- //#endregion
300
- //#region ../agent-core/src/process/git.ts
294
+ // ../agent-core/src/process/git.ts
301
295
  const GIT_TIMEOUT_MS$1 = 1e3;
302
296
  const execGit = (file, args, env, timeoutMs) => new Promise((resolve) => {
303
297
  execFile(file, args, {
@@ -335,8 +329,7 @@ function createGitRunner({ platform, env, processCwd, excludeTarget, timeoutMs =
335
329
  ], gitEnv, timeoutMs);
336
330
  };
337
331
  }
338
- //#endregion
339
- //#region ../agent-core/src/process/runtime-env.ts
332
+ // ../agent-core/src/process/runtime-env.ts
340
333
  const RUNTIME_VARIABLES = [
341
334
  "NODE_TLS_REJECT_UNAUTHORIZED",
342
335
  "NODE_EXTRA_CA_CERTS",
@@ -413,43 +406,51 @@ function takeStashedRuntimeEnv(env, platform) {
413
406
  if (!isObject(value)) return {};
414
407
  return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string" && isRuntimeVariable(entry[0], platform)));
415
408
  }
416
- //#endregion
417
- //#region src/shared/messages.ts
409
+ // src/shared/messages.ts
418
410
  const INIT_MESSAGES = {
419
- success: "Harnessforceに接続しました。Claude Codeを再起動すると設定が反映されます",
420
- invalidUrl: "接続先のURLが不正です",
421
- keychainUnavailable: "OSのキーチェーンを利用できないため、送信キーを保存できません",
422
- linuxKeychainUnavailable: "Secret Service(GNOME KeyringまたはKWallet)とkernelのkeyringのどちらも使えません。コンテナやCIでは、Workspace用の送信キーをmanaged settingsで配るようOwnerかAdminに依頼してください",
423
- waitingForSecretService: "Secret Serviceの解錠の確認を待っています(最大60秒)",
424
- savedToKernelKeyring: "Secret Serviceを使えないため、送信キーをkernelのkeyringに保存しました。再起動するか、一定期間使わないと消えます。消えたら`harnessforce init`を再実行してください",
425
- tooManyKeys: "keychainにある送信キーが多すぎるため、`harnessforce init`を実行できません",
426
- tooManyCredentials: "keychainにある送信キーまたはログインの情報が多すぎるため、`harnessforce init`を実行できません",
427
- settingsUnreadable: "Claude Codeのuser settingsを読めません",
428
- network: "Harnessforceとの通信に失敗しました。もう一度`harnessforce init`を実行してください",
429
- listenFailed: "ログインの待ち受けを開始できませんでした",
430
- timeout: "ログインが時間内に完了しませんでした。もう一度`harnessforce init`を実行してください",
431
- accessDenied: "ログインが拒否されました",
432
- noWorkspace: "参加しているWorkspaceがありません。Workspaceを作成するか招待を受諾してから、もう一度`harnessforce init`を実行してください",
433
- loginFailed: "ログインに失敗しました。もう一度`harnessforce init`を実行してください",
434
- viewer: "閲覧のみのロールでは送信キーを発行できません。Workspaceの管理者に連絡してください",
435
- limitOwnerAdmin: "有効な送信キーの数が上限に達しました。送信キーの画面で使っていないキーを失効してください",
436
- limitMember: "送信キーの上限に達しています。Workspaceの管理者に連絡してください",
437
- gateUnavailable: "受信側へ反映できませんでした。変更はしていません。もう一度お試しください",
438
- ingestUnavailable: "この環境はテレメトリを受信しないため、送信キーを発行できません",
439
- saveFailed: "送信キーを保存できませんでした。もう一度`harnessforce init`を実行してください",
440
- codexSaveFailed: "Codexの設定を書き込めませんでした。もう一度`harnessforce init`を実行してください",
441
- contentEnabled: "入力した指示の本文を送る設定を有効にしました。パターンに一致した秘密値と個人情報を伏せ字にしますが、すべてを検出できるとは限りません。保存した本文は、Workspaceのメンバーの中ではあなただけが読め、本文データの保持期間(最大30日)が過ぎると削除されます",
442
- contentNotOptedIn: "このWorkspaceは本文データをopt-inしていないため、本文を送る設定は有効にしませんでした。Workspaceの設定のデータの保持でopt-inしてから、もう一度`harnessforce init --send-content`を実行してください"
411
+ success: "Connected to Harnessforce. Restart Claude Code to apply the settings",
412
+ pluginAutoUpdateOn: "Automatic updates for the Harnessforce plugin are on. To turn them off, run `harnessforce init --no-plugin-auto-update` or use /plugin",
413
+ invalidUrl: "The connection URL is invalid",
414
+ keychainUnavailable: "The OS keychain is unavailable, so the ingest key cannot be saved",
415
+ linuxKeychainUnavailable: "Neither the Secret Service (GNOME Keyring or KWallet) nor the kernel keyring is available. In containers and CI, ask an Owner or Admin to distribute a Workspace ingest key through managed settings",
416
+ waitingForSecretService: "Waiting for the Secret Service unlock prompt (up to 60 seconds)",
417
+ savedToKernelKeyring: "The Secret Service is unavailable, so the ingest key was saved in the kernel keyring. It is lost on reboot or after a period of disuse. If it disappears, run `harnessforce init` again",
418
+ tooManyKeys: "There are too many ingest keys in the keychain, so `harnessforce init` cannot run",
419
+ tooManyCredentials: "There are too many ingest keys or login entries in the keychain, so `harnessforce init` cannot run",
420
+ settingsUnreadable: "Cannot read the Claude Code user settings",
421
+ network: "Communication with Harnessforce failed. Run `harnessforce init` again",
422
+ listenFailed: "Could not start waiting for the login",
423
+ timeout: "The login did not complete in time. Run `harnessforce init` again",
424
+ accessDenied: "The login was denied",
425
+ noWorkspace: "You are not a member of any Workspace. Create a Workspace or accept an invitation, then run `harnessforce init` again",
426
+ loginFailed: "The login failed. Run `harnessforce init` again",
427
+ viewer: "The view-only role cannot issue an ingest key. Contact a Workspace administrator",
428
+ limitOwnerAdmin: "The number of active ingest keys has reached the limit. Ask members to revoke unused keys in My ingest keys on their account page",
429
+ limitMember: "The ingest key limit has been reached. Contact a Workspace administrator",
430
+ gateUnavailable: "Could not apply the change on the receiving side. Nothing was changed. Try again",
431
+ ingestUnavailable: "This environment does not receive telemetry, so an ingest key cannot be issued",
432
+ saveFailed: "Could not save the ingest key. Run `harnessforce init` again",
433
+ codexSaveFailed: "Could not write the Codex configuration. Run `harnessforce init` again",
434
+ contentNotStored: "This workspace does not store content, so sending content was not enabled. An owner or admin can turn it on under Collection & retention in the workspace settings, then run `harnessforce init --send-content` again",
435
+ contentNotSent: "Content is not sent from this machine. Run `harnessforce init --send-content` to send it"
443
436
  };
444
437
  const codexNotes = (configPath) => [
445
- `Codexの設定を書き込みました(${configPath})。Codexを再起動すると反映されます`,
446
- "Codexのhookは、Codexの/hooksで内容をtrustするまで実行されません。/hooksでharnessforceのSessionStartのhookをtrustしてください",
447
- "HarnessforceのMCPを使う場合は、`codex mcp login harnessforce`でログインしてください"
438
+ `Wrote the Codex configuration (${configPath}). Restart Codex to apply it`,
439
+ "Codex hooks do not run until you trust them in Codex's /hooks. Trust the harnessforce SessionStart, PostToolUse, and Stop hooks in /hooks",
440
+ "To use the Harnessforce MCP server, log in with `codex mcp login harnessforce`"
448
441
  ].join("\n");
449
- const listenFailedMessage = (port) => `${INIT_MESSAGES.listenFailed}。ポート ${port}を使っているプロセスを終えるか、\`harnessforce init --port\`で別のポートを指定してください`;
450
- const keychainUnavailableMessage = (keychain, message = INIT_MESSAGES.keychainUnavailable) => keychain.unavailableHint ? `${message}。${keychain.unavailableHint}` : message;
451
- //#endregion
452
- //#region src/credentials/keychain.ts
442
+ const nonDefaultConnectionMessage = (url) => `Connecting to ${url} (not the default production endpoint)`;
443
+ const listenFailedMessage = (port) => `${INIT_MESSAGES.listenFailed}. Stop the process using port ${port}, or specify another port with \`harnessforce init --port\``;
444
+ const keychainUnavailableMessage = (keychain, message = INIT_MESSAGES.keychainUnavailable) => keychain.unavailableHint ? `${message}. ${keychain.unavailableHint}` : message;
445
+ const CONTENT_READERS = {
446
+ all_members: "Everyone in the workspace can read it",
447
+ owners_admins_and_sender: "Owners, admins and you can read it",
448
+ sender_only: "Within the workspace, only you can read it"
449
+ };
450
+ function contentEnabledMessage(content) {
451
+ return `Sending ${[...content.isPromptsStored ? ["the prompts you type"] : [], ...content.isModelAndToolsStored ? ["model responses and tool input and output"] : []].join(", ")} is now enabled. Values that match known patterns for secrets and personal information are masked, but not everything can be detected. ${CONTENT_READERS[content.visibility]}. It is deleted after ${content.retentionDays} days`;
452
+ }
453
+ // src/credentials/keychain.ts
453
454
  async function openSingleStore(store) {
454
455
  if (!await store.isAvailable()) return void 0;
455
456
  return {
@@ -475,12 +476,14 @@ const workspaceAccounts = (workspaceId) => [
475
476
  ];
476
477
  const isIngestKeyAccount = (account) => account.endsWith(INGEST_KEY_SUFFIX);
477
478
  const apiTokenWorkspaceId = (account) => account.endsWith(API_TOKEN_SUFFIX) ? account.slice(0, -10) : void 0;
478
- //#endregion
479
- //#region src/credentials/linux-keychain.ts
479
+ // src/credentials/linux-keychain.ts
480
480
  const SECRET_SERVICE_TIMEOUT_MS = 6e4;
481
481
  const WAITING_NOTICE_DELAY_MS = 2e3;
482
482
  const PROBE_ACCOUNT$1 = "availability-probe";
483
483
  const PROBE_VALUE = "harnessforce";
484
+ const MIGRATION_PROBE_TIMEOUT_MS = 200;
485
+ const MIGRATION_BUDGET_MS = 1e3;
486
+ const CLEANUP_TIMEOUT_MS = 200;
484
487
  const succeeds = (operation) => operation().then(() => true, () => false);
485
488
  const workspaceOf = (account) => account.slice(0, account.lastIndexOf(":"));
486
489
  function createLinuxKeychain(options) {
@@ -527,6 +530,11 @@ function build(options, timeoutMs) {
527
530
  const { location } = await locate(workspaceOf(account));
528
531
  await storeAt(location).delete(account, timeoutMs);
529
532
  },
533
+ async migrateFromKernel(workspaceId) {
534
+ const { location } = await locate(workspaceId);
535
+ if (location !== "kernel") return;
536
+ await migrate(options, workspaceId).catch(() => {});
537
+ },
530
538
  withTimeout: (ms) => build(options, ms),
531
539
  openForInit: (init) => openForInit(options, timeoutMs, init)
532
540
  };
@@ -569,8 +577,60 @@ async function openForInit({ kernel, secretService }, timeoutMs, { previousWorks
569
577
  deleteElsewhere: async () => {}
570
578
  };
571
579
  }
572
- //#endregion
573
- //#region src/credentials/os-keychain.ts
580
+ var MigrationExpired = class extends Error {};
581
+ const MIGRATION_ORDER = (workspaceId) => [
582
+ ingestOriginAccount(workspaceId),
583
+ urlOriginAccount(workspaceId),
584
+ apiTokenAccount(workspaceId),
585
+ ingestKeyAccount(workspaceId)
586
+ ];
587
+ async function migrate({ kernel, secretService }, workspaceId) {
588
+ let isExpired = false;
589
+ const deadline = Date.now() + MIGRATION_BUDGET_MS;
590
+ const remaining = () => {
591
+ if (isExpired) throw new MigrationExpired();
592
+ return Math.max(deadline - Date.now(), 1);
593
+ };
594
+ const written = [];
595
+ let isVerified = false;
596
+ const move = async () => {
597
+ await secretService.get(PROBE_ACCOUNT$1, MIGRATION_PROBE_TIMEOUT_MS);
598
+ const items = [];
599
+ for (const account of MIGRATION_ORDER(workspaceId)) {
600
+ const secret = await kernel.get(account, remaining());
601
+ if (secret !== void 0) items.push({
602
+ account,
603
+ secret
604
+ });
605
+ }
606
+ for (const account of workspaceAccounts(workspaceId)) await secretService.delete(account, remaining());
607
+ for (const { account, secret } of items) {
608
+ written.push(account);
609
+ await secretService.set(account, secret, remaining());
610
+ }
611
+ for (const { account, secret } of items) if (await secretService.get(account, remaining()) !== secret) throw new Error("the Secret Service returned a different value");
612
+ remaining();
613
+ isVerified = true;
614
+ for (const { account } of [...items].reverse()) await kernel.delete(account, remaining());
615
+ };
616
+ let timer;
617
+ const expiry = new Promise((_, reject) => {
618
+ timer = setTimeout(() => {
619
+ isExpired = true;
620
+ reject(new MigrationExpired());
621
+ }, MIGRATION_BUDGET_MS);
622
+ });
623
+ try {
624
+ await Promise.race([move(), expiry]);
625
+ } catch (error) {
626
+ if (isVerified) throw error;
627
+ await Promise.allSettled(written.map((account) => secretService.delete(account, CLEANUP_TIMEOUT_MS)));
628
+ throw error;
629
+ } finally {
630
+ clearTimeout(timer);
631
+ }
632
+ }
633
+ // src/credentials/os-keychain.ts
574
634
  const PROBE_ACCOUNT = "availability-probe";
575
635
  function createOsKeychain({ platform, load, secretService }) {
576
636
  let loaded;
@@ -652,8 +712,7 @@ const unsupported = {
652
712
  withTimeout: () => unsupported,
653
713
  openForInit: async () => void 0
654
714
  };
655
- //#endregion
656
- //#region src/credentials/secret-service-process.ts
715
+ // src/credentials/secret-service-process.ts
657
716
  const CHILD_SCRIPT = `
658
717
  const [op, service, account, timeoutMs, modulePath] = process.argv.slice(1);
659
718
  setTimeout(() => process.kill(process.pid, "SIGKILL"), Number(timeoutMs));
@@ -728,8 +787,7 @@ function createSecretServiceProcess({ execPath, resolveKeyring }) {
728
787
  }
729
788
  };
730
789
  }
731
- //#endregion
732
- //#region src/import/repository.ts
790
+ // src/import/repository.ts
733
791
  const GIT_TIMEOUT_MS = 5e3;
734
792
  const createImportGit = (platform, env, processCwd) => createGitRunner({
735
793
  platform,
@@ -738,8 +796,7 @@ const createImportGit = (platform, env, processCwd) => createGitRunner({
738
796
  excludeTarget: false,
739
797
  timeoutMs: GIT_TIMEOUT_MS
740
798
  });
741
- //#endregion
742
- //#region src/init/browser.ts
799
+ // src/init/browser.ts
743
800
  const STILL_RUNNING_MS = 3e3;
744
801
  function openerFor(platform, url) {
745
802
  if (platform === "darwin") return ["open", [url]];
@@ -776,35 +833,63 @@ async function openBrowser(url, { platform, env, cwd, restoredEnv, spawn: spawnI
776
833
  child.once("exit", (code) => finish(code === 0));
777
834
  });
778
835
  }
779
- //#endregion
780
- //#region src/init/loopback.ts
836
+ // src/init/loopback.ts
781
837
  const HOST = "127.0.0.1";
782
838
  const CALLBACK_PATH = "/callback";
783
- const page = (message) => `<!doctype html><html lang="ja"><meta charset="utf-8"><title>Harnessforce</title><p>${message}</p></html>`;
784
- const SUCCESS_PAGE = page("ターミナルに戻ってください");
785
- const FAILURE_PAGE = page("ログインに失敗しました。ターミナルを確認してください");
839
+ const COMPLETION_PATH = "/cli/connected";
840
+ function completionUrl(webOrigin, completion) {
841
+ const url = new URL(COMPLETION_PATH, webOrigin);
842
+ url.searchParams.set("status", completion.status);
843
+ if (completion.status === "connected" && completion.workspaceId) url.searchParams.set("workspace", completion.workspaceId);
844
+ return url.href;
845
+ }
846
+ function parseRequestUrl(target) {
847
+ try {
848
+ return new URL(target ?? "/", `http://${HOST}`);
849
+ } catch {
850
+ return;
851
+ }
852
+ }
853
+ function isSameState(received, expected) {
854
+ if (received === null) return false;
855
+ const receivedBytes = Buffer.from(received, "utf8");
856
+ const expectedBytes = Buffer.from(expected, "utf8");
857
+ return receivedBytes.length === expectedBytes.length && timingSafeEqual(receivedBytes, expectedBytes);
858
+ }
786
859
  async function startLoopback(options) {
787
860
  let settle = () => {};
788
861
  const callback = new Promise((resolve) => {
789
862
  settle = resolve;
790
863
  });
864
+ let held;
791
865
  const server = createServer((req, res) => {
792
- const url = new URL(req.url ?? "/", `http://${HOST}`);
866
+ const url = parseRequestUrl(req.url);
867
+ if (url === void 0) {
868
+ res.writeHead(400).end();
869
+ return;
870
+ }
793
871
  if (url.pathname !== CALLBACK_PATH) {
794
872
  res.writeHead(404).end();
795
873
  return;
796
874
  }
797
- const received = {
875
+ if (isStopped) {
876
+ res.writeHead(404).end();
877
+ return;
878
+ }
879
+ if (!isSameState(url.searchParams.get("state"), options.state)) {
880
+ res.writeHead(303, {
881
+ location: completionUrl(options.webOrigin, { status: "failed" }),
882
+ "cache-control": "no-store"
883
+ });
884
+ res.end();
885
+ return;
886
+ }
887
+ held = res;
888
+ stop({
798
889
  code: url.searchParams.get("code") || void 0,
799
890
  state: url.searchParams.get("state") ?? void 0,
800
891
  error: url.searchParams.get("error") ?? void 0
801
- };
802
- const isSuccess = !received.error && Boolean(received.code) && received.state === options.state;
803
- res.writeHead(200, {
804
- "content-type": "text/html; charset=utf-8",
805
- "cache-control": "no-store"
806
892
  });
807
- res.end(isSuccess ? SUCCESS_PAGE : FAILURE_PAGE, () => stop(received));
808
893
  });
809
894
  const timer = setTimeout(() => stop(void 0), options.timeoutMs);
810
895
  let isStopped = false;
@@ -813,9 +898,19 @@ async function startLoopback(options) {
813
898
  isStopped = true;
814
899
  clearTimeout(timer);
815
900
  server.close();
816
- server.closeAllConnections();
901
+ if (!held) server.closeAllConnections();
817
902
  settle(received);
818
903
  }
904
+ function finish(completion) {
905
+ const response = held;
906
+ held = void 0;
907
+ if (!response) return;
908
+ response.writeHead(303, {
909
+ location: completionUrl(options.webOrigin, completion),
910
+ "cache-control": "no-store"
911
+ });
912
+ response.end(() => server.closeAllConnections());
913
+ }
819
914
  await new Promise((resolve, reject) => {
820
915
  server.once("error", reject);
821
916
  server.listen(options.port, HOST, resolve);
@@ -826,16 +921,11 @@ async function startLoopback(options) {
826
921
  const { port } = server.address();
827
922
  return {
828
923
  redirectUri: `http://${HOST}:${port}${CALLBACK_PATH}`,
829
- callback
924
+ callback,
925
+ finish
830
926
  };
831
927
  }
832
- //#endregion
833
- //#region ../agent-core/src/hook-warning.ts
834
- const RESTART_FAILED_WARNING = "Harnessforceのhookを起動し直せませんでした";
835
- const INVALID_ENDPOINT_WARNING = "Harnessforceの送信先が不正です。`harnessforce init`を実行し直してください";
836
- const notConfiguredWarning = (agent) => agent === "claude_code" ? "Harnessforceの送信が設定されていません。`/harnessforce:setup`を実行してください" : "Harnessforceの送信が設定されていません。`harnessforce init`を実行してください";
837
- //#endregion
838
- //#region ../agent-core/src/url.ts
928
+ // ../agent-core/src/url.ts
839
929
  const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
840
930
  "localhost",
841
931
  "127.0.0.1",
@@ -857,9 +947,167 @@ function underBase(base, path) {
857
947
  return url;
858
948
  }
859
949
  const withoutExtras = (url) => `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
860
- //#endregion
861
- //#region ../agent-core/src/ingest.ts
862
- const SEND_TIMEOUT_MS$1 = 2e3;
950
+ // ../agent-core/src/client-update.ts
951
+ const CLIENT_UPDATE_HEADER = "harnessforce-client-update";
952
+ const UPDATE_COMMANDS = {
953
+ cli: "npm install -g @harnessforce/cli@latest",
954
+ "claude-code-plugin": "claude plugin update harnessforce@harnessforce-agent"
955
+ };
956
+ const DISPLAY_NAMES = {
957
+ cli: "CLI",
958
+ "claude-code-plugin": "plugin"
959
+ };
960
+ const DAY_MS$2 = 864e5;
961
+ const RECHECK_TIMEOUT_MS = 2e3;
962
+ const userAgentOf = (client) => `harnessforce-${client.component}${client.dev ? "-dev" : ""}/${client.version}`;
963
+ const STATUSES = /* @__PURE__ */ new Set([
964
+ "current",
965
+ "outdated",
966
+ "deprecated",
967
+ "unsupported"
968
+ ]);
969
+ const REASONS = /* @__PURE__ */ new Set(["security", "compatibility"]);
970
+ function parseClientUpdateHeader(value) {
971
+ if (!value) return void 0;
972
+ const members = /* @__PURE__ */ new Map();
973
+ for (const member of value.split(",")) {
974
+ const [key, raw] = member.trim().split("=", 2);
975
+ if (key && raw !== void 0) members.set(key, raw);
976
+ }
977
+ const status = members.get("status");
978
+ if (!status || !STATUSES.has(status)) return void 0;
979
+ const update = { status };
980
+ const latest = /^"([^"\\]*)"$/.exec(members.get("latest") ?? "")?.[1];
981
+ if (latest) update.latest = latest;
982
+ const reason = members.get("reason");
983
+ if (reason && REASONS.has(reason)) update.reason = reason;
984
+ const seconds = /^@(\d+)$/.exec(members.get("deadline") ?? "")?.[1];
985
+ if (seconds) update.deadline = (/* @__PURE__ */ new Date(Number(seconds) * 1e3)).toISOString();
986
+ return isComplete(update) ? update : void 0;
987
+ }
988
+ function isComplete(update) {
989
+ switch (update.status) {
990
+ case "outdated": return Boolean(update.latest);
991
+ case "deprecated": return Boolean(update.reason) && !Number.isNaN(Date.parse(update.deadline ?? ""));
992
+ case "unsupported": return Boolean(update.reason);
993
+ default: return true;
994
+ }
995
+ }
996
+ function stateFrom(value, version) {
997
+ if (!isObject(value) || value.version !== version) return void 0;
998
+ if (typeof value.receivedAt !== "string") return void 0;
999
+ if (typeof value.status !== "string" || !STATUSES.has(value.status)) return void 0;
1000
+ const state = value;
1001
+ return isComplete(state) ? state : void 0;
1002
+ }
1003
+ async function readUpdateState(path, version) {
1004
+ const saved = await readSavedUpdateState(path);
1005
+ return saved?.version === version ? saved : void 0;
1006
+ }
1007
+ async function readSavedUpdateState(path) {
1008
+ try {
1009
+ const value = JSON.parse(await readFile(path, "utf8"));
1010
+ return isObject(value) && typeof value.version === "string" ? stateFrom(value, value.version) : void 0;
1011
+ } catch {
1012
+ return;
1013
+ }
1014
+ }
1015
+ async function writeUpdateState(path, state) {
1016
+ await mkdir(dirname(path), {
1017
+ recursive: true,
1018
+ mode: 448
1019
+ });
1020
+ const temporary = `${path}.${randomUUID()}.tmp`;
1021
+ await writeFile(temporary, `${JSON.stringify(state)}\n`, { mode: 384 });
1022
+ await rename(temporary, path);
1023
+ }
1024
+ /**
1025
+ * Harnessforceへの要求に`User-Agent`を付け、応答の`Harnessforce-Client-Update`を状態のfileへ保存するfetch。
1026
+ * 保存に失敗しても要求の結果は変えない。statePathが無い(pluginのdata directoryが無い)場合は版の申告だけを行う。
1027
+ */
1028
+ function recordingFetch(fetchImpl, client, statePath, now) {
1029
+ return async (url, init) => {
1030
+ const headers = {
1031
+ ...Object.fromEntries(new Headers(init.headers)),
1032
+ "user-agent": userAgentOf(client)
1033
+ };
1034
+ const response = await fetchImpl(url, {
1035
+ ...init,
1036
+ headers
1037
+ });
1038
+ const update = parseClientUpdateHeader(response.headers.get(CLIENT_UPDATE_HEADER));
1039
+ if (update && statePath) {
1040
+ const previous = await readUpdateState(statePath, client.version);
1041
+ await writeUpdateState(statePath, {
1042
+ ...update,
1043
+ version: client.version,
1044
+ receivedAt: now().toISOString(),
1045
+ ...previous?.outdatedShownAt ? { outdatedShownAt: previous.outdatedShownAt } : {}
1046
+ }).catch(() => {});
1047
+ }
1048
+ return response;
1049
+ };
1050
+ }
1051
+ const utcDate = (instant) => new Intl.DateTimeFormat("en-CA", {
1052
+ timeZone: "UTC",
1053
+ year: "numeric",
1054
+ month: "2-digit",
1055
+ day: "2-digit"
1056
+ }).format(new Date(instant));
1057
+ const unsupportedLine = (component) => `This Harnessforce ${DISPLAY_NAMES[component]} version is no longer supported, so nothing is being sent to Harnessforce. Update it: \`${UPDATE_COMMANDS[component]}\``;
1058
+ function updateLine(component, update) {
1059
+ const name = DISPLAY_NAMES[component];
1060
+ const command = UPDATE_COMMANDS[component];
1061
+ switch (update.status) {
1062
+ case "unsupported": return unsupportedLine(component);
1063
+ case "deprecated": {
1064
+ const stops = `stops sending on ${utcDate(update.deadline ?? "")} (00:00 UTC)`;
1065
+ return update.reason === "security" ? `A security update for Harnessforce ${name} is available. This version ${stops}. Update it: \`${command}\`` : `This Harnessforce ${name} version ${stops}. Update it: \`${command}\``;
1066
+ }
1067
+ case "outdated": return `Harnessforce ${name} ${update.latest} is available. Update it: \`${command}\``;
1068
+ default: return;
1069
+ }
1070
+ }
1071
+ function shouldShowOutdated(state, now, env) {
1072
+ if (state.status !== "outdated" || env.NO_UPDATE_NOTIFIER || env.CI) return false;
1073
+ const shownAt = state.outdatedShownAt ? Date.parse(state.outdatedShownAt) : NaN;
1074
+ return Number.isNaN(shownAt) || now.getTime() - shownAt >= DAY_MS$2;
1075
+ }
1076
+ const markOutdatedShown = (path, state, now) => writeUpdateState(path, {
1077
+ ...state,
1078
+ outdatedShownAt: now.toISOString()
1079
+ });
1080
+ /**
1081
+ * 表示する1行。outdatedは24時間に1回までとし、表示する場合はその時刻を書く。isSuppressedは、pluginを
1082
+ * Claude Codeが自動更新する間など、呼び出し側がoutdatedを示さない条件。
1083
+ */
1084
+ async function noticeLine(component, statePath, state, options) {
1085
+ if (state.status === "outdated") {
1086
+ if (!shouldShowOutdated(state, options.now, options.env) || await options.isSuppressed?.()) return void 0;
1087
+ await markOutdatedShown(statePath, state, options.now).catch(() => {});
1088
+ }
1089
+ return updateLine(component, state);
1090
+ }
1091
+ /**
1092
+ * unsupportedを受けてから24時間以上経っていれば、資格情報を送らずに`GET /v1/client-status`で確かめ直す
1093
+ * (client-updates.md「送信の停止」)。失敗した場合は状態を変えない。
1094
+ */
1095
+ async function recheckClientStatus(options) {
1096
+ const { state } = options;
1097
+ if (state?.status !== "unsupported" || options.now().getTime() - Date.parse(state.receivedAt) < DAY_MS$2) return state;
1098
+ try {
1099
+ await (await options.fetch(underBase(options.ingestBase, "v1/client-status"), {
1100
+ method: "GET",
1101
+ redirect: "error",
1102
+ signal: AbortSignal.timeout(RECHECK_TIMEOUT_MS)
1103
+ })).body?.cancel();
1104
+ } catch {
1105
+ return state;
1106
+ }
1107
+ return await readUpdateState(options.statePath, options.version) ?? state;
1108
+ }
1109
+ // ../agent-core/src/ingest.ts
1110
+ const SEND_TIMEOUT_MS = 2e3;
863
1111
  function ingestBaseFrom(endpoint) {
864
1112
  const base = parseAllowedUrl(endpoint);
865
1113
  if (!base) return void 0;
@@ -867,8 +1115,8 @@ function ingestBaseFrom(endpoint) {
867
1115
  ingestBase.pathname = base.pathname.replace(/\/+$/, "");
868
1116
  return ingestBase;
869
1117
  }
870
- const postItem = (destination, path, item, fetchImpl, timeoutMs = SEND_TIMEOUT_MS$1) => postItems(destination, path, [item], fetchImpl, timeoutMs);
871
- async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_TIMEOUT_MS$1) {
1118
+ const postItem = (destination, path, item, fetchImpl, timeoutMs = SEND_TIMEOUT_MS) => postItems(destination, path, [item], fetchImpl, timeoutMs);
1119
+ async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_TIMEOUT_MS) {
872
1120
  try {
873
1121
  const response = await fetchImpl(underBase(destination.ingestBase, path), {
874
1122
  method: "POST",
@@ -893,72 +1141,39 @@ async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_T
893
1141
  };
894
1142
  }
895
1143
  }
896
- //#endregion
897
- //#region ../agent-core/src/issue.ts
898
- const isIssueIdentifier = (value) => isToken(value, 300);
899
- //#endregion
900
- //#region ../agent-core/src/using-harnessforce.ts
901
- const usingHarnessforceBody = "---\nname: using-harnessforce\ndescription: Harnessforceのhookが入った環境でsessionを始めたとき、HarnessforceのIssue(ENG-42のような識別子)に取り組む作業を始めるとき、Harnessforceが何を記録し何を収集しないかを尋ねられたときに使う。sessionの開始時にhookがこの本文をcontextへ注入する。\n---\n\n# Harnessforceを使う\n\nこのsessionにはHarnessforceのhookが入っている。Harnessforceは、人とAIの作業をIssueに結び付けて記録する。次の規則に従う。\n\n## 何を記録するか\n\n- session registration: agent、session ID、repository、branch、commit、開始時刻、最初のprompt ID。\n- config snapshot: 利用中の設定の構成(Claude Codeだけ)。\n- OTelのテレメトリ: 利用者の設定で送るもの。\n- Issueに取り組む作業で`record-run`が記録する、Plan、Decision、Definition of Doneの充足。人が承認するまで確定しない。\n\n## 何を収集しないか\n\n- prompt、応答、toolの入出力、code diffの本文は、利用者が`--send-content`で選び、Workspaceがopt-inした場合だけ届く。既定では収集しない。\n- MCPのtoolの入力にも記録にも、これらの本文、secret、環境変数の値を含めない。\n\n## どのhookが何を送るか\n\n- SessionStart: session registrationとconfig snapshot(Codexはsession registrationだけ)。\n- UserPromptSubmit: 最初のpromptでの再送(Claude Codeだけ)。\n- hookは失敗してもsessionを止めない。送れない状態のときは、hookが警告を1行表示する。警告が出たら利用者に伝える。\n\n## どのskillとtoolをいつ使うか\n\n- IssueのIDを指定された作業、または環境変数`HARNESSFORCE_ISSUE`がある作業は、`record-run`に従い、`start_run`、`get_issue`、`record_plan`、`record_decision`、`complete_run`を使う。skillを読み込めないagentは、同じ順にtoolを呼ぶ。\n- `start_run`には、contextの`harnessforce session_id:`の行の値を渡す。複数あれば最後の値を使う。行が無ければ、session IDを推測せず、`start_run`と`complete_run`を呼ばない。\n- `/harnessforce:tune`の分析は`propose-improvements`を使う。\n- Issueが指定されていない作業では、Runの記録を始めない。\n\n## Red Flags\n\n次の考えが浮かんだら、立ち止まる。\n\n| 考え | 現実 |\n| --- | --- |\n| 小さな作業なので記録は要らない | Issueが指定された作業は、大きさによらず記録する |\n| session IDの行が無いが、推測して`start_run`を呼ぶ | 推測しない。Runがsessionと結び付かなくなる |\n| 本文を要約して渡せば記録の役に立つ | 本文はtoolに渡さない。収集しない範囲を超える |\n| 警告が出たが、気にしなくてよい | 送れていない。利用者に伝える |\n\n## 優先順位\n\n1. 利用者の指示(CLAUDE.md、AGENTS.md、直接の依頼)。利用者が記録を止めるよう指示したら、Runの記録を止める。\n2. この導入文。\n3. 既定の動作。\n\n## subagent\n\n親から特定の作業だけを委譲されたsubagentは、この導入文に従ってRunを登録しない。Runの登録は親が行う。\n".replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---\n+/, "").trimEnd();
902
- //#endregion
903
- //#region ../agent-core/src/session-context.ts
904
- const sessionContextLine = (sessionId) => `harnessforce session_id: ${sessionId}`;
905
- const sessionContext = (sessionId) => ({ hookSpecificOutput: {
906
- hookEventName: "SessionStart",
907
- additionalContext: `${sessionContextLine(sessionId)}\n\n${usingHarnessforceBody}`
908
- } });
909
- //#endregion
910
- //#region ../agent-core/src/vcs.ts
911
- async function selectRemote(cwd, git) {
912
- if (await git(cwd, ["rev-parse", "--is-inside-work-tree"]) !== "true") return void 0;
913
- const remotes = (await git(cwd, ["remote"]))?.split("\n") ?? [];
914
- return remotes.includes("origin") ? "origin" : remotes[0];
915
- }
916
- async function resolveVcs(cwd, git) {
917
- const remote = await selectRemote(cwd, git);
918
- if (!remote) return void 0;
919
- const [url, branch, commit] = await Promise.all([
920
- git(cwd, [
921
- "remote",
922
- "get-url",
923
- remote
924
- ]),
925
- git(cwd, [
926
- "symbolic-ref",
927
- "--quiet",
928
- "--short",
929
- "HEAD"
930
- ]),
931
- git(cwd, [
932
- "rev-parse",
933
- "--verify",
934
- "--quiet",
935
- "HEAD"
936
- ])
937
- ]);
938
- const repository = url === void 0 ? void 0 : normalizeRepository(url);
939
- return {
940
- ...repository === void 0 ? {} : { repository },
941
- ...branch === void 0 ? {} : { branch },
942
- ...commit === void 0 ? {} : { commit }
943
- };
944
- }
945
- async function resolveRepository(cwd, git) {
946
- if (!isAbsolute(cwd)) return void 0;
947
- const remote = await selectRemote(cwd, git);
948
- if (!remote) return void 0;
949
- const url = await git(cwd, [
950
- "remote",
951
- "get-url",
952
- remote
953
- ]);
954
- return url === void 0 ? void 0 : normalizeRepository(url);
1144
+ // ../agent-core/src/config/hash-input.ts
1145
+ function withoutSecrets(entry) {
1146
+ const { env, headers, url, args: _args, ...rest } = entry;
1147
+ const result = rest;
1148
+ if (isObject(env)) result.env = Object.keys(env).sort(byCodeUnit$1);
1149
+ if (isObject(headers)) result.headers = Object.keys(headers).sort(byCodeUnit$1);
1150
+ const strippedUrl = typeof url === "string" ? withoutCredentials(url) : void 0;
1151
+ if (strippedUrl !== void 0) result.url = strippedUrl;
1152
+ return result;
955
1153
  }
956
- async function resolveProjectRoot(cwd, git) {
957
- const topLevel = await git(cwd, ["rev-parse", "--show-toplevel"]);
958
- return topLevel && isAbsolute(topLevel) ? topLevel : cwd;
1154
+ function withoutCredentials(url) {
1155
+ let parsed;
1156
+ try {
1157
+ parsed = new URL(url);
1158
+ } catch {
1159
+ return;
1160
+ }
1161
+ parsed.username = "";
1162
+ parsed.password = "";
1163
+ parsed.search = "";
1164
+ parsed.hash = "";
1165
+ return parsed.href;
1166
+ }
1167
+ const reduceHookGroup = (group) => isObject(group) && Array.isArray(group.hooks) ? {
1168
+ ...group,
1169
+ hooks: group.hooks.map((handler) => isObject(handler) ? withoutSecrets(handler) : handler)
1170
+ } : group;
1171
+ function hashInput(kind, value) {
1172
+ if (kind === "mcp_server" && isObject(value)) return withoutSecrets(value);
1173
+ if (kind === "hook" && Array.isArray(value)) return value.map(reduceHookGroup);
1174
+ return value;
959
1175
  }
960
- //#endregion
961
- //#region ../agent-core/src/config/scope.ts
1176
+ // ../agent-core/src/config/scope.ts
962
1177
  const MAX_COMPONENTS = 1e3;
963
1178
  var TooManyComponents = class extends Error {};
964
1179
  var Sink = class {
@@ -985,7 +1200,7 @@ var Sink = class {
985
1200
  });
986
1201
  return {
987
1202
  guard: this.guard,
988
- addValue: (kind, id, value) => add(kind, id, hashValue(value)),
1203
+ addValue: (kind, id, value) => add(kind, id, hashValue(hashInput(kind, value))),
989
1204
  addFile: async (kind, id, path) => {
990
1205
  const content = await readFileIfExists(path, this.guard);
991
1206
  if (content) add(kind, id, hashFileContent(content));
@@ -1014,8 +1229,9 @@ function collectMcpServers$1(s, servers) {
1014
1229
  if (!isObject(servers)) return;
1015
1230
  for (const [name, value] of Object.entries(servers)) if (isObject(value)) s.addValue("mcp_server", name, value);
1016
1231
  }
1017
- //#endregion
1018
- //#region src/shared/settings.ts
1232
+ // ../agent-core/src/plugin-auto-update.ts
1233
+ const MARKETPLACE_NAME = "harnessforce-agent";
1234
+ // src/shared/settings.ts
1019
1235
  const OTEL_HEADERS_HELPER = "harnessforce otel-headers";
1020
1236
  const PLUGIN_ID$1 = "harnessforce@harnessforce-agent";
1021
1237
  const NEW_FILE_MODE = 384;
@@ -1043,6 +1259,37 @@ async function readUserSettings(path) {
1043
1259
  return { kind: "invalid" };
1044
1260
  }
1045
1261
  }
1262
+ const MARKETPLACE_KEYS = ["extraKnownMarketplaces", "additionalMarketplaces"];
1263
+ const MARKETPLACE_SOURCE = {
1264
+ source: "github",
1265
+ repo: "lambda-script/harnessforce-agent"
1266
+ };
1267
+ const isMarketplacesValid = (value) => value === void 0 || isObject(value) && (value["harnessforce-agent"] === void 0 || isObject(value["harnessforce-agent"]));
1268
+ const hasWritableMarketplaces = (settings) => MARKETPLACE_KEYS.every((key) => isMarketplacesValid(settings[key]));
1269
+ /**
1270
+ * marketplaceの登録の鍵の`harnessforce-agent`に、sourceと自動更新を書く。sourceと、利用者が切り替えた真偽値は残し、
1271
+ * turnOffのときだけ偽にする。自動更新が有効になったかも返す。
1272
+ */
1273
+ function withPluginAutoUpdate(settings, turnOff) {
1274
+ const key = settings.additionalMarketplaces !== void 0 && settings.extraKnownMarketplaces === void 0 ? "additionalMarketplaces" : "extraKnownMarketplaces";
1275
+ const marketplaces = settings[key] ?? {};
1276
+ const entry = marketplaces["harnessforce-agent"] ?? {};
1277
+ const autoUpdate = turnOff ? false : typeof entry.autoUpdate === "boolean" ? entry.autoUpdate : true;
1278
+ return {
1279
+ settings: {
1280
+ ...settings,
1281
+ [key]: {
1282
+ ...marketplaces,
1283
+ [MARKETPLACE_NAME]: {
1284
+ ...entry,
1285
+ source: entry.source ?? MARKETPLACE_SOURCE,
1286
+ autoUpdate
1287
+ }
1288
+ }
1289
+ },
1290
+ isOn: autoUpdate
1291
+ };
1292
+ }
1046
1293
  function mergeUserSettings(settings, env) {
1047
1294
  return {
1048
1295
  ...settings,
@@ -1072,8 +1319,7 @@ async function writeUserSettings(path, settings) {
1072
1319
  await rm(temporary, { force: true });
1073
1320
  }
1074
1321
  }
1075
- //#endregion
1076
- //#region src/destinations.ts
1322
+ // src/destinations.ts
1077
1323
  const NAMES = [
1078
1324
  "HARNESSFORCE_URL",
1079
1325
  "HARNESSFORCE_ENDPOINT",
@@ -1094,8 +1340,400 @@ async function resolveCliDestinations(env, settingsPath, defaultUrl) {
1094
1340
  readApiUrl: pick("HARNESSFORCE_URL") ?? defaultUrl
1095
1341
  };
1096
1342
  }
1097
- //#endregion
1098
- //#region src/credentials/user-destination.ts
1343
+ // src/client-update.ts
1344
+ const updateStatePath = (homeDir) => join(homeDir, ".harnessforce", "update-status.json");
1345
+ const readCliUpdateState = (homeDir, version) => readUpdateState(updateStatePath(homeDir), version);
1346
+ /**
1347
+ * 送信を伴うcommandの前に呼ぶ。unsupportedを受けてから24時間以上経っていれば資格情報を送らずに確かめ直し、
1348
+ * それでもunsupportedなら真を返す(client-updates.md「送信の停止」)。
1349
+ */
1350
+ async function isSendingBlocked(deps) {
1351
+ const statePath = updateStatePath(deps.homeDir);
1352
+ const state = await readUpdateState(statePath, deps.client.version);
1353
+ if (state?.status !== "unsupported") return false;
1354
+ const { ingestEndpoint } = await resolveCliDestinations(deps.env, userSettingsPath(deps.env, deps.homeDir), deps.defaultUrl);
1355
+ const ingestBase = ingestBaseFrom(ingestEndpoint);
1356
+ return (ingestBase ? await recheckClientStatus({
1357
+ state,
1358
+ ingestBase,
1359
+ fetch: deps.fetch,
1360
+ statePath,
1361
+ version: deps.client.version,
1362
+ now: deps.now
1363
+ }) : state)?.status === "unsupported";
1364
+ }
1365
+ /** commandの出力の後に、更新の状態の1行をstderrへ書く。outdatedは24時間に1回までとする。 */
1366
+ async function noticeUpdate(deps) {
1367
+ const statePath = updateStatePath(deps.homeDir);
1368
+ const state = await readUpdateState(statePath, deps.client.version);
1369
+ if (!state) return;
1370
+ const line = await noticeLine("cli", statePath, state, {
1371
+ now: deps.now(),
1372
+ env: deps.env
1373
+ });
1374
+ if (line) deps.stderr(`${line}\n`);
1375
+ }
1376
+ // src/help.ts
1377
+ const SENDS_NOTHING_LOCAL = "Nothing. It reads local files only and makes no network request.";
1378
+ const COMMANDS$1 = {
1379
+ init: {
1380
+ summary: "Connect this machine to a Harnessforce Workspace",
1381
+ text: `Usage: harnessforce init [--port <port>] [--send-content] [--no-plugin-auto-update]
1382
+
1383
+ When to use:
1384
+ Once per machine, before anything else, and again whenever the login expires.
1385
+
1386
+ What it does:
1387
+ Opens your browser to log in to a Workspace, then stores a user ingest key and
1388
+ an API token in the OS keychain. It configures Claude Code (user settings) and
1389
+ Codex (config.toml) to export telemetry to the Workspace.
1390
+
1391
+ Options:
1392
+ --port <port> Loopback port for the login callback, 1-65535 (default 8080).
1393
+ --send-content Also send the prompts you type, model responses and tool
1394
+ input and output. Takes effect only for the categories the
1395
+ Workspace stores. Without it, init turns all of them off.
1396
+ --no-plugin-auto-update
1397
+ Turn off automatic updates of the Harnessforce plugin in
1398
+ Claude Code. On by default; a value you set is kept.
1399
+
1400
+ Sends:
1401
+ The login exchange with the Workspace. After init, the agents you run send
1402
+ telemetry (token counts, tool names, timings) with that key.
1403
+
1404
+ Does not send:
1405
+ Prompt text, model responses and tool input or output (unless --send-content
1406
+ and the Workspace stores that category), code diffs, and secrets. The key is
1407
+ never written to a plain file.
1408
+
1409
+ Exit codes:
1410
+ 0 Connected and configured.
1411
+ 1 Failed (login cancelled or timed out, keychain unavailable, or a write
1412
+ failed). The reason is printed to stderr.
1413
+
1414
+ Examples:
1415
+ harnessforce init
1416
+ harnessforce init --send-content
1417
+ harnessforce init --port 8081
1418
+ `
1419
+ },
1420
+ import: {
1421
+ summary: "Send past local sessions to the Workspace",
1422
+ text: `Usage: harnessforce import
1423
+
1424
+ When to use:
1425
+ Right after init, to backfill sessions that ran before Harnessforce was
1426
+ connected. It is safe to run again; sessions already sent are skipped.
1427
+
1428
+ What it does:
1429
+ Reads Claude Code and Codex session records on this machine, keeps the
1430
+ sessions of repositories connected to the Workspace and inside its import
1431
+ window, and sends a summary of each. When it imports nothing, it prints why
1432
+ and what to do next.
1433
+
1434
+ Sends:
1435
+ Per session: agent, session ID, repository, branch, commit, start and end
1436
+ time, model, input and output token counts, and tool call counts.
1437
+
1438
+ Does not send:
1439
+ Prompt text, model responses, tool input or output, and code diffs. Sessions
1440
+ of repositories that are not connected are not sent.
1441
+
1442
+ Exit codes:
1443
+ 0 Finished (including "nothing to import").
1444
+ 1 Not logged in, keychain unavailable, a limit was reached, or sending failed.
1445
+
1446
+ Examples:
1447
+ harnessforce import
1448
+ `
1449
+ },
1450
+ "otel-headers": {
1451
+ summary: "Print the telemetry auth header (used by Claude Code)",
1452
+ text: `Usage: harnessforce otel-headers
1453
+
1454
+ When to use:
1455
+ You normally never run it. Claude Code calls it as its otelHeadersHelper.
1456
+
1457
+ What it does:
1458
+ Reads the ingest key from the keychain and prints {"Authorization": "Bearer <key>"}
1459
+ as JSON on stdout.
1460
+
1461
+ Sends:
1462
+ ${SENDS_NOTHING_LOCAL}
1463
+
1464
+ Does not send:
1465
+ Anything. Do not paste its output into chats or logs: it contains the key.
1466
+
1467
+ Exit codes:
1468
+ 0 Header printed.
1469
+ 1 No usable key (not logged in, keychain unavailable, or the destination was
1470
+ not verified). Nothing is printed to stdout.
1471
+
1472
+ Examples:
1473
+ harnessforce otel-headers
1474
+ `
1475
+ },
1476
+ run: {
1477
+ summary: "Start an agent tied to an Issue",
1478
+ text: `Usage: harnessforce run --issue <identifier> -- <agent> [args]
1479
+
1480
+ When to use:
1481
+ When the branch name does not contain the Issue identifier and you want the
1482
+ work tied to the Issue anyway.
1483
+
1484
+ What it does:
1485
+ Starts <agent> (for example claude or codex) with your arguments, with the
1486
+ Issue identifier added to the telemetry it exports. Everything after "--" is
1487
+ passed to the agent unchanged, including "--help". The agent's exit code is
1488
+ returned.
1489
+
1490
+ Options:
1491
+ --issue <identifier> The Issue identifier, for example ABC-123.
1492
+
1493
+ Sends:
1494
+ The same telemetry as an agent started directly, plus the Issue identifier.
1495
+
1496
+ Does not send:
1497
+ Anything beyond what init configured. Prompt text, responses and tool input
1498
+ and output are sent only if you chose --send-content at init.
1499
+
1500
+ Exit codes:
1501
+ The agent's own exit code when it ran.
1502
+ 1 Could not start: not logged in, bad arguments, or the agent was not found.
1503
+
1504
+ Examples:
1505
+ harnessforce run --issue ABC-123 -- claude
1506
+ harnessforce run --issue ABC-123 -- codex --model gpt-5
1507
+ `
1508
+ },
1509
+ tune: {
1510
+ summary: "Analyze your past sessions and report counts",
1511
+ text: `Usage: harnessforce tune [--all] [--no-send] [--show-report] [--json]
1512
+ harnessforce tune record
1513
+ harnessforce tune --purge
1514
+
1515
+ When to use:
1516
+ Periodically (or through /harnessforce:tune) to see where human intervention,
1517
+ repeated work, and MCP or tool usage could improve. It needs at least 10
1518
+ sessions.
1519
+
1520
+ What it does:
1521
+ Imports sessions that were not sent yet, analyzes the local session records,
1522
+ and prints the result. Files it writes live under ~/.harnessforce/tune/.
1523
+
1524
+ Options:
1525
+ --all Analyze sessions of repositories not connected too (they are
1526
+ never sent).
1527
+ --no-send Analyze only; send nothing.
1528
+ --show-report Print the exact values that would be sent.
1529
+ --json Print the result as one JSON object on stdout. Messages go to
1530
+ stderr.
1531
+ record Read one proposal as JSON from stdin and store it locally.
1532
+ --purge Delete the analysis, proposals, and unsent results.
1533
+
1534
+ Sends:
1535
+ Counts computed by public rules (no text), for connected repositories only.
1536
+ It also runs the same session import as "harnessforce import".
1537
+
1538
+ Does not send:
1539
+ Prompt text, model responses, tool input or output, and code diffs. Usage
1540
+ statistics and proposals stay on this machine.
1541
+
1542
+ Exit codes:
1543
+ 0 Done (including "not enough data" and "nothing sent").
1544
+ 1 Not logged in, keychain unavailable, another tune is running, or a local
1545
+ write failed.
1546
+ 2 Bad option, or invalid input to "record".
1547
+ 3 Analysis shown, but sending is not possible until you run "harnessforce init".
1548
+
1549
+ Examples:
1550
+ harnessforce tune
1551
+ harnessforce tune --no-send --json
1552
+ harnessforce tune --purge
1553
+ `
1554
+ },
1555
+ top: {
1556
+ summary: "Show running and recent sessions on this machine",
1557
+ text: `Usage: harnessforce top [--once] [--json] [--ascii] [--theme <auto|light|dark|ansi>]
1558
+
1559
+ When to use:
1560
+ To watch what your agents are doing right now. In a terminal it is a live
1561
+ view; with --once or --json it prints a snapshot and exits.
1562
+
1563
+ What it does:
1564
+ Reads Claude Code and Codex session records on this machine and shows each
1565
+ session's state, repository, branch, tokens, and tool calls.
1566
+
1567
+ Options:
1568
+ --once Print one snapshot and exit.
1569
+ --json Print one snapshot as JSON and exit.
1570
+ --ascii Use plain ASCII instead of box-drawing characters.
1571
+ --theme Color theme: auto (default), light, dark, or ansi.
1572
+
1573
+ Sends:
1574
+ ${SENDS_NOTHING_LOCAL}
1575
+
1576
+ Does not send:
1577
+ Anything. It never changes any file.
1578
+
1579
+ Exit codes:
1580
+ 0 Done (including quitting the live view).
1581
+ 1 Bad arguments.
1582
+
1583
+ Examples:
1584
+ harnessforce top
1585
+ harnessforce top --once --json
1586
+ `
1587
+ },
1588
+ hook: {
1589
+ summary: "Session hooks (used by Codex)",
1590
+ text: `Usage: harnessforce hook session-start|post-tool-use|stop
1591
+
1592
+ When to use:
1593
+ You normally never run it. Codex runs session-start when a session starts,
1594
+ post-tool-use after it edits files with apply_patch, and stop at the end of
1595
+ each turn.
1596
+
1597
+ What it does:
1598
+ session-start reads the hook input from stdin, registers the session with the
1599
+ Workspace, and prints context for the agent (the session ID, plus a one-line
1600
+ warning if registration failed).
1601
+ post-tool-use and stop record the repository and branch of the Git working
1602
+ trees the session worked in, and stop sends them when they changed.
1603
+
1604
+ Sends:
1605
+ Session registration: agent, session ID, repository, branch, commit, and
1606
+ start time.
1607
+ Work locations: agent, session ID, and the repository and branch of each
1608
+ working tree. Paths, commits, and file names are not sent.
1609
+
1610
+ Does not send:
1611
+ Prompt text, model responses, tool input or output, and code diffs.
1612
+
1613
+ Exit codes:
1614
+ 0 Always, even on failure: a hook must never stop your session. A failure is
1615
+ shown as a one-line warning.
1616
+ 1 Wrong arguments.
1617
+
1618
+ Examples:
1619
+ harnessforce hook session-start < hook-input.json
1620
+ `
1621
+ }
1622
+ };
1623
+ const ORDER = [
1624
+ "init",
1625
+ "import",
1626
+ "run",
1627
+ "tune",
1628
+ "top",
1629
+ "otel-headers",
1630
+ "hook"
1631
+ ];
1632
+ const NAME_WIDTH = Math.max(...ORDER.map((name) => name.length)) + 2;
1633
+ const OVERVIEW = `Usage: harnessforce <command> [options]
1634
+
1635
+ Connect your AI coding agents to a Harnessforce Workspace.
1636
+
1637
+ Commands:
1638
+ ${ORDER.map((name) => ` ${name.padEnd(NAME_WIDTH)}${COMMANDS$1[name].summary}`).join("\n")}
1639
+
1640
+ Run "harnessforce <command> --help" for what a command does, what it sends and
1641
+ does not send, and its exit codes.
1642
+
1643
+ Options:
1644
+ -h, --help Show this help.
1645
+ --version Show the version.
1646
+ `;
1647
+ const UNKNOWN_COMMAND_HINT = `Usage: harnessforce <command> [options]
1648
+ Run "harnessforce --help" to list the commands.
1649
+ `;
1650
+ const isCommand = (name) => name !== void 0 && Object.hasOwn(COMMANDS$1, name);
1651
+ const isHelpFlag = (arg) => arg === "--help" || arg === "-h";
1652
+ function resolveHelp(argv) {
1653
+ const [command, ...rest] = argv;
1654
+ if (command === void 0) return void 0;
1655
+ if (isHelpFlag(command) && rest.length === 0) return OVERVIEW;
1656
+ if (command === "help") {
1657
+ if (rest.length === 0) return OVERVIEW;
1658
+ if (rest.length === 1 && isCommand(rest[0])) return COMMANDS$1[rest[0]].text;
1659
+ return;
1660
+ }
1661
+ if (!isCommand(command)) return void 0;
1662
+ const separator = rest.indexOf("--");
1663
+ return (separator === -1 ? rest : rest.slice(0, separator)).some(isHelpFlag) ? COMMANDS$1[command].text : void 0;
1664
+ }
1665
+ // ../agent-core/src/hook-warning.ts
1666
+ const RESTART_FAILED_WARNING = "Could not restart the Harnessforce hook";
1667
+ const INVALID_ENDPOINT_WARNING = "The Harnessforce destination is invalid. Run `harnessforce init` again";
1668
+ const notConfiguredWarning = (agent) => agent === "claude_code" ? "Harnessforce sending is not configured. Run `/harnessforce:setup`" : "Harnessforce sending is not configured. Run `harnessforce init`";
1669
+ const stopped = (agent) => agent === "claude_code" ? "telemetry is not being sent" : "sessions are not being registered";
1670
+ const keyMissingWarning = (agent) => `Harnessforce cannot find your ingest key, so ${stopped(agent)}. Run \`harnessforce init\``;
1671
+ const keyUnreadableWarning = (agent) => `Harnessforce could not read your ingest key from the OS keychain, so ${agent === "claude_code" ? "telemetry may not be sent" : "sessions may not be registered"}. Unlock the keychain or run \`harnessforce init\``;
1672
+ const DESTINATION_MISMATCH_WARNING = {
1673
+ claude_code: "Telemetry is not being sent because the Harnessforce destination differs from the one saved by `harnessforce init`. Check this repository's settings or run `harnessforce init` again",
1674
+ codex: "Sessions are not being registered because the Harnessforce destination differs from the one saved by `harnessforce init`. Run `harnessforce init` again"
1675
+ };
1676
+ // ../agent-core/src/issue.ts
1677
+ const isIssueIdentifier = (value) => isToken(value, 300);
1678
+ // ../agent-core/src/using-harnessforce.ts
1679
+ const usingHarnessforceBody = "---\nname: using-harnessforce\ndescription: Use when starting a session in an environment with the Harnessforce hook, when starting work on an Issue, or when asked what is recorded or collected. The hook injects this text into the context at session start.\n---\n\n# Using Harnessforce\n\nThis session has the Harnessforce hook installed. Harnessforce links the work of people and AI to Issues and records it. Follow these rules.\n\n## What is recorded\n\n- Session registration: agent, session ID, repository, branch, commit, start time, and the first prompt ID.\n- Work locations: the repository and branch of each Git working tree the session works in. Paths are not sent.\n- Config snapshot: the structure of the configuration in use (Claude Code only).\n- OTel telemetry: what the user chooses to send.\n- For work on an Issue, `record-run` registers the session as a Run and closes it. It records no Plan, Decision, or Definition of Done.\n\n## What is not collected\n\n- Prompts, responses, tool input and output, and the body of code diffs arrive only when the user sends them (`--send-content` or managed settings) and the Workspace stores that category. The members the Workspace chooses can read them. `harnessforce import` never sends them.\n- Never put these contents, secrets, or environment variable values in MCP tool input or in a record.\n\n## Which hook sends what\n\n- SessionStart: session registration and the config snapshot (Codex sends session registration only).\n- UserPromptSubmit: resends on the first prompt (Claude Code only).\n- Stop: sends the work locations at the end of a turn in which they changed.\n- A failing hook never stops the session. When the hook cannot send, it shows a one-line warning. Tell the user when a warning appears.\n\n## Which skill and tool to use, and when\n\n- For work on a specified Issue ID, or when the environment variable `HARNESSFORCE_ISSUE` is set, follow `record-run` and use `start_run`, `get_issue`, and `complete_run`. An agent that cannot load the skill calls the tools in the same order.\n- Pass the value of the `harnessforce session_id:` line in the context to `start_run`. If there are several, use the last one. If the line is missing, do not guess the session ID, and do not call `start_run` or `complete_run`.\n- Use `propose-improvements` for the analysis of `/harnessforce:tune`.\n- When no Issue is specified, do not start recording a Run.\n\n## Red Flags\n\nStop when one of these thoughts comes up.\n\n| Thought | Reality |\n| --- | --- |\n| An Issue was specified, but the work is small, so recording is unnecessary | Work on a specified Issue is recorded regardless of its size |\n| The session ID line is missing, so I will guess it and call `start_run` | Do not guess. The Run would no longer be linked to the session |\n| Summarizing the content before recording would help | Do not pass content to the tool. It goes beyond what is collected |\n| A warning appeared, but I can ignore it | Sending is not working. Tell the user |\n\n## Priority\n\n1. The user's instructions (CLAUDE.md, AGENTS.md, direct requests). If the user tells you to stop recording, stop recording the Run.\n2. This introduction.\n3. Default behavior.\n\n## Subagents\n\nA subagent that was delegated only a specific task by its parent does not register a Run by following this introduction. The parent registers the Run.\n".replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---\n+/, "").trimEnd();
1680
+ // ../agent-core/src/session-context.ts
1681
+ const sessionContextLine = (sessionId) => `harnessforce session_id: ${sessionId}`;
1682
+ const sessionContext = (sessionId) => ({ hookSpecificOutput: {
1683
+ hookEventName: "SessionStart",
1684
+ additionalContext: `${sessionContextLine(sessionId)}\n\n${usingHarnessforceBody}`
1685
+ } });
1686
+ // ../agent-core/src/vcs.ts
1687
+ async function selectRemote(cwd, git) {
1688
+ if (await git(cwd, ["rev-parse", "--is-inside-work-tree"]) !== "true") return void 0;
1689
+ const remotes = (await git(cwd, ["remote"]))?.split("\n") ?? [];
1690
+ return remotes.includes("origin") ? "origin" : remotes[0];
1691
+ }
1692
+ async function resolveVcs(cwd, git) {
1693
+ const remote = await selectRemote(cwd, git);
1694
+ if (!remote) return void 0;
1695
+ const [url, branch, commit] = await Promise.all([
1696
+ git(cwd, [
1697
+ "remote",
1698
+ "get-url",
1699
+ remote
1700
+ ]),
1701
+ git(cwd, [
1702
+ "symbolic-ref",
1703
+ "--quiet",
1704
+ "--short",
1705
+ "HEAD"
1706
+ ]),
1707
+ git(cwd, [
1708
+ "rev-parse",
1709
+ "--verify",
1710
+ "--quiet",
1711
+ "HEAD"
1712
+ ])
1713
+ ]);
1714
+ const repository = url === void 0 ? void 0 : normalizeRepository(url);
1715
+ return {
1716
+ ...repository === void 0 ? {} : { repository },
1717
+ ...branch === void 0 ? {} : { branch },
1718
+ ...commit === void 0 ? {} : { commit }
1719
+ };
1720
+ }
1721
+ async function resolveRepository(cwd, git) {
1722
+ if (!isAbsolute(cwd)) return void 0;
1723
+ const remote = await selectRemote(cwd, git);
1724
+ if (!remote) return void 0;
1725
+ const url = await git(cwd, [
1726
+ "remote",
1727
+ "get-url",
1728
+ remote
1729
+ ]);
1730
+ return url === void 0 ? void 0 : normalizeRepository(url);
1731
+ }
1732
+ async function resolveProjectRoot(cwd, git) {
1733
+ const topLevel = await git(cwd, ["rev-parse", "--show-toplevel"]);
1734
+ return topLevel && isAbsolute(topLevel) ? topLevel : cwd;
1735
+ }
1736
+ // src/credentials/user-destination.ts
1099
1737
  async function resolveUserDestination(deps) {
1100
1738
  const { workspaceId, ingestEndpoint } = await resolveCliDestinations(deps.env, userSettingsPath(deps.env, deps.homeDir), "");
1101
1739
  if (!workspaceId && !ingestEndpoint) return { kind: "none" };
@@ -1110,7 +1748,7 @@ async function resolveUserDestination(deps) {
1110
1748
  };
1111
1749
  if (!await deps.keychain.isAvailable()) return {
1112
1750
  kind: "skipped",
1113
- reason: "no ingest key"
1751
+ reason: "keychain unreadable"
1114
1752
  };
1115
1753
  const key = await deps.keychain.get(ingestKeyAccount(workspaceId));
1116
1754
  if (!key) return {
@@ -1130,14 +1768,13 @@ async function resolveUserDestination(deps) {
1130
1768
  }
1131
1769
  };
1132
1770
  }
1133
- //#endregion
1134
- //#region src/hook/session-start.ts
1135
- const HOOK_USAGE = "Usage: harnessforce hook session-start\n";
1136
- const MAX_INPUT_BYTES$1 = 1048576;
1771
+ // src/hook/session-start.ts
1772
+ const HOOK_USAGE = "Usage: harnessforce hook session-start|post-tool-use|stop\n";
1773
+ const MAX_INPUT_BYTES = 1048576;
1137
1774
  const REGISTERING_SOURCES = /* @__PURE__ */ new Set(["startup", "clear"]);
1138
1775
  const KEYCHAIN_READ_TIMEOUT_MS = 1e3;
1139
- const REVOKED_MESSAGE = "送信キーが失効しています。`harnessforce init`を実行してください";
1140
- function parseInput(raw) {
1776
+ const REVOKED_MESSAGE = "Your ingest key has been revoked. Run `harnessforce init`";
1777
+ function parseInput$1(raw) {
1141
1778
  let fields;
1142
1779
  try {
1143
1780
  fields = JSON.parse(raw);
@@ -1153,30 +1790,40 @@ function parseInput(raw) {
1153
1790
  source: typeof source === "string" ? source : void 0
1154
1791
  };
1155
1792
  }
1156
- const report$1 = (deps, detail) => deps.stderr(`harnessforce: session registration ${detail}\n`);
1793
+ const report$2 = (deps, detail) => deps.stderr(`harnessforce: session registration ${detail}\n`);
1794
+ const SKIPPED_WARNINGS = {
1795
+ "not configured": notConfiguredWarning("codex"),
1796
+ "invalid endpoint": INVALID_ENDPOINT_WARNING,
1797
+ "no ingest key": keyMissingWarning("codex"),
1798
+ "keychain unreadable": keyUnreadableWarning("codex"),
1799
+ "destination not verified": DESTINATION_MISMATCH_WARNING.codex
1800
+ };
1157
1801
  function resolveWithinLimit(deps) {
1158
1802
  let timer;
1159
1803
  const slow = new Promise((resolve) => {
1160
1804
  timer = setTimeout(() => resolve({
1161
1805
  kind: "skipped",
1162
- reason: "no ingest key"
1806
+ reason: "keychain unreadable"
1163
1807
  }), KEYCHAIN_READ_TIMEOUT_MS);
1164
1808
  });
1165
1809
  return Promise.race([resolveUserDestination({
1166
1810
  ...deps,
1167
1811
  keychain: deps.keychain.withTimeout(KEYCHAIN_READ_TIMEOUT_MS)
1168
- }), slow]).finally(() => clearTimeout(timer));
1812
+ }).catch(() => ({
1813
+ kind: "skipped",
1814
+ reason: "keychain unreadable"
1815
+ })), slow]).finally(() => clearTimeout(timer));
1169
1816
  }
1170
1817
  async function resolveDestination(deps) {
1171
1818
  const resolved = await resolveWithinLimit(deps);
1172
1819
  if (resolved.kind === "ok") return { destination: resolved.destination };
1173
1820
  if (resolved.kind === "none") {
1174
- report$1(deps, "skipped (not configured)");
1821
+ report$2(deps, "skipped (not configured)");
1175
1822
  return { warning: notConfiguredWarning("codex") };
1176
1823
  }
1177
- report$1(deps, `skipped (${resolved.reason})`);
1178
- if (resolved.reason === "not configured") return { warning: notConfiguredWarning("codex") };
1179
- return resolved.reason === "invalid endpoint" ? { warning: INVALID_ENDPOINT_WARNING } : {};
1824
+ const detail = resolved.reason === "keychain unreadable" ? "no ingest key" : resolved.reason;
1825
+ report$2(deps, `skipped (${detail})`);
1826
+ return { warning: SKIPPED_WARNINGS[resolved.reason] };
1180
1827
  }
1181
1828
  function registrationSource(env) {
1182
1829
  const issue = env.HARNESSFORCE_ISSUE;
@@ -1198,27 +1845,216 @@ async function register(input, deps) {
1198
1845
  started_at: deps.now().toISOString()
1199
1846
  };
1200
1847
  const outcome = await postItem(destination, "v1/sessions", registration, deps.fetch);
1201
- if (outcome.kind === "failed") report$1(deps, `failed (${outcome.reason})`);
1848
+ if (outcome.kind === "failed") report$2(deps, `failed (${outcome.reason})`);
1202
1849
  return { outcome };
1203
1850
  }
1851
+ async function selectWarning(registered, deps) {
1852
+ if (registered.outcome?.kind === "unauthorized") return REVOKED_MESSAGE;
1853
+ const state = await readCliUpdateState(deps.homeDir, deps.client.version);
1854
+ if (state?.status === "unsupported") return updateLine("cli", state);
1855
+ const sending = registered.warning ?? (registered.outcome?.kind === "failed" ? "Harnessforce session registration failed. Check your network connection" : void 0);
1856
+ if (sending !== void 0 || !state) return sending;
1857
+ return noticeLine("cli", updateStatePath(deps.homeDir), state, {
1858
+ now: deps.now(),
1859
+ env: deps.env
1860
+ });
1861
+ }
1204
1862
  async function sessionStart(deps) {
1205
- const input = parseInput(await deps.readStdin(MAX_INPUT_BYTES$1).then((buffer) => buffer.toString("utf8")).catch(() => ""));
1863
+ const input = parseInput$1(await deps.readStdin(MAX_INPUT_BYTES).then((buffer) => buffer.toString("utf8")).catch(() => ""));
1206
1864
  if (!input) return 0;
1207
- const registered = input.source === void 0 || REGISTERING_SOURCES.has(input.source) ? await register(input, deps).catch((error) => {
1208
- report$1(deps, `failed (${error instanceof Error ? error.name : "unknown"})`);
1865
+ const registers = input.source === void 0 || REGISTERING_SOURCES.has(input.source);
1866
+ const blocked = registers && await isSendingBlocked(deps);
1867
+ const registered = registers && !blocked ? await register(input, deps).catch((error) => {
1868
+ report$2(deps, `failed (${error instanceof Error ? error.name : "unknown"})`);
1209
1869
  return {};
1210
1870
  }) : {};
1211
- const warning = registered.outcome?.kind === "unauthorized" ? REVOKED_MESSAGE : registered.warning ?? (registered.outcome?.kind === "failed" ? "Harnessforceへのsession登録に失敗しました。通信を確かめてください" : void 0);
1212
- if (warning !== void 0) {
1213
- deps.stderr(`${warning}\n`);
1214
- deps.stdout(`${sessionContextLine(input.sessionId)}\n${usingHarnessforceBody}\n${warning}\n`);
1215
- return 0;
1871
+ const warning = registers ? await selectWarning(registered, deps) : void 0;
1872
+ if (warning !== void 0) deps.stderr(`${warning}\n`);
1873
+ deps.stdout(`${JSON.stringify({
1874
+ ...sessionContext(input.sessionId),
1875
+ ...warning === void 0 ? {} : { systemMessage: warning }
1876
+ })}\n`);
1877
+ return 0;
1878
+ }
1879
+ // ../agent-core/src/work-location.ts
1880
+ const absolute = (value) => typeof value === "string" && isAbsolute(value) ? value : void 0;
1881
+ const toolInputOf = (fields) => isObject(fields.tool_input) ? fields.tool_input : {};
1882
+ const PATCH_FILE_LINE = /^\*\*\* (?:Add File|Update File|Delete File|Move to): (.+)$/;
1883
+ function patchedDirectories(command, cwd) {
1884
+ if (typeof command !== "string") return [];
1885
+ return command.split(/\r?\n/).flatMap((line) => {
1886
+ const path = PATCH_FILE_LINE.exec(line)?.[1]?.trim();
1887
+ return path ? [dirname(resolve(cwd, path))] : [];
1888
+ });
1889
+ }
1890
+ /**
1891
+ * eventの入力から、作業位置を求めるdirectoryを返す。読むのは`new_cwd`、`file_path`、patchのfileの行のpathだけで、編集の
1892
+ * 本文を読まない。
1893
+ */
1894
+ function observedDirectories(agent, event, fields, cwd) {
1895
+ if (event === "stop") return [cwd];
1896
+ if (agent === "claude_code" && event === "cwd-changed") {
1897
+ const newCwd = absolute(fields.new_cwd);
1898
+ return newCwd ? [newCwd] : [];
1899
+ }
1900
+ if (event !== "post-tool-use") return [];
1901
+ const input = toolInputOf(fields);
1902
+ if (agent === "codex") return fields.tool_name === "apply_patch" ? [...new Set(patchedDirectories(input.command, cwd))] : [];
1903
+ if (fields.tool_name !== "Write" && fields.tool_name !== "Edit") return [];
1904
+ const filePath = absolute(input.file_path);
1905
+ return filePath ? [dirname(filePath)] : [];
1906
+ }
1907
+ const isDirectory = (path) => stat(path).then((found) => found.isDirectory(), () => false);
1908
+ /** directoryのworking treeの、その時点のrepositoryとbranch。repositoryかbranchを求められなければundefined。 */
1909
+ async function observePair(directory, git) {
1910
+ if (!await isDirectory(directory)) return void 0;
1911
+ const vcs = await resolveVcs(directory, git);
1912
+ return vcs?.repository && vcs.branch ? {
1913
+ repository: vcs.repository,
1914
+ branch: vcs.branch
1915
+ } : void 0;
1916
+ }
1917
+ const emptyLocationState = () => ({
1918
+ pairs: [],
1919
+ sent: [],
1920
+ unauthorized: false
1921
+ });
1922
+ const pairKey = (pair) => JSON.stringify([pair.repository, pair.branch]);
1923
+ /** 組を加える。記録済みの組と、10件に達した後の組は加えない。 */
1924
+ function withObservedPair(state, pair) {
1925
+ if (state.pairs.some((saved) => pairKey(saved) === pairKey(pair)) || state.pairs.length >= MAX_WORK_LOCATIONS) return state;
1926
+ return {
1927
+ ...state,
1928
+ pairs: [...state.pairs, pair]
1929
+ };
1930
+ }
1931
+ /** 記録した組の集合が、最後に送った集合と異なる場合だけ送る組。 */
1932
+ function locationsToSend(state) {
1933
+ if (state.unauthorized || state.pairs.length === 0) return void 0;
1934
+ const sent = new Set(state.sent.map(pairKey));
1935
+ return sent.size === state.pairs.length && state.pairs.every((pair) => sent.has(pairKey(pair))) ? void 0 : state.pairs;
1936
+ }
1937
+ const isPair = (value) => isObject(value) && typeof value.repository === "string" && typeof value.branch === "string";
1938
+ async function readLocationState(path) {
1939
+ try {
1940
+ const saved = JSON.parse(await readFile(path, "utf8"));
1941
+ if (!isObject(saved) || !Array.isArray(saved.pairs) || !Array.isArray(saved.sent)) return emptyLocationState();
1942
+ return {
1943
+ pairs: saved.pairs.filter(isPair),
1944
+ sent: saved.sent.filter(isPair),
1945
+ unauthorized: saved.unauthorized === true
1946
+ };
1947
+ } catch {
1948
+ return emptyLocationState();
1949
+ }
1950
+ }
1951
+ async function writeLocationState(path, state) {
1952
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
1953
+ await writeFile(temporary, JSON.stringify(state), { mode: 384 });
1954
+ await rename(temporary, path);
1955
+ }
1956
+ const FILE_SAFE_SESSION_ID = /^[A-Za-z0-9_-]+$/;
1957
+ const LOCATION_DIRECTORY = "harnessforce-locations";
1958
+ async function isPrivateDirectory(path) {
1959
+ const found = await lstat(path).catch(() => void 0);
1960
+ if (!found) return false;
1961
+ const uid = process.getuid?.();
1962
+ return found.isDirectory() && !found.isSymbolicLink() && (uid === void 0 || found.uid === uid) && (found.mode & 511) === 448;
1963
+ }
1964
+ /**
1965
+ * Codexの記録のfile。Codexのhookは`scratchpad_dir`を受け取らないため、OSの一時directoryの下に置く。macOSとLinuxでは
1966
+ * directoryの所有者とmodeを確かめ、満たさなければundefinedを返す(記録も送信もしない)。Windowsの`TEMP`は利用者ごとに分かれる。
1967
+ */
1968
+ async function codexLocationStatePath(temporaryDirectory, sessionId, platform = process.platform) {
1969
+ if (!FILE_SAFE_SESSION_ID.test(sessionId)) return void 0;
1970
+ const directory = join(temporaryDirectory, LOCATION_DIRECTORY);
1971
+ if (platform !== "win32") {
1972
+ await mkdir(directory, { mode: 448 }).catch(() => {});
1973
+ if (!await isPrivateDirectory(directory)) return void 0;
1974
+ } else await mkdir(directory, { recursive: true }).catch(() => {});
1975
+ return join(directory, `codex-${sessionId}.json`);
1976
+ }
1977
+ /** 観測したdirectoryの組を記録のfileへ加える。gitの外のdirectoryは記録しない。 */
1978
+ async function recordObservedPairs(statePath, directories, deps) {
1979
+ const pairs = await Promise.all(directories.map((directory) => observePair(directory, deps.git)));
1980
+ const before = await readLocationState(statePath);
1981
+ const after = pairs.reduce((state, pair) => pair ? withObservedPair(state, pair) : state, before);
1982
+ if (after !== before) await writeLocationState(statePath, after);
1983
+ return after;
1984
+ }
1985
+ /**
1986
+ * 記録した組の集合が最後に送った集合と異なれば`/v1/sessions/locations`へ送る。2xxなら送った集合を記録し、401なら以後送らない
1987
+ * ことを記録する。それ以外の失敗では記録を変えず、次の`Stop`が送り直しを兼ねる。
1988
+ */
1989
+ async function sendLocations(statePath, state, item, destination, fetchImpl) {
1990
+ const locations = locationsToSend(state);
1991
+ if (!locations) return { kind: "unchanged" };
1992
+ const outcome = await postItem(destination, "v1/sessions/locations", {
1993
+ agent: item.agent,
1994
+ session_id: item.sessionId,
1995
+ locations
1996
+ }, fetchImpl);
1997
+ if (outcome.kind === "accepted") await writeLocationState(statePath, {
1998
+ ...state,
1999
+ sent: locations
2000
+ });
2001
+ if (outcome.kind === "unauthorized") await writeLocationState(statePath, {
2002
+ ...state,
2003
+ unauthorized: true
2004
+ });
2005
+ return {
2006
+ kind: "sent",
2007
+ outcome
2008
+ };
2009
+ }
2010
+ // src/hook/work-location.ts
2011
+ function parseInput(raw) {
2012
+ let fields;
2013
+ try {
2014
+ fields = JSON.parse(raw);
2015
+ } catch {
2016
+ return;
1216
2017
  }
1217
- deps.stdout(`${JSON.stringify(sessionContext(input.sessionId))}\n`);
2018
+ if (!isObject(fields)) return void 0;
2019
+ const { session_id: sessionId, cwd } = fields;
2020
+ if (typeof sessionId !== "string" || !isToken(sessionId) || typeof cwd !== "string" || !isAbsolute(cwd)) return void 0;
2021
+ return {
2022
+ sessionId,
2023
+ cwd,
2024
+ fields
2025
+ };
2026
+ }
2027
+ const report$1 = (deps, detail) => deps.stderr(`harnessforce: session location ${detail}\n`);
2028
+ async function observe(event, input, deps) {
2029
+ const statePath = await codexLocationStatePath(deps.hookTmpDir, input.sessionId);
2030
+ if (!statePath) return;
2031
+ const state = await recordObservedPairs(statePath, observedDirectories("codex", event, input.fields, input.cwd), deps);
2032
+ if (event !== "stop") return;
2033
+ if (state.unauthorized) return;
2034
+ if (await isSendingBlocked(deps)) return;
2035
+ const { destination } = await resolveDestination({
2036
+ ...deps,
2037
+ stderr: () => {}
2038
+ });
2039
+ if (!destination) return;
2040
+ const sent = await sendLocations(statePath, state, {
2041
+ agent: "codex",
2042
+ sessionId: input.sessionId
2043
+ }, destination, deps.fetch);
2044
+ if (sent.kind !== "sent" || sent.outcome.kind === "accepted") return;
2045
+ report$1(deps, `failed (${sent.outcome.kind === "unauthorized" ? "unauthorized" : sent.outcome.reason})`);
2046
+ }
2047
+ /**
2048
+ * correlation.md「Codexのhook」「sessionの作業位置」。Codexの`PostToolUse`と`Stop`のhookが起動する。どの失敗でもsessionを
2049
+ * 止めず、常にexit 0で終える。`Stop`はstdoutにJSONを期待するため、空のobjectだけを書く。
2050
+ */
2051
+ async function workLocationHook(event, deps) {
2052
+ const input = parseInput(await deps.readStdin(MAX_INPUT_BYTES).then((buffer) => buffer.toString("utf8")).catch(() => ""));
2053
+ if (input) await observe(event, input, deps).catch((error) => report$1(deps, `failed (${error instanceof Error ? error.name : "unknown"})`));
2054
+ if (event === "stop") deps.stdout("{}\n");
1218
2055
  return 0;
1219
2056
  }
1220
- //#endregion
1221
- //#region src/shared/stop.ts
2057
+ // src/shared/stop.ts
1222
2058
  var CommandStop = class {
1223
2059
  text;
1224
2060
  constructor(text) {
@@ -1238,8 +2074,7 @@ async function runUntilStop(body, stderr) {
1238
2074
  return 1;
1239
2075
  }
1240
2076
  }
1241
- //#endregion
1242
- //#region src/credentials/api-token.ts
2077
+ // src/credentials/api-token.ts
1243
2078
  const NO_WHITESPACE$1 = /^\S+$/;
1244
2079
  const INSTANT$2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
1245
2080
  const isInstant = (value) => typeof value === "string" && INSTANT$2.test(value) && !Number.isNaN(Date.parse(value));
@@ -1268,8 +2103,7 @@ function parseStoredApiToken(secret, workspaceId) {
1268
2103
  return;
1269
2104
  }
1270
2105
  }
1271
- //#endregion
1272
- //#region src/credentials/access.ts
2106
+ // src/credentials/access.ts
1273
2107
  async function verifyCliAccess(deps, messages) {
1274
2108
  const { keychain } = deps;
1275
2109
  const stop = (message) => stopWith(messages[message]);
@@ -1293,8 +2127,7 @@ async function verifyCliAccess(deps, messages) {
1293
2127
  accessToken: apiToken.accessToken
1294
2128
  };
1295
2129
  }
1296
- //#endregion
1297
- //#region src/shared/http.ts
2130
+ // src/shared/http.ts
1298
2131
  const REQUEST_TIMEOUT_MS = 3e4;
1299
2132
  async function readJsonObjectBody(response) {
1300
2133
  try {
@@ -1304,8 +2137,7 @@ async function readJsonObjectBody(response) {
1304
2137
  return;
1305
2138
  }
1306
2139
  }
1307
- //#endregion
1308
- //#region src/shared/metadata.ts
2140
+ // src/shared/metadata.ts
1309
2141
  const WELL_KNOWN = "/.well-known/oauth-authorization-server";
1310
2142
  const withoutTrailingSlash = (value) => value.replace(/\/+$/, "");
1311
2143
  async function fetchMetadata(base, fetchImpl) {
@@ -1340,14 +2172,13 @@ async function discoverTokenEndpoint(base, fetchImpl) {
1340
2172
  const endpoint = endpointOf(await fetchMetadata(base, fetchImpl), "token_endpoint");
1341
2173
  return endpoint?.origin === base.origin ? endpoint : void 0;
1342
2174
  }
1343
- //#endregion
1344
- //#region src/credentials/token-lock.ts
2175
+ // src/credentials/token-lock.ts
1345
2176
  const tokenLockPath = (homeDir) => join(homeDir, ".harnessforce", "token.lock");
1346
2177
  const LOCK_WAIT_MS$1 = 3e4;
1347
2178
  const STALE_LOCK_MS$1 = 6e4;
1348
2179
  const RETRY_INTERVAL_MS$1 = 100;
1349
2180
  const DIR_MODE$2 = 448;
1350
- const FILE_MODE$5 = 384;
2181
+ const FILE_MODE$4 = 384;
1351
2182
  const isAlreadyExists$1 = (error) => error?.code === "EEXIST";
1352
2183
  async function removeIfStale$1(path, now) {
1353
2184
  const created = await stat(path).catch(() => void 0);
@@ -1362,7 +2193,7 @@ async function acquireTokenLock(path, deps) {
1362
2193
  const deadline = deps.now() + LOCK_WAIT_MS$1;
1363
2194
  while (deps.now() < deadline) {
1364
2195
  try {
1365
- await (await open(path, "wx", FILE_MODE$5)).close();
2196
+ await (await open(path, "wx", FILE_MODE$4)).close();
1366
2197
  return () => rm(path, { force: true });
1367
2198
  } catch (error) {
1368
2199
  if (!isAlreadyExists$1(error)) throw error;
@@ -1371,8 +2202,7 @@ async function acquireTokenLock(path, deps) {
1371
2202
  await deps.sleep(RETRY_INTERVAL_MS$1);
1372
2203
  }
1373
2204
  }
1374
- //#endregion
1375
- //#region src/credentials/api-token-session.ts
2205
+ // src/credentials/api-token-session.ts
1376
2206
  const CLIENT_ID$1 = "harnessforce-cli";
1377
2207
  const LOCKED_KEYCHAIN_TIMEOUT_MS = 1e4;
1378
2208
  const isPast = (instant, now) => now >= Date.parse(instant);
@@ -1482,8 +2312,7 @@ function createApiTokenSession(deps) {
1482
2312
  authorizedFetch
1483
2313
  };
1484
2314
  }
1485
- //#endregion
1486
- //#region src/import/read-api.ts
2315
+ // src/import/read-api.ts
1487
2316
  const FAILED$1 = { kind: "failed" };
1488
2317
  async function getJson(url, apiToken, fetchImpl) {
1489
2318
  try {
@@ -1553,8 +2382,7 @@ async function fetchSessionImportDays(base, apiToken, fetchImpl) {
1553
2382
  value: days
1554
2383
  } : FAILED$1;
1555
2384
  }
1556
- //#endregion
1557
- //#region src/import/record-base.ts
2385
+ // src/import/record-base.ts
1558
2386
  async function readSettingsEnv(path) {
1559
2387
  const read = await readUserSettings(path);
1560
2388
  const env = read.kind === "ok" ? read.settings.env : void 0;
@@ -1575,11 +2403,10 @@ async function recordBase(deps) {
1575
2403
  const { CLAUDE_CONFIG_DIR: configDir } = await readTrustedEnv(deps, ["CLAUDE_CONFIG_DIR"]);
1576
2404
  return configDir ?? join(deps.homeDir, ".claude");
1577
2405
  }
1578
- //#endregion
1579
- //#region src/import/state.ts
2406
+ // src/import/state.ts
1580
2407
  const STATE_VERSION = 1;
1581
2408
  const DIR_MODE$1 = 448;
1582
- const FILE_MODE$4 = 384;
2409
+ const FILE_MODE$3 = 384;
1583
2410
  const sessionStateKey = (session) => session.agent === "claude_code" ? session.session_id : `${session.agent}:${session.session_id}`;
1584
2411
  const importStatePath = (homeDir) => join(homeDir, ".harnessforce", "import-state.json");
1585
2412
  const keyOf = ({ workspaceId, endpoint }) => `${workspaceId} ${endpoint}`;
@@ -1624,17 +2451,16 @@ async function recordSentSessions(path, destination, sessionIds) {
1624
2451
  const temporary = `${path}.${randomUUID()}.tmp`;
1625
2452
  try {
1626
2453
  await writeFile(temporary, `${JSON.stringify(next)}\n`, {
1627
- mode: FILE_MODE$4,
2454
+ mode: FILE_MODE$3,
1628
2455
  flag: "wx"
1629
2456
  });
1630
- await chmod(temporary, FILE_MODE$4);
2457
+ await chmod(temporary, FILE_MODE$3);
1631
2458
  await rename(temporary, path);
1632
2459
  } finally {
1633
2460
  await rm(temporary, { force: true });
1634
2461
  }
1635
2462
  }
1636
- //#endregion
1637
- //#region src/import/send.ts
2463
+ // src/import/send.ts
1638
2464
  const BATCH_SIZE$1 = 100;
1639
2465
  const MAX_RESENDS = 3;
1640
2466
  const MAX_RETRY_AFTER_SECONDS = 60;
@@ -1851,8 +2677,7 @@ var SessionAccumulator = class {
1851
2677
  };
1852
2678
  }
1853
2679
  };
1854
- //#endregion
1855
- //#region src/import/rollout.ts
2680
+ // src/import/rollout.ts
1856
2681
  const CODEX_PARSER_VERSION = "1.0.0";
1857
2682
  const DETACHED$2 = "HEAD";
1858
2683
  const COMMIT_SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
@@ -1965,7 +2790,7 @@ async function parseRollout(path) {
1965
2790
  const accumulator = new Accumulator$1();
1966
2791
  let skippedLines = 0;
1967
2792
  try {
1968
- const lines = createInterface$1({
2793
+ const lines = createInterface({
1969
2794
  input: createReadStream(path, { encoding: "utf8" }),
1970
2795
  crlfDelay: Number.POSITIVE_INFINITY
1971
2796
  });
@@ -1989,8 +2814,7 @@ async function parseRollout(path) {
1989
2814
  }
1990
2815
  return accumulator.result(skippedLines);
1991
2816
  }
1992
- //#endregion
1993
- //#region src/import/transcript.ts
2817
+ // src/import/transcript.ts
1994
2818
  const PARSER_VERSION = "1.1.0";
1995
2819
  const isToken$1 = (value, maxLength = 256) => typeof value === "string" && value.length >= 1 && value.length <= maxLength && /^\S+$/.test(value);
1996
2820
  const MAX_TOOL_NAME = 128;
@@ -2194,7 +3018,7 @@ async function readTranscript(path, accumulator) {
2194
3018
  let lines = 0;
2195
3019
  let skippedLines = 0;
2196
3020
  try {
2197
- const reader = createInterface$1({
3021
+ const reader = createInterface({
2198
3022
  input: createReadStream(path, { encoding: "utf8" }),
2199
3023
  crlfDelay: Number.POSITIVE_INFINITY
2200
3024
  });
@@ -2267,8 +3091,12 @@ async function parseTranscriptUsage(path) {
2267
3091
  const { usage, isReadable } = await readTranscript(path, new Accumulator());
2268
3092
  return isReadable ? usage : void 0;
2269
3093
  }
2270
- //#endregion
2271
- //#region src/import/sessions.ts
3094
+ // src/import/sessions.ts
3095
+ const noneNotImported = () => ({
3096
+ outOfRange: 0,
3097
+ outsideGit: 0,
3098
+ notConnected: 0
3099
+ });
2272
3100
  const readDir$1 = (dir) => readdir(dir, { withFileTypes: true }).catch(() => []);
2273
3101
  async function listTranscripts(projectsDir) {
2274
3102
  const projects = (await readDir$1(projectsDir)).filter((entry) => entry.isDirectory());
@@ -2295,33 +3123,49 @@ function toSessionImport(session, repository) {
2295
3123
  };
2296
3124
  }
2297
3125
  const sortedSessions = (sessions) => [...sessions.values()].sort((a, b) => Date.parse(a.started_at) - Date.parse(b.started_at) || (a.session_id < b.session_id ? -1 : a.session_id > b.session_id ? 1 : 0));
2298
- async function scanSessions(options) {
3126
+ const cachedRepositoryOf = (git) => {
2299
3127
  const repositories = /* @__PURE__ */ new Map();
2300
- const repositoryOf = (cwd) => {
2301
- const cached = repositories.get(cwd) ?? resolveRepository(cwd, options.git);
3128
+ return (cwd) => {
3129
+ const cached = repositories.get(cwd) ?? resolveRepository(cwd, git);
2302
3130
  repositories.set(cwd, cached);
2303
3131
  return cached;
2304
3132
  };
3133
+ };
3134
+ async function judge(session, options, repositoryOf) {
3135
+ if (Date.parse(session.endedAt) < options.sinceMs) return { kind: "outOfRange" };
3136
+ const repository = session.cwd ? await repositoryOf(session.cwd) : void 0;
3137
+ if (repository === void 0) return { kind: "outsideGit" };
3138
+ return options.connected.has(repository) ? {
3139
+ kind: "import",
3140
+ repository
3141
+ } : { kind: "notConnected" };
3142
+ }
3143
+ async function scanSessions(options) {
3144
+ const repositoryOf = cachedRepositoryOf(options.git);
2305
3145
  const sessions = /* @__PURE__ */ new Map();
3146
+ const notImported = noneNotImported();
2306
3147
  let skippedLines = 0;
2307
3148
  let skippedFiles = 0;
2308
3149
  for (const file of await listTranscripts(options.projectsDir)) {
2309
3150
  const modifiedMs = (await stat(file).catch(() => void 0))?.mtimeMs;
2310
- if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) continue;
3151
+ if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) {
3152
+ notImported.outOfRange += 1;
3153
+ continue;
3154
+ }
2311
3155
  const result = await parseTranscript(file);
2312
3156
  skippedLines += result.skippedLines;
2313
3157
  if (result.kind === "unreadable") skippedFiles += 1;
2314
3158
  if (result.kind !== "session") continue;
2315
3159
  const { session } = result;
2316
- if (Date.parse(session.endedAt) < options.sinceMs || !session.cwd) continue;
2317
- const repository = await repositoryOf(session.cwd);
2318
- if (repository === void 0 || !options.connected.has(repository)) continue;
2319
- if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, toSessionImport(session, repository));
3160
+ const verdict = await judge(session, options, repositoryOf);
3161
+ if (verdict.kind !== "import") notImported[verdict.kind] += 1;
3162
+ else if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, toSessionImport(session, verdict.repository));
2320
3163
  }
2321
3164
  return {
2322
3165
  sessions: sortedSessions(sessions),
2323
3166
  skippedLines,
2324
- skippedFiles
3167
+ skippedFiles,
3168
+ notImported
2325
3169
  };
2326
3170
  }
2327
3171
  const numericName = (name, length) => name.length === length && /^\d+$/.test(name) ? Number(name) : void 0;
@@ -2371,93 +3215,117 @@ function toCodexSessionImport(session, repository) {
2371
3215
  };
2372
3216
  }
2373
3217
  async function scanCodexSessions(options) {
2374
- const repositories = /* @__PURE__ */ new Map();
2375
- const repositoryOf = (cwd) => {
2376
- const cached = repositories.get(cwd) ?? resolveRepository(cwd, options.git);
2377
- repositories.set(cwd, cached);
2378
- return cached;
2379
- };
2380
- const isSent = async (cwd) => {
2381
- if (!cwd) return void 0;
2382
- const repository = await repositoryOf(cwd);
2383
- return repository !== void 0 && options.connected.has(repository) ? repository : void 0;
2384
- };
3218
+ const repositoryOf = cachedRepositoryOf(options.git);
2385
3219
  const { files, compressed } = await listRollouts$1(join(options.codexDir, "sessions"), localDayKey(options.sinceMs));
2386
3220
  const sessions = /* @__PURE__ */ new Map();
3221
+ const notImported = noneNotImported();
2387
3222
  let skippedLines = 0;
2388
3223
  let skippedFiles = 0;
2389
3224
  let unextractable = 0;
2390
3225
  for (const file of files) {
2391
3226
  const modifiedMs = (await stat(file).catch(() => void 0))?.mtimeMs;
2392
- if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) continue;
3227
+ if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) {
3228
+ notImported.outOfRange += 1;
3229
+ continue;
3230
+ }
2393
3231
  const result = await parseRollout(file);
2394
3232
  skippedLines += result.skippedLines;
2395
3233
  if (result.kind === "unreadable") skippedFiles += 1;
2396
3234
  if (result.kind === "unextractable") {
2397
- if (Date.parse(result.endedAt) >= options.sinceMs && await isSent(result.cwd)) unextractable += 1;
3235
+ const verdict = await judge(result, options, repositoryOf);
3236
+ if (verdict.kind === "import") unextractable += 1;
3237
+ else notImported[verdict.kind] += 1;
2398
3238
  continue;
2399
3239
  }
2400
3240
  if (result.kind !== "session") continue;
2401
3241
  const { session } = result;
2402
- if (Date.parse(session.endedAt) < options.sinceMs) continue;
2403
- const repository = await isSent(session.cwd);
2404
- if (repository === void 0) continue;
2405
- if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, toCodexSessionImport(session, repository));
3242
+ const verdict = await judge(session, options, repositoryOf);
3243
+ if (verdict.kind !== "import") notImported[verdict.kind] += 1;
3244
+ else if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, toCodexSessionImport(session, verdict.repository));
2406
3245
  }
2407
3246
  return {
2408
3247
  sessions: sortedSessions(sessions),
2409
3248
  skippedLines,
2410
3249
  skippedFiles,
3250
+ notImported,
2411
3251
  unextractable,
2412
3252
  compressed
2413
3253
  };
2414
3254
  }
2415
- //#endregion
2416
- //#region src/import/command.ts
3255
+ // src/import/command.ts
2417
3256
  const DAY_MS$1 = 864e5;
2418
- const MESSAGES$1 = {
2419
- runInit: "`harnessforce init`を実行してください",
2420
- invalidUrl: "接続先のURLが不正です",
3257
+ const MESSAGES = {
3258
+ runInit: "Run `harnessforce init`",
3259
+ invalidUrl: "The connection URL is invalid",
2421
3260
  keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
2422
- readFailed: "Harnessforceとの通信に失敗しました。もう一度`harnessforce import`を実行してください",
2423
- sendFailed: "Harnessforceとの通信に失敗しました。もう一度`harnessforce import`を実行すると続きから取り込みます",
2424
- revoked: "送信キーが失効しています。`harnessforce init`を実行してください",
2425
- loginExpired: "ログインの有効期限が切れました。`harnessforce init`を実行してください",
2426
- stateFailed: "取り込みの状態を保存できませんでした"
3261
+ readFailed: "Communication with Harnessforce failed. Run `harnessforce import` again",
3262
+ sendFailed: "Communication with Harnessforce failed. Run `harnessforce import` again to resume importing",
3263
+ revoked: "Your ingest key has been revoked. Run `harnessforce init`",
3264
+ loginExpired: "Your login has expired. Run `harnessforce init`",
3265
+ stateFailed: "Could not save the import state",
3266
+ connectNone: "No GitHub repository is connected to the Workspace. Connect the GitHub App in Harnessforce to import sessions with `harnessforce import`",
3267
+ connectOther: "The session's repository is not connected to the Workspace. Connect that repository to the GitHub App in Harnessforce to import it with `harnessforce import`",
3268
+ nothingFound: "No session records to import were found"
2427
3269
  };
2428
3270
  const limitMessage = {
2429
- monthly_event_limit: (count) => `月間イベント数の上限に達したため、${count}件のセッションを取り込めませんでした。上限が解除された後に\`harnessforce import\`を再実行すると続きから取り込みます`,
2430
- workspace_read_only: (count) => `Workspaceが閲覧のみのため、${count}件のセッションを取り込めませんでした。閲覧のみが解除された後に\`harnessforce import\`を再実行すると続きから取り込みます`
3271
+ monthly_event_limit: (count) => `Could not import ${count} session(s) because the monthly event limit was reached. Run \`harnessforce import\` again after the limit is lifted to resume importing`,
3272
+ workspace_read_only: (count) => `Could not import ${count} session(s) because the Workspace is view-only. Run \`harnessforce import\` again after view-only mode is lifted to resume importing`
2431
3273
  };
2432
- const ACCESS_MESSAGES$3 = {
2433
- keychainUnavailable: MESSAGES$1.keychainUnavailable,
2434
- initRequired: MESSAGES$1.runInit,
2435
- invalidUrl: MESSAGES$1.invalidUrl,
2436
- apiTokenMissing: MESSAGES$1.runInit
3274
+ const ACCESS_MESSAGES$2 = {
3275
+ keychainUnavailable: MESSAGES.keychainUnavailable,
3276
+ initRequired: MESSAGES.runInit,
3277
+ invalidUrl: MESSAGES.invalidUrl,
3278
+ apiTokenMissing: MESSAGES.runInit
2437
3279
  };
2438
3280
  function unwrap(outcome) {
2439
3281
  if (outcome.kind === "ok") return outcome.value;
2440
- return stopWith(outcome.kind === "unauthorized" ? MESSAGES$1.loginExpired : MESSAGES$1.readFailed);
3282
+ return stopWith(outcome.kind === "unauthorized" ? MESSAGES.loginExpired : MESSAGES.readFailed);
2441
3283
  }
2442
- function report(result, deps) {
3284
+ const NOT_IMPORTED_LABELS = [
3285
+ ["alreadySent", "already sent"],
3286
+ ["outOfRange", "outside the period"],
3287
+ ["outsideGit", "outside a git repository"],
3288
+ ["notConnected", "in a repository not connected to the Workspace"]
3289
+ ];
3290
+ function breakdownLine(breakdown) {
3291
+ const parts = NOT_IMPORTED_LABELS.filter(([key]) => breakdown[key] > 0).map(([key, label]) => `${label} ${breakdown[key]}`);
3292
+ return parts.length > 0 ? `Sessions not imported: ${parts.join(", ")}` : void 0;
3293
+ }
3294
+ function reportNotImported(imported, notes, deps) {
3295
+ const line = breakdownLine(notes.breakdown);
3296
+ if (line) deps.stdout(`${line}\n`);
3297
+ if (imported > 0) return;
3298
+ if (notes.breakdown.notConnected > 0) deps.stdout(`${notes.connectedCount === 0 ? MESSAGES.connectNone : MESSAGES.connectOther}\n`);
3299
+ else if (!line && !notes.otherNoticeShown) deps.stdout(`${MESSAGES.nothingFound}\n`);
3300
+ }
3301
+ function report(result, notes, deps) {
2443
3302
  switch (result.kind) {
2444
3303
  case "done":
2445
- deps.stdout(`${result.imported}件のセッションを取り込みました\n`);
2446
- if (result.invalid > 0) deps.stdout(`形式が不正なため${result.invalid}件のセッションを取り込めませんでした\n`);
3304
+ deps.stdout(`Sessions imported: ${result.imported}\n`);
3305
+ if (result.invalid > 0) deps.stdout(`Could not import ${result.invalid} session(s) because their format is invalid\n`);
3306
+ reportNotImported(result.imported, {
3307
+ ...notes,
3308
+ otherNoticeShown: notes.otherNoticeShown || result.invalid > 0
3309
+ }, deps);
2447
3310
  return 0;
2448
3311
  case "limited":
2449
3312
  deps.stderr(`${limitMessage[result.reason](result.notImported)}\n`);
2450
3313
  return 1;
2451
3314
  case "unauthorized":
2452
- deps.stderr(`${MESSAGES$1.revoked}\n`);
3315
+ deps.stderr(`${MESSAGES.revoked}\n`);
2453
3316
  return 1;
2454
3317
  case "failed":
2455
- deps.stderr(`${MESSAGES$1.sendFailed}\n`);
3318
+ deps.stderr(`${MESSAGES.sendFailed}\n`);
2456
3319
  return 1;
2457
3320
  }
2458
3321
  }
3322
+ const sumNotImported = (a, b) => ({
3323
+ outOfRange: a.outOfRange + b.outOfRange,
3324
+ outsideGit: a.outsideGit + b.outsideGit,
3325
+ notConnected: a.notConnected + b.notConnected
3326
+ });
2459
3327
  async function runImport(deps) {
2460
- const access = await verifyCliAccess(deps, ACCESS_MESSAGES$3);
3328
+ const access = await verifyCliAccess(deps, ACCESS_MESSAGES$2);
2461
3329
  const destination = {
2462
3330
  workspaceId: access.workspaceId,
2463
3331
  endpoint: withoutExtras(access.ingest)
@@ -2488,27 +3356,34 @@ async function runImport(deps) {
2488
3356
  });
2489
3357
  const skippedLines = claude.skippedLines + codex.skippedLines;
2490
3358
  const skippedFiles = claude.skippedFiles + codex.skippedFiles;
2491
- if (skippedLines > 0 || skippedFiles > 0) deps.stdout(`読めなかった${skippedLines}行と${skippedFiles}個のファイルを読み飛ばしました\n`);
2492
- if (codex.unextractable > 0) deps.stdout(`modelまたはtoken数を読めないCodexのセッション${codex.unextractable}個は取り込みませんでした\n`);
2493
- if (codex.compressed > 0) deps.stdout(`圧縮されたCodexのセッションの記録${codex.compressed}個は読めないため取り込みませんでした\n`);
3359
+ if (skippedLines > 0 || skippedFiles > 0) deps.stdout(`Skipped ${skippedLines} unreadable line(s) and ${skippedFiles} unreadable file(s)\n`);
3360
+ if (codex.unextractable > 0) deps.stdout(`Did not import ${codex.unextractable} Codex session(s) whose model or token count could not be read\n`);
3361
+ if (codex.compressed > 0) deps.stdout(`Did not import ${codex.compressed} compressed Codex session record(s) because they cannot be read\n`);
2494
3362
  const scanned = [...claude.sessions, ...codex.sessions];
2495
3363
  const statePath = importStatePath(deps.homeDir);
2496
3364
  const sent = await readSentSessions(statePath, destination);
3365
+ const unsent = scanned.filter((s) => !sent.has(sessionStateKey(s)));
2497
3366
  return report(await sendSessions({
2498
3367
  endpoint: access.ingest,
2499
3368
  ingestKey: access.ingestKey,
2500
- sessions: scanned.filter((s) => !sent.has(sessionStateKey(s))),
3369
+ sessions: unsent,
2501
3370
  fetch: deps.fetch,
2502
3371
  sleep: deps.sleep,
2503
- record: (ids) => recordSentSessions(statePath, destination, ids).catch(() => stopWith(MESSAGES$1.stateFailed))
2504
- }), deps);
3372
+ record: (ids) => recordSentSessions(statePath, destination, ids).catch(() => stopWith(MESSAGES.stateFailed))
3373
+ }), {
3374
+ breakdown: {
3375
+ alreadySent: scanned.length - unsent.length,
3376
+ ...sumNotImported(claude.notImported, codex.notImported)
3377
+ },
3378
+ otherNoticeShown: skippedLines > 0 || skippedFiles > 0 || codex.unextractable > 0 || codex.compressed > 0,
3379
+ connectedCount: connected.size
3380
+ }, deps);
2505
3381
  }
2506
3382
  function importCommand(deps) {
2507
3383
  return runUntilStop(() => runImport(deps), deps.stderr);
2508
3384
  }
2509
- //#endregion
2510
- //#region ../../node_modules/.pnpm/@decimalturn+toml-patch@3.3.0/node_modules/@decimalturn/toml-patch/dist/toml-patch.js
2511
- //! @decimalturn/toml-patch v3.3.0 - https://github.com/DecimalTurn/toml-patch - @license: MIT
3385
+ // ../../node_modules/.pnpm/@decimalturn+toml-patch@3.3.1/node_modules/@decimalturn/toml-patch/dist/toml-patch.js
3386
+ //! @decimalturn/toml-patch v3.3.1 - https://github.com/DecimalTurn/toml-patch - @license: MIT
2512
3387
  function e(e) {
2513
3388
  return e.type === `Document`;
2514
3389
  }
@@ -3884,13 +4759,12 @@ function on(e) {
3884
4759
  if (e[t] !== `\\`) continue;
3885
4760
  let n = e[t + 1];
3886
4761
  if (!n) return null;
3887
- if (n === `\\`) {
3888
- t++;
3889
- continue;
4762
+ if (n === `\\`) t++;
4763
+ else {
4764
+ if (n === `x` && /^[0-9A-Fa-f]{2}$/.test(e.slice(t + 2, t + 4))) return /[A-F]/.test(e.slice(t + 2, t + 4));
4765
+ if (n === `u` && /^[0-9A-Fa-f]{4}$/.test(e.slice(t + 2, t + 6))) return /[A-F]/.test(e.slice(t + 2, t + 6));
4766
+ if (n === `U` && /^[0-9A-Fa-f]{8}$/.test(e.slice(t + 2, t + 10))) return /[A-F]/.test(e.slice(t + 2, t + 10));
3890
4767
  }
3891
- if (n === `x` && /^[0-9A-Fa-f]{2}$/.test(e.slice(t + 2, t + 4))) return /[A-F]/.test(e.slice(t + 2, t + 4));
3892
- if (n === `u` && /^[0-9A-Fa-f]{4}$/.test(e.slice(t + 2, t + 6))) return /[A-F]/.test(e.slice(t + 2, t + 6));
3893
- if (n === `U` && /^[0-9A-Fa-f]{8}$/.test(e.slice(t + 2, t + 10))) return /[A-F]/.test(e.slice(t + 2, t + 10));
3894
4768
  }
3895
4769
  return null;
3896
4770
  }
@@ -4202,14 +5076,11 @@ function qn(e, t) {
4202
5076
  if (e === t) return r;
4203
5077
  if (a(e)) return n(e.value, r);
4204
5078
  if (d(e)) return n(e.item, r);
4205
- if (u(e) || f(e)) {
4206
- for (let t of e.items) {
4207
- let e = n(t, r + 1);
4208
- if (e !== void 0) return e;
4209
- }
4210
- return;
5079
+ if (u(e) || f(e)) for (let t of e.items) {
5080
+ let e = n(t, r + 1);
5081
+ if (e !== void 0) return e;
4211
5082
  }
4212
- if (m(e)) for (let t of e.items) {
5083
+ else if (m(e)) for (let t of e.items) {
4213
5084
  let e = n(t, r);
4214
5085
  if (e !== void 0) return e;
4215
5086
  }
@@ -4221,14 +5092,11 @@ function Jn(e, t) {
4221
5092
  if (e === t) return r;
4222
5093
  if (a(e)) return n(e.value, r);
4223
5094
  if (d(e)) return n(e.item, r);
4224
- if (u(e) || f(e)) {
4225
- for (let t of e.items) {
4226
- let r = n(t, e);
4227
- if (r) return r;
4228
- }
4229
- return;
5095
+ if (u(e) || f(e)) for (let t of e.items) {
5096
+ let r = n(t, e);
5097
+ if (r) return r;
4230
5098
  }
4231
- if (m(e)) for (let t of e.items) {
5099
+ else if (m(e)) for (let t of e.items) {
4232
5100
  let e = n(t, r);
4233
5101
  if (e) return e;
4234
5102
  }
@@ -4928,25 +5796,17 @@ function Nr(e) {
4928
5796
  let r = e[n + 1];
4929
5797
  if (!r) continue;
4930
5798
  let i = kr(r);
4931
- if (i !== null) {
4932
- i !== `
5799
+ if (i !== null) i !== `
4933
5800
  ` && i !== `\r` && !t.has(i) && t.set(i, `\\` + r), n += 1;
4934
- continue;
4935
- }
4936
- if (r === `x` && /^[0-9A-Fa-f]{2}$/.test(e.slice(n + 2, n + 4))) {
5801
+ else if (r === `x` && /^[0-9A-Fa-f]{2}$/.test(e.slice(n + 2, n + 4))) {
4937
5802
  let r = e.slice(n, n + 4), i = Number.parseInt(e.slice(n + 2, n + 4), 16), a = String.fromCharCode(i);
4938
5803
  t.has(a) || t.set(a, r), n += 3;
4939
- continue;
4940
- }
4941
- if (r === `u` && /^[0-9A-Fa-f]{4}$/.test(e.slice(n + 2, n + 6))) {
5804
+ } else if (r === `u` && /^[0-9A-Fa-f]{4}$/.test(e.slice(n + 2, n + 6))) {
4942
5805
  let r = e.slice(n, n + 6), i = Number.parseInt(e.slice(n + 2, n + 6), 16), a = String.fromCodePoint(i);
4943
5806
  t.has(a) || t.set(a, r), n += 5;
4944
- continue;
4945
- }
4946
- if (r === `U` && /^[0-9A-Fa-f]{8}$/.test(e.slice(n + 2, n + 10))) {
5807
+ } else if (r === `U` && /^[0-9A-Fa-f]{8}$/.test(e.slice(n + 2, n + 10))) {
4947
5808
  let r = e.slice(n, n + 10), i = Number.parseInt(e.slice(n + 2, n + 10), 16), a = String.fromCodePoint(i);
4948
5809
  t.has(a) || t.set(a, r), n += 9;
4949
- continue;
4950
5810
  }
4951
5811
  }
4952
5812
  return t;
@@ -4955,11 +5815,7 @@ function Pr(e, t, n, r) {
4955
5815
  let i = ``;
4956
5816
  for (let a of e) {
4957
5817
  let e = t.get(a);
4958
- if (e) {
4959
- i += e;
4960
- continue;
4961
- }
4962
- i += jr(a, n, r);
5818
+ i += e || jr(a, n, r);
4963
5819
  }
4964
5820
  return i;
4965
5821
  }
@@ -5363,7 +6219,7 @@ function oi(e) {
5363
6219
  return !f(e) || e.items.length === 0 ? !1 : e.items[e.items.length - 1].comma === !0;
5364
6220
  }
5365
6221
  function si(e, t) {
5366
- return t.inlineTableStart === 0 ? e : (e.items.filter((e) => {
6222
+ return t.inlineTableStart === 0 || (e.items.filter((e) => {
5367
6223
  if (!a(e)) return !1;
5368
6224
  let n = f(e.value), r = u(e.value) && e.value.items.length > 0 && e.value.items.every((e) => f(e.item));
5369
6225
  if (n || r) {
@@ -5375,7 +6231,7 @@ function si(e, t) {
5375
6231
  V(e, e, n), f(n.value) ? n.value.items.length === 0 ? B(e, e, W(n.key.value)) : B(e, e, ci(n, t.bracketSpacing, t.escapeSequenceUpperCase)) : li(n, t.bracketSpacing, t.escapeSequenceUpperCase).forEach((t) => {
5376
6232
  B(e, e, t);
5377
6233
  });
5378
- }), H(e), e);
6234
+ }), H(e)), e;
5379
6235
  }
5380
6236
  function ci(e, t, n) {
5381
6237
  let r = W(e.key.value, n);
@@ -6412,11 +7268,7 @@ function ia(e, t, n = [], r = {}) {
6412
7268
  let h = 0;
6413
7269
  for (let t = e; t < o.length; t++) o[t] === r && h++;
6414
7270
  let g = p > h;
6415
- if (s && (o.indexOf(a[e]) === -1 || g)) {
6416
- i.push(d(e + l)), a.splice(e, 1), c.splice(e, 1), l++, e--;
6417
- continue;
6418
- }
6419
- if (l > 0 && g) {
7271
+ if (s && (o.indexOf(a[e]) === -1 || g) || l > 0 && g) {
6420
7272
  i.push(d(e + l)), a.splice(e, 1), c.splice(e, 1), l++, e--;
6421
7273
  continue;
6422
7274
  }
@@ -6447,14 +7299,10 @@ function ia(e, t, n = [], r = {}) {
6447
7299
  for (let n = e; n < o.length; n++) o[n] === t && r++;
6448
7300
  h = n > r;
6449
7301
  }
6450
- if (!p && h) {
6451
- Vt(i, $i(c[e], t[e], n.concat(e), r)), a[e] = f, c[e] = t[e];
6452
- continue;
6453
- }
6454
- i.push({
7302
+ !p && h ? (Vt(i, $i(c[e], t[e], n.concat(e), r)), a[e] = f, c[e] = t[e]) : (i.push({
6455
7303
  type: `Add`,
6456
7304
  path: n.concat(e)
6457
- }), u = !0, a.splice(e, 0, f), c.splice(e, 0, t[e]), l > 0 && l--;
7305
+ }), u = !0, a.splice(e, 0, f), c.splice(e, 0, t[e]), l > 0 && l--);
6458
7306
  }
6459
7307
  for (let e = o.length; e < a.length; e++) i.push(d(e + l));
6460
7308
  return i;
@@ -6540,19 +7388,10 @@ function pa(e) {
6540
7388
  let n = !1, r = !1;
6541
7389
  for (let i = t + 1; i < e.raw.length; i++) {
6542
7390
  let t = e.raw[i];
6543
- if (t === `\\`) {
6544
- i++;
6545
- continue;
6546
- }
6547
- if (!r && t === `"`) {
6548
- n = !n;
6549
- continue;
6550
- }
6551
- if (!n && t === `'`) {
6552
- r = !r;
6553
- continue;
6554
- }
6555
- if (!n && !r && t === `#`) return !0;
7391
+ if (t === `\\`) i++;
7392
+ else if (!r && t === `"`) n = !n;
7393
+ else if (!n && t === `'`) r = !r;
7394
+ else if (!n && !r && t === `#`) return !0;
6556
7395
  }
6557
7396
  return !1;
6558
7397
  }
@@ -6820,13 +7659,7 @@ function Ta(t, n, r, i) {
6820
7659
  e && (e.lines = 0, e.columns = 0);
6821
7660
  }
6822
7661
  H(t), p && h && r.loc.end.line > r.loc.start.line && (r.loc.end.column = h.column, d(r) && g && r.item.loc.end.line > r.item.loc.start.line && (r.item.loc.end.column = g.column));
6823
- for (let { node: e, start: t, end: n, endOnly: r, equals: i } of _) {
6824
- if (r) {
6825
- e.loc.end.column = n.column;
6826
- continue;
6827
- }
6828
- e.loc.start.column = t.column, e.loc.end.line === e.loc.start.line && (e.loc.end.column = n.column), i !== void 0 && a(e) && (e.equals = i);
6829
- }
7662
+ for (let { node: e, start: t, end: n, endOnly: r, equals: i } of _) r ? e.loc.end.column = n.column : (e.loc.start.column = t.column, e.loc.end.line === e.loc.start.line && (e.loc.end.column = n.column), i !== void 0 && a(e) && (e.equals = i));
6830
7663
  for (let { owner: t, ownerOriginalStart: n, comments: i } of l) {
6831
7664
  let a;
6832
7665
  t === r ? (a = {
@@ -6951,11 +7784,7 @@ function Ea(e) {
6951
7784
  continue;
6952
7785
  }
6953
7786
  let e = t(i.member) || r(i.member), a = n[n.length - 1];
6954
- if (a && a.kind === `member` && i.key !== void 0 && a.key === i.key) {
6955
- a.groups.push(i), a.endLine = i.endLine, a.items = a.items.concat(i.items);
6956
- continue;
6957
- }
6958
- n.push({
7787
+ a && a.kind === `member` && i.key !== void 0 && a.key === i.key ? (a.groups.push(i), a.endLine = i.endLine, a.items = a.items.concat(i.items)) : n.push({
6959
7788
  kind: `member`,
6960
7789
  key: i.key,
6961
7790
  isSection: e,
@@ -7180,23 +8009,11 @@ function Ua(e, t, n) {
7180
8009
  return r.columns + (i || a ? 2 : 0) + (s ? 1 + (c ? -1 : 0) : 0);
7181
8010
  }
7182
8011
  function Wa(e, n = []) {
7183
- for (let i of e.items) {
7184
- if (p(i)) {
7185
- n.push(i);
7186
- continue;
7187
- }
7188
- (t(i) || r(i)) && Wa(i, n);
7189
- }
8012
+ for (let i of e.items) p(i) ? n.push(i) : (t(i) || r(i)) && Wa(i, n);
7190
8013
  return n;
7191
8014
  }
7192
8015
  function Ga(e, n = /* @__PURE__ */ new Set()) {
7193
- for (let i of e.items) {
7194
- if (a(i) && f(i.value) && i.loc.start.line === i.loc.end.line) {
7195
- n.add(i.loc.end.line);
7196
- continue;
7197
- }
7198
- (t(i) || r(i)) && Ga(i, n);
7199
- }
8016
+ for (let i of e.items) a(i) && f(i.value) && i.loc.start.line === i.loc.end.line ? n.add(i.loc.end.line) : (t(i) || r(i)) && Ga(i, n);
7200
8017
  return n;
7201
8018
  }
7202
8019
  function Ka(e) {
@@ -7568,11 +8385,7 @@ function so(e) {
7568
8385
  let t = [], n = 0, r, i = !1;
7569
8386
  for (let a = 0; a < e.length; a++) {
7570
8387
  let o = e[a];
7571
- if (r) {
7572
- r === `"` && i ? i = !1 : r === `"` && o === `\\` ? i = !0 : o === r && (r = void 0);
7573
- continue;
7574
- }
7575
- o === `"` || o === `'` ? r = o : o === `.` && (t.push(e.slice(n, a)), n = a + 1);
8388
+ r ? r === `"` && i ? i = !1 : r === `"` && o === `\\` ? i = !0 : o === r && (r = void 0) : o === `"` || o === `'` ? r = o : o === `.` && (t.push(e.slice(n, a)), n = a + 1);
7576
8389
  }
7577
8390
  return t.push(e.slice(n)), t;
7578
8391
  }
@@ -8324,11 +9137,20 @@ function yo(n, i, s, c, l = !1, g = /* @__PURE__ */ new WeakSet(), _ = void 0, v
8324
9137
  for (let e of s) Q(n, l, e);
8325
9138
  }
8326
9139
  if ((t(o) || r(o)) && m(l)) {
8327
- let e = o.key.item.value, i = l.items, s = [];
8328
- for (let n of i) {
8329
- if (n === o) continue;
8330
- let i = a(n) ? n.key.value : t(n) || r(n) ? n.key.item.value : void 0;
8331
- i && i.length > e.length && F(i.slice(0, e.length), e) && s.push(n);
9140
+ let e = o.key.item.value, i = l.items, s = [], c = r(o), u = i.indexOf(o), d = i.length;
9141
+ if (c) for (let t = u + 1; t < i.length; t++) {
9142
+ let n = i[t];
9143
+ if (!r(n)) continue;
9144
+ let a = n.key.item.value;
9145
+ if (a.length <= e.length && F(a, e.slice(0, a.length))) {
9146
+ d = t;
9147
+ break;
9148
+ }
9149
+ }
9150
+ for (let [n, l] of i.entries()) {
9151
+ if (l === o || c && (n < u || n >= d)) continue;
9152
+ let i = a(l) ? l.key.value : t(l) || r(l) ? l.key.item.value : void 0;
9153
+ i && i.length > e.length && F(i.slice(0, e.length), e) && s.push(l);
8332
9154
  }
8333
9155
  for (let e of s) Q(n, l, e);
8334
9156
  }
@@ -8714,23 +9536,18 @@ function To(e, n, i, o, s, c, l) {
8714
9536
  let n = s.items.find((e) => a(e) && F(e.key.value, t.key.value));
8715
9537
  n ? pr(e, s, n, t) : B(e, s, t, void 0);
8716
9538
  };
8717
- for (let e of m) {
8718
- if (a(e)) {
8719
- h(e);
8720
- continue;
8721
- }
8722
- if (t(e)) {
8723
- let t = e.key.item.value;
8724
- for (let n of e.items) {
8725
- if (!a(n)) continue;
8726
- let e = n.key.raw, r = t.concat(n.key.value);
8727
- n.key.value = r, n.key.raw = Ur(r, o.escapeSequenceUpperCase).raw;
8728
- let i = n.key.raw.length - e.length;
8729
- n.key.loc.end.column = n.key.loc.start.column + n.key.raw.length, n.equals += i, U(n.value, {
8730
- lines: 0,
8731
- columns: i
8732
- }, { first_line_only: !0 }), n.loc.end.line === n.loc.start.line && (n.loc.end.column += i), h(n);
8733
- }
9539
+ for (let e of m) if (a(e)) h(e);
9540
+ else if (t(e)) {
9541
+ let t = e.key.item.value;
9542
+ for (let n of e.items) {
9543
+ if (!a(n)) continue;
9544
+ let e = n.key.raw, r = t.concat(n.key.value);
9545
+ n.key.value = r, n.key.raw = Ur(r, o.escapeSequenceUpperCase).raw;
9546
+ let i = n.key.raw.length - e.length;
9547
+ n.key.loc.end.column = n.key.loc.start.column + n.key.raw.length, n.equals += i, U(n.value, {
9548
+ lines: 0,
9549
+ columns: i
9550
+ }, { first_line_only: !0 }), n.loc.end.line === n.loc.start.line && (n.loc.end.column += i), h(n);
8734
9551
  }
8735
9552
  }
8736
9553
  return;
@@ -8749,51 +9566,51 @@ function To(e, n, i, o, s, c, l) {
8749
9566
  if (o) {
8750
9567
  for (let t of i.items) B(e, o, t, void 0);
8751
9568
  v ||= o;
8752
- continue;
8753
- }
8754
- if (a(i) && f(i.value)) {
8755
- let o = i.key.value, s = e.items.find((e) => t(e) && F(e.key.item.value, o));
8756
- if (s) {
8757
- for (let t of i.value.items) B(e, s, d(t) ? t.item : t, void 0);
8758
- v ||= s;
8759
- continue;
8760
- }
8761
- if ($(e, o).filter((e) => a(e) || t(e) || r(e)).length > 0) {
8762
- for (let t of i.value.items) {
8763
- let r = d(t) ? t.item : t;
8764
- if (a(r)) {
8765
- let e = r.key.raw, t = o.concat(r.key.value);
8766
- r.key.value = t, r.key.raw = t.map((e) => A.test(e) ? e : JSON.stringify(e).replace(/\x7f/g, `\\u007f`)).join(`.`);
8767
- let n = r.key.raw.length - e.length;
8768
- r.key.loc.end.column = r.key.loc.start.column + r.key.raw.length, r.equals += n, U(r.value, {
8769
- lines: 0,
8770
- columns: n
8771
- }, { first_line_only: !0 }), r.loc.end.line === r.loc.start.line && (r.loc.end.column += n);
9569
+ } else {
9570
+ if (a(i) && f(i.value)) {
9571
+ let o = i.key.value, s = e.items.find((e) => t(e) && F(e.key.item.value, o));
9572
+ if (s) {
9573
+ for (let t of i.value.items) B(e, s, d(t) ? t.item : t, void 0);
9574
+ v ||= s;
9575
+ continue;
9576
+ }
9577
+ if ($(e, o).filter((e) => a(e) || t(e) || r(e)).length > 0) {
9578
+ for (let t of i.value.items) {
9579
+ let r = d(t) ? t.item : t;
9580
+ if (a(r)) {
9581
+ let e = r.key.raw, t = o.concat(r.key.value);
9582
+ r.key.value = t, r.key.raw = t.map((e) => A.test(e) ? e : JSON.stringify(e).replace(/\x7f/g, `\\u007f`)).join(`.`);
9583
+ let n = r.key.raw.length - e.length;
9584
+ r.key.loc.end.column = r.key.loc.start.column + r.key.raw.length, r.equals += n, U(r.value, {
9585
+ lines: 0,
9586
+ columns: n
9587
+ }, { first_line_only: !0 }), r.loc.end.line === r.loc.start.line && (r.loc.end.column += n);
9588
+ }
9589
+ B(e, e, r, n === 0 ? _ : void 0), v ||= r;
8772
9590
  }
8773
- B(e, e, r, n === 0 ? _ : void 0), v ||= r;
9591
+ continue;
8774
9592
  }
8775
- continue;
8776
9593
  }
8777
- }
8778
- if (t(i)) {
8779
- let t = i.key.item.value;
8780
- if ($(e, t).filter((e) => a(e)).length > 0) {
8781
- for (let r of i.items) {
8782
- if (a(r)) {
8783
- let e = r.key.raw, n = t.concat(r.key.value);
8784
- r.key.value = n, r.key.raw = n.map((e) => A.test(e) ? e : JSON.stringify(e).replace(/\x7f/g, `\\u007f`)).join(`.`);
8785
- let i = r.key.raw.length - e.length;
8786
- r.key.loc.end.column = r.key.loc.start.column + r.key.raw.length, r.equals += i, U(r.value, {
8787
- lines: 0,
8788
- columns: i
8789
- }, { first_line_only: !0 }), r.loc.end.line === r.loc.start.line && (r.loc.end.column += i);
9594
+ if (t(i)) {
9595
+ let t = i.key.item.value;
9596
+ if ($(e, t).filter((e) => a(e)).length > 0) {
9597
+ for (let r of i.items) {
9598
+ if (a(r)) {
9599
+ let e = r.key.raw, n = t.concat(r.key.value);
9600
+ r.key.value = n, r.key.raw = n.map((e) => A.test(e) ? e : JSON.stringify(e).replace(/\x7f/g, `\\u007f`)).join(`.`);
9601
+ let i = r.key.raw.length - e.length;
9602
+ r.key.loc.end.column = r.key.loc.start.column + r.key.raw.length, r.equals += i, U(r.value, {
9603
+ lines: 0,
9604
+ columns: i
9605
+ }, { first_line_only: !0 }), r.loc.end.line === r.loc.start.line && (r.loc.end.column += i);
9606
+ }
9607
+ B(e, e, r, n === 0 ? _ : void 0), v ||= r;
8790
9608
  }
8791
- B(e, e, r, n === 0 ? _ : void 0), v ||= r;
9609
+ continue;
8792
9610
  }
8793
- continue;
8794
9611
  }
9612
+ n > 0 && v && t(v) && a(i) ? B(e, v, i, void 0) : B(e, e, i, n === 0 ? _ : void 0), v ||= i;
8795
9613
  }
8796
- n > 0 && v && t(v) && a(i) ? B(e, v, i, void 0) : B(e, e, i, n === 0 ? _ : void 0), v ||= i;
8797
9614
  }
8798
9615
  let y = v;
8799
9616
  t(y) && l.add(y), c.add(y);
@@ -8976,12 +9793,27 @@ function Ko(e, t) {
8976
9793
  temporal: (t == null ? void 0 : t.temporal) ?? !1
8977
9794
  });
8978
9795
  }
8979
- //#endregion
8980
- //#region src/shared/codex-config.ts
8981
- const HOOK_COMMAND = "harnessforce hook session-start";
8982
- const HOOK_COMMAND_WINDOWS = "harnessforce.cmd hook session-start";
9796
+ // src/shared/codex-config.ts
8983
9797
  const HOOK_TIMEOUT_SECONDS = 10;
8984
- const FILE_MODE$3 = 384;
9798
+ const HOOKS = [
9799
+ {
9800
+ event: "SessionStart",
9801
+ subcommand: "session-start"
9802
+ },
9803
+ {
9804
+ event: "PostToolUse",
9805
+ subcommand: "post-tool-use",
9806
+ matcher: "^apply_patch$",
9807
+ isAsync: true
9808
+ },
9809
+ {
9810
+ event: "Stop",
9811
+ subcommand: "stop",
9812
+ isAsync: true
9813
+ }
9814
+ ];
9815
+ const hookCommand = (spec) => `harnessforce hook ${spec.subcommand}`;
9816
+ const FILE_MODE$2 = 384;
8985
9817
  function codexConfigPath(env, homeDir) {
8986
9818
  return join(absoluteEnv(env.CODEX_HOME) ?? join(homeDir, ".codex"), "config.toml");
8987
9819
  }
@@ -9016,11 +9848,14 @@ function withHarnessforce(config, input) {
9016
9848
  headers: { Authorization: `Bearer ${input.ingestKey}` },
9017
9849
  protocol: "binary"
9018
9850
  } };
9019
- if (input.logUserPrompt) otel.log_user_prompt = true;
9851
+ otel.log_user_prompt = input.logUserPrompt;
9852
+ otel.log_agent_responses = input.logAgentResponses;
9020
9853
  const hooks = tableAt(config, "hooks");
9021
- const groups = hooks.SessionStart ?? [];
9022
- if (!Array.isArray(groups)) throw new Error("SessionStart");
9023
- hooks.SessionStart = withHandler(groups);
9854
+ for (const spec of HOOKS) {
9855
+ const groups = hooks[spec.event] ?? [];
9856
+ if (!Array.isArray(groups)) throw new Error(spec.event);
9857
+ hooks[spec.event] = withHandler(groups, spec);
9858
+ }
9024
9859
  const servers = tableAt(config, "mcp_servers");
9025
9860
  const existing = servers.harnessforce;
9026
9861
  if (existing !== void 0 && !isObject(existing)) throw new Error("server");
@@ -9036,16 +9871,21 @@ function tableAt(parent, key) {
9036
9871
  parent[key] = value;
9037
9872
  return value;
9038
9873
  }
9039
- const isOurs = (handler) => isObject(handler) && handler.command === HOOK_COMMAND;
9040
- const hasOurs = (group) => isObject(group) && Array.isArray(group.hooks) && group.hooks.some(isOurs);
9041
- function withHandler(groups) {
9874
+ function withHandler(groups, spec) {
9875
+ const command = hookCommand(spec);
9876
+ const isOurs = (entry) => isObject(entry) && entry.command === command;
9877
+ const hasOurs = (group) => isObject(group) && Array.isArray(group.hooks) && group.hooks.some(isOurs);
9042
9878
  const handler = {
9043
9879
  type: "command",
9044
- command: HOOK_COMMAND,
9045
- commandWindows: HOOK_COMMAND_WINDOWS,
9046
- timeout: HOOK_TIMEOUT_SECONDS
9047
- };
9048
- if (!groups.some(hasOurs)) return [...groups, { hooks: [handler] }];
9880
+ command,
9881
+ commandWindows: `harnessforce.cmd hook ${spec.subcommand}`,
9882
+ timeout: HOOK_TIMEOUT_SECONDS,
9883
+ ...spec.isAsync ? { async: true } : {}
9884
+ };
9885
+ if (!groups.some(hasOurs)) return [...groups, {
9886
+ ...spec.matcher === void 0 ? {} : { matcher: spec.matcher },
9887
+ hooks: [handler]
9888
+ }];
9049
9889
  return groups.map((group) => hasOurs(group) ? {
9050
9890
  ...group,
9051
9891
  hooks: group.hooks.map((entry) => isOurs(entry) ? handler : entry)
@@ -9056,17 +9896,38 @@ async function writeCodexConfig(path, text) {
9056
9896
  const temporary = `${path}.${randomUUID()}.tmp`;
9057
9897
  try {
9058
9898
  await writeFile(temporary, text, {
9059
- mode: FILE_MODE$3,
9899
+ mode: FILE_MODE$2,
9060
9900
  flag: "wx"
9061
9901
  });
9062
- await chmod(temporary, FILE_MODE$3);
9902
+ await chmod(temporary, FILE_MODE$2);
9063
9903
  await rename(temporary, path);
9064
9904
  } finally {
9065
9905
  await rm(temporary, { force: true });
9066
9906
  }
9067
9907
  }
9068
- //#endregion
9069
- //#region src/init/credentials-api.ts
9908
+ // src/init/credentials-api.ts
9909
+ const VISIBILITIES = /* @__PURE__ */ new Set([
9910
+ "all_members",
9911
+ "owners_admins_and_sender",
9912
+ "sender_only"
9913
+ ]);
9914
+ const RETENTION_DAYS = /* @__PURE__ */ new Set([
9915
+ 7,
9916
+ 14,
9917
+ 30
9918
+ ]);
9919
+ function parseContent(value) {
9920
+ if (!isObject(value)) return void 0;
9921
+ const { prompts, model_and_tools: modelAndTools, visibility } = value;
9922
+ const retentionDays = value.retention_days;
9923
+ if (typeof prompts !== "boolean" || typeof modelAndTools !== "boolean" || typeof visibility !== "string" || !VISIBILITIES.has(visibility) || typeof retentionDays !== "number" || !RETENTION_DAYS.has(retentionDays)) return void 0;
9924
+ return {
9925
+ isPromptsStored: prompts,
9926
+ isModelAndToolsStored: modelAndTools,
9927
+ visibility,
9928
+ retentionDays
9929
+ };
9930
+ }
9070
9931
  const WORKSPACE_ID = /^[A-Za-z0-9-]{1,128}$/;
9071
9932
  const NO_WHITESPACE = /^\S+$/;
9072
9933
  const LIMIT_ROLES = /* @__PURE__ */ new Set([
@@ -9075,15 +9936,16 @@ const LIMIT_ROLES = /* @__PURE__ */ new Set([
9075
9936
  "member"
9076
9937
  ]);
9077
9938
  function parseIssued(body) {
9078
- const { workspace_id: workspaceId, ingest_key: ingestKey, ingest_endpoint: ingestEndpoint, content_opt_in: contentOptIn } = body;
9079
- if (typeof workspaceId !== "string" || typeof ingestKey !== "string" || typeof ingestEndpoint !== "string" || typeof contentOptIn !== "boolean" || !WORKSPACE_ID.test(workspaceId)) return void 0;
9939
+ const { workspace_id: workspaceId, ingest_key: ingestKey, ingest_endpoint: ingestEndpoint, content } = body;
9940
+ if (typeof workspaceId !== "string" || typeof ingestKey !== "string" || typeof ingestEndpoint !== "string" || !WORKSPACE_ID.test(workspaceId)) return void 0;
9080
9941
  const apiToken = parseTokenPair(body, workspaceId);
9081
- return ingestKey.startsWith(`hf_ik_${workspaceId}_`) && NO_WHITESPACE.test(ingestKey) && apiToken !== void 0 && parseAllowedUrl(ingestEndpoint) !== void 0 && apiToken ? {
9942
+ const parsedContent = parseContent(content);
9943
+ return ingestKey.startsWith(`hf_ik_${workspaceId}_`) && NO_WHITESPACE.test(ingestKey) && apiToken !== void 0 && parsedContent !== void 0 && parseAllowedUrl(ingestEndpoint) !== void 0 && apiToken && parsedContent ? {
9082
9944
  workspaceId,
9083
9945
  ingestKey,
9084
9946
  apiToken,
9085
9947
  ingestEndpoint,
9086
- contentOptIn
9948
+ content: parsedContent
9087
9949
  } : void 0;
9088
9950
  }
9089
9951
  function classify(status, body) {
@@ -9123,8 +9985,7 @@ async function requestCredentials(base, request, fetchImpl) {
9123
9985
  return { kind: "unexpected" };
9124
9986
  }
9125
9987
  }
9126
- //#endregion
9127
- //#region src/init/pkce.ts
9988
+ // src/init/pkce.ts
9128
9989
  const RANDOM_BYTES = 32;
9129
9990
  const codeChallenge = (codeVerifier) => createHash("sha256").update(codeVerifier).digest("base64url");
9130
9991
  function createLoginSecrets() {
@@ -9135,8 +9996,7 @@ function createLoginSecrets() {
9135
9996
  state: randomBytes(RANDOM_BYTES).toString("base64url")
9136
9997
  };
9137
9998
  }
9138
- //#endregion
9139
- //#region src/init/init.ts
9999
+ // src/init/init.ts
9140
10000
  const CLIENT_ID = "harnessforce-cli";
9141
10001
  const DEFAULT_PORT = 8080;
9142
10002
  const MAX_REVOKE_HASHES = 100;
@@ -9144,10 +10004,12 @@ const stop$1 = (message) => stopWith(INIT_MESSAGES[message]);
9144
10004
  const sha256Hex = (value) => createHash("sha256").update(value, "utf8").digest("hex");
9145
10005
  function init(args, deps) {
9146
10006
  return runUntilStop(async () => {
9147
- const { codexPath, isKernelKeyring } = await runInit(args, deps);
10007
+ const { codexPath, connection, isKernelKeyring, isPluginAutoUpdateOn } = await runInit(args, deps);
10008
+ if (connection !== withoutExtras(new URL(deps.defaultUrl))) deps.stdout(`${nonDefaultConnectionMessage(connection)}\n`);
9148
10009
  const lines = [
9149
10010
  INIT_MESSAGES.success,
9150
10011
  ...isKernelKeyring ? [INIT_MESSAGES.savedToKernelKeyring] : [],
10012
+ ...isPluginAutoUpdateOn ? [INIT_MESSAGES.pluginAutoUpdateOn] : [],
9151
10013
  codexNotes(codexPath)
9152
10014
  ];
9153
10015
  deps.stdout(`${lines.join("\n")}\n`);
@@ -9161,29 +10023,47 @@ async function runInit(args, deps) {
9161
10023
  const settingsPath = userSettingsPath(deps.env, deps.homeDir);
9162
10024
  const store = await openKeychain(deps, (await readSettingsEnv$1(settingsPath)).HARNESSFORCE_WORKSPACE_ID);
9163
10025
  const revoke = revokeHashes(store);
9164
- if ((await readUserSettings(settingsPath)).kind === "invalid") stop$1("settingsUnreadable");
10026
+ const settings = await readUserSettings(settingsPath);
10027
+ if (settings.kind === "invalid" || !hasWritableMarketplaces(settings.settings)) stop$1("settingsUnreadable");
9165
10028
  const authorizationEndpoint = await discoverAuthorizationEndpoint(base, deps.fetch) ?? stop$1("network");
9166
10029
  const secrets = createLoginSecrets();
9167
- const { code, redirectUri } = await logIn(authorizationEndpoint, secrets, port, deps);
9168
- const issued = issuedOrStop(await requestCredentials(base, {
9169
- code,
9170
- code_verifier: secrets.codeVerifier,
9171
- client_id: CLIENT_ID,
9172
- redirect_uri: redirectUri,
9173
- revoke_key_hashes: revoke.keyHashes,
9174
- revoke_api_token_hashes: revoke.apiTokenHashes
9175
- }, deps.fetch));
9176
- await save(issued, recordedUrl, settingsPath, args.sendContent, store).catch((error) => {
9177
- if (isStop(error)) throw error;
9178
- return stop$1("saveFailed");
9179
- });
9180
- const codexPath = codexConfigPath(deps.env, deps.homeDir);
9181
- await saveCodexConfig(issued, recordedUrl, codexPath, args.sendContent).catch(() => stop$1("codexSaveFailed"));
9182
- if (args.sendContent) deps.stdout(`${issued.contentOptIn ? INIT_MESSAGES.contentEnabled : INIT_MESSAGES.contentNotOptedIn}\n`);
9183
- return {
9184
- codexPath,
9185
- isKernelKeyring: store.isKernelKeyring
9186
- };
10030
+ const { code, redirectUri, loopback } = await logIn(authorizationEndpoint, secrets, port, new URL(recordedUrl).origin, deps);
10031
+ let completion = "failed";
10032
+ try {
10033
+ const outcome = await requestCredentials(base, {
10034
+ code,
10035
+ code_verifier: secrets.codeVerifier,
10036
+ client_id: CLIENT_ID,
10037
+ redirect_uri: redirectUri,
10038
+ revoke_key_hashes: revoke.keyHashes,
10039
+ revoke_api_token_hashes: revoke.apiTokenHashes
10040
+ }, deps.fetch);
10041
+ if (outcome.kind === "viewer") completion = "viewer";
10042
+ const issued = issuedOrStop(outcome);
10043
+ const isPluginAutoUpdateOn = await save(issued, recordedUrl, settingsPath, args, store).catch((error) => {
10044
+ if (isStop(error)) throw error;
10045
+ return stop$1("saveFailed");
10046
+ });
10047
+ const codexPath = codexConfigPath(deps.env, deps.homeDir);
10048
+ await saveCodexConfig(issued, recordedUrl, codexPath, args.sendContent).catch(() => stop$1("codexSaveFailed"));
10049
+ completion = "connected";
10050
+ loopback.finish({
10051
+ status: completion,
10052
+ workspaceId: issued.workspaceId
10053
+ });
10054
+ return finishInit(issued, args, deps, {
10055
+ codexPath,
10056
+ connection: recordedUrl,
10057
+ isKernelKeyring: store.isKernelKeyring,
10058
+ isPluginAutoUpdateOn
10059
+ });
10060
+ } finally {
10061
+ loopback.finish({ status: completion });
10062
+ }
10063
+ }
10064
+ function finishInit(issued, args, deps, result) {
10065
+ deps.stdout(`${contentNotice(issued.content, args.sendContent)}\n`);
10066
+ return result;
9187
10067
  }
9188
10068
  async function openKeychain(deps, previousWorkspaceId) {
9189
10069
  return await deps.keychain.openForInit({
@@ -9205,11 +10085,12 @@ function revokeHashes({ items }) {
9205
10085
  apiTokenHashes
9206
10086
  };
9207
10087
  }
9208
- async function logIn(authorizationEndpoint, secrets, port, deps) {
10088
+ async function logIn(authorizationEndpoint, secrets, port, webOrigin, deps) {
9209
10089
  const loopback = await deps.startLoopback({
9210
10090
  state: secrets.state,
9211
10091
  timeoutMs: deps.callbackTimeoutMs,
9212
- port
10092
+ port,
10093
+ webOrigin
9213
10094
  }).catch(() => stopWith(listenFailedMessage(port)));
9214
10095
  const authorization = new URL(authorizationEndpoint);
9215
10096
  for (const [name, value] of Object.entries({
@@ -9221,10 +10102,23 @@ async function logIn(authorizationEndpoint, secrets, port, deps) {
9221
10102
  state: secrets.state
9222
10103
  })) authorization.searchParams.set(name, value);
9223
10104
  if (!await deps.openBrowser(authorization.href)) deps.stderr(`${authorization.href}\n`);
9224
- return {
9225
- code: codeOrStop(await loopback.callback ?? stop$1("timeout"), secrets.state),
9226
- redirectUri: loopback.redirectUri
9227
- };
10105
+ const callback = await loopback.callback ?? stop$1("timeout");
10106
+ try {
10107
+ return {
10108
+ code: codeOrStop(callback, secrets.state),
10109
+ redirectUri: loopback.redirectUri,
10110
+ loopback
10111
+ };
10112
+ } catch (error) {
10113
+ loopback.finish({ status: callbackFailure(callback, secrets.state) });
10114
+ throw error;
10115
+ }
10116
+ }
10117
+ function callbackFailure(callback, state) {
10118
+ if (callback.state !== state) return "failed";
10119
+ if (callback.error === "access_denied") return "denied";
10120
+ if (callback.error === "no_workspace") return "no_workspace";
10121
+ return "failed";
9228
10122
  }
9229
10123
  function codeOrStop(callback, state) {
9230
10124
  if (callback.state !== state) return stop$1("loginFailed");
@@ -9244,7 +10138,7 @@ function issuedOrStop(outcome) {
9244
10138
  case "unexpected": return stop$1("network");
9245
10139
  }
9246
10140
  }
9247
- async function save(issued, connection, settingsPath, sendContent, store) {
10141
+ async function save(issued, connection, settingsPath, args, store) {
9248
10142
  await store.delete(ingestOriginAccount(issued.workspaceId));
9249
10143
  await store.delete(urlOriginAccount(issued.workspaceId));
9250
10144
  await store.set(ingestKeyAccount(issued.workspaceId), issued.ingestKey);
@@ -9253,7 +10147,7 @@ async function save(issued, connection, settingsPath, sendContent, store) {
9253
10147
  await store.set(urlOriginAccount(issued.workspaceId), new URL(connection).origin);
9254
10148
  await store.deleteElsewhere(issued.workspaceId);
9255
10149
  const current = await readUserSettings(settingsPath);
9256
- if (current.kind === "invalid") return stop$1("saveFailed");
10150
+ if (current.kind === "invalid" || !hasWritableMarketplaces(current.settings)) return stop$1("saveFailed");
9257
10151
  const env = {
9258
10152
  HARNESSFORCE_URL: connection,
9259
10153
  HARNESSFORCE_ENDPOINT: issued.ingestEndpoint,
@@ -9267,19 +10161,35 @@ async function save(issued, connection, settingsPath, sendContent, store) {
9267
10161
  OTEL_TRACES_EXPORTER: "otlp",
9268
10162
  OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf"
9269
10163
  };
9270
- if (sendContent && issued.contentOptIn) env.OTEL_LOG_USER_PROMPTS = "1";
9271
- await writeUserSettings(settingsPath, mergeUserSettings(current.settings, env));
10164
+ Object.assign(env, contentEnv(issued.content, args.sendContent));
10165
+ const { settings, isOn } = withPluginAutoUpdate(mergeUserSettings(current.settings, env), args.noPluginAutoUpdate);
10166
+ await writeUserSettings(settingsPath, settings);
10167
+ return isOn;
10168
+ }
10169
+ function contentEnv(content, sendContent) {
10170
+ const flag = (isStored) => sendContent && isStored ? "1" : "0";
10171
+ const modelAndTools = flag(content.isModelAndToolsStored);
10172
+ return {
10173
+ OTEL_LOG_USER_PROMPTS: flag(content.isPromptsStored),
10174
+ OTEL_LOG_ASSISTANT_RESPONSES: modelAndTools,
10175
+ OTEL_LOG_TOOL_DETAILS: modelAndTools,
10176
+ OTEL_LOG_TOOL_CONTENT: modelAndTools
10177
+ };
10178
+ }
10179
+ function contentNotice(content, sendContent) {
10180
+ if (!sendContent) return INIT_MESSAGES.contentNotSent;
10181
+ return content.isPromptsStored || content.isModelAndToolsStored ? contentEnabledMessage(content) : INIT_MESSAGES.contentNotStored;
9272
10182
  }
9273
10183
  async function saveCodexConfig(issued, connection, path, sendContent) {
9274
10184
  await writeCodexConfig(path, mergeCodexConfig(await readCodexConfig(path), {
9275
10185
  connection,
9276
10186
  ingestEndpoint: issued.ingestEndpoint,
9277
10187
  ingestKey: issued.ingestKey,
9278
- logUserPrompt: sendContent && issued.contentOptIn
10188
+ logUserPrompt: sendContent && issued.content.isPromptsStored,
10189
+ logAgentResponses: sendContent && issued.content.isModelAndToolsStored
9279
10190
  }));
9280
10191
  }
9281
- //#endregion
9282
- //#region src/otel-headers.ts
10192
+ // src/otel-headers.ts
9283
10193
  const DESTINATION_VARIABLES = [
9284
10194
  "HARNESSFORCE_ENDPOINT",
9285
10195
  "OTEL_EXPORTER_OTLP_ENDPOINT",
@@ -9288,11 +10198,23 @@ const DESTINATION_VARIABLES = [
9288
10198
  "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"
9289
10199
  ];
9290
10200
  const WITHHELD = "harnessforce: user key withheld (destination not verified)\n";
9291
- async function otelHeaders(env, managedDir, keychain, stdout, stderr) {
9292
- const selection = await selectKey(env, managedDir, keychain).catch(() => ({ kind: "none" }));
9293
- if (selection.kind === "withheld") stderr(WITHHELD);
9294
- if (selection.kind !== "key") return 1;
10201
+ const EXIT_KEY_MISSING = 3;
10202
+ const EXIT_WITHHELD = 4;
10203
+ const EXIT_FAILED = 1;
10204
+ async function otelHeaders(env, managedDir, keychain, stdout, stderr, endStdout) {
10205
+ const selection = await selectKey(env, managedDir, keychain).catch(() => ({ kind: "failed" }));
10206
+ if (selection.kind === "withheld") {
10207
+ stderr(WITHHELD);
10208
+ return EXIT_WITHHELD;
10209
+ }
10210
+ if (selection.kind === "missing") return EXIT_KEY_MISSING;
10211
+ if (selection.kind === "failed") return EXIT_FAILED;
9295
10212
  stdout(`${JSON.stringify({ Authorization: `Bearer ${selection.key}` })}\n`);
10213
+ const { user } = selection;
10214
+ if (user && !env.HARNESSFORCE_NO_MIGRATE) {
10215
+ await endStdout();
10216
+ await user.keychain.migrateFromKernel?.(user.workspaceId).catch(() => {});
10217
+ }
9296
10218
  return 0;
9297
10219
  }
9298
10220
  const HOOK_READ_TIMEOUT_MS = 1e3;
@@ -9303,22 +10225,25 @@ async function selectKey(env, managedDir, unlimited) {
9303
10225
  key: managed.HARNESSFORCE_INGEST_KEY
9304
10226
  };
9305
10227
  const workspaceId = env.HARNESSFORCE_WORKSPACE_ID;
9306
- if (!workspaceId) return { kind: "none" };
10228
+ if (!workspaceId) return { kind: "missing" };
9307
10229
  const keychain = unlimited.withTimeout(HOOK_READ_TIMEOUT_MS);
9308
- if (!await keychain.isAvailable()) return { kind: "none" };
10230
+ if (!await keychain.isAvailable()) return { kind: "failed" };
9309
10231
  const key = await keychain.get(ingestKeyAccount(workspaceId));
9310
- if (!key) return { kind: "none" };
10232
+ if (!key) return { kind: "missing" };
9311
10233
  const pinnedOrigin = await keychain.get(ingestOriginAccount(workspaceId));
9312
10234
  return pinnedOrigin && isPinnedDestination(env, pinnedOrigin) ? {
9313
10235
  kind: "key",
9314
- key
10236
+ key,
10237
+ user: {
10238
+ keychain,
10239
+ workspaceId
10240
+ }
9315
10241
  } : { kind: "withheld" };
9316
10242
  }
9317
10243
  function isPinnedDestination(env, pinnedOrigin) {
9318
10244
  return DESTINATION_VARIABLES.map((name) => env[name]).filter((value) => Boolean(value)).every((value) => parseAllowedUrl(value)?.origin === pinnedOrigin);
9319
10245
  }
9320
- //#endregion
9321
- //#region ../agent-core/src/config/plugins.ts
10246
+ // ../agent-core/src/config/plugins.ts
9322
10247
  const NON_MARKETPLACE_ORIGINS = /* @__PURE__ */ new Set([
9323
10248
  "inline",
9324
10249
  "skills-dir",
@@ -9359,8 +10284,7 @@ async function collectPlugins(pluginsRoot, settingsByPrecedence, scope, guard) {
9359
10284
  collectMcpServers$1(s, (await readJsonObjectFile(join(root, ".mcp.json"), guard))?.mcpServers);
9360
10285
  }
9361
10286
  }
9362
- //#endregion
9363
- //#region ../agent-core/src/config/collect.ts
10287
+ // ../agent-core/src/config/collect.ts
9364
10288
  const COLLECT_BUDGET_MS = 1e3;
9365
10289
  async function readManagedSettings(managedDir, guard) {
9366
10290
  let merged = await readJsonObjectFile(join(managedDir, "managed-settings.json"), guard);
@@ -9494,8 +10418,7 @@ async function collectConfig(options) {
9494
10418
  components: sortComponents(sink.components)
9495
10419
  };
9496
10420
  }
9497
- //#endregion
9498
- //#region src/run/context.ts
10421
+ // src/run/context.ts
9499
10422
  async function resolveConfigVersion(options) {
9500
10423
  const startedMs = options.now().getTime();
9501
10424
  const result = await collectConfig({
@@ -9515,8 +10438,7 @@ async function resolveLaunchContext(options) {
9515
10438
  ...configVersion === void 0 ? {} : { configVersion }
9516
10439
  };
9517
10440
  }
9518
- //#endregion
9519
- //#region src/run/issues.ts
10441
+ // src/run/issues.ts
9520
10442
  const MAX_CANDIDATES = 10;
9521
10443
  const DOT_SEGMENT = /^\.{1,2}$/;
9522
10444
  async function get(url, apiToken, fetchImpl, timeoutMs) {
@@ -9575,8 +10497,7 @@ async function resolveIssue(base, identifier, apiToken, fetchImpl, timeoutMs = R
9575
10497
  }
9576
10498
  return fetchCandidates(base, identifier, apiToken, fetchImpl, timeoutMs);
9577
10499
  }
9578
- //#endregion
9579
- //#region src/run/launch.ts
10500
+ // src/run/launch.ts
9580
10501
  const STRIPPED_FROM_SHELL = /* @__PURE__ */ new Set(["OTEL_EXPORTER_OTLP_HEADERS", ...[
9581
10502
  "TRACES",
9582
10503
  "LOGS",
@@ -9638,36 +10559,34 @@ function buildLaunch(request) {
9638
10559
  ...isClaudeCode(request.agent) ? { settingsEnv: injectedEnv } : {}
9639
10560
  };
9640
10561
  }
9641
- //#endregion
9642
- //#region src/run/messages.ts
10562
+ // src/run/messages.ts
9643
10563
  const RUN_MESSAGES = {
9644
10564
  keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
9645
- initRequired: "`harnessforce init`を実行してください",
10565
+ initRequired: "Run `harnessforce init`",
9646
10566
  invalidUrl: INIT_MESSAGES.invalidUrl,
9647
- invalidIssue: "Issueの識別子が不正です",
9648
- issueInitRequired: "Issueを解決できませんでした。`harnessforce init`を実行してください",
9649
- loginExpired: "ログインの有効期限が切れました。`harnessforce init`を実行してください",
9650
- issueFailed: "Issueを解決できず、候補も取得できませんでした",
9651
- noMatch: "一致するIssueがありません",
9652
- candidatesHeader: "Issueを解決できませんでした。候補:"
10567
+ invalidIssue: "The Issue identifier is invalid",
10568
+ issueInitRequired: "Could not resolve the Issue. Run `harnessforce init`",
10569
+ loginExpired: "Your login has expired. Run `harnessforce init`",
10570
+ issueFailed: "Could not resolve the Issue or fetch candidates",
10571
+ noMatch: "No matching Issue",
10572
+ candidatesHeader: "Could not resolve the Issue. Candidates:"
9653
10573
  };
9654
- const launchFailedMessage = (agent) => `${agent}を起動できませんでした`;
9655
- //#endregion
9656
- //#region src/run/run.ts
10574
+ const launchFailedMessage = (agent) => `Could not start ${agent}`;
10575
+ // src/run/run.ts
9657
10576
  const stop = (message) => stopWith(RUN_MESSAGES[message]);
9658
10577
  const printable = (text) => text.replace(/\p{Cc}/gu, "");
9659
10578
  function candidateList(candidates) {
9660
10579
  const lines = candidates.map(({ identifier, title }) => `${printable(identifier)} ${printable(title)}`);
9661
10580
  return [RUN_MESSAGES.candidatesHeader, ...lines].join("\n");
9662
10581
  }
9663
- const ACCESS_MESSAGES$2 = {
10582
+ const ACCESS_MESSAGES$1 = {
9664
10583
  keychainUnavailable: RUN_MESSAGES.keychainUnavailable,
9665
10584
  initRequired: RUN_MESSAGES.initRequired,
9666
10585
  invalidUrl: RUN_MESSAGES.invalidUrl,
9667
10586
  apiTokenMissing: RUN_MESSAGES.issueInitRequired
9668
10587
  };
9669
10588
  async function prepareLaunch(args, deps) {
9670
- const { workspaceId, ingestEndpoint, readApiBase, accessToken } = await verifyCliAccess(deps, ACCESS_MESSAGES$2);
10589
+ const { workspaceId, ingestEndpoint, readApiBase, accessToken } = await verifyCliAccess(deps, ACCESS_MESSAGES$1);
9671
10590
  if (!isIssueIdentifier(args.issue)) stop("invalidIssue");
9672
10591
  const readFetch = createApiTokenSession({
9673
10592
  keychain: deps.keychain,
@@ -9718,8 +10637,21 @@ function runIssue(args, deps) {
9718
10637
  return 1;
9719
10638
  }, deps.stderr);
9720
10639
  }
9721
- //#endregion
9722
- //#region src/top/args.ts
10640
+ async function runWithoutHarnessforce(args, deps) {
10641
+ const env = Object.fromEntries(Object.entries({
10642
+ ...deps.env,
10643
+ ...deps.restoredEnv
10644
+ }).filter((entry) => entry[1] !== void 0));
10645
+ const outcome = await deps.launch({
10646
+ command: args.agent,
10647
+ args: [...args.agentArgs],
10648
+ env
10649
+ });
10650
+ if (outcome.kind === "exited") return outcome.code;
10651
+ deps.stderr(`${launchFailedMessage(args.agent)}\n`);
10652
+ return 1;
10653
+ }
10654
+ // src/top/args.ts
9723
10655
  const TOP_USAGE = "Usage: harnessforce top [--once] [--json] [--ascii] [--theme <auto|light|dark|ansi>]\n";
9724
10656
  const THEMES = [
9725
10657
  "auto",
@@ -9748,8 +10680,7 @@ function parseTopArgs(args) {
9748
10680
  theme
9749
10681
  };
9750
10682
  }
9751
- //#endregion
9752
- //#region src/top/width.ts
10683
+ // src/top/width.ts
9753
10684
  const SGR = /\x1b\[[0-9;]*m/g;
9754
10685
  const stripSgr = (text) => text.replace(SGR, "");
9755
10686
  const WIDE_RANGES = [
@@ -9814,8 +10745,7 @@ function truncate(text, maxWidth, ambiguousWide, ellipsis) {
9814
10745
  function padEnd(text, width, ambiguousWide) {
9815
10746
  return text + " ".repeat(Math.max(0, width - displayWidth(text, ambiguousWide)));
9816
10747
  }
9817
- //#endregion
9818
- //#region src/top/frame.ts
10748
+ // src/top/frame.ts
9819
10749
  const plainPainter = {
9820
10750
  color: (_token, text) => text,
9821
10751
  bold: (text) => text,
@@ -9953,7 +10883,7 @@ function renderList(sessions, options) {
9953
10883
  const active = sessions.filter((s) => sessionState(s.lastEventAtMs, options.nowMs) === "active").length;
9954
10884
  return [clip(`${sessions.length} sessions, ${active} active`, options)];
9955
10885
  }
9956
- const body = sessions.length === 0 ? ["直近24時間に記録のあるsessionはありません"].map((t) => clip(t, options)) : options.width >= COMPACT_WIDTH ? table(sessions, options, options.width >= FULL_WIDTH) : shortRows(sessions, options);
10886
+ const body = sessions.length === 0 ? ["No sessions recorded in the last 24 hours"].map((t) => clip(t, options)) : options.width >= COMPACT_WIDTH ? table(sessions, options, options.width >= FULL_WIDTH) : shortRows(sessions, options);
9957
10887
  return [
9958
10888
  header(sessions, options),
9959
10889
  ...body,
@@ -10014,9 +10944,8 @@ function renderDetail(row, options) {
10014
10944
  clip(options.painter.dim(DETAIL_HINT), options)
10015
10945
  ];
10016
10946
  }
10017
- const skippedNotice = (snapshot) => snapshot.skippedLines > 0 || snapshot.skippedFiles > 0 ? [`読めなかった${snapshot.skippedLines}行と${snapshot.skippedFiles}個のファイルを読み飛ばしました`] : [];
10018
- //#endregion
10019
- //#region src/top/codex-session.ts
10947
+ const skippedNotice = (snapshot) => snapshot.skippedLines > 0 || snapshot.skippedFiles > 0 ? [`Skipped ${snapshot.skippedLines} unreadable line(s) and ${snapshot.skippedFiles} unreadable file(s)`] : [];
10948
+ // src/top/codex-session.ts
10020
10949
  const DETACHED = "HEAD";
10021
10950
  const SHELL = "shell";
10022
10951
  const COUNTED_CALLS = /* @__PURE__ */ new Set(["function_call", "custom_tool_call"]);
@@ -10134,8 +11063,7 @@ var CodexSessionAccumulator = class {
10134
11063
  };
10135
11064
  }
10136
11065
  };
10137
- //#endregion
10138
- //#region src/top/reader.ts
11066
+ // src/top/reader.ts
10139
11067
  const WINDOW_MS = 864e5;
10140
11068
  const freshState = (newAccumulator) => ({
10141
11069
  offset: 0,
@@ -10260,8 +11188,7 @@ function createTopReader({ projectsDir, codexSessionsDir, git }) {
10260
11188
  };
10261
11189
  } };
10262
11190
  }
10263
- //#endregion
10264
- //#region src/top/color.ts
11191
+ // src/top/color.ts
10265
11192
  const MIN_CONTRAST = 3;
10266
11193
  const TOKEN_VALUES = {
10267
11194
  good: [[
@@ -10400,8 +11327,7 @@ function createPainter(input) {
10400
11327
  dim: (text) => open$1("2", text, "22")
10401
11328
  };
10402
11329
  }
10403
- //#endregion
10404
- //#region src/top/screen.ts
11330
+ // src/top/screen.ts
10405
11331
  const ENTER = "\x1B[?1049h\x1B[?25l";
10406
11332
  const RESTORE = "\x1B[?25h\x1B[?1049l";
10407
11333
  const HOME = "\x1B[H";
@@ -10511,7 +11437,7 @@ async function runScreen(options) {
10511
11437
  };
10512
11438
  const indexOfSelected = (rows) => {
10513
11439
  const index = rows.findIndex((row) => row.sessionId === selectedId);
10514
- return index >= 0 ? index : 0;
11440
+ return Math.max(index, 0);
10515
11441
  };
10516
11442
  const select = (rows, index) => {
10517
11443
  selectedId = rows[Math.max(0, Math.min(index, rows.length - 1))]?.sessionId;
@@ -10621,15 +11547,14 @@ async function runScreen(options) {
10621
11547
  render();
10622
11548
  });
10623
11549
  }
10624
- //#endregion
10625
- //#region src/top/settings.ts
11550
+ // src/top/settings.ts
10626
11551
  const DEFAULT_SETTINGS = {
10627
11552
  ambiguousWide: false,
10628
11553
  braille: false
10629
11554
  };
10630
11555
  const unreadable = {
10631
11556
  settings: DEFAULT_SETTINGS,
10632
- notice: "端末の設定(~/.harnessforce/config.json)を読めないため、既定の表示にします"
11557
+ notice: "Cannot read the device settings (`~/.harnessforce/config.json`), so the default display is used"
10633
11558
  };
10634
11559
  async function readTopSettings(homeDir) {
10635
11560
  let text;
@@ -10657,8 +11582,7 @@ async function readTopSettings(homeDir) {
10657
11582
  braille: braille === true
10658
11583
  } };
10659
11584
  }
10660
- //#endregion
10661
- //#region src/top/command.ts
11585
+ // src/top/command.ts
10662
11586
  const iso$1 = (ms) => new Date(ms).toISOString();
10663
11587
  function toJson(row, nowMs) {
10664
11588
  return {
@@ -10721,8 +11645,7 @@ async function topCommand(args, deps) {
10721
11645
  deps.stdout(`${lines.join("\n")}\n`);
10722
11646
  return 0;
10723
11647
  }
10724
- //#endregion
10725
- //#region src/tune/mcp-config.ts
11648
+ // src/tune/mcp-config.ts
10726
11649
  const UNLISTED = "unlisted";
10727
11650
  const PLUGIN_SERVER = /^([A-Za-z0-9._-]+)@[A-Za-z0-9._-]+:(.+)$/;
10728
11651
  const toToolName = (name) => name.replace(/[^A-Za-z0-9_-]/g, "_");
@@ -10742,8 +11665,7 @@ function mcpConfigOf(identifiers) {
10742
11665
  }
10743
11666
  };
10744
11667
  }
10745
- //#endregion
10746
- //#region src/tune/vocabulary.ts
11668
+ // src/tune/vocabulary.ts
10747
11669
  const CONTINUE_PHRASES = /* @__PURE__ */ new Set([
10748
11670
  "continue",
10749
11671
  "go",
@@ -10804,8 +11726,7 @@ const EDIT_TOOLS = /* @__PURE__ */ new Set([
10804
11726
  "NotebookEdit"
10805
11727
  ]);
10806
11728
  const ISSUE_TOOL = /^mcp__.+__get_issue$/;
10807
- //#endregion
10808
- //#region src/tune/analyze.ts
11729
+ // src/tune/analyze.ts
10809
11730
  const ANALYZER_VERSION = "1.1.0";
10810
11731
  const NOT_MEASURED_INTERVENTIONS = /* @__PURE__ */ new Set(["approval", "answer"]);
10811
11732
  const emptyProposalCounts = () => Object.fromEntries(PROPOSAL_KINDS.map((kind) => [kind, {
@@ -10994,8 +11915,7 @@ function analyzeSession(input) {
10994
11915
  records_skipped: input.skippedLines
10995
11916
  };
10996
11917
  }
10997
- //#endregion
10998
- //#region src/tune/config-snapshot.ts
11918
+ // src/tune/config-snapshot.ts
10999
11919
  async function collectAt(projectRoot, deps, startedMs = deps.now()) {
11000
11920
  const result = await collectConfig({
11001
11921
  projectRoot,
@@ -11014,8 +11934,7 @@ async function collectMcpServers(cwd, git, deps) {
11014
11934
  if (collected.kind === "fixed_failure") return [];
11015
11935
  return [...new Set(collected.components.filter((c) => c.kind === "mcp_server").map((c) => c.id))];
11016
11936
  }
11017
- //#endregion
11018
- //#region src/tune/applied.ts
11937
+ // src/tune/applied.ts
11019
11938
  async function detectApplied(proposals, snapshot) {
11020
11939
  const byRoot = /* @__PURE__ */ new Map();
11021
11940
  const collect = (root) => {
@@ -11039,8 +11958,7 @@ async function detectApplied(proposals, snapshot) {
11039
11958
  }
11040
11959
  return detected;
11041
11960
  }
11042
- //#endregion
11043
- //#region src/tune/usage.ts
11961
+ // src/tune/usage.ts
11044
11962
  const USAGE_KINDS = [
11045
11963
  "frequent_compaction",
11046
11964
  "low_cache_reuse",
@@ -11111,15 +12029,16 @@ function modelShares(usages) {
11111
12029
  subagent: shares(subagent)
11112
12030
  };
11113
12031
  }
11114
- //#endregion
11115
- //#region src/tune/display.ts
12032
+ // src/tune/display.ts
11116
12033
  const median = (values) => {
11117
12034
  if (values.length === 0) return null;
11118
12035
  const sorted = [...values].sort((a, b) => a - b);
11119
12036
  const middle = Math.floor(sorted.length / 2);
11120
12037
  return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
11121
12038
  };
11122
- const secondsText = (value) => value === null ? "—" : `${value.toFixed(1)}秒`;
12039
+ const escapeForTerminal = (text) => text.replace(/[^\x20-\x7e]/gu, (c) => `\\u{${c.codePointAt(0).toString(16)}}`);
12040
+ const asciiJson = (value, space) => JSON.stringify(value, null, space).replace(/[^\n\x20-\x7e]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
12041
+ const secondsText = (value) => value === null ? "—" : `${value.toFixed(1)}s`;
11123
12042
  function summarize(reports, group, kind, countField, medianField) {
11124
12043
  const values = reports.map((r) => r[group][kind]).filter((v) => v?.measurement === "measured");
11125
12044
  if (values.length === 0) return void 0;
@@ -11132,16 +12051,16 @@ function summarize(reports, group, kind, countField, medianField) {
11132
12051
  };
11133
12052
  }
11134
12053
  function renderAnalysis(reports, usages, header) {
11135
- const lines = [`分析したsession: ${header.sessionCount}件(analyzer ${header.analyzerVersion}、parser ${header.parserVersion})`, "人の介入(回数、待ち時間のsessionごとの中央値の中央値):"];
12054
+ const lines = [`Sessions analyzed: ${header.sessionCount} (analyzer ${header.analyzerVersion}, parser ${header.parserVersion})`, "Human interventions (count, median of per-session median wait time):"];
11136
12055
  for (const kind of INTERVENTION_KINDS) {
11137
12056
  const s = summarize(reports, "interventions", kind, "count", "wait_seconds_median");
11138
- lines.push(s === void 0 ? ` ${kind}: 未計測` : ` ${kind}: ${s.total}回、${secondsText(s.median)}`);
12057
+ lines.push(s === void 0 ? ` ${kind}: not measured` : ` ${kind}: ${s.total} time(s), ${secondsText(s.median)}`);
11139
12058
  }
11140
- lines.push("ループにできる繰り返し(回数、その間の介入、所要時間の中央値の中央値):");
12059
+ lines.push("Repeatable loops (count, interventions during them, median of median duration):");
11141
12060
  for (const kind of LOOP_KINDS) {
11142
12061
  const s = summarize(reports, "loops", kind, "occurrences", "duration_seconds_median");
11143
12062
  const interventions = s?.values.reduce((sum, v) => sum + (v.interventions ?? 0), 0) ?? 0;
11144
- lines.push(s === void 0 ? ` ${kind}: 未計測` : ` ${kind}: ${s.total}回、介入${interventions}回、${secondsText(s.median)}`);
12063
+ lines.push(s === void 0 ? ` ${kind}: not measured` : ` ${kind}: ${s.total} time(s), ${interventions} intervention(s), ${secondsText(s.median)}`);
11145
12064
  }
11146
12065
  const servers = /* @__PURE__ */ new Map();
11147
12066
  for (const report of reports) for (const s of report.mcp_servers) {
@@ -11156,19 +12075,19 @@ function renderAnalysis(reports, usages, header) {
11156
12075
  configured: current.configured + (s.configured ? 1 : 0)
11157
12076
  });
11158
12077
  }
11159
- lines.push("MCP server(呼び出し、失敗、設定されていたsession):");
11160
- if (servers.size === 0) lines.push(" なし");
11161
- for (const [server, s] of [...servers].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) lines.push(` ${server}: ${s.calls === null ? "未計測" : `${s.calls}回`}、${s.failures === null ? "未計測" : `${s.failures}回`}、${s.configured}件`);
12078
+ lines.push("MCP servers (calls, failures, sessions where configured):");
12079
+ if (servers.size === 0) lines.push(" none");
12080
+ for (const [server, s] of [...servers].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) lines.push(` ${escapeForTerminal(server)}: ${s.calls === null ? "not measured" : `${s.calls} call(s)`}, ${s.failures === null ? "not measured" : `${s.failures} failure(s)`}, ${s.configured} session(s)`);
11162
12081
  lines.push(...renderUsage(usages));
11163
12082
  return `${lines.join("\n")}\n`;
11164
12083
  }
11165
12084
  const percent = (share) => `${(share * 100).toFixed(1)}%`;
11166
12085
  function renderUsage(usages) {
11167
- const lines = ["使い方(端末だけの値。送信しません):"];
12086
+ const lines = ["Usage (this device only; not sent):"];
11168
12087
  for (const kind of ["frequent_compaction", "low_cache_reuse"]) lines.push(` ${kind}: ${usages.filter((u) => u.kinds.includes(kind)).length} session`);
11169
12088
  const shares = modelShares(usages);
11170
- const parts = shares && [...shares.main.map((m) => `本体 ${m.model} ${percent(m.share)}`), ...shares.subagent.map((m) => `subagent ${m.model} ${percent(m.share)}`)];
11171
- lines.push(` model(output_tokensの割合): ${parts ? parts.join("、") : "なし"}`);
12089
+ const parts = shares && [...shares.main.map((m) => `main ${escapeForTerminal(m.model)} ${percent(m.share)}`), ...shares.subagent.map((m) => `subagent ${escapeForTerminal(m.model)} ${percent(m.share)}`)];
12090
+ lines.push(` model (share of output_tokens): ${parts ? parts.join(", ") : "none"}`);
11172
12091
  return lines;
11173
12092
  }
11174
12093
  function localIso(ms) {
@@ -11178,8 +12097,7 @@ function localIso(ms) {
11178
12097
  const pad = (n) => String(n).padStart(2, "0");
11179
12098
  return `${new Date(ms + offsetMinutes * 6e4).toISOString().slice(0, 19)}${sign}${pad(Math.floor(absolute / 60))}:${pad(absolute % 60)}`;
11180
12099
  }
11181
- //#endregion
11182
- //#region src/tune/followups.ts
12100
+ // src/tune/followups.ts
11183
12101
  const PERIOD_MS = 12096e5;
11184
12102
  const MIN_SESSIONS = 5;
11185
12103
  const ratio = (numerator, denominator) => denominator === 0 ? null : numerator / denominator;
@@ -11283,13 +12201,12 @@ function computeFollowups(proposals, sessions, nowMs, rangeSinceMs) {
11283
12201
  ended
11284
12202
  };
11285
12203
  }
11286
- //#endregion
11287
- //#region src/tune/lock.ts
12204
+ // src/tune/lock.ts
11288
12205
  const LOCK_WAIT_MS = 3e4;
11289
12206
  const STALE_LOCK_MS = 6e4;
11290
12207
  const HEARTBEAT_MS = 1e4;
11291
12208
  const RETRY_INTERVAL_MS = 100;
11292
- const FILE_MODE$2 = 384;
12209
+ const FILE_MODE$1 = 384;
11293
12210
  const isAlreadyExists = (error) => error?.code === "EEXIST";
11294
12211
  async function removeIfStale(path, now) {
11295
12212
  const current = await stat(path).catch(() => void 0);
@@ -11299,7 +12216,7 @@ async function acquireTuneLock(path, deps) {
11299
12216
  const deadline = deps.now() + LOCK_WAIT_MS;
11300
12217
  while (deps.now() < deadline) {
11301
12218
  try {
11302
- const handle = await open(path, "wx", FILE_MODE$2);
12219
+ const handle = await open(path, "wx", FILE_MODE$1);
11303
12220
  const token = randomUUID();
11304
12221
  await handle.writeFile(token);
11305
12222
  await handle.close();
@@ -11319,8 +12236,7 @@ async function acquireTuneLock(path, deps) {
11319
12236
  await deps.sleep(RETRY_INTERVAL_MS);
11320
12237
  }
11321
12238
  }
11322
- //#endregion
11323
- //#region src/tune/messages.ts
12239
+ // src/tune/messages.ts
11324
12240
  const NOTICE_ORDER = [
11325
12241
  "insufficient",
11326
12242
  "outOfRange",
@@ -11344,33 +12260,33 @@ const NOTICE_ORDER = [
11344
12260
  ];
11345
12261
  const TUNE_MESSAGES = {
11346
12262
  keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
11347
- initRequired: "`harnessforce init`を実行してください。Viewerのロールでは`harnessforce tune`を利用できません",
11348
- invalidUrl: "接続先のURLが不正です",
11349
- locked: "別の`harnessforce tune`が実行中のため、実行できません",
11350
- insufficient: (count) => `データ不足: 分析したセッションは${count}件です。提案には10件以上が必要です(あと${10 - count}件)`,
11351
- outOfRange: (days, count) => `送信の範囲外: 開始が${days}日より前の${count}件のセッションは送信しません`,
11352
- notConnected: (count) => `接続済みでないリポジトリの${count}件のセッションは送信しません`,
11353
- partlyUnreadable: (lines, skipped) => `記録の一部を読めていない: 読んだ${lines}行のうち${skipped}行を読み飛ばしました`,
11354
- savedList: (fetchedAt) => `接続済みのリポジトリの一覧を取得できないため、${fetchedAt}に保存した一覧を使います`,
11355
- noList: "接続済みのリポジトリを確認できないため送信しません",
11356
- noSend: "送信しない設定のため、分析結果を送信しません",
11357
- settingUnreadable: "端末の設定(`~/.harnessforce/config.json`)を読めないため、分析結果を送信しません",
11358
- importLimited: (count) => `月間イベント数の上限または閲覧のみのため、${count}件のセッションを取り込めませんでした。分析結果は送信し、取り込みは解除の後の実行でやり直します`,
11359
- importFailed: "送信の前のセッションの取り込みに失敗しました。分析結果は送信し、取り込みは次の実行でやり直します",
11360
- sent: (count) => `${count}件の分析結果を送信しました`,
11361
- unreachable: (count) => `Harnessforceへ送信できませんでした。${count}件の分析結果は次の実行で送ります`,
11362
- expiredUnsent: (count) => `送信の範囲外になった未送信の分析結果${count}件を削除しました`,
11363
- readOnly: (count) => `Workspaceが閲覧のみのため、${count}件の分析結果を送信できませんでした。次の実行で送ります`,
11364
- invalidReports: (count, reasons) => `形式が不正なため${count}件の分析結果を送信せずに削除しました(${reasons.join("、")})`,
11365
- rejected: (status, count) => `Harnessforceが送信を拒否しました(HTTP ${status})。${count}件の分析結果を送信せずに削除しました`,
11366
- keyUnusable: (count) => `送信キーを使えません。未送信の分析結果${count}件を削除しました。\`harnessforce init\`を実行してください`,
11367
- loginExpired: "ログインの有効期限が切れました。`harnessforce init`を実行してください",
11368
- unwritable: "`~/.harnessforce/tune/`に書き込めないため、分析結果を送信しません",
11369
- recordFailed: "提案を記録できませんでした",
11370
- recordInvalid: (field) => `提案の記録の入力が不正です: ${field}`,
11371
- usage: "使い方: harnessforce tune [--all] [--no-send] [--show-report] [--json] | harnessforce tune record | harnessforce tune --purge",
11372
- purged: "`~/.harnessforce/tune/`の分析結果、提案、未送信の分析結果を削除しました",
11373
- purgeFailed: "`~/.harnessforce/tune/`の一部のファイルを削除できませんでした"
12263
+ initRequired: "Run `harnessforce init`. The Viewer role cannot use `harnessforce tune`",
12264
+ invalidUrl: "The connection URL is invalid",
12265
+ locked: "Another `harnessforce tune` is running, so this run cannot start",
12266
+ insufficient: (count) => `Not enough data: ${count} session(s) analyzed. Suggestions need at least 10 (${10 - count} more needed)`,
12267
+ outOfRange: (days, count) => `Outside the sending range: ${count} session(s) that started more than ${days} days ago will not be sent`,
12268
+ notConnected: (count) => `${count} session(s) in repositories that are not connected will not be sent`,
12269
+ partlyUnreadable: (lines, skipped) => `Some records could not be read: skipped ${skipped} of ${lines} lines read`,
12270
+ savedList: (fetchedAt) => `Could not fetch the list of connected repositories, so the list saved at ${fetchedAt} is used`,
12271
+ noList: "Could not verify the connected repositories, so nothing is sent",
12272
+ noSend: "Sending is turned off, so the analysis results are not sent",
12273
+ settingUnreadable: "Cannot read the device settings (`~/.harnessforce/config.json`), so the analysis results are not sent",
12274
+ importLimited: (count) => `Could not import ${count} session(s) because of the monthly event limit or view-only mode. The analysis results are sent; importing is retried in a run after the restriction is lifted`,
12275
+ importFailed: "Importing sessions before sending failed. The analysis results are sent; importing is retried in the next run",
12276
+ sent: (count) => `Sent ${count} analysis result(s)`,
12277
+ unreachable: (count) => `Could not send to Harnessforce. ${count} analysis result(s) will be sent in the next run`,
12278
+ expiredUnsent: (count) => `Deleted ${count} unsent analysis result(s) that fell outside the sending range`,
12279
+ readOnly: (count) => `Could not send ${count} analysis result(s) because the Workspace is view-only. They will be sent in the next run`,
12280
+ invalidReports: (count, reasons) => `Deleted ${count} analysis result(s) without sending because their format is invalid (${reasons.join(", ")})`,
12281
+ rejected: (status, count) => `Harnessforce rejected the submission (HTTP ${status}). Deleted ${count} analysis result(s) without sending`,
12282
+ keyUnusable: (count) => `The ingest key cannot be used. Deleted ${count} unsent analysis result(s). Run \`harnessforce init\``,
12283
+ loginExpired: "Your login has expired. Run `harnessforce init`",
12284
+ unwritable: "Cannot write to `~/.harnessforce/tune/`, so the analysis results are not sent",
12285
+ recordFailed: "Could not record the suggestion",
12286
+ recordInvalid: (field) => `The suggestion record input is invalid: ${field}`,
12287
+ usage: "Usage: harnessforce tune [--all] [--no-send] [--show-report] [--json], harnessforce tune record, or harnessforce tune --purge",
12288
+ purged: "Deleted the analysis results, suggestions and unsent analysis results in `~/.harnessforce/tune/`",
12289
+ purgeFailed: "Could not delete some files in `~/.harnessforce/tune/`"
11374
12290
  };
11375
12291
  var Notices = class {
11376
12292
  entries = /* @__PURE__ */ new Map();
@@ -11394,8 +12310,7 @@ var Notices = class {
11394
12310
  return code;
11395
12311
  }
11396
12312
  };
11397
- //#endregion
11398
- //#region src/tune/store.ts
12313
+ // src/tune/store.ts
11399
12314
  const tuneDir = (homeDir) => join(homeDir, ".harnessforce", "tune");
11400
12315
  const tunePaths = (homeDir) => {
11401
12316
  const dir = tuneDir(homeDir);
@@ -11411,7 +12326,7 @@ const tunePaths = (homeDir) => {
11411
12326
  };
11412
12327
  };
11413
12328
  const DIR_MODE = 448;
11414
- const FILE_MODE$1 = 384;
12329
+ const FILE_MODE = 384;
11415
12330
  const destinationKey = ({ workspaceId, endpoint }) => `${workspaceId} ${endpoint}`;
11416
12331
  async function readJsonFile(path) {
11417
12332
  try {
@@ -11431,18 +12346,17 @@ async function writeFileAtomic(path, content) {
11431
12346
  const temporary = `${path}.${randomUUID()}.tmp`;
11432
12347
  try {
11433
12348
  await writeFile(temporary, content, {
11434
- mode: FILE_MODE$1,
12349
+ mode: FILE_MODE,
11435
12350
  flag: "wx"
11436
12351
  });
11437
- await chmod(temporary, FILE_MODE$1);
12352
+ await chmod(temporary, FILE_MODE);
11438
12353
  await rename(temporary, path);
11439
12354
  } finally {
11440
12355
  await rm(temporary, { force: true });
11441
12356
  }
11442
12357
  }
11443
12358
  const writeJsonFile = (path, value) => writeFileAtomic(path, `${JSON.stringify(value)}\n`);
11444
- //#endregion
11445
- //#region src/tune/proposals.ts
12359
+ // src/tune/proposals.ts
11446
12360
  const isRecord = (value) => isObject(value) && typeof value.proposal_id === "string" && typeof value.change_type === "string" && PROPOSAL_KINDS.includes(value.change_type) && Array.isArray(value.evidence_session_ids) && (value.attribution === "pending" || value.attribution === "decided");
11447
12361
  async function readProposals(path) {
11448
12362
  const value = await readJsonFile(path);
@@ -11481,8 +12395,7 @@ function countProposals(proposals, sessionId) {
11481
12395
  }
11482
12396
  return counts;
11483
12397
  }
11484
- //#endregion
11485
- //#region src/tune/saved-state.ts
12398
+ // src/tune/saved-state.ts
11486
12399
  async function readDestinations(path) {
11487
12400
  const value = await readJsonFile(path);
11488
12401
  return isObject(value) && isObject(value.destinations) ? value.destinations : {};
@@ -11541,8 +12454,7 @@ const writeSessionConfigs = (path, configs) => writeJsonFile(path, {
11541
12454
  transcript_path: config.transcriptPath
11542
12455
  }]))
11543
12456
  });
11544
- //#endregion
11545
- //#region src/tune/send.ts
12457
+ // src/tune/send.ts
11546
12458
  const BATCH_SIZE = 100;
11547
12459
  const READ_ONLY = "workspace_read_only";
11548
12460
  function parseReply(body, size) {
@@ -11643,8 +12555,7 @@ async function sendReports(options) {
11643
12555
  result.invalid.reasons = [...reasons];
11644
12556
  return result;
11645
12557
  }
11646
- //#endregion
11647
- //#region src/tune/sessions.ts
12558
+ // src/tune/sessions.ts
11648
12559
  const byCodeUnit = (a, b) => a < b ? -1 : a > b ? 1 : 0;
11649
12560
  async function readSubagentUsages(transcriptPath) {
11650
12561
  const dir = join(dirname(transcriptPath), basename(transcriptPath, ".jsonl"), "subagents");
@@ -11693,8 +12604,7 @@ async function readTuneSessions(options) {
11693
12604
  }
11694
12605
  return [...sessions.values()].sort((a, b) => Date.parse(a.session.startedAt) - Date.parse(b.session.startedAt) || byCodeUnit(a.session.sessionId, b.session.sessionId));
11695
12606
  }
11696
- //#endregion
11697
- //#region src/tune/settings.ts
12607
+ // src/tune/settings.ts
11698
12608
  const tuneConfigPath = (homeDir) => join(homeDir, ".harnessforce", "config.json");
11699
12609
  const isMissing = (error) => error?.code === "ENOENT";
11700
12610
  async function readSendSetting(homeDir) {
@@ -11719,12 +12629,11 @@ async function readSendSetting(homeDir) {
11719
12629
  if (typeof sendReport !== "boolean") return "unreadable";
11720
12630
  return sendReport ? "send" : "no_send";
11721
12631
  }
11722
- //#endregion
11723
- //#region src/tune/command.ts
12632
+ // src/tune/command.ts
11724
12633
  const DAY_MS = 864e5;
11725
12634
  const DEFAULT_RANGE_DAYS = 30;
11726
12635
  const SKIPPED_RATIO = .05;
11727
- const ACCESS_MESSAGES$1 = {
12636
+ const ACCESS_MESSAGES = {
11728
12637
  keychainUnavailable: TUNE_MESSAGES.keychainUnavailable,
11729
12638
  initRequired: TUNE_MESSAGES.initRequired,
11730
12639
  invalidUrl: TUNE_MESSAGES.invalidUrl,
@@ -11960,14 +12869,14 @@ async function analyzeUnderLock(options, access, paths, deps) {
11960
12869
  writeSessionConfigs(paths.sessionConfigs, configs)
11961
12870
  ]).then(() => true, () => false);
11962
12871
  if (!isWritten) notices.add("unwritable", TUNE_MESSAGES.unwritable, 1);
11963
- if (options.json) deps.stdout(`${JSON.stringify(output)}\n`);
12872
+ if (options.json) deps.stdout(`${asciiJson(output)}\n`);
11964
12873
  else {
11965
12874
  deps.stdout(renderAnalysis(analyzed.map((a) => a.report), analyzed.map((a) => a.tune.usage), {
11966
12875
  sessionCount: analyzed.length,
11967
12876
  analyzerVersion: ANALYZER_VERSION,
11968
12877
  parserVersion: PARSER_VERSION
11969
12878
  }));
11970
- if (options.showReport) deps.stdout(`${JSON.stringify(analyzed.filter((a) => a.sendable === true).map((a) => a.report), null, 2)}\n`);
12879
+ if (options.showReport) deps.stdout(`${asciiJson(analyzed.filter((a) => a.sendable === true).map((a) => a.report), 2)}\n`);
11971
12880
  }
11972
12881
  if (analyzed.length < 10) notices.add("insufficient", TUNE_MESSAGES.insufficient(analyzed.length));
11973
12882
  const lines = analyzed.reduce((sum, a) => sum + a.tune.lines, 0);
@@ -11990,7 +12899,7 @@ async function analyzeUnderLock(options, access, paths, deps) {
11990
12899
  }
11991
12900
  function analyzeCommand(options, deps) {
11992
12901
  return runUntilStop(async () => {
11993
- const access = await verifyCliAccess(deps, ACCESS_MESSAGES$1);
12902
+ const access = await verifyCliAccess(deps, ACCESS_MESSAGES);
11994
12903
  const paths = tunePaths(deps.homeDir);
11995
12904
  await ensureDir(paths.dir).catch(() => stopWith(TUNE_MESSAGES.unwritable));
11996
12905
  const release = await acquireTuneLock(paths.lock, deps).catch(() => stopWith(TUNE_MESSAGES.unwritable));
@@ -12002,8 +12911,7 @@ function analyzeCommand(options, deps) {
12002
12911
  }
12003
12912
  }, deps.stderr);
12004
12913
  }
12005
- //#endregion
12006
- //#region src/tune/purge.ts
12914
+ // src/tune/purge.ts
12007
12915
  async function purgeTuneFiles(paths) {
12008
12916
  const targets = [
12009
12917
  paths.repositories,
@@ -12018,8 +12926,7 @@ async function purgeTuneFiles(paths) {
12018
12926
  force: true
12019
12927
  })))).every((r) => r.status === "fulfilled");
12020
12928
  }
12021
- //#endregion
12022
- //#region src/tune/analysis-file.ts
12929
+ // src/tune/analysis-file.ts
12023
12930
  async function readAnalysisSessions(path) {
12024
12931
  const value = await readJsonFile(path);
12025
12932
  const sessions = isObject(value) && Array.isArray(value.sessions) ? value.sessions : [];
@@ -12034,8 +12941,7 @@ async function readAnalysisSessions(path) {
12034
12941
  }
12035
12942
  return found;
12036
12943
  }
12037
- //#endregion
12038
- //#region src/tune/record.ts
12944
+ // src/tune/record.ts
12039
12945
  const MAX_RECORD_INPUT_BYTES = 1048576;
12040
12946
  const SCOPES = [
12041
12947
  "user",
@@ -12119,7 +13025,7 @@ const sha256 = (text) => createHash("sha256").update(Buffer.from(text, "utf8")).
12119
13025
  function expectedHash(input, component) {
12120
13026
  if (component !== null && VALUE_KINDS.has(component.kind)) {
12121
13027
  if (!("value" in input)) return invalid("value");
12122
- return hashValue(input.value);
13028
+ return hashValue(hashInput(component.kind, input.value));
12123
13029
  }
12124
13030
  if (typeof input.content !== "string") return invalid("content");
12125
13031
  return hashFileContent(Buffer.from(input.content, "utf8"));
@@ -12199,8 +13105,7 @@ async function recordProposal(text, paths, now) {
12199
13105
  attributed_session_id: record.attributed_session_id
12200
13106
  };
12201
13107
  }
12202
- //#endregion
12203
- //#region src/tune/tune.ts
13108
+ // src/tune/tune.ts
12204
13109
  const FLAGS = {
12205
13110
  "--all": "all",
12206
13111
  "--no-send": "noSend",
@@ -12226,6 +13131,10 @@ function parseArgs(args) {
12226
13131
  options
12227
13132
  };
12228
13133
  }
13134
+ function canTuneSend(args) {
13135
+ const parsed = parseArgs(args);
13136
+ return parsed?.kind === "analyze" && !parsed.options.noSend;
13137
+ }
12229
13138
  async function underLock(deps, failure, body) {
12230
13139
  return runUntilStop(async () => {
12231
13140
  const paths = tunePaths(deps.homeDir);
@@ -12278,465 +13187,14 @@ async function tuneCommand(args, deps) {
12278
13187
  });
12279
13188
  return analyzeCommand(parsed.options, deps);
12280
13189
  }
12281
- //#endregion
12282
- //#region src/usage-limits/consent.ts
12283
- const consentUrl = (base) => underBase(base, "api/v1/cli/usage-limits/consent");
12284
- async function call(base, fetchImpl, method, body) {
12285
- try {
12286
- return await fetchImpl(consentUrl(base), {
12287
- method,
12288
- headers: {
12289
- accept: "application/json",
12290
- ...body ? { "content-type": "application/json" } : {}
12291
- },
12292
- ...body ? { body: JSON.stringify(body) } : {},
12293
- redirect: "manual",
12294
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
12295
- });
12296
- } catch {
12297
- return;
12298
- }
12299
- }
12300
- async function readConsent(base, fetchImpl) {
12301
- const response = await call(base, fetchImpl, "GET");
12302
- if (response?.status === 401) return { kind: "unauthorized" };
12303
- if (response?.status !== 200) return { kind: "failed" };
12304
- const body = await readJsonObjectBody(response);
12305
- if (typeof body?.workspace_opted_in !== "boolean" || typeof body.shared !== "boolean" || typeof body.current_text_version !== "number") return { kind: "failed" };
12306
- return {
12307
- kind: "ok",
12308
- state: {
12309
- workspaceOptedIn: body.workspace_opted_in,
12310
- shared: body.shared,
12311
- currentTextVersion: body.current_text_version
12312
- }
12313
- };
12314
- }
12315
- async function recordConsent(base, fetchImpl, textVersion) {
12316
- const response = await call(base, fetchImpl, "PUT", { text_version: textVersion });
12317
- if (!response) return { kind: "failed" };
12318
- if (response.status === 204) return { kind: "ok" };
12319
- if (response.status === 401) return { kind: "unauthorized" };
12320
- const body = await readJsonObjectBody(response);
12321
- if (response.status === 400 && body?.code === "invalid_text_version") return { kind: "invalid_text_version" };
12322
- if (response.status === 403) return body?.code === "workspace_read_only" ? { kind: "read_only" } : { kind: "viewer" };
12323
- return { kind: "failed" };
12324
- }
12325
- async function revokeConsent(base, fetchImpl) {
12326
- const response = await call(base, fetchImpl, "DELETE");
12327
- if (response?.status === 204) return { kind: "ok" };
12328
- return response?.status === 401 ? { kind: "unauthorized" } : { kind: "failed" };
12329
- }
12330
- //#endregion
12331
- //#region src/usage-limits/files.ts
12332
- const DIRECTORY_MODE = 448;
12333
- const FILE_MODE = 384;
12334
- const markPath = (homeDir) => join(homeDir, ".harnessforce", "usage-limits.json");
12335
- const statePath = (homeDir) => join(homeDir, ".harnessforce", "usage-limits-state.json");
12336
- async function readJson(path) {
12337
- try {
12338
- return JSON.parse(await readFile(path, "utf8"));
12339
- } catch {
12340
- return;
12341
- }
12342
- }
12343
- async function readMark(homeDir) {
12344
- const value = await readJson(markPath(homeDir));
12345
- if (!isObject(value)) return void 0;
12346
- const { consented, text_version: textVersion, original } = value;
12347
- if (typeof consented !== "boolean" || typeof textVersion !== "number" || !(original === null || isObject(original))) return void 0;
12348
- return {
12349
- consented,
12350
- text_version: textVersion,
12351
- original
12352
- };
12353
- }
12354
- async function readSendState(homeDir) {
12355
- const value = await readJson(statePath(homeDir));
12356
- if (!isObject(value) || typeof value.sent_at !== "number") return void 0;
12357
- const resets = value.resets_at;
12358
- if (!isObject(resets)) return void 0;
12359
- const entries = Object.entries(resets);
12360
- if (entries.some(([, at]) => typeof at !== "number")) return void 0;
12361
- return {
12362
- sent_at: value.sent_at,
12363
- resets_at: Object.fromEntries(entries)
12364
- };
12365
- }
12366
- async function writeOwnerOnly(path, value) {
12367
- await mkdir(dirname(path), {
12368
- recursive: true,
12369
- mode: DIRECTORY_MODE
12370
- });
12371
- const temporary = `${path}.${randomUUID()}.tmp`;
12372
- try {
12373
- await writeFile(temporary, `${JSON.stringify(value)}\n`, {
12374
- mode: FILE_MODE,
12375
- flag: "wx"
12376
- });
12377
- await chmod(temporary, FILE_MODE);
12378
- await rename(temporary, path);
12379
- } finally {
12380
- await rm(temporary, { force: true });
12381
- }
12382
- }
12383
- const writeSendState = (homeDir, state) => writeOwnerOnly(statePath(homeDir), state);
12384
- const writeMark = (homeDir, mark) => writeOwnerOnly(markPath(homeDir), mark);
12385
- async function removeMarkAndState(homeDir) {
12386
- await rm(markPath(homeDir), { force: true });
12387
- await rm(statePath(homeDir), { force: true });
12388
- }
12389
- //#endregion
12390
- //#region src/usage-limits/messages.ts
12391
- const CONSENT_TEXT = {
12392
- ja: "あなたの名前とともに、あなたの利用枠(5時間・7日などの窓の使用率とリセット時刻だけ)を、このWorkspaceのメンバー全員が見られます。プロンプトなどの本文は送りません。いつでもやめられ、やめると保存済みの値も削除します。メンバーの順位づけには使いません。",
12393
- en: "Everyone in this workspace can see your usage limits together with your name (only the usage percentage and reset time of each window, such as 5 hours and 7 days). No prompt or other content is sent. You can stop at any time, and stopping deletes the stored values. They are not used to rank members."
12394
- };
12395
- const CONSENT_PROMPT = {
12396
- ja: "同意しますか? [y/N] ",
12397
- en: "Do you agree? [y/N] "
12398
- };
12399
- const MESSAGES = {
12400
- windows: "この版のWindowsでは、利用枠の共有を設定できません",
12401
- settingsUnreadable: "Claude Codeのuser settingsを読めません",
12402
- statusLineUnsupported: "このstatusLineは組み込めません",
12403
- noUserKey: "この端末には利用者用の送信キーがありません。`harnessforce init`を実行してください",
12404
- keychainUnavailable: "OSのキーチェーンを利用できないため、利用枠の共有を設定できません",
12405
- invalidUrl: "接続先のURLが不正です",
12406
- loginExpired: "ログインの有効期限が切れました。`harnessforce init`をもう一度実行してください",
12407
- notAgreed: "同意されなかったため、何も変えませんでした",
12408
- invalidTextVersion: "このCLIの利用枠の文面が古いか新しすぎます。`harnessforce`を更新してください",
12409
- viewer: "閲覧のみのロールでは、利用枠を共有できません。Workspaceの管理者に連絡してください",
12410
- readOnly: "このWorkspaceは閲覧のみのため、利用枠の共有を設定できません",
12411
- failed: "Harnessforceとの通信に失敗しました。もう一度お試しください",
12412
- saveFailed: "端末の設定を書き込めなかったため、同意を取り消しました。もう一度お試しください",
12413
- revokeFailedAfterSave: "端末の設定を書き込めず、同意の取り消しにも失敗しました。`harnessforce usage-limits status`で状態を確かめ、`harnessforce usage-limits off`を実行してください",
12414
- notOptedIn: "同意を記録しました。Workspaceが利用枠の記録をopt-inするまで、値は保存されません",
12415
- enabled: "利用枠の共有を有効にしました。Claude Codeを再起動すると反映されます",
12416
- disabled: "利用枠の共有をやめ、保存済みの値の削除を依頼しました",
12417
- revokedButNotRestored: "同意は撤回しましたが、statusLineを元に戻せませんでした。`harnessforce usage-limits off`をもう一度実行してください",
12418
- usage: "Usage: harnessforce usage-limits on [--yes] | harnessforce usage-limits off | harnessforce usage-limits status | harnessforce usage-limits statusline\n"
12419
- };
12420
- const STATUS_LABELS = {
12421
- consent: {
12422
- shared: "同意している",
12423
- notShared: "同意していない",
12424
- outdated: "文面が古い",
12425
- unknown: "確認できません"
12426
- },
12427
- optIn: {
12428
- on: "有効",
12429
- off: "無効",
12430
- unknown: "確認できません"
12431
- },
12432
- statusLine: {
12433
- embeddedWithOriginal: "組み込まれている(元のcommandあり)",
12434
- embeddedWithoutOriginal: "組み込まれている(元のcommandなし)",
12435
- notEmbedded: "組み込まれていない"
12436
- }
12437
- };
12438
- //#endregion
12439
- //#region src/usage-limits/statusline-settings.ts
12440
- const STATUSLINE_COMMAND = "harnessforce usage-limits statusline";
12441
- const isEmbedded = (statusLine) => isObject(statusLine) && statusLine.command === STATUSLINE_COMMAND;
12442
- function canEmbed(settings) {
12443
- const { statusLine } = settings;
12444
- if (statusLine === void 0) return true;
12445
- return isObject(statusLine) && statusLine.type === "command" && typeof statusLine.command === "string";
12446
- }
12447
- function originalFor(settings, mark) {
12448
- const { statusLine } = settings;
12449
- if (isEmbedded(statusLine)) return mark ? mark.original : null;
12450
- return isObject(statusLine) ? statusLine : null;
12451
- }
12452
- function embedStatusLine(settings) {
12453
- const { statusLine } = settings;
12454
- if (isEmbedded(statusLine)) return settings;
12455
- return {
12456
- ...settings,
12457
- statusLine: isObject(statusLine) ? {
12458
- ...statusLine,
12459
- command: STATUSLINE_COMMAND
12460
- } : {
12461
- type: "command",
12462
- command: STATUSLINE_COMMAND
12463
- }
12464
- };
12465
- }
12466
- function restoreStatusLine(settings, original) {
12467
- if (!isEmbedded(settings.statusLine)) return settings;
12468
- const { statusLine: _embedded, ...rest } = settings;
12469
- return original === null ? rest : {
12470
- ...settings,
12471
- statusLine: original
12472
- };
12473
- }
12474
- const isStatusLineEmbedded = (settings) => isEmbedded(settings.statusLine);
12475
- //#endregion
12476
- //#region src/usage-limits/manage.ts
12477
- const ACCESS_MESSAGES = {
12478
- keychainUnavailable: MESSAGES.keychainUnavailable,
12479
- initRequired: MESSAGES.noUserKey,
12480
- invalidUrl: MESSAGES.invalidUrl,
12481
- apiTokenMissing: MESSAGES.noUserKey
12482
- };
12483
- function language(env) {
12484
- return (env.LC_ALL || env.LC_MESSAGES || env.LANG || "").toLowerCase().startsWith("ja") ? "ja" : "en";
12485
- }
12486
- function consentClient(deps, access) {
12487
- const authorized = createApiTokenSession({
12488
- keychain: deps.keychain,
12489
- workspaceId: access.workspaceId,
12490
- readBase: access.readApiBase,
12491
- fetch: deps.fetch,
12492
- now: () => deps.now().getTime(),
12493
- sleep: deps.sleep,
12494
- homeDir: deps.homeDir
12495
- }).authorizedFetch(deps.fetch);
12496
- const base = access.readApiBase;
12497
- return {
12498
- read: () => readConsent(base, authorized),
12499
- record: () => recordConsent(base, authorized, 1),
12500
- revoke: () => revokeConsent(base, authorized)
12501
- };
12502
- }
12503
- const settingsPathOf = (deps) => userSettingsPath(deps.env, deps.homeDir);
12504
- async function runOn(yes, deps) {
12505
- if (deps.platform === "win32") return stopWith(MESSAGES.windows);
12506
- const client = consentClient(deps, await verifyCliAccess(deps, ACCESS_MESSAGES));
12507
- const read = await readUserSettings(settingsPathOf(deps));
12508
- if (read.kind === "invalid") return stopWith(MESSAGES.settingsUnreadable);
12509
- if (!canEmbed(read.settings)) return stopWith(MESSAGES.statusLineUnsupported);
12510
- const settings = read.settings;
12511
- const before = await client.read();
12512
- const lang = language(deps.env);
12513
- deps.stdout(`${CONSENT_TEXT[lang]}\n`);
12514
- if (!yes && (await deps.ask(CONSENT_PROMPT[lang])).trim() !== "y") return stopWith(MESSAGES.notAgreed);
12515
- const recorded = await client.record();
12516
- if (recorded.kind === "unauthorized") return stopWith(MESSAGES.loginExpired);
12517
- if (recorded.kind === "invalid_text_version") return stopWith(MESSAGES.invalidTextVersion);
12518
- if (recorded.kind === "viewer") return stopWith(MESSAGES.viewer);
12519
- if (recorded.kind === "read_only") return stopWith(MESSAGES.readOnly);
12520
- if (recorded.kind === "failed") return stopWith(MESSAGES.failed);
12521
- const mark = await readMark(deps.homeDir);
12522
- const undo = async () => {
12523
- await (mark ? writeMark(deps.homeDir, {
12524
- ...mark,
12525
- consented: false
12526
- }) : removeMarkAndState(deps.homeDir)).catch(() => {});
12527
- const revoked = await client.revoke();
12528
- return stopWith(revoked.kind === "ok" ? MESSAGES.saveFailed : MESSAGES.revokeFailedAfterSave);
12529
- };
12530
- try {
12531
- await writeMark(deps.homeDir, {
12532
- consented: true,
12533
- text_version: 1,
12534
- original: originalFor(settings, mark)
12535
- });
12536
- } catch {
12537
- return undo();
12538
- }
12539
- try {
12540
- await writeUserSettings(settingsPathOf(deps), embedStatusLine(settings));
12541
- } catch {
12542
- return undo();
12543
- }
12544
- const optedOut = before.kind === "ok" && !before.state.workspaceOptedIn;
12545
- deps.stdout(`${optedOut ? MESSAGES.notOptedIn : MESSAGES.enabled}\n`);
12546
- return 0;
12547
- }
12548
- async function runOff(deps) {
12549
- const revoked = await consentClient(deps, await verifyCliAccess(deps, ACCESS_MESSAGES)).revoke();
12550
- if (revoked.kind === "unauthorized") return stopWith(MESSAGES.loginExpired);
12551
- if (revoked.kind === "failed") return stopWith(MESSAGES.failed);
12552
- const mark = await readMark(deps.homeDir);
12553
- const read = await readUserSettings(settingsPathOf(deps));
12554
- const keepWithoutConsent = async () => {
12555
- if (mark) await writeMark(deps.homeDir, {
12556
- ...mark,
12557
- consented: false
12558
- }).catch(() => {});
12559
- return stopWith(MESSAGES.revokedButNotRestored);
12560
- };
12561
- if (read.kind === "invalid") return keepWithoutConsent();
12562
- if (isStatusLineEmbedded(read.settings)) try {
12563
- await writeUserSettings(settingsPathOf(deps), restoreStatusLine(read.settings, mark?.original ?? null));
12564
- } catch {
12565
- return keepWithoutConsent();
12566
- }
12567
- await removeMarkAndState(deps.homeDir).catch(() => {});
12568
- deps.stdout(`${MESSAGES.disabled}\n`);
12569
- return 0;
12570
- }
12571
- function consentLabel(result, mark) {
12572
- if (result.kind !== "ok") return STATUS_LABELS.consent.unknown;
12573
- if (result.state.shared) return STATUS_LABELS.consent.shared;
12574
- return mark?.consented === true && mark.text_version !== result.state.currentTextVersion ? STATUS_LABELS.consent.outdated : STATUS_LABELS.consent.notShared;
12575
- }
12576
- async function runStatus(deps) {
12577
- const mark = await readMark(deps.homeDir);
12578
- const read = await readUserSettings(settingsPathOf(deps));
12579
- const statusLine = !(read.kind === "ok" && isStatusLineEmbedded(read.settings)) ? STATUS_LABELS.statusLine.notEmbedded : mark?.original ? STATUS_LABELS.statusLine.embeddedWithOriginal : STATUS_LABELS.statusLine.embeddedWithoutOriginal;
12580
- const result = await readRemote(deps);
12581
- deps.stdout([
12582
- `本人の同意: ${consentLabel(result, mark)}`,
12583
- `Workspaceのopt-in: ${result.kind !== "ok" ? STATUS_LABELS.optIn.unknown : result.state.workspaceOptedIn ? STATUS_LABELS.optIn.on : STATUS_LABELS.optIn.off}`,
12584
- `statusLineの組み込み: ${statusLine}`,
12585
- ""
12586
- ].join("\n"));
12587
- return 0;
12588
- }
12589
- async function readRemote(deps) {
12590
- try {
12591
- return await consentClient(deps, await verifyCliAccess(deps, ACCESS_MESSAGES)).read();
12592
- } catch (error) {
12593
- if (isStop(error)) return { kind: "failed" };
12594
- throw error;
12595
- }
12596
- }
12597
- function onCommand(yes, deps) {
12598
- return runUntilStop(() => runOn(yes, deps), deps.stderr);
12599
- }
12600
- function offCommand(deps) {
12601
- return runUntilStop(() => runOff(deps), deps.stderr);
12602
- }
12603
- const statusCommand = runStatus;
12604
- //#endregion
12605
- //#region src/usage-limits/summary.ts
12606
- const WINDOWS = [{
12607
- name: "five_hour",
12608
- minutes: 300
12609
- }, {
12610
- name: "seven_day",
12611
- minutes: 10080
12612
- }];
12613
- const MIN_SEND_INTERVAL_MS = 3e5;
12614
- const instant = (ms) => new Date(ms).toISOString().replace(".000Z", "Z");
12615
- function windowFrom(value, minutes, nowMs) {
12616
- if (!isObject(value)) return void 0;
12617
- const { used_percentage: used, resets_at: resetsAt } = value;
12618
- if (typeof used !== "number" || !(used >= 0 && used <= 100)) return void 0;
12619
- if (typeof resetsAt !== "number" || !Number.isFinite(resetsAt)) return void 0;
12620
- const resetsAtMs = Math.floor(resetsAt) * 1e3;
12621
- if (resetsAtMs <= nowMs) return void 0;
12622
- return {
12623
- window_minutes: minutes,
12624
- used_percent: used,
12625
- resets_at: instant(resetsAtMs)
12626
- };
12627
- }
12628
- function summaryFrom(stdin, nowMs) {
12629
- let json;
12630
- try {
12631
- json = JSON.parse(stdin);
12632
- } catch {
12633
- return;
12634
- }
12635
- const limits = isObject(json) ? json.rate_limits : void 0;
12636
- if (!isObject(limits)) return void 0;
12637
- const windows = WINDOWS.flatMap(({ name, minutes }) => {
12638
- const window = windowFrom(limits[name], minutes, nowMs);
12639
- return window ? [window] : [];
12640
- });
12641
- return windows.length === 0 ? void 0 : {
12642
- agent: "claude_code",
12643
- observed_at: instant(Math.floor(nowMs / 1e3) * 1e3),
12644
- windows
12645
- };
12646
- }
12647
- const resetSeconds = (window) => Date.parse(window.resets_at) / 1e3;
12648
- function isDue(summary, state, nowMs) {
12649
- if (!state) return true;
12650
- return summary.windows.some((window) => state.resets_at[String(window.window_minutes)] !== resetSeconds(window)) || nowMs - state.sent_at >= MIN_SEND_INTERVAL_MS;
12651
- }
12652
- const stateFor = (summary, nowMs) => ({
12653
- sent_at: nowMs,
12654
- resets_at: Object.fromEntries(summary.windows.map((window) => [String(window.window_minutes), resetSeconds(window)]))
12655
- });
12656
- function parseSummary(raw) {
12657
- let value;
12658
- try {
12659
- value = JSON.parse(raw);
12660
- } catch {
12661
- return;
12662
- }
12663
- if (!isObject(value) || value.agent !== "claude_code") return void 0;
12664
- const { observed_at: observedAt, windows } = value;
12665
- if (typeof observedAt !== "string" || !Array.isArray(windows)) return void 0;
12666
- const rebuilt = windows.flatMap((window) => {
12667
- if (!isObject(window)) return [];
12668
- const { window_minutes: minutes, used_percent: used } = window;
12669
- const resetsAt = window.resets_at;
12670
- return WINDOWS.some((w) => w.minutes === minutes) && typeof minutes === "number" && typeof used === "number" && used >= 0 && used <= 100 && typeof resetsAt === "string" && !Number.isNaN(Date.parse(resetsAt)) ? [{
12671
- window_minutes: minutes,
12672
- used_percent: used,
12673
- resets_at: resetsAt
12674
- }] : [];
12675
- });
12676
- return rebuilt.length > 0 && rebuilt.length === windows.length ? {
12677
- agent: "claude_code",
12678
- observed_at: observedAt,
12679
- windows: rebuilt
12680
- } : void 0;
12681
- }
12682
- //#endregion
12683
- //#region src/usage-limits/send.ts
12684
- const SEND_TIMEOUT_MS = 5e3;
12685
- async function sendUsageLimits(raw, deps) {
12686
- const summary = parseSummary(raw);
12687
- if (!summary) return 0;
12688
- const mark = await readMark(deps.homeDir);
12689
- if (!mark?.consented || mark.text_version !== 1) return 0;
12690
- const resolved = await resolveUserDestination(deps).catch(() => void 0);
12691
- if (resolved?.kind !== "ok") return 0;
12692
- await postItem(resolved.destination, "v1/usage-limits", summary, deps.fetch, SEND_TIMEOUT_MS);
12693
- return 0;
12694
- }
12695
- //#endregion
12696
- //#region src/usage-limits/statusline.ts
12697
- const MAX_INPUT_BYTES = Number.MAX_SAFE_INTEGER;
12698
- function originalCommand(original) {
12699
- const command = original?.command;
12700
- return isObject(original) && typeof command === "string" && command !== "" ? command : void 0;
12701
- }
12702
- async function statusline(deps) {
12703
- const input = await deps.readStdin(MAX_INPUT_BYTES).catch(() => Buffer.alloc(0));
12704
- const mark = await readMark(deps.homeDir);
12705
- const command = originalCommand(mark?.original ?? null);
12706
- const code = command ? await deps.runOriginal(command, input) : void 0;
12707
- await sendIfDue(deps, input, mark).catch(() => {});
12708
- return code ?? 0;
12709
- }
12710
- async function sendIfDue(deps, input, mark) {
12711
- if (!mark?.consented || mark.text_version !== 1) return;
12712
- const nowMs = deps.now().getTime();
12713
- const summary = summaryFrom(input.toString("utf8"), nowMs);
12714
- if (!summary) return;
12715
- if (!isDue(summary, await readSendState(deps.homeDir), nowMs)) return;
12716
- await writeSendState(deps.homeDir, stateFor(summary, nowMs));
12717
- deps.spawnSender(JSON.stringify(summary));
12718
- }
12719
- //#endregion
12720
- //#region src/usage-limits/command.ts
12721
- async function usageLimitsCommand(args, deps) {
12722
- const [subcommand, payload] = args;
12723
- if (subcommand === "statusline" && args.length === 1) return statusline(deps);
12724
- if (subcommand === "send" && payload !== void 0 && args.length === 2) return sendUsageLimits(payload, deps);
12725
- const isYes = args[1] === "--yes";
12726
- if (subcommand === "on" && (args.length === 1 || args.length === 2 && isYes)) return onCommand(isYes, deps);
12727
- if (subcommand === "off" && args.length === 1) return offCommand(deps);
12728
- if (subcommand === "status" && args.length === 1) return statusCommand(deps);
12729
- deps.stderr(MESSAGES.usage);
12730
- return 1;
12731
- }
12732
- //#endregion
12733
- //#region src/main.ts
13190
+ // src/main.ts
12734
13191
  const { version } = createRequire(import.meta.url)("../package.json");
12735
- const USAGE = "Usage: harnessforce --version | harnessforce init [--url <base URL>] [--port <port>] [--send-content] | harnessforce import | harnessforce otel-headers | harnessforce run --issue <identifier> -- <agent> [args] | harnessforce tune [--all] [--no-send] [--show-report] [--json] | harnessforce tune record | harnessforce tune --purge | harnessforce hook session-start | harnessforce top [--once] [--json] [--ascii] [--theme <auto|light|dark|ansi>] | harnessforce usage-limits on [--yes] | harnessforce usage-limits off | harnessforce usage-limits status | harnessforce usage-limits statusline\n";
13192
+ const EXIT_CLIENT_UNSUPPORTED = 5;
12736
13193
  function parseInitArgs(args) {
12737
13194
  let url;
12738
13195
  let port;
12739
13196
  let sendContent = false;
13197
+ let noPluginAutoUpdate = false;
12740
13198
  for (let i = 0; i < args.length;) {
12741
13199
  if (args[i] === "--url") {
12742
13200
  if (url !== void 0 || args[i + 1] === void 0) return void 0;
@@ -12759,12 +13217,19 @@ function parseInitArgs(args) {
12759
13217
  i += 1;
12760
13218
  continue;
12761
13219
  }
13220
+ if (args[i] === "--no-plugin-auto-update") {
13221
+ if (noPluginAutoUpdate) return void 0;
13222
+ noPluginAutoUpdate = true;
13223
+ i += 1;
13224
+ continue;
13225
+ }
12762
13226
  return;
12763
13227
  }
12764
13228
  return {
12765
13229
  url,
12766
13230
  port,
12767
- sendContent
13231
+ sendContent,
13232
+ noPluginAutoUpdate
12768
13233
  };
12769
13234
  }
12770
13235
  function parseRunArgs(args) {
@@ -12777,68 +13242,110 @@ function parseRunArgs(args) {
12777
13242
  } : void 0;
12778
13243
  }
12779
13244
  async function run(argv, deps) {
13245
+ const help = resolveHelp(argv);
13246
+ if (help !== void 0) {
13247
+ deps.stdout(help);
13248
+ return 0;
13249
+ }
12780
13250
  const [command, ...rest] = argv;
12781
13251
  if (command === "--version" && rest.length === 0) {
12782
13252
  deps.stdout(`${version}\n`);
12783
13253
  return 0;
12784
13254
  }
12785
- if (command === "otel-headers" && rest.length === 0) return otelHeaders(deps.env, deps.managedDir, deps.keychain, deps.stdout, deps.stderr);
12786
- if (command === "import" && rest.length === 0) return importCommand({
12787
- ...deps,
12788
- git: deps.importGit,
12789
- now: () => deps.now().getTime()
12790
- });
12791
- if (command === "tune") return tuneCommand(rest, {
13255
+ const client = {
13256
+ component: "cli",
13257
+ version,
13258
+ dev: deps.devBuild
13259
+ };
13260
+ const cli = {
12792
13261
  ...deps,
12793
- git: deps.importGit,
12794
- now: () => deps.now().getTime()
12795
- });
12796
- if (command === "hook") {
12797
- if (rest.length !== 1 || rest[0] !== "session-start") {
12798
- deps.stderr(HOOK_USAGE);
13262
+ fetch: recordingFetch(deps.fetch, client, updateStatePath(deps.homeDir), deps.now)
13263
+ };
13264
+ const updateDeps = {
13265
+ ...cli,
13266
+ client
13267
+ };
13268
+ const unsupported = async () => (await readCliUpdateState(deps.homeDir, version))?.status === "unsupported";
13269
+ const guarded = async (command) => {
13270
+ if (await isSendingBlocked(updateDeps)) {
13271
+ await noticeUpdate(updateDeps);
12799
13272
  return 1;
12800
13273
  }
12801
- return sessionStart({
12802
- env: deps.env,
12803
- homeDir: deps.homeDir,
12804
- now: deps.now,
12805
- git: deps.hookGit,
12806
- fetch: deps.fetch,
12807
- keychain: deps.keychain,
12808
- readStdin: deps.readStdin,
12809
- stdout: deps.stdout,
12810
- stderr: deps.stderr
13274
+ const code = await command();
13275
+ await noticeUpdate(updateDeps);
13276
+ return code;
13277
+ };
13278
+ if (command === "otel-headers" && rest.length === 0) {
13279
+ if (await unsupported()) return EXIT_CLIENT_UNSUPPORTED;
13280
+ return otelHeaders(cli.env, cli.managedDir, cli.keychain, cli.stdout, cli.stderr, cli.endStdout);
13281
+ }
13282
+ if (command === "import" && rest.length === 0) return guarded(() => importCommand({
13283
+ ...cli,
13284
+ git: cli.importGit,
13285
+ now: () => cli.now().getTime()
13286
+ }));
13287
+ if (command === "tune") return guarded(() => tuneCommand(rest, {
13288
+ ...cli,
13289
+ git: cli.importGit,
13290
+ now: () => cli.now().getTime()
13291
+ }));
13292
+ if (command === "hook") {
13293
+ const [event] = rest;
13294
+ const hookDeps = {
13295
+ env: cli.env,
13296
+ homeDir: cli.homeDir,
13297
+ now: cli.now,
13298
+ git: cli.hookGit,
13299
+ fetch: cli.fetch,
13300
+ keychain: cli.keychain,
13301
+ readStdin: cli.readStdin,
13302
+ stdout: cli.stdout,
13303
+ stderr: cli.stderr,
13304
+ client,
13305
+ defaultUrl: cli.defaultUrl
13306
+ };
13307
+ if (rest.length === 1 && event === "session-start") return sessionStart(hookDeps);
13308
+ if (rest.length === 1 && (event === "post-tool-use" || event === "stop")) return workLocationHook(event, {
13309
+ ...hookDeps,
13310
+ hookTmpDir: cli.hookTmpDir
12811
13311
  });
13312
+ cli.stderr(HOOK_USAGE);
13313
+ return 1;
12812
13314
  }
12813
13315
  if (command === "top") return topCommand(rest, {
12814
- homeDir: deps.homeDir,
12815
- managedDir: deps.managedDir,
12816
- env: deps.env,
12817
- stdout: deps.stdout,
12818
- stderr: deps.stderr,
12819
- now: () => deps.now().getTime(),
12820
- git: deps.importGit,
12821
- top: deps.top
13316
+ homeDir: cli.homeDir,
13317
+ managedDir: cli.managedDir,
13318
+ env: cli.env,
13319
+ stdout: cli.stdout,
13320
+ stderr: cli.stderr,
13321
+ now: () => cli.now().getTime(),
13322
+ git: cli.importGit,
13323
+ top: cli.top
12822
13324
  });
12823
- if (command === "usage-limits") return usageLimitsCommand(rest, deps);
12824
13325
  const initArgs = command === "init" ? parseInitArgs(rest) : void 0;
12825
- if (initArgs) return init(initArgs, deps);
13326
+ if (initArgs) return guarded(() => init(initArgs, cli));
12826
13327
  const runArgs = command === "run" ? parseRunArgs(rest) : void 0;
12827
- if (runArgs) return runIssue(runArgs, deps);
12828
- deps.stderr(USAGE);
13328
+ if (runArgs) {
13329
+ if (await isSendingBlocked(updateDeps)) {
13330
+ await noticeUpdate(updateDeps);
13331
+ return runWithoutHarnessforce(runArgs, cli);
13332
+ }
13333
+ await noticeUpdate(updateDeps);
13334
+ return runIssue(runArgs, cli);
13335
+ }
13336
+ cli.stderr(UNKNOWN_COMMAND_HINT);
12829
13337
  return 1;
12830
13338
  }
12831
- //#endregion
12832
- //#region src/relaunch.ts
13339
+ // src/relaunch.ts
12833
13340
  const SENDING_COMMANDS = /* @__PURE__ */ new Set([
12834
13341
  "init",
12835
13342
  "run",
12836
13343
  "import",
12837
13344
  "hook"
12838
13345
  ]);
12839
- const isSending = (argv) => SENDING_COMMANDS.has(argv[0] ?? "") || argv[0] === "usage-limits" && argv[1] !== "statusline";
13346
+ const isSending = (argv) => SENDING_COMMANDS.has(argv[0] ?? "") || argv[0] === "tune" && canTuneSend(argv.slice(1));
12840
13347
  const HOOK_RESTART_FAILED = "harnessforce: session registration skipped (restart failed)";
12841
- const RESTART_FAILED = "harnessforceを起動し直せませんでした";
13348
+ const RESTART_FAILED = "Could not restart harnessforce";
12842
13349
  async function relaunchHf(argv, deps) {
12843
13350
  if (!isSending(argv)) return void 0;
12844
13351
  const outcome = await relaunchWithoutRuntimeVariables({
@@ -12857,8 +13364,7 @@ async function relaunchHf(argv, deps) {
12857
13364
  deps.stderr(`${RESTART_FAILED}\n`);
12858
13365
  return 1;
12859
13366
  }
12860
- //#endregion
12861
- //#region src/run/command.ts
13367
+ // src/run/command.ts
12862
13368
  const hasSeparator = (command, platform) => platform === "win32" ? /[\\/]/.test(command) : command.includes("/");
12863
13369
  const contextFor = ({ platform, env, cwd, fs }) => ({
12864
13370
  platform,
@@ -12874,8 +13380,7 @@ async function resolveAgentFile(command, options) {
12874
13380
  function commandLine(file, args, options) {
12875
13381
  return commandLineFor(file, args, contextFor(options));
12876
13382
  }
12877
- //#endregion
12878
- //#region src/run/process.ts
13383
+ // src/run/process.ts
12879
13384
  const IGNORED_WHILE_RUNNING = ["SIGINT", "SIGQUIT"];
12880
13385
  const FORWARDED = ["SIGTERM", "SIGHUP"];
12881
13386
  const FAILED = { kind: "failed" };
@@ -12937,8 +13442,7 @@ function spawnAndWait(command, env) {
12937
13442
  }));
12938
13443
  });
12939
13444
  }
12940
- //#endregion
12941
- //#region src/top/process-io.ts
13445
+ // src/top/process-io.ts
12942
13446
  const EXIT_SIGNALS = ["SIGTERM", "SIGHUP"];
12943
13447
  function createProcessIo(proc = process) {
12944
13448
  const { stdin, stdout } = proc;
@@ -12999,44 +13503,9 @@ function createProcessIo(proc = process) {
12999
13503
  }
13000
13504
  };
13001
13505
  }
13002
- //#endregion
13003
- //#region src/usage-limits/original.ts
13004
- function runOriginalCommand(command, input) {
13005
- return new Promise((resolve) => {
13006
- let child;
13007
- try {
13008
- child = spawn("sh", ["-c", command], { stdio: [
13009
- "pipe",
13010
- "inherit",
13011
- "inherit"
13012
- ] });
13013
- } catch {
13014
- return resolve(void 0);
13015
- }
13016
- child.once("error", () => resolve(void 0));
13017
- child.once("exit", (code, signal) => resolve(code ?? 128 + (signal ? constants.signals[signal] : 0)));
13018
- child.stdin?.on("error", () => {});
13019
- child.stdin?.end(input);
13020
- });
13021
- }
13022
- //#endregion
13023
- //#region src/usage-limits/spawn-sender.ts
13024
- function spawnUsageSender(payload) {
13025
- spawn(process.execPath, [
13026
- ...process.execArgv,
13027
- ...process.argv.slice(1, 2),
13028
- "usage-limits",
13029
- "send",
13030
- payload
13031
- ], {
13032
- detached: true,
13033
- stdio: "ignore"
13034
- }).unref();
13035
- }
13036
- //#endregion
13037
- //#region src/bin.ts
13506
+ // src/bin.ts
13038
13507
  const require = createRequire(import.meta.url);
13039
- const { url: defaultUrl } = require("./build-config.json");
13508
+ const { url: defaultUrl, isDefault } = require("./build-config.json");
13040
13509
  const CALLBACK_TIMEOUT_MS = 3e5;
13041
13510
  const argv = process.argv.slice(2);
13042
13511
  const restoredEnv = takeStashedRuntimeEnv(process.env, process.platform);
@@ -13059,6 +13528,7 @@ process.exitCode = relaunched ?? await run(argv, {
13059
13528
  }),
13060
13529
  stdout: (text) => process.stdout.write(text),
13061
13530
  stderr: (text) => process.stderr.write(text),
13531
+ endStdout: () => new Promise((resolve) => process.stdout.end(() => resolve())),
13062
13532
  fetch: (url, init) => fetch(url, init),
13063
13533
  openBrowser: (url) => openBrowser(url, {
13064
13534
  platform: process.platform,
@@ -13070,6 +13540,7 @@ process.exitCode = relaunched ?? await run(argv, {
13070
13540
  homeDir: homedir(),
13071
13541
  managedDir: managedDirFor(process.platform),
13072
13542
  defaultUrl,
13543
+ devBuild: !isDefault,
13073
13544
  callbackTimeoutMs: CALLBACK_TIMEOUT_MS,
13074
13545
  cwd: process.cwd(),
13075
13546
  git: createGitRunner({
@@ -13094,21 +13565,9 @@ process.exitCode = relaunched ?? await run(argv, {
13094
13565
  processCwd: process.cwd(),
13095
13566
  excludeTarget: true
13096
13567
  }),
13568
+ hookTmpDir: tmpdir(),
13097
13569
  sleep: (ms) => setTimeout$1(ms),
13098
13570
  top: createProcessIo(),
13099
- runOriginal: runOriginalCommand,
13100
- spawnSender: spawnUsageSender,
13101
- ask: async (question) => {
13102
- const lines = createInterface({
13103
- input: process.stdin,
13104
- output: process.stdout
13105
- });
13106
- try {
13107
- return await lines.question(question);
13108
- } finally {
13109
- lines.close();
13110
- }
13111
- },
13112
13571
  readStdin: async (maxBytes) => {
13113
13572
  const chunks = [];
13114
13573
  let size = 0;
@@ -13120,5 +13579,4 @@ process.exitCode = relaunched ?? await run(argv, {
13120
13579
  return Buffer.concat(chunks);
13121
13580
  }
13122
13581
  });
13123
- //#endregion
13124
13582
  export {};