@harnessforce/cli 0.1.1 → 0.2.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 +30 -28
  2. package/dist/bin.js +1080 -797
  3. package/package.json +8 -8
package/dist/bin.js CHANGED
@@ -1,7 +1,6 @@
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
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
@@ -10,7 +9,7 @@ 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
11
  import { ATTR, INTERVENTION_KINDS, LOOP_KINDS, PROPOSAL_KINDS, isToken, normalizeRepository } from "@harnessforce/semconv";
13
- import { createInterface as createInterface$1 } from "node:readline";
12
+ import { createInterface } from "node:readline";
14
13
  import { isDeepStrictEqual } from "node:util";
15
14
  //#region ../agent-core/src/config/canonical.ts
16
15
  const byCodeUnit$1 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
@@ -416,38 +415,40 @@ function takeStashedRuntimeEnv(env, platform) {
416
415
  //#endregion
417
416
  //#region src/shared/messages.ts
418
417
  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`を実行してください"
418
+ success: "Connected to Harnessforce. Restart Claude Code to apply the settings",
419
+ pluginAutoUpdateOn: "Automatic updates for the Harnessforce plugin are on. To turn them off, run `harnessforce init --no-plugin-auto-update` or use /plugin",
420
+ invalidUrl: "The connection URL is invalid",
421
+ keychainUnavailable: "The OS keychain is unavailable, so the ingest key cannot be saved",
422
+ 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",
423
+ waitingForSecretService: "Waiting for the Secret Service unlock prompt (up to 60 seconds)",
424
+ 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",
425
+ tooManyKeys: "There are too many ingest keys in the keychain, so `harnessforce init` cannot run",
426
+ tooManyCredentials: "There are too many ingest keys or login entries in the keychain, so `harnessforce init` cannot run",
427
+ settingsUnreadable: "Cannot read the Claude Code user settings",
428
+ network: "Communication with Harnessforce failed. Run `harnessforce init` again",
429
+ listenFailed: "Could not start waiting for the login",
430
+ timeout: "The login did not complete in time. Run `harnessforce init` again",
431
+ accessDenied: "The login was denied",
432
+ noWorkspace: "You are not a member of any Workspace. Create a Workspace or accept an invitation, then run `harnessforce init` again",
433
+ loginFailed: "The login failed. Run `harnessforce init` again",
434
+ viewer: "The view-only role cannot issue an ingest key. Contact a Workspace administrator",
435
+ limitOwnerAdmin: "The number of active ingest keys has reached the limit. Revoke unused keys on the ingest keys page",
436
+ limitMember: "The ingest key limit has been reached. Contact a Workspace administrator",
437
+ gateUnavailable: "Could not apply the change on the receiving side. Nothing was changed. Try again",
438
+ ingestUnavailable: "This environment does not receive telemetry, so an ingest key cannot be issued",
439
+ saveFailed: "Could not save the ingest key. Run `harnessforce init` again",
440
+ codexSaveFailed: "Could not write the Codex configuration. Run `harnessforce init` again",
441
+ contentEnabled: "Sending the prompts you type is now enabled. Values that match known patterns for secrets and personal information are masked, but not everything can be detected. Within the workspace, only you can read your stored content, and it is deleted after the content retention period (at most 30 days)",
442
+ contentNotOptedIn: "This workspace has not opted in to prompts you typed, so sending content was not enabled. Opt in under Data retention in the workspace settings, then run `harnessforce init --send-content` again"
443
443
  };
444
444
  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`でログインしてください"
445
+ `Wrote the Codex configuration (${configPath}). Restart Codex to apply it`,
446
+ "Codex hooks do not run until you trust them in Codex's /hooks. Trust the harnessforce SessionStart hook in /hooks",
447
+ "To use the Harnessforce MCP server, log in with `codex mcp login harnessforce`"
448
448
  ].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;
449
+ const nonDefaultConnectionMessage = (url) => `Connecting to ${url} (not the default production endpoint)`;
450
+ const listenFailedMessage = (port) => `${INIT_MESSAGES.listenFailed}. Stop the process using port ${port}, or specify another port with \`harnessforce init --port\``;
451
+ const keychainUnavailableMessage = (keychain, message = INIT_MESSAGES.keychainUnavailable) => keychain.unavailableHint ? `${message}. ${keychain.unavailableHint}` : message;
451
452
  //#endregion
452
453
  //#region src/credentials/keychain.ts
453
454
  async function openSingleStore(store) {
@@ -481,6 +482,9 @@ const SECRET_SERVICE_TIMEOUT_MS = 6e4;
481
482
  const WAITING_NOTICE_DELAY_MS = 2e3;
482
483
  const PROBE_ACCOUNT$1 = "availability-probe";
483
484
  const PROBE_VALUE = "harnessforce";
485
+ const MIGRATION_PROBE_TIMEOUT_MS = 200;
486
+ const MIGRATION_BUDGET_MS = 1e3;
487
+ const CLEANUP_TIMEOUT_MS = 200;
484
488
  const succeeds = (operation) => operation().then(() => true, () => false);
485
489
  const workspaceOf = (account) => account.slice(0, account.lastIndexOf(":"));
486
490
  function createLinuxKeychain(options) {
@@ -527,6 +531,11 @@ function build(options, timeoutMs) {
527
531
  const { location } = await locate(workspaceOf(account));
528
532
  await storeAt(location).delete(account, timeoutMs);
529
533
  },
534
+ async migrateFromKernel(workspaceId) {
535
+ const { location } = await locate(workspaceId);
536
+ if (location !== "kernel") return;
537
+ await migrate(options, workspaceId).catch(() => {});
538
+ },
530
539
  withTimeout: (ms) => build(options, ms),
531
540
  openForInit: (init) => openForInit(options, timeoutMs, init)
532
541
  };
@@ -569,6 +578,59 @@ async function openForInit({ kernel, secretService }, timeoutMs, { previousWorks
569
578
  deleteElsewhere: async () => {}
570
579
  };
571
580
  }
581
+ var MigrationExpired = class extends Error {};
582
+ const MIGRATION_ORDER = (workspaceId) => [
583
+ ingestOriginAccount(workspaceId),
584
+ urlOriginAccount(workspaceId),
585
+ apiTokenAccount(workspaceId),
586
+ ingestKeyAccount(workspaceId)
587
+ ];
588
+ async function migrate({ kernel, secretService }, workspaceId) {
589
+ let isExpired = false;
590
+ const deadline = Date.now() + MIGRATION_BUDGET_MS;
591
+ const remaining = () => {
592
+ if (isExpired) throw new MigrationExpired();
593
+ return Math.max(deadline - Date.now(), 1);
594
+ };
595
+ const written = [];
596
+ let isVerified = false;
597
+ const move = async () => {
598
+ await secretService.get(PROBE_ACCOUNT$1, MIGRATION_PROBE_TIMEOUT_MS);
599
+ const items = [];
600
+ for (const account of MIGRATION_ORDER(workspaceId)) {
601
+ const secret = await kernel.get(account, remaining());
602
+ if (secret !== void 0) items.push({
603
+ account,
604
+ secret
605
+ });
606
+ }
607
+ for (const account of workspaceAccounts(workspaceId)) await secretService.delete(account, remaining());
608
+ for (const { account, secret } of items) {
609
+ written.push(account);
610
+ await secretService.set(account, secret, remaining());
611
+ }
612
+ for (const { account, secret } of items) if (await secretService.get(account, remaining()) !== secret) throw new Error("the Secret Service returned a different value");
613
+ remaining();
614
+ isVerified = true;
615
+ for (const { account } of [...items].reverse()) await kernel.delete(account, remaining());
616
+ };
617
+ let timer;
618
+ const expiry = new Promise((_, reject) => {
619
+ timer = setTimeout(() => {
620
+ isExpired = true;
621
+ reject(new MigrationExpired());
622
+ }, MIGRATION_BUDGET_MS);
623
+ });
624
+ try {
625
+ await Promise.race([move(), expiry]);
626
+ } catch (error) {
627
+ if (isVerified) throw error;
628
+ await Promise.allSettled(written.map((account) => secretService.delete(account, CLEANUP_TIMEOUT_MS)));
629
+ throw error;
630
+ } finally {
631
+ clearTimeout(timer);
632
+ }
633
+ }
572
634
  //#endregion
573
635
  //#region src/credentials/os-keychain.ts
574
636
  const PROBE_ACCOUNT = "availability-probe";
@@ -780,9 +842,9 @@ async function openBrowser(url, { platform, env, cwd, restoredEnv, spawn: spawnI
780
842
  //#region src/init/loopback.ts
781
843
  const HOST = "127.0.0.1";
782
844
  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("ログインに失敗しました。ターミナルを確認してください");
845
+ const page = (message) => `<!doctype html><html lang="en"><meta charset="utf-8"><title>Harnessforce</title><p>${message}</p></html>`;
846
+ const SUCCESS_PAGE = page("Return to the terminal");
847
+ const FAILURE_PAGE = page("The login failed. Check the terminal");
786
848
  async function startLoopback(options) {
787
849
  let settle = () => {};
788
850
  const callback = new Promise((resolve) => {
@@ -830,11 +892,6 @@ async function startLoopback(options) {
830
892
  };
831
893
  }
832
894
  //#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
895
  //#region ../agent-core/src/url.ts
839
896
  const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
840
897
  "localhost",
@@ -858,8 +915,168 @@ function underBase(base, path) {
858
915
  }
859
916
  const withoutExtras = (url) => `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
860
917
  //#endregion
918
+ //#region ../agent-core/src/client-update.ts
919
+ const CLIENT_UPDATE_HEADER = "harnessforce-client-update";
920
+ const UPDATE_COMMANDS = {
921
+ cli: "npm install -g @harnessforce/cli@latest",
922
+ "claude-code-plugin": "claude plugin update harnessforce@harnessforce-agent"
923
+ };
924
+ const DISPLAY_NAMES = {
925
+ cli: "CLI",
926
+ "claude-code-plugin": "plugin"
927
+ };
928
+ const DAY_MS$2 = 864e5;
929
+ const RECHECK_TIMEOUT_MS = 2e3;
930
+ const userAgentOf = (client) => `harnessforce-${client.component}${client.dev ? "-dev" : ""}/${client.version}`;
931
+ const STATUSES = /* @__PURE__ */ new Set([
932
+ "current",
933
+ "outdated",
934
+ "deprecated",
935
+ "unsupported"
936
+ ]);
937
+ const REASONS = /* @__PURE__ */ new Set(["security", "compatibility"]);
938
+ function parseClientUpdateHeader(value) {
939
+ if (!value) return void 0;
940
+ const members = /* @__PURE__ */ new Map();
941
+ for (const member of value.split(",")) {
942
+ const [key, raw] = member.trim().split("=", 2);
943
+ if (key && raw !== void 0) members.set(key, raw);
944
+ }
945
+ const status = members.get("status");
946
+ if (!status || !STATUSES.has(status)) return void 0;
947
+ const update = { status };
948
+ const latest = /^"([^"\\]*)"$/.exec(members.get("latest") ?? "")?.[1];
949
+ if (latest) update.latest = latest;
950
+ const reason = members.get("reason");
951
+ if (reason && REASONS.has(reason)) update.reason = reason;
952
+ const seconds = /^@(\d+)$/.exec(members.get("deadline") ?? "")?.[1];
953
+ if (seconds) update.deadline = (/* @__PURE__ */ new Date(Number(seconds) * 1e3)).toISOString();
954
+ return isComplete(update) ? update : void 0;
955
+ }
956
+ function isComplete(update) {
957
+ switch (update.status) {
958
+ case "outdated": return Boolean(update.latest);
959
+ case "deprecated": return Boolean(update.reason) && !Number.isNaN(Date.parse(update.deadline ?? ""));
960
+ case "unsupported": return Boolean(update.reason);
961
+ default: return true;
962
+ }
963
+ }
964
+ function stateFrom(value, version) {
965
+ if (!isObject(value) || value.version !== version) return void 0;
966
+ if (typeof value.receivedAt !== "string") return void 0;
967
+ if (typeof value.status !== "string" || !STATUSES.has(value.status)) return void 0;
968
+ const state = value;
969
+ return isComplete(state) ? state : void 0;
970
+ }
971
+ async function readUpdateState(path, version) {
972
+ const saved = await readSavedUpdateState(path);
973
+ return saved?.version === version ? saved : void 0;
974
+ }
975
+ async function readSavedUpdateState(path) {
976
+ try {
977
+ const value = JSON.parse(await readFile(path, "utf8"));
978
+ return isObject(value) && typeof value.version === "string" ? stateFrom(value, value.version) : void 0;
979
+ } catch {
980
+ return;
981
+ }
982
+ }
983
+ async function writeUpdateState(path, state) {
984
+ await mkdir(dirname(path), {
985
+ recursive: true,
986
+ mode: 448
987
+ });
988
+ const temporary = `${path}.${randomUUID()}.tmp`;
989
+ await writeFile(temporary, `${JSON.stringify(state)}\n`, { mode: 384 });
990
+ await rename(temporary, path);
991
+ }
992
+ /**
993
+ * Harnessforceへの要求に`User-Agent`を付け、応答の`Harnessforce-Client-Update`を状態のfileへ保存するfetch。
994
+ * 保存に失敗しても要求の結果は変えない。statePathが無い(pluginのdata directoryが無い)場合は版の申告だけを行う。
995
+ */
996
+ function recordingFetch(fetchImpl, client, statePath, now) {
997
+ return async (url, init) => {
998
+ const headers = {
999
+ ...Object.fromEntries(new Headers(init.headers)),
1000
+ "user-agent": userAgentOf(client)
1001
+ };
1002
+ const response = await fetchImpl(url, {
1003
+ ...init,
1004
+ headers
1005
+ });
1006
+ const update = parseClientUpdateHeader(response.headers.get(CLIENT_UPDATE_HEADER));
1007
+ if (update && statePath) {
1008
+ const previous = await readUpdateState(statePath, client.version);
1009
+ await writeUpdateState(statePath, {
1010
+ ...update,
1011
+ version: client.version,
1012
+ receivedAt: now().toISOString(),
1013
+ ...previous?.outdatedShownAt ? { outdatedShownAt: previous.outdatedShownAt } : {}
1014
+ }).catch(() => {});
1015
+ }
1016
+ return response;
1017
+ };
1018
+ }
1019
+ const utcDate = (instant) => new Intl.DateTimeFormat("en-CA", {
1020
+ timeZone: "UTC",
1021
+ year: "numeric",
1022
+ month: "2-digit",
1023
+ day: "2-digit"
1024
+ }).format(new Date(instant));
1025
+ 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]}\``;
1026
+ function updateLine(component, update) {
1027
+ const name = DISPLAY_NAMES[component];
1028
+ const command = UPDATE_COMMANDS[component];
1029
+ switch (update.status) {
1030
+ case "unsupported": return unsupportedLine(component);
1031
+ case "deprecated": {
1032
+ const stops = `stops sending on ${utcDate(update.deadline ?? "")} (00:00 UTC)`;
1033
+ 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}\``;
1034
+ }
1035
+ case "outdated": return `Harnessforce ${name} ${update.latest} is available. Update it: \`${command}\``;
1036
+ default: return;
1037
+ }
1038
+ }
1039
+ function shouldShowOutdated(state, now, env) {
1040
+ if (state.status !== "outdated" || env.NO_UPDATE_NOTIFIER || env.CI) return false;
1041
+ const shownAt = state.outdatedShownAt ? Date.parse(state.outdatedShownAt) : NaN;
1042
+ return Number.isNaN(shownAt) || now.getTime() - shownAt >= DAY_MS$2;
1043
+ }
1044
+ const markOutdatedShown = (path, state, now) => writeUpdateState(path, {
1045
+ ...state,
1046
+ outdatedShownAt: now.toISOString()
1047
+ });
1048
+ /**
1049
+ * 表示する1行。outdatedは24時間に1回までとし、表示する場合はその時刻を書く。isSuppressedは、pluginを
1050
+ * Claude Codeが自動更新する間など、呼び出し側がoutdatedを示さない条件。
1051
+ */
1052
+ async function noticeLine(component, statePath, state, options) {
1053
+ if (state.status === "outdated") {
1054
+ if (!shouldShowOutdated(state, options.now, options.env) || await options.isSuppressed?.()) return void 0;
1055
+ await markOutdatedShown(statePath, state, options.now).catch(() => {});
1056
+ }
1057
+ return updateLine(component, state);
1058
+ }
1059
+ /**
1060
+ * unsupportedを受けてから24時間以上経っていれば、資格情報を送らずに`GET /v1/client-status`で確かめ直す
1061
+ * (client-updates.md「送信の停止」)。失敗した場合は状態を変えない。
1062
+ */
1063
+ async function recheckClientStatus(options) {
1064
+ const { state } = options;
1065
+ if (state?.status !== "unsupported" || options.now().getTime() - Date.parse(state.receivedAt) < DAY_MS$2) return state;
1066
+ try {
1067
+ await (await options.fetch(underBase(options.ingestBase, "v1/client-status"), {
1068
+ method: "GET",
1069
+ redirect: "error",
1070
+ signal: AbortSignal.timeout(RECHECK_TIMEOUT_MS)
1071
+ })).body?.cancel();
1072
+ } catch {
1073
+ return state;
1074
+ }
1075
+ return await readUpdateState(options.statePath, options.version) ?? state;
1076
+ }
1077
+ //#endregion
861
1078
  //#region ../agent-core/src/ingest.ts
862
- const SEND_TIMEOUT_MS$1 = 2e3;
1079
+ const SEND_TIMEOUT_MS = 2e3;
863
1080
  function ingestBaseFrom(endpoint) {
864
1081
  const base = parseAllowedUrl(endpoint);
865
1082
  if (!base) return void 0;
@@ -867,8 +1084,8 @@ function ingestBaseFrom(endpoint) {
867
1084
  ingestBase.pathname = base.pathname.replace(/\/+$/, "");
868
1085
  return ingestBase;
869
1086
  }
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) {
1087
+ const postItem = (destination, path, item, fetchImpl, timeoutMs = SEND_TIMEOUT_MS) => postItems(destination, path, [item], fetchImpl, timeoutMs);
1088
+ async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_TIMEOUT_MS) {
872
1089
  try {
873
1090
  const response = await fetchImpl(underBase(destination.ingestBase, path), {
874
1091
  method: "POST",
@@ -894,68 +1111,37 @@ async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_T
894
1111
  }
895
1112
  }
896
1113
  //#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);
1114
+ //#region ../agent-core/src/config/hash-input.ts
1115
+ function withoutSecrets(entry) {
1116
+ const { env, headers, url, args: _args, ...rest } = entry;
1117
+ const result = rest;
1118
+ if (isObject(env)) result.env = Object.keys(env).sort(byCodeUnit$1);
1119
+ if (isObject(headers)) result.headers = Object.keys(headers).sort(byCodeUnit$1);
1120
+ const strippedUrl = typeof url === "string" ? withoutCredentials(url) : void 0;
1121
+ if (strippedUrl !== void 0) result.url = strippedUrl;
1122
+ return result;
955
1123
  }
956
- async function resolveProjectRoot(cwd, git) {
957
- const topLevel = await git(cwd, ["rev-parse", "--show-toplevel"]);
958
- return topLevel && isAbsolute(topLevel) ? topLevel : cwd;
1124
+ function withoutCredentials(url) {
1125
+ let parsed;
1126
+ try {
1127
+ parsed = new URL(url);
1128
+ } catch {
1129
+ return;
1130
+ }
1131
+ parsed.username = "";
1132
+ parsed.password = "";
1133
+ parsed.search = "";
1134
+ parsed.hash = "";
1135
+ return parsed.href;
1136
+ }
1137
+ const reduceHookGroup = (group) => isObject(group) && Array.isArray(group.hooks) ? {
1138
+ ...group,
1139
+ hooks: group.hooks.map((handler) => isObject(handler) ? withoutSecrets(handler) : handler)
1140
+ } : group;
1141
+ function hashInput(kind, value) {
1142
+ if (kind === "mcp_server" && isObject(value)) return withoutSecrets(value);
1143
+ if (kind === "hook" && Array.isArray(value)) return value.map(reduceHookGroup);
1144
+ return value;
959
1145
  }
960
1146
  //#endregion
961
1147
  //#region ../agent-core/src/config/scope.ts
@@ -985,7 +1171,7 @@ var Sink = class {
985
1171
  });
986
1172
  return {
987
1173
  guard: this.guard,
988
- addValue: (kind, id, value) => add(kind, id, hashValue(value)),
1174
+ addValue: (kind, id, value) => add(kind, id, hashValue(hashInput(kind, value))),
989
1175
  addFile: async (kind, id, path) => {
990
1176
  const content = await readFileIfExists(path, this.guard);
991
1177
  if (content) add(kind, id, hashFileContent(content));
@@ -1015,6 +1201,9 @@ function collectMcpServers$1(s, servers) {
1015
1201
  for (const [name, value] of Object.entries(servers)) if (isObject(value)) s.addValue("mcp_server", name, value);
1016
1202
  }
1017
1203
  //#endregion
1204
+ //#region ../agent-core/src/plugin-auto-update.ts
1205
+ const MARKETPLACE_NAME = "harnessforce-agent";
1206
+ //#endregion
1018
1207
  //#region src/shared/settings.ts
1019
1208
  const OTEL_HEADERS_HELPER = "harnessforce otel-headers";
1020
1209
  const PLUGIN_ID$1 = "harnessforce@harnessforce-agent";
@@ -1043,6 +1232,37 @@ async function readUserSettings(path) {
1043
1232
  return { kind: "invalid" };
1044
1233
  }
1045
1234
  }
1235
+ const MARKETPLACE_KEYS = ["extraKnownMarketplaces", "additionalMarketplaces"];
1236
+ const MARKETPLACE_SOURCE = {
1237
+ source: "github",
1238
+ repo: "lambda-script/harnessforce-agent"
1239
+ };
1240
+ const isMarketplacesValid = (value) => value === void 0 || isObject(value) && (value["harnessforce-agent"] === void 0 || isObject(value["harnessforce-agent"]));
1241
+ const hasWritableMarketplaces = (settings) => MARKETPLACE_KEYS.every((key) => isMarketplacesValid(settings[key]));
1242
+ /**
1243
+ * marketplaceの登録の鍵の`harnessforce-agent`に、sourceと自動更新を書く。sourceと、利用者が切り替えた真偽値は残し、
1244
+ * turnOffのときだけ偽にする。自動更新が有効になったかも返す。
1245
+ */
1246
+ function withPluginAutoUpdate(settings, turnOff) {
1247
+ const key = settings.additionalMarketplaces !== void 0 && settings.extraKnownMarketplaces === void 0 ? "additionalMarketplaces" : "extraKnownMarketplaces";
1248
+ const marketplaces = settings[key] ?? {};
1249
+ const entry = marketplaces["harnessforce-agent"] ?? {};
1250
+ const autoUpdate = turnOff ? false : typeof entry.autoUpdate === "boolean" ? entry.autoUpdate : true;
1251
+ return {
1252
+ settings: {
1253
+ ...settings,
1254
+ [key]: {
1255
+ ...marketplaces,
1256
+ [MARKETPLACE_NAME]: {
1257
+ ...entry,
1258
+ source: entry.source ?? MARKETPLACE_SOURCE,
1259
+ autoUpdate
1260
+ }
1261
+ }
1262
+ },
1263
+ isOn: autoUpdate
1264
+ };
1265
+ }
1046
1266
  function mergeUserSettings(settings, env) {
1047
1267
  return {
1048
1268
  ...settings,
@@ -1095,6 +1315,399 @@ async function resolveCliDestinations(env, settingsPath, defaultUrl) {
1095
1315
  };
1096
1316
  }
1097
1317
  //#endregion
1318
+ //#region src/client-update.ts
1319
+ const updateStatePath = (homeDir) => join(homeDir, ".harnessforce", "update-status.json");
1320
+ const readCliUpdateState = (homeDir, version) => readUpdateState(updateStatePath(homeDir), version);
1321
+ /**
1322
+ * 送信を伴うcommandの前に呼ぶ。unsupportedを受けてから24時間以上経っていれば資格情報を送らずに確かめ直し、
1323
+ * それでもunsupportedなら真を返す(client-updates.md「送信の停止」)。
1324
+ */
1325
+ async function isSendingBlocked(deps) {
1326
+ const statePath = updateStatePath(deps.homeDir);
1327
+ const state = await readUpdateState(statePath, deps.client.version);
1328
+ if (state?.status !== "unsupported") return false;
1329
+ const { ingestEndpoint } = await resolveCliDestinations(deps.env, userSettingsPath(deps.env, deps.homeDir), deps.defaultUrl);
1330
+ const ingestBase = ingestBaseFrom(ingestEndpoint);
1331
+ return (ingestBase ? await recheckClientStatus({
1332
+ state,
1333
+ ingestBase,
1334
+ fetch: deps.fetch,
1335
+ statePath,
1336
+ version: deps.client.version,
1337
+ now: deps.now
1338
+ }) : state)?.status === "unsupported";
1339
+ }
1340
+ /** commandの出力の後に、更新の状態の1行をstderrへ書く。outdatedは24時間に1回までとする。 */
1341
+ async function noticeUpdate(deps) {
1342
+ const statePath = updateStatePath(deps.homeDir);
1343
+ const state = await readUpdateState(statePath, deps.client.version);
1344
+ if (!state) return;
1345
+ const line = await noticeLine("cli", statePath, state, {
1346
+ now: deps.now(),
1347
+ env: deps.env
1348
+ });
1349
+ if (line) deps.stderr(`${line}\n`);
1350
+ }
1351
+ //#endregion
1352
+ //#region src/help.ts
1353
+ const SENDS_NOTHING_LOCAL = "Nothing. It reads local files only and makes no network request.";
1354
+ const COMMANDS$1 = {
1355
+ init: {
1356
+ summary: "Connect this machine to a Harnessforce Workspace",
1357
+ text: `Usage: harnessforce init [--port <port>] [--send-content] [--no-plugin-auto-update]
1358
+
1359
+ When to use:
1360
+ Once per machine, before anything else, and again whenever the login expires.
1361
+
1362
+ What it does:
1363
+ Opens your browser to log in to a Workspace, then stores a user ingest key and
1364
+ an API token in the OS keychain. It configures Claude Code (user settings) and
1365
+ Codex (config.toml) to export telemetry to the Workspace.
1366
+
1367
+ Options:
1368
+ --port <port> Loopback port for the login callback, 1-65535 (default 8080).
1369
+ --send-content Also send the text of the prompts you type. Takes effect only
1370
+ when the Workspace has opted in. Off by default.
1371
+ --no-plugin-auto-update
1372
+ Turn off automatic updates of the Harnessforce plugin in
1373
+ Claude Code. On by default; a value you set is kept.
1374
+
1375
+ Sends:
1376
+ The login exchange with the Workspace. After init, the agents you run send
1377
+ telemetry (token counts, tool names, timings) with that key.
1378
+
1379
+ Does not send:
1380
+ Prompt text (unless --send-content and the Workspace opt-in), model responses,
1381
+ tool input or output, code diffs, and secrets. The key is never written to a
1382
+ plain file.
1383
+
1384
+ Exit codes:
1385
+ 0 Connected and configured.
1386
+ 1 Failed (login cancelled or timed out, keychain unavailable, or a write
1387
+ failed). The reason is printed to stderr.
1388
+
1389
+ Examples:
1390
+ harnessforce init
1391
+ harnessforce init --send-content
1392
+ harnessforce init --port 8081
1393
+ `
1394
+ },
1395
+ import: {
1396
+ summary: "Send past local sessions to the Workspace",
1397
+ text: `Usage: harnessforce import
1398
+
1399
+ When to use:
1400
+ Right after init, to backfill sessions that ran before Harnessforce was
1401
+ connected. It is safe to run again; sessions already sent are skipped.
1402
+
1403
+ What it does:
1404
+ Reads Claude Code and Codex session records on this machine, keeps the
1405
+ sessions of repositories connected to the Workspace and inside its import
1406
+ window, and sends a summary of each. When it imports nothing, it prints why
1407
+ and what to do next.
1408
+
1409
+ Sends:
1410
+ Per session: agent, session ID, repository, branch, commit, start and end
1411
+ time, model, input and output token counts, and tool call counts.
1412
+
1413
+ Does not send:
1414
+ Prompt text, model responses, tool input or output, and code diffs. Sessions
1415
+ of repositories that are not connected are not sent.
1416
+
1417
+ Exit codes:
1418
+ 0 Finished (including "nothing to import").
1419
+ 1 Not logged in, keychain unavailable, a limit was reached, or sending failed.
1420
+
1421
+ Examples:
1422
+ harnessforce import
1423
+ `
1424
+ },
1425
+ "otel-headers": {
1426
+ summary: "Print the telemetry auth header (used by Claude Code)",
1427
+ text: `Usage: harnessforce otel-headers
1428
+
1429
+ When to use:
1430
+ You normally never run it. Claude Code calls it as its otelHeadersHelper.
1431
+
1432
+ What it does:
1433
+ Reads the ingest key from the keychain and prints {"Authorization": "Bearer <key>"}
1434
+ as JSON on stdout.
1435
+
1436
+ Sends:
1437
+ ${SENDS_NOTHING_LOCAL}
1438
+
1439
+ Does not send:
1440
+ Anything. Do not paste its output into chats or logs: it contains the key.
1441
+
1442
+ Exit codes:
1443
+ 0 Header printed.
1444
+ 1 No usable key (not logged in, keychain unavailable, or the destination was
1445
+ not verified). Nothing is printed to stdout.
1446
+
1447
+ Examples:
1448
+ harnessforce otel-headers
1449
+ `
1450
+ },
1451
+ run: {
1452
+ summary: "Start an agent tied to an Issue",
1453
+ text: `Usage: harnessforce run --issue <identifier> -- <agent> [args]
1454
+
1455
+ When to use:
1456
+ When the branch name does not contain the Issue identifier and you want the
1457
+ work tied to the Issue anyway.
1458
+
1459
+ What it does:
1460
+ Starts <agent> (for example claude or codex) with your arguments, with the
1461
+ Issue identifier added to the telemetry it exports. Everything after "--" is
1462
+ passed to the agent unchanged, including "--help". The agent's exit code is
1463
+ returned.
1464
+
1465
+ Options:
1466
+ --issue <identifier> The Issue identifier, for example ABC-123.
1467
+
1468
+ Sends:
1469
+ The same telemetry as an agent started directly, plus the Issue identifier.
1470
+
1471
+ Does not send:
1472
+ Anything beyond what init configured. Prompt text is sent only if you chose
1473
+ --send-content at init.
1474
+
1475
+ Exit codes:
1476
+ The agent's own exit code when it ran.
1477
+ 1 Could not start: not logged in, bad arguments, or the agent was not found.
1478
+
1479
+ Examples:
1480
+ harnessforce run --issue ABC-123 -- claude
1481
+ harnessforce run --issue ABC-123 -- codex --model gpt-5
1482
+ `
1483
+ },
1484
+ tune: {
1485
+ summary: "Analyze your past sessions and report counts",
1486
+ text: `Usage: harnessforce tune [--all] [--no-send] [--show-report] [--json]
1487
+ harnessforce tune record
1488
+ harnessforce tune --purge
1489
+
1490
+ When to use:
1491
+ Periodically (or through /harnessforce:tune) to see where human intervention,
1492
+ repeated work, and MCP or tool usage could improve. It needs at least 10
1493
+ sessions.
1494
+
1495
+ What it does:
1496
+ Imports sessions that were not sent yet, analyzes the local session records,
1497
+ and prints the result. Files it writes live under ~/.harnessforce/tune/.
1498
+
1499
+ Options:
1500
+ --all Analyze sessions of repositories not connected too (they are
1501
+ never sent).
1502
+ --no-send Analyze only; send nothing.
1503
+ --show-report Print the exact values that would be sent.
1504
+ --json Print the result as one JSON object on stdout. Messages go to
1505
+ stderr.
1506
+ record Read one proposal as JSON from stdin and store it locally.
1507
+ --purge Delete the analysis, proposals, and unsent results.
1508
+
1509
+ Sends:
1510
+ Counts computed by public rules (no text), for connected repositories only.
1511
+ It also runs the same session import as "harnessforce import".
1512
+
1513
+ Does not send:
1514
+ Prompt text, model responses, tool input or output, and code diffs. Usage
1515
+ statistics and proposals stay on this machine.
1516
+
1517
+ Exit codes:
1518
+ 0 Done (including "not enough data" and "nothing sent").
1519
+ 1 Not logged in, keychain unavailable, another tune is running, or a local
1520
+ write failed.
1521
+ 2 Bad option, or invalid input to "record".
1522
+ 3 Analysis shown, but sending is not possible until you run "harnessforce init".
1523
+
1524
+ Examples:
1525
+ harnessforce tune
1526
+ harnessforce tune --no-send --json
1527
+ harnessforce tune --purge
1528
+ `
1529
+ },
1530
+ top: {
1531
+ summary: "Show running and recent sessions on this machine",
1532
+ text: `Usage: harnessforce top [--once] [--json] [--ascii] [--theme <auto|light|dark|ansi>]
1533
+
1534
+ When to use:
1535
+ To watch what your agents are doing right now. In a terminal it is a live
1536
+ view; with --once or --json it prints a snapshot and exits.
1537
+
1538
+ What it does:
1539
+ Reads Claude Code and Codex session records on this machine and shows each
1540
+ session's state, repository, branch, tokens, and tool calls.
1541
+
1542
+ Options:
1543
+ --once Print one snapshot and exit.
1544
+ --json Print one snapshot as JSON and exit.
1545
+ --ascii Use plain ASCII instead of box-drawing characters.
1546
+ --theme Color theme: auto (default), light, dark, or ansi.
1547
+
1548
+ Sends:
1549
+ ${SENDS_NOTHING_LOCAL}
1550
+
1551
+ Does not send:
1552
+ Anything. It never changes any file.
1553
+
1554
+ Exit codes:
1555
+ 0 Done (including quitting the live view).
1556
+ 1 Bad arguments.
1557
+
1558
+ Examples:
1559
+ harnessforce top
1560
+ harnessforce top --once --json
1561
+ `
1562
+ },
1563
+ hook: {
1564
+ summary: "Session-start hook (used by Codex)",
1565
+ text: `Usage: harnessforce hook session-start
1566
+
1567
+ When to use:
1568
+ You normally never run it. Codex runs it when a session starts.
1569
+
1570
+ What it does:
1571
+ Reads the hook input from stdin, registers the session with the Workspace, and
1572
+ prints context for the agent (the session ID, plus a one-line warning if
1573
+ registration failed).
1574
+
1575
+ Sends:
1576
+ Session registration: agent, session ID, repository, branch, commit, and
1577
+ start time.
1578
+
1579
+ Does not send:
1580
+ Prompt text, model responses, tool input or output, and code diffs.
1581
+
1582
+ Exit codes:
1583
+ 0 Always, even on failure: a hook must never stop your session. A failure is
1584
+ shown as a one-line warning.
1585
+ 1 Wrong arguments.
1586
+
1587
+ Examples:
1588
+ harnessforce hook session-start < hook-input.json
1589
+ `
1590
+ }
1591
+ };
1592
+ const ORDER = [
1593
+ "init",
1594
+ "import",
1595
+ "run",
1596
+ "tune",
1597
+ "top",
1598
+ "otel-headers",
1599
+ "hook"
1600
+ ];
1601
+ const NAME_WIDTH = Math.max(...ORDER.map((name) => name.length)) + 2;
1602
+ const OVERVIEW = `Usage: harnessforce <command> [options]
1603
+
1604
+ Connect your AI coding agents to a Harnessforce Workspace.
1605
+
1606
+ Commands:
1607
+ ${ORDER.map((name) => ` ${name.padEnd(NAME_WIDTH)}${COMMANDS$1[name].summary}`).join("\n")}
1608
+
1609
+ Run "harnessforce <command> --help" for what a command does, what it sends and
1610
+ does not send, and its exit codes.
1611
+
1612
+ Options:
1613
+ -h, --help Show this help.
1614
+ --version Show the version.
1615
+ `;
1616
+ const UNKNOWN_COMMAND_HINT = `Usage: harnessforce <command> [options]
1617
+ Run "harnessforce --help" to list the commands.
1618
+ `;
1619
+ const isCommand = (name) => name !== void 0 && Object.hasOwn(COMMANDS$1, name);
1620
+ const isHelpFlag = (arg) => arg === "--help" || arg === "-h";
1621
+ function resolveHelp(argv) {
1622
+ const [command, ...rest] = argv;
1623
+ if (command === void 0) return void 0;
1624
+ if (isHelpFlag(command) && rest.length === 0) return OVERVIEW;
1625
+ if (command === "help") {
1626
+ if (rest.length === 0) return OVERVIEW;
1627
+ if (rest.length === 1 && isCommand(rest[0])) return COMMANDS$1[rest[0]].text;
1628
+ return;
1629
+ }
1630
+ if (!isCommand(command)) return void 0;
1631
+ const separator = rest.indexOf("--");
1632
+ return (separator === -1 ? rest : rest.slice(0, separator)).some(isHelpFlag) ? COMMANDS$1[command].text : void 0;
1633
+ }
1634
+ //#endregion
1635
+ //#region ../agent-core/src/hook-warning.ts
1636
+ const RESTART_FAILED_WARNING = "Could not restart the Harnessforce hook";
1637
+ const INVALID_ENDPOINT_WARNING = "The Harnessforce destination is invalid. Run `harnessforce init` again";
1638
+ const notConfiguredWarning = (agent) => agent === "claude_code" ? "Harnessforce sending is not configured. Run `/harnessforce:setup`" : "Harnessforce sending is not configured. Run `harnessforce init`";
1639
+ const stopped = (agent) => agent === "claude_code" ? "telemetry is not being sent" : "sessions are not being registered";
1640
+ const keyMissingWarning = (agent) => `Harnessforce cannot find your ingest key, so ${stopped(agent)}. Run \`harnessforce init\``;
1641
+ 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\``;
1642
+ const DESTINATION_MISMATCH_WARNING = {
1643
+ 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",
1644
+ codex: "Sessions are not being registered because the Harnessforce destination differs from the one saved by `harnessforce init`. Run `harnessforce init` again"
1645
+ };
1646
+ //#endregion
1647
+ //#region ../agent-core/src/issue.ts
1648
+ const isIssueIdentifier = (value) => isToken(value, 300);
1649
+ //#endregion
1650
+ //#region ../agent-core/src/using-harnessforce.ts
1651
+ 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- 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 opts in with `--send-content` and the Workspace has opted in. They are not collected by default.\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- 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();
1652
+ //#endregion
1653
+ //#region ../agent-core/src/session-context.ts
1654
+ const sessionContextLine = (sessionId) => `harnessforce session_id: ${sessionId}`;
1655
+ const sessionContext = (sessionId) => ({ hookSpecificOutput: {
1656
+ hookEventName: "SessionStart",
1657
+ additionalContext: `${sessionContextLine(sessionId)}\n\n${usingHarnessforceBody}`
1658
+ } });
1659
+ //#endregion
1660
+ //#region ../agent-core/src/vcs.ts
1661
+ async function selectRemote(cwd, git) {
1662
+ if (await git(cwd, ["rev-parse", "--is-inside-work-tree"]) !== "true") return void 0;
1663
+ const remotes = (await git(cwd, ["remote"]))?.split("\n") ?? [];
1664
+ return remotes.includes("origin") ? "origin" : remotes[0];
1665
+ }
1666
+ async function resolveVcs(cwd, git) {
1667
+ const remote = await selectRemote(cwd, git);
1668
+ if (!remote) return void 0;
1669
+ const [url, branch, commit] = await Promise.all([
1670
+ git(cwd, [
1671
+ "remote",
1672
+ "get-url",
1673
+ remote
1674
+ ]),
1675
+ git(cwd, [
1676
+ "symbolic-ref",
1677
+ "--quiet",
1678
+ "--short",
1679
+ "HEAD"
1680
+ ]),
1681
+ git(cwd, [
1682
+ "rev-parse",
1683
+ "--verify",
1684
+ "--quiet",
1685
+ "HEAD"
1686
+ ])
1687
+ ]);
1688
+ const repository = url === void 0 ? void 0 : normalizeRepository(url);
1689
+ return {
1690
+ ...repository === void 0 ? {} : { repository },
1691
+ ...branch === void 0 ? {} : { branch },
1692
+ ...commit === void 0 ? {} : { commit }
1693
+ };
1694
+ }
1695
+ async function resolveRepository(cwd, git) {
1696
+ if (!isAbsolute(cwd)) return void 0;
1697
+ const remote = await selectRemote(cwd, git);
1698
+ if (!remote) return void 0;
1699
+ const url = await git(cwd, [
1700
+ "remote",
1701
+ "get-url",
1702
+ remote
1703
+ ]);
1704
+ return url === void 0 ? void 0 : normalizeRepository(url);
1705
+ }
1706
+ async function resolveProjectRoot(cwd, git) {
1707
+ const topLevel = await git(cwd, ["rev-parse", "--show-toplevel"]);
1708
+ return topLevel && isAbsolute(topLevel) ? topLevel : cwd;
1709
+ }
1710
+ //#endregion
1098
1711
  //#region src/credentials/user-destination.ts
1099
1712
  async function resolveUserDestination(deps) {
1100
1713
  const { workspaceId, ingestEndpoint } = await resolveCliDestinations(deps.env, userSettingsPath(deps.env, deps.homeDir), "");
@@ -1110,7 +1723,7 @@ async function resolveUserDestination(deps) {
1110
1723
  };
1111
1724
  if (!await deps.keychain.isAvailable()) return {
1112
1725
  kind: "skipped",
1113
- reason: "no ingest key"
1726
+ reason: "keychain unreadable"
1114
1727
  };
1115
1728
  const key = await deps.keychain.get(ingestKeyAccount(workspaceId));
1116
1729
  if (!key) return {
@@ -1133,10 +1746,10 @@ async function resolveUserDestination(deps) {
1133
1746
  //#endregion
1134
1747
  //#region src/hook/session-start.ts
1135
1748
  const HOOK_USAGE = "Usage: harnessforce hook session-start\n";
1136
- const MAX_INPUT_BYTES$1 = 1048576;
1749
+ const MAX_INPUT_BYTES = 1048576;
1137
1750
  const REGISTERING_SOURCES = /* @__PURE__ */ new Set(["startup", "clear"]);
1138
1751
  const KEYCHAIN_READ_TIMEOUT_MS = 1e3;
1139
- const REVOKED_MESSAGE = "送信キーが失効しています。`harnessforce init`を実行してください";
1752
+ const REVOKED_MESSAGE = "Your ingest key has been revoked. Run `harnessforce init`";
1140
1753
  function parseInput(raw) {
1141
1754
  let fields;
1142
1755
  try {
@@ -1154,18 +1767,28 @@ function parseInput(raw) {
1154
1767
  };
1155
1768
  }
1156
1769
  const report$1 = (deps, detail) => deps.stderr(`harnessforce: session registration ${detail}\n`);
1770
+ const SKIPPED_WARNINGS = {
1771
+ "not configured": notConfiguredWarning("codex"),
1772
+ "invalid endpoint": INVALID_ENDPOINT_WARNING,
1773
+ "no ingest key": keyMissingWarning("codex"),
1774
+ "keychain unreadable": keyUnreadableWarning("codex"),
1775
+ "destination not verified": DESTINATION_MISMATCH_WARNING.codex
1776
+ };
1157
1777
  function resolveWithinLimit(deps) {
1158
1778
  let timer;
1159
1779
  const slow = new Promise((resolve) => {
1160
1780
  timer = setTimeout(() => resolve({
1161
1781
  kind: "skipped",
1162
- reason: "no ingest key"
1782
+ reason: "keychain unreadable"
1163
1783
  }), KEYCHAIN_READ_TIMEOUT_MS);
1164
1784
  });
1165
1785
  return Promise.race([resolveUserDestination({
1166
1786
  ...deps,
1167
1787
  keychain: deps.keychain.withTimeout(KEYCHAIN_READ_TIMEOUT_MS)
1168
- }), slow]).finally(() => clearTimeout(timer));
1788
+ }).catch(() => ({
1789
+ kind: "skipped",
1790
+ reason: "keychain unreadable"
1791
+ })), slow]).finally(() => clearTimeout(timer));
1169
1792
  }
1170
1793
  async function resolveDestination(deps) {
1171
1794
  const resolved = await resolveWithinLimit(deps);
@@ -1174,9 +1797,9 @@ async function resolveDestination(deps) {
1174
1797
  report$1(deps, "skipped (not configured)");
1175
1798
  return { warning: notConfiguredWarning("codex") };
1176
1799
  }
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 } : {};
1800
+ const detail = resolved.reason === "keychain unreadable" ? "no ingest key" : resolved.reason;
1801
+ report$1(deps, `skipped (${detail})`);
1802
+ return { warning: SKIPPED_WARNINGS[resolved.reason] };
1180
1803
  }
1181
1804
  function registrationSource(env) {
1182
1805
  const issue = env.HARNESSFORCE_ISSUE;
@@ -1201,20 +1824,32 @@ async function register(input, deps) {
1201
1824
  if (outcome.kind === "failed") report$1(deps, `failed (${outcome.reason})`);
1202
1825
  return { outcome };
1203
1826
  }
1827
+ async function selectWarning(registered, deps) {
1828
+ if (registered.outcome?.kind === "unauthorized") return REVOKED_MESSAGE;
1829
+ const state = await readCliUpdateState(deps.homeDir, deps.client.version);
1830
+ if (state?.status === "unsupported") return updateLine("cli", state);
1831
+ const sending = registered.warning ?? (registered.outcome?.kind === "failed" ? "Harnessforce session registration failed. Check your network connection" : void 0);
1832
+ if (sending !== void 0 || !state) return sending;
1833
+ return noticeLine("cli", updateStatePath(deps.homeDir), state, {
1834
+ now: deps.now(),
1835
+ env: deps.env
1836
+ });
1837
+ }
1204
1838
  async function sessionStart(deps) {
1205
- const input = parseInput(await deps.readStdin(MAX_INPUT_BYTES$1).then((buffer) => buffer.toString("utf8")).catch(() => ""));
1839
+ const input = parseInput(await deps.readStdin(MAX_INPUT_BYTES).then((buffer) => buffer.toString("utf8")).catch(() => ""));
1206
1840
  if (!input) return 0;
1207
- const registered = input.source === void 0 || REGISTERING_SOURCES.has(input.source) ? await register(input, deps).catch((error) => {
1841
+ const registers = input.source === void 0 || REGISTERING_SOURCES.has(input.source);
1842
+ const blocked = registers && await isSendingBlocked(deps);
1843
+ const registered = registers && !blocked ? await register(input, deps).catch((error) => {
1208
1844
  report$1(deps, `failed (${error instanceof Error ? error.name : "unknown"})`);
1209
1845
  return {};
1210
1846
  }) : {};
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;
1216
- }
1217
- deps.stdout(`${JSON.stringify(sessionContext(input.sessionId))}\n`);
1847
+ const warning = registers ? await selectWarning(registered, deps) : void 0;
1848
+ if (warning !== void 0) deps.stderr(`${warning}\n`);
1849
+ deps.stdout(`${JSON.stringify({
1850
+ ...sessionContext(input.sessionId),
1851
+ ...warning === void 0 ? {} : { systemMessage: warning }
1852
+ })}\n`);
1218
1853
  return 0;
1219
1854
  }
1220
1855
  //#endregion
@@ -1347,7 +1982,7 @@ const LOCK_WAIT_MS$1 = 3e4;
1347
1982
  const STALE_LOCK_MS$1 = 6e4;
1348
1983
  const RETRY_INTERVAL_MS$1 = 100;
1349
1984
  const DIR_MODE$2 = 448;
1350
- const FILE_MODE$5 = 384;
1985
+ const FILE_MODE$4 = 384;
1351
1986
  const isAlreadyExists$1 = (error) => error?.code === "EEXIST";
1352
1987
  async function removeIfStale$1(path, now) {
1353
1988
  const created = await stat(path).catch(() => void 0);
@@ -1362,7 +1997,7 @@ async function acquireTokenLock(path, deps) {
1362
1997
  const deadline = deps.now() + LOCK_WAIT_MS$1;
1363
1998
  while (deps.now() < deadline) {
1364
1999
  try {
1365
- await (await open(path, "wx", FILE_MODE$5)).close();
2000
+ await (await open(path, "wx", FILE_MODE$4)).close();
1366
2001
  return () => rm(path, { force: true });
1367
2002
  } catch (error) {
1368
2003
  if (!isAlreadyExists$1(error)) throw error;
@@ -1579,7 +2214,7 @@ async function recordBase(deps) {
1579
2214
  //#region src/import/state.ts
1580
2215
  const STATE_VERSION = 1;
1581
2216
  const DIR_MODE$1 = 448;
1582
- const FILE_MODE$4 = 384;
2217
+ const FILE_MODE$3 = 384;
1583
2218
  const sessionStateKey = (session) => session.agent === "claude_code" ? session.session_id : `${session.agent}:${session.session_id}`;
1584
2219
  const importStatePath = (homeDir) => join(homeDir, ".harnessforce", "import-state.json");
1585
2220
  const keyOf = ({ workspaceId, endpoint }) => `${workspaceId} ${endpoint}`;
@@ -1624,10 +2259,10 @@ async function recordSentSessions(path, destination, sessionIds) {
1624
2259
  const temporary = `${path}.${randomUUID()}.tmp`;
1625
2260
  try {
1626
2261
  await writeFile(temporary, `${JSON.stringify(next)}\n`, {
1627
- mode: FILE_MODE$4,
2262
+ mode: FILE_MODE$3,
1628
2263
  flag: "wx"
1629
2264
  });
1630
- await chmod(temporary, FILE_MODE$4);
2265
+ await chmod(temporary, FILE_MODE$3);
1631
2266
  await rename(temporary, path);
1632
2267
  } finally {
1633
2268
  await rm(temporary, { force: true });
@@ -1965,7 +2600,7 @@ async function parseRollout(path) {
1965
2600
  const accumulator = new Accumulator$1();
1966
2601
  let skippedLines = 0;
1967
2602
  try {
1968
- const lines = createInterface$1({
2603
+ const lines = createInterface({
1969
2604
  input: createReadStream(path, { encoding: "utf8" }),
1970
2605
  crlfDelay: Number.POSITIVE_INFINITY
1971
2606
  });
@@ -2194,7 +2829,7 @@ async function readTranscript(path, accumulator) {
2194
2829
  let lines = 0;
2195
2830
  let skippedLines = 0;
2196
2831
  try {
2197
- const reader = createInterface$1({
2832
+ const reader = createInterface({
2198
2833
  input: createReadStream(path, { encoding: "utf8" }),
2199
2834
  crlfDelay: Number.POSITIVE_INFINITY
2200
2835
  });
@@ -2269,6 +2904,11 @@ async function parseTranscriptUsage(path) {
2269
2904
  }
2270
2905
  //#endregion
2271
2906
  //#region src/import/sessions.ts
2907
+ const noneNotImported = () => ({
2908
+ outOfRange: 0,
2909
+ outsideGit: 0,
2910
+ notConnected: 0
2911
+ });
2272
2912
  const readDir$1 = (dir) => readdir(dir, { withFileTypes: true }).catch(() => []);
2273
2913
  async function listTranscripts(projectsDir) {
2274
2914
  const projects = (await readDir$1(projectsDir)).filter((entry) => entry.isDirectory());
@@ -2295,33 +2935,49 @@ function toSessionImport(session, repository) {
2295
2935
  };
2296
2936
  }
2297
2937
  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) {
2938
+ const cachedRepositoryOf = (git) => {
2299
2939
  const repositories = /* @__PURE__ */ new Map();
2300
- const repositoryOf = (cwd) => {
2301
- const cached = repositories.get(cwd) ?? resolveRepository(cwd, options.git);
2940
+ return (cwd) => {
2941
+ const cached = repositories.get(cwd) ?? resolveRepository(cwd, git);
2302
2942
  repositories.set(cwd, cached);
2303
2943
  return cached;
2304
2944
  };
2945
+ };
2946
+ async function judge(session, options, repositoryOf) {
2947
+ if (Date.parse(session.endedAt) < options.sinceMs) return { kind: "outOfRange" };
2948
+ const repository = session.cwd ? await repositoryOf(session.cwd) : void 0;
2949
+ if (repository === void 0) return { kind: "outsideGit" };
2950
+ return options.connected.has(repository) ? {
2951
+ kind: "import",
2952
+ repository
2953
+ } : { kind: "notConnected" };
2954
+ }
2955
+ async function scanSessions(options) {
2956
+ const repositoryOf = cachedRepositoryOf(options.git);
2305
2957
  const sessions = /* @__PURE__ */ new Map();
2958
+ const notImported = noneNotImported();
2306
2959
  let skippedLines = 0;
2307
2960
  let skippedFiles = 0;
2308
2961
  for (const file of await listTranscripts(options.projectsDir)) {
2309
2962
  const modifiedMs = (await stat(file).catch(() => void 0))?.mtimeMs;
2310
- if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) continue;
2963
+ if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) {
2964
+ notImported.outOfRange += 1;
2965
+ continue;
2966
+ }
2311
2967
  const result = await parseTranscript(file);
2312
2968
  skippedLines += result.skippedLines;
2313
2969
  if (result.kind === "unreadable") skippedFiles += 1;
2314
2970
  if (result.kind !== "session") continue;
2315
2971
  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));
2972
+ const verdict = await judge(session, options, repositoryOf);
2973
+ if (verdict.kind !== "import") notImported[verdict.kind] += 1;
2974
+ else if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, toSessionImport(session, verdict.repository));
2320
2975
  }
2321
2976
  return {
2322
2977
  sessions: sortedSessions(sessions),
2323
2978
  skippedLines,
2324
- skippedFiles
2979
+ skippedFiles,
2980
+ notImported
2325
2981
  };
2326
2982
  }
2327
2983
  const numericName = (name, length) => name.length === length && /^\d+$/.test(name) ? Number(name) : void 0;
@@ -2371,43 +3027,39 @@ function toCodexSessionImport(session, repository) {
2371
3027
  };
2372
3028
  }
2373
3029
  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
- };
3030
+ const repositoryOf = cachedRepositoryOf(options.git);
2385
3031
  const { files, compressed } = await listRollouts$1(join(options.codexDir, "sessions"), localDayKey(options.sinceMs));
2386
3032
  const sessions = /* @__PURE__ */ new Map();
3033
+ const notImported = noneNotImported();
2387
3034
  let skippedLines = 0;
2388
3035
  let skippedFiles = 0;
2389
3036
  let unextractable = 0;
2390
3037
  for (const file of files) {
2391
3038
  const modifiedMs = (await stat(file).catch(() => void 0))?.mtimeMs;
2392
- if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) continue;
3039
+ if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) {
3040
+ notImported.outOfRange += 1;
3041
+ continue;
3042
+ }
2393
3043
  const result = await parseRollout(file);
2394
3044
  skippedLines += result.skippedLines;
2395
3045
  if (result.kind === "unreadable") skippedFiles += 1;
2396
3046
  if (result.kind === "unextractable") {
2397
- if (Date.parse(result.endedAt) >= options.sinceMs && await isSent(result.cwd)) unextractable += 1;
3047
+ const verdict = await judge(result, options, repositoryOf);
3048
+ if (verdict.kind === "import") unextractable += 1;
3049
+ else notImported[verdict.kind] += 1;
2398
3050
  continue;
2399
3051
  }
2400
3052
  if (result.kind !== "session") continue;
2401
3053
  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));
3054
+ const verdict = await judge(session, options, repositoryOf);
3055
+ if (verdict.kind !== "import") notImported[verdict.kind] += 1;
3056
+ else if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, toCodexSessionImport(session, verdict.repository));
2406
3057
  }
2407
3058
  return {
2408
3059
  sessions: sortedSessions(sessions),
2409
3060
  skippedLines,
2410
3061
  skippedFiles,
3062
+ notImported,
2411
3063
  unextractable,
2412
3064
  compressed
2413
3065
  };
@@ -2415,49 +3067,78 @@ async function scanCodexSessions(options) {
2415
3067
  //#endregion
2416
3068
  //#region src/import/command.ts
2417
3069
  const DAY_MS$1 = 864e5;
2418
- const MESSAGES$1 = {
2419
- runInit: "`harnessforce init`を実行してください",
2420
- invalidUrl: "接続先のURLが不正です",
3070
+ const MESSAGES = {
3071
+ runInit: "Run `harnessforce init`",
3072
+ invalidUrl: "The connection URL is invalid",
2421
3073
  keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
2422
- readFailed: "Harnessforceとの通信に失敗しました。もう一度`harnessforce import`を実行してください",
2423
- sendFailed: "Harnessforceとの通信に失敗しました。もう一度`harnessforce import`を実行すると続きから取り込みます",
2424
- revoked: "送信キーが失効しています。`harnessforce init`を実行してください",
2425
- loginExpired: "ログインの有効期限が切れました。`harnessforce init`を実行してください",
2426
- stateFailed: "取り込みの状態を保存できませんでした"
3074
+ readFailed: "Communication with Harnessforce failed. Run `harnessforce import` again",
3075
+ sendFailed: "Communication with Harnessforce failed. Run `harnessforce import` again to resume importing",
3076
+ revoked: "Your ingest key has been revoked. Run `harnessforce init`",
3077
+ loginExpired: "Your login has expired. Run `harnessforce init`",
3078
+ stateFailed: "Could not save the import state",
3079
+ connectNone: "No GitHub repository is connected to the Workspace. Connect the GitHub App in Harnessforce to import sessions with `harnessforce import`",
3080
+ 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`",
3081
+ nothingFound: "No session records to import were found"
2427
3082
  };
2428
3083
  const limitMessage = {
2429
- monthly_event_limit: (count) => `月間イベント数の上限に達したため、${count}件のセッションを取り込めませんでした。上限が解除された後に\`harnessforce import\`を再実行すると続きから取り込みます`,
2430
- workspace_read_only: (count) => `Workspaceが閲覧のみのため、${count}件のセッションを取り込めませんでした。閲覧のみが解除された後に\`harnessforce import\`を再実行すると続きから取り込みます`
3084
+ 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`,
3085
+ 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
3086
  };
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
3087
+ const ACCESS_MESSAGES$2 = {
3088
+ keychainUnavailable: MESSAGES.keychainUnavailable,
3089
+ initRequired: MESSAGES.runInit,
3090
+ invalidUrl: MESSAGES.invalidUrl,
3091
+ apiTokenMissing: MESSAGES.runInit
2437
3092
  };
2438
3093
  function unwrap(outcome) {
2439
3094
  if (outcome.kind === "ok") return outcome.value;
2440
- return stopWith(outcome.kind === "unauthorized" ? MESSAGES$1.loginExpired : MESSAGES$1.readFailed);
3095
+ return stopWith(outcome.kind === "unauthorized" ? MESSAGES.loginExpired : MESSAGES.readFailed);
2441
3096
  }
2442
- function report(result, deps) {
3097
+ const NOT_IMPORTED_LABELS = [
3098
+ ["alreadySent", "already sent"],
3099
+ ["outOfRange", "outside the period"],
3100
+ ["outsideGit", "outside a git repository"],
3101
+ ["notConnected", "in a repository not connected to the Workspace"]
3102
+ ];
3103
+ function breakdownLine(breakdown) {
3104
+ const parts = NOT_IMPORTED_LABELS.filter(([key]) => breakdown[key] > 0).map(([key, label]) => `${label} ${breakdown[key]}`);
3105
+ return parts.length > 0 ? `Sessions not imported: ${parts.join(", ")}` : void 0;
3106
+ }
3107
+ function reportNotImported(imported, notes, deps) {
3108
+ const line = breakdownLine(notes.breakdown);
3109
+ if (line) deps.stdout(`${line}\n`);
3110
+ if (imported > 0) return;
3111
+ if (notes.breakdown.notConnected > 0) deps.stdout(`${notes.connectedCount === 0 ? MESSAGES.connectNone : MESSAGES.connectOther}\n`);
3112
+ else if (!line && !notes.otherNoticeShown) deps.stdout(`${MESSAGES.nothingFound}\n`);
3113
+ }
3114
+ function report(result, notes, deps) {
2443
3115
  switch (result.kind) {
2444
3116
  case "done":
2445
- deps.stdout(`${result.imported}件のセッションを取り込みました\n`);
2446
- if (result.invalid > 0) deps.stdout(`形式が不正なため${result.invalid}件のセッションを取り込めませんでした\n`);
3117
+ deps.stdout(`Sessions imported: ${result.imported}\n`);
3118
+ if (result.invalid > 0) deps.stdout(`Could not import ${result.invalid} session(s) because their format is invalid\n`);
3119
+ reportNotImported(result.imported, {
3120
+ ...notes,
3121
+ otherNoticeShown: notes.otherNoticeShown || result.invalid > 0
3122
+ }, deps);
2447
3123
  return 0;
2448
3124
  case "limited":
2449
3125
  deps.stderr(`${limitMessage[result.reason](result.notImported)}\n`);
2450
3126
  return 1;
2451
3127
  case "unauthorized":
2452
- deps.stderr(`${MESSAGES$1.revoked}\n`);
3128
+ deps.stderr(`${MESSAGES.revoked}\n`);
2453
3129
  return 1;
2454
3130
  case "failed":
2455
- deps.stderr(`${MESSAGES$1.sendFailed}\n`);
3131
+ deps.stderr(`${MESSAGES.sendFailed}\n`);
2456
3132
  return 1;
2457
3133
  }
2458
3134
  }
3135
+ const sumNotImported = (a, b) => ({
3136
+ outOfRange: a.outOfRange + b.outOfRange,
3137
+ outsideGit: a.outsideGit + b.outsideGit,
3138
+ notConnected: a.notConnected + b.notConnected
3139
+ });
2459
3140
  async function runImport(deps) {
2460
- const access = await verifyCliAccess(deps, ACCESS_MESSAGES$3);
3141
+ const access = await verifyCliAccess(deps, ACCESS_MESSAGES$2);
2461
3142
  const destination = {
2462
3143
  workspaceId: access.workspaceId,
2463
3144
  endpoint: withoutExtras(access.ingest)
@@ -2488,20 +3169,28 @@ async function runImport(deps) {
2488
3169
  });
2489
3170
  const skippedLines = claude.skippedLines + codex.skippedLines;
2490
3171
  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`);
3172
+ if (skippedLines > 0 || skippedFiles > 0) deps.stdout(`Skipped ${skippedLines} unreadable line(s) and ${skippedFiles} unreadable file(s)\n`);
3173
+ if (codex.unextractable > 0) deps.stdout(`Did not import ${codex.unextractable} Codex session(s) whose model or token count could not be read\n`);
3174
+ if (codex.compressed > 0) deps.stdout(`Did not import ${codex.compressed} compressed Codex session record(s) because they cannot be read\n`);
2494
3175
  const scanned = [...claude.sessions, ...codex.sessions];
2495
3176
  const statePath = importStatePath(deps.homeDir);
2496
3177
  const sent = await readSentSessions(statePath, destination);
3178
+ const unsent = scanned.filter((s) => !sent.has(sessionStateKey(s)));
2497
3179
  return report(await sendSessions({
2498
3180
  endpoint: access.ingest,
2499
3181
  ingestKey: access.ingestKey,
2500
- sessions: scanned.filter((s) => !sent.has(sessionStateKey(s))),
3182
+ sessions: unsent,
2501
3183
  fetch: deps.fetch,
2502
3184
  sleep: deps.sleep,
2503
- record: (ids) => recordSentSessions(statePath, destination, ids).catch(() => stopWith(MESSAGES$1.stateFailed))
2504
- }), deps);
3185
+ record: (ids) => recordSentSessions(statePath, destination, ids).catch(() => stopWith(MESSAGES.stateFailed))
3186
+ }), {
3187
+ breakdown: {
3188
+ alreadySent: scanned.length - unsent.length,
3189
+ ...sumNotImported(claude.notImported, codex.notImported)
3190
+ },
3191
+ otherNoticeShown: skippedLines > 0 || skippedFiles > 0 || codex.unextractable > 0 || codex.compressed > 0,
3192
+ connectedCount: connected.size
3193
+ }, deps);
2505
3194
  }
2506
3195
  function importCommand(deps) {
2507
3196
  return runUntilStop(() => runImport(deps), deps.stderr);
@@ -8981,7 +9670,7 @@ function Ko(e, t) {
8981
9670
  const HOOK_COMMAND = "harnessforce hook session-start";
8982
9671
  const HOOK_COMMAND_WINDOWS = "harnessforce.cmd hook session-start";
8983
9672
  const HOOK_TIMEOUT_SECONDS = 10;
8984
- const FILE_MODE$3 = 384;
9673
+ const FILE_MODE$2 = 384;
8985
9674
  function codexConfigPath(env, homeDir) {
8986
9675
  return join(absoluteEnv(env.CODEX_HOME) ?? join(homeDir, ".codex"), "config.toml");
8987
9676
  }
@@ -9056,10 +9745,10 @@ async function writeCodexConfig(path, text) {
9056
9745
  const temporary = `${path}.${randomUUID()}.tmp`;
9057
9746
  try {
9058
9747
  await writeFile(temporary, text, {
9059
- mode: FILE_MODE$3,
9748
+ mode: FILE_MODE$2,
9060
9749
  flag: "wx"
9061
9750
  });
9062
- await chmod(temporary, FILE_MODE$3);
9751
+ await chmod(temporary, FILE_MODE$2);
9063
9752
  await rename(temporary, path);
9064
9753
  } finally {
9065
9754
  await rm(temporary, { force: true });
@@ -9144,10 +9833,12 @@ const stop$1 = (message) => stopWith(INIT_MESSAGES[message]);
9144
9833
  const sha256Hex = (value) => createHash("sha256").update(value, "utf8").digest("hex");
9145
9834
  function init(args, deps) {
9146
9835
  return runUntilStop(async () => {
9147
- const { codexPath, isKernelKeyring } = await runInit(args, deps);
9836
+ const { codexPath, connection, isKernelKeyring, isPluginAutoUpdateOn } = await runInit(args, deps);
9837
+ if (connection !== withoutExtras(new URL(deps.defaultUrl))) deps.stdout(`${nonDefaultConnectionMessage(connection)}\n`);
9148
9838
  const lines = [
9149
9839
  INIT_MESSAGES.success,
9150
9840
  ...isKernelKeyring ? [INIT_MESSAGES.savedToKernelKeyring] : [],
9841
+ ...isPluginAutoUpdateOn ? [INIT_MESSAGES.pluginAutoUpdateOn] : [],
9151
9842
  codexNotes(codexPath)
9152
9843
  ];
9153
9844
  deps.stdout(`${lines.join("\n")}\n`);
@@ -9161,7 +9852,8 @@ async function runInit(args, deps) {
9161
9852
  const settingsPath = userSettingsPath(deps.env, deps.homeDir);
9162
9853
  const store = await openKeychain(deps, (await readSettingsEnv$1(settingsPath)).HARNESSFORCE_WORKSPACE_ID);
9163
9854
  const revoke = revokeHashes(store);
9164
- if ((await readUserSettings(settingsPath)).kind === "invalid") stop$1("settingsUnreadable");
9855
+ const settings = await readUserSettings(settingsPath);
9856
+ if (settings.kind === "invalid" || !hasWritableMarketplaces(settings.settings)) stop$1("settingsUnreadable");
9165
9857
  const authorizationEndpoint = await discoverAuthorizationEndpoint(base, deps.fetch) ?? stop$1("network");
9166
9858
  const secrets = createLoginSecrets();
9167
9859
  const { code, redirectUri } = await logIn(authorizationEndpoint, secrets, port, deps);
@@ -9173,7 +9865,7 @@ async function runInit(args, deps) {
9173
9865
  revoke_key_hashes: revoke.keyHashes,
9174
9866
  revoke_api_token_hashes: revoke.apiTokenHashes
9175
9867
  }, deps.fetch));
9176
- await save(issued, recordedUrl, settingsPath, args.sendContent, store).catch((error) => {
9868
+ const isPluginAutoUpdateOn = await save(issued, recordedUrl, settingsPath, args, store).catch((error) => {
9177
9869
  if (isStop(error)) throw error;
9178
9870
  return stop$1("saveFailed");
9179
9871
  });
@@ -9182,7 +9874,9 @@ async function runInit(args, deps) {
9182
9874
  if (args.sendContent) deps.stdout(`${issued.contentOptIn ? INIT_MESSAGES.contentEnabled : INIT_MESSAGES.contentNotOptedIn}\n`);
9183
9875
  return {
9184
9876
  codexPath,
9185
- isKernelKeyring: store.isKernelKeyring
9877
+ connection: recordedUrl,
9878
+ isKernelKeyring: store.isKernelKeyring,
9879
+ isPluginAutoUpdateOn
9186
9880
  };
9187
9881
  }
9188
9882
  async function openKeychain(deps, previousWorkspaceId) {
@@ -9244,7 +9938,7 @@ function issuedOrStop(outcome) {
9244
9938
  case "unexpected": return stop$1("network");
9245
9939
  }
9246
9940
  }
9247
- async function save(issued, connection, settingsPath, sendContent, store) {
9941
+ async function save(issued, connection, settingsPath, args, store) {
9248
9942
  await store.delete(ingestOriginAccount(issued.workspaceId));
9249
9943
  await store.delete(urlOriginAccount(issued.workspaceId));
9250
9944
  await store.set(ingestKeyAccount(issued.workspaceId), issued.ingestKey);
@@ -9253,7 +9947,7 @@ async function save(issued, connection, settingsPath, sendContent, store) {
9253
9947
  await store.set(urlOriginAccount(issued.workspaceId), new URL(connection).origin);
9254
9948
  await store.deleteElsewhere(issued.workspaceId);
9255
9949
  const current = await readUserSettings(settingsPath);
9256
- if (current.kind === "invalid") return stop$1("saveFailed");
9950
+ if (current.kind === "invalid" || !hasWritableMarketplaces(current.settings)) return stop$1("saveFailed");
9257
9951
  const env = {
9258
9952
  HARNESSFORCE_URL: connection,
9259
9953
  HARNESSFORCE_ENDPOINT: issued.ingestEndpoint,
@@ -9267,8 +9961,10 @@ async function save(issued, connection, settingsPath, sendContent, store) {
9267
9961
  OTEL_TRACES_EXPORTER: "otlp",
9268
9962
  OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf"
9269
9963
  };
9270
- if (sendContent && issued.contentOptIn) env.OTEL_LOG_USER_PROMPTS = "1";
9271
- await writeUserSettings(settingsPath, mergeUserSettings(current.settings, env));
9964
+ if (args.sendContent && issued.contentOptIn) env.OTEL_LOG_USER_PROMPTS = "1";
9965
+ const { settings, isOn } = withPluginAutoUpdate(mergeUserSettings(current.settings, env), args.noPluginAutoUpdate);
9966
+ await writeUserSettings(settingsPath, settings);
9967
+ return isOn;
9272
9968
  }
9273
9969
  async function saveCodexConfig(issued, connection, path, sendContent) {
9274
9970
  await writeCodexConfig(path, mergeCodexConfig(await readCodexConfig(path), {
@@ -9288,11 +9984,23 @@ const DESTINATION_VARIABLES = [
9288
9984
  "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"
9289
9985
  ];
9290
9986
  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;
9987
+ const EXIT_KEY_MISSING = 3;
9988
+ const EXIT_WITHHELD = 4;
9989
+ const EXIT_FAILED = 1;
9990
+ async function otelHeaders(env, managedDir, keychain, stdout, stderr, endStdout) {
9991
+ const selection = await selectKey(env, managedDir, keychain).catch(() => ({ kind: "failed" }));
9992
+ if (selection.kind === "withheld") {
9993
+ stderr(WITHHELD);
9994
+ return EXIT_WITHHELD;
9995
+ }
9996
+ if (selection.kind === "missing") return EXIT_KEY_MISSING;
9997
+ if (selection.kind === "failed") return EXIT_FAILED;
9295
9998
  stdout(`${JSON.stringify({ Authorization: `Bearer ${selection.key}` })}\n`);
9999
+ const { user } = selection;
10000
+ if (user && !env.HARNESSFORCE_NO_MIGRATE) {
10001
+ await endStdout();
10002
+ await user.keychain.migrateFromKernel?.(user.workspaceId).catch(() => {});
10003
+ }
9296
10004
  return 0;
9297
10005
  }
9298
10006
  const HOOK_READ_TIMEOUT_MS = 1e3;
@@ -9303,15 +10011,19 @@ async function selectKey(env, managedDir, unlimited) {
9303
10011
  key: managed.HARNESSFORCE_INGEST_KEY
9304
10012
  };
9305
10013
  const workspaceId = env.HARNESSFORCE_WORKSPACE_ID;
9306
- if (!workspaceId) return { kind: "none" };
10014
+ if (!workspaceId) return { kind: "missing" };
9307
10015
  const keychain = unlimited.withTimeout(HOOK_READ_TIMEOUT_MS);
9308
- if (!await keychain.isAvailable()) return { kind: "none" };
10016
+ if (!await keychain.isAvailable()) return { kind: "failed" };
9309
10017
  const key = await keychain.get(ingestKeyAccount(workspaceId));
9310
- if (!key) return { kind: "none" };
10018
+ if (!key) return { kind: "missing" };
9311
10019
  const pinnedOrigin = await keychain.get(ingestOriginAccount(workspaceId));
9312
10020
  return pinnedOrigin && isPinnedDestination(env, pinnedOrigin) ? {
9313
10021
  kind: "key",
9314
- key
10022
+ key,
10023
+ user: {
10024
+ keychain,
10025
+ workspaceId
10026
+ }
9315
10027
  } : { kind: "withheld" };
9316
10028
  }
9317
10029
  function isPinnedDestination(env, pinnedOrigin) {
@@ -9642,16 +10354,16 @@ function buildLaunch(request) {
9642
10354
  //#region src/run/messages.ts
9643
10355
  const RUN_MESSAGES = {
9644
10356
  keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
9645
- initRequired: "`harnessforce init`を実行してください",
10357
+ initRequired: "Run `harnessforce init`",
9646
10358
  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を解決できませんでした。候補:"
10359
+ invalidIssue: "The Issue identifier is invalid",
10360
+ issueInitRequired: "Could not resolve the Issue. Run `harnessforce init`",
10361
+ loginExpired: "Your login has expired. Run `harnessforce init`",
10362
+ issueFailed: "Could not resolve the Issue or fetch candidates",
10363
+ noMatch: "No matching Issue",
10364
+ candidatesHeader: "Could not resolve the Issue. Candidates:"
9653
10365
  };
9654
- const launchFailedMessage = (agent) => `${agent}を起動できませんでした`;
10366
+ const launchFailedMessage = (agent) => `Could not start ${agent}`;
9655
10367
  //#endregion
9656
10368
  //#region src/run/run.ts
9657
10369
  const stop = (message) => stopWith(RUN_MESSAGES[message]);
@@ -9660,14 +10372,14 @@ function candidateList(candidates) {
9660
10372
  const lines = candidates.map(({ identifier, title }) => `${printable(identifier)} ${printable(title)}`);
9661
10373
  return [RUN_MESSAGES.candidatesHeader, ...lines].join("\n");
9662
10374
  }
9663
- const ACCESS_MESSAGES$2 = {
10375
+ const ACCESS_MESSAGES$1 = {
9664
10376
  keychainUnavailable: RUN_MESSAGES.keychainUnavailable,
9665
10377
  initRequired: RUN_MESSAGES.initRequired,
9666
10378
  invalidUrl: RUN_MESSAGES.invalidUrl,
9667
10379
  apiTokenMissing: RUN_MESSAGES.issueInitRequired
9668
10380
  };
9669
10381
  async function prepareLaunch(args, deps) {
9670
- const { workspaceId, ingestEndpoint, readApiBase, accessToken } = await verifyCliAccess(deps, ACCESS_MESSAGES$2);
10382
+ const { workspaceId, ingestEndpoint, readApiBase, accessToken } = await verifyCliAccess(deps, ACCESS_MESSAGES$1);
9671
10383
  if (!isIssueIdentifier(args.issue)) stop("invalidIssue");
9672
10384
  const readFetch = createApiTokenSession({
9673
10385
  keychain: deps.keychain,
@@ -9718,6 +10430,20 @@ function runIssue(args, deps) {
9718
10430
  return 1;
9719
10431
  }, deps.stderr);
9720
10432
  }
10433
+ async function runWithoutHarnessforce(args, deps) {
10434
+ const env = Object.fromEntries(Object.entries({
10435
+ ...deps.env,
10436
+ ...deps.restoredEnv
10437
+ }).filter((entry) => entry[1] !== void 0));
10438
+ const outcome = await deps.launch({
10439
+ command: args.agent,
10440
+ args: [...args.agentArgs],
10441
+ env
10442
+ });
10443
+ if (outcome.kind === "exited") return outcome.code;
10444
+ deps.stderr(`${launchFailedMessage(args.agent)}\n`);
10445
+ return 1;
10446
+ }
9721
10447
  //#endregion
9722
10448
  //#region src/top/args.ts
9723
10449
  const TOP_USAGE = "Usage: harnessforce top [--once] [--json] [--ascii] [--theme <auto|light|dark|ansi>]\n";
@@ -9953,7 +10679,7 @@ function renderList(sessions, options) {
9953
10679
  const active = sessions.filter((s) => sessionState(s.lastEventAtMs, options.nowMs) === "active").length;
9954
10680
  return [clip(`${sessions.length} sessions, ${active} active`, options)];
9955
10681
  }
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);
10682
+ 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
10683
  return [
9958
10684
  header(sessions, options),
9959
10685
  ...body,
@@ -10014,7 +10740,7 @@ function renderDetail(row, options) {
10014
10740
  clip(options.painter.dim(DETAIL_HINT), options)
10015
10741
  ];
10016
10742
  }
10017
- const skippedNotice = (snapshot) => snapshot.skippedLines > 0 || snapshot.skippedFiles > 0 ? [`読めなかった${snapshot.skippedLines}行と${snapshot.skippedFiles}個のファイルを読み飛ばしました`] : [];
10743
+ const skippedNotice = (snapshot) => snapshot.skippedLines > 0 || snapshot.skippedFiles > 0 ? [`Skipped ${snapshot.skippedLines} unreadable line(s) and ${snapshot.skippedFiles} unreadable file(s)`] : [];
10018
10744
  //#endregion
10019
10745
  //#region src/top/codex-session.ts
10020
10746
  const DETACHED = "HEAD";
@@ -10629,7 +11355,7 @@ const DEFAULT_SETTINGS = {
10629
11355
  };
10630
11356
  const unreadable = {
10631
11357
  settings: DEFAULT_SETTINGS,
10632
- notice: "端末の設定(~/.harnessforce/config.json)を読めないため、既定の表示にします"
11358
+ notice: "Cannot read the device settings (`~/.harnessforce/config.json`), so the default display is used"
10633
11359
  };
10634
11360
  async function readTopSettings(homeDir) {
10635
11361
  let text;
@@ -11119,7 +11845,9 @@ const median = (values) => {
11119
11845
  const middle = Math.floor(sorted.length / 2);
11120
11846
  return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
11121
11847
  };
11122
- const secondsText = (value) => value === null ? "—" : `${value.toFixed(1)}秒`;
11848
+ const escapeForTerminal = (text) => text.replace(/[^\x20-\x7e]/gu, (c) => `\\u{${c.codePointAt(0).toString(16)}}`);
11849
+ const asciiJson = (value, space) => JSON.stringify(value, null, space).replace(/[^\n\x20-\x7e]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
11850
+ const secondsText = (value) => value === null ? "—" : `${value.toFixed(1)}s`;
11123
11851
  function summarize(reports, group, kind, countField, medianField) {
11124
11852
  const values = reports.map((r) => r[group][kind]).filter((v) => v?.measurement === "measured");
11125
11853
  if (values.length === 0) return void 0;
@@ -11132,16 +11860,16 @@ function summarize(reports, group, kind, countField, medianField) {
11132
11860
  };
11133
11861
  }
11134
11862
  function renderAnalysis(reports, usages, header) {
11135
- const lines = [`分析したsession: ${header.sessionCount}件(analyzer ${header.analyzerVersion}、parser ${header.parserVersion})`, "人の介入(回数、待ち時間のsessionごとの中央値の中央値):"];
11863
+ const lines = [`Sessions analyzed: ${header.sessionCount} (analyzer ${header.analyzerVersion}, parser ${header.parserVersion})`, "Human interventions (count, median of per-session median wait time):"];
11136
11864
  for (const kind of INTERVENTION_KINDS) {
11137
11865
  const s = summarize(reports, "interventions", kind, "count", "wait_seconds_median");
11138
- lines.push(s === void 0 ? ` ${kind}: 未計測` : ` ${kind}: ${s.total}回、${secondsText(s.median)}`);
11866
+ lines.push(s === void 0 ? ` ${kind}: not measured` : ` ${kind}: ${s.total} time(s), ${secondsText(s.median)}`);
11139
11867
  }
11140
- lines.push("ループにできる繰り返し(回数、その間の介入、所要時間の中央値の中央値):");
11868
+ lines.push("Repeatable loops (count, interventions during them, median of median duration):");
11141
11869
  for (const kind of LOOP_KINDS) {
11142
11870
  const s = summarize(reports, "loops", kind, "occurrences", "duration_seconds_median");
11143
11871
  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)}`);
11872
+ lines.push(s === void 0 ? ` ${kind}: not measured` : ` ${kind}: ${s.total} time(s), ${interventions} intervention(s), ${secondsText(s.median)}`);
11145
11873
  }
11146
11874
  const servers = /* @__PURE__ */ new Map();
11147
11875
  for (const report of reports) for (const s of report.mcp_servers) {
@@ -11156,19 +11884,19 @@ function renderAnalysis(reports, usages, header) {
11156
11884
  configured: current.configured + (s.configured ? 1 : 0)
11157
11885
  });
11158
11886
  }
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}件`);
11887
+ lines.push("MCP servers (calls, failures, sessions where configured):");
11888
+ if (servers.size === 0) lines.push(" none");
11889
+ 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
11890
  lines.push(...renderUsage(usages));
11163
11891
  return `${lines.join("\n")}\n`;
11164
11892
  }
11165
11893
  const percent = (share) => `${(share * 100).toFixed(1)}%`;
11166
11894
  function renderUsage(usages) {
11167
- const lines = ["使い方(端末だけの値。送信しません):"];
11895
+ const lines = ["Usage (this device only; not sent):"];
11168
11896
  for (const kind of ["frequent_compaction", "low_cache_reuse"]) lines.push(` ${kind}: ${usages.filter((u) => u.kinds.includes(kind)).length} session`);
11169
11897
  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("、") : "なし"}`);
11898
+ 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)}`)];
11899
+ lines.push(` model (share of output_tokens): ${parts ? parts.join(", ") : "none"}`);
11172
11900
  return lines;
11173
11901
  }
11174
11902
  function localIso(ms) {
@@ -11289,7 +12017,7 @@ const LOCK_WAIT_MS = 3e4;
11289
12017
  const STALE_LOCK_MS = 6e4;
11290
12018
  const HEARTBEAT_MS = 1e4;
11291
12019
  const RETRY_INTERVAL_MS = 100;
11292
- const FILE_MODE$2 = 384;
12020
+ const FILE_MODE$1 = 384;
11293
12021
  const isAlreadyExists = (error) => error?.code === "EEXIST";
11294
12022
  async function removeIfStale(path, now) {
11295
12023
  const current = await stat(path).catch(() => void 0);
@@ -11299,7 +12027,7 @@ async function acquireTuneLock(path, deps) {
11299
12027
  const deadline = deps.now() + LOCK_WAIT_MS;
11300
12028
  while (deps.now() < deadline) {
11301
12029
  try {
11302
- const handle = await open(path, "wx", FILE_MODE$2);
12030
+ const handle = await open(path, "wx", FILE_MODE$1);
11303
12031
  const token = randomUUID();
11304
12032
  await handle.writeFile(token);
11305
12033
  await handle.close();
@@ -11344,33 +12072,33 @@ const NOTICE_ORDER = [
11344
12072
  ];
11345
12073
  const TUNE_MESSAGES = {
11346
12074
  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/`の一部のファイルを削除できませんでした"
12075
+ initRequired: "Run `harnessforce init`. The Viewer role cannot use `harnessforce tune`",
12076
+ invalidUrl: "The connection URL is invalid",
12077
+ locked: "Another `harnessforce tune` is running, so this run cannot start",
12078
+ insufficient: (count) => `Not enough data: ${count} session(s) analyzed. Suggestions need at least 10 (${10 - count} more needed)`,
12079
+ outOfRange: (days, count) => `Outside the sending range: ${count} session(s) that started more than ${days} days ago will not be sent`,
12080
+ notConnected: (count) => `${count} session(s) in repositories that are not connected will not be sent`,
12081
+ partlyUnreadable: (lines, skipped) => `Some records could not be read: skipped ${skipped} of ${lines} lines read`,
12082
+ savedList: (fetchedAt) => `Could not fetch the list of connected repositories, so the list saved at ${fetchedAt} is used`,
12083
+ noList: "Could not verify the connected repositories, so nothing is sent",
12084
+ noSend: "Sending is turned off, so the analysis results are not sent",
12085
+ settingUnreadable: "Cannot read the device settings (`~/.harnessforce/config.json`), so the analysis results are not sent",
12086
+ 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`,
12087
+ importFailed: "Importing sessions before sending failed. The analysis results are sent; importing is retried in the next run",
12088
+ sent: (count) => `Sent ${count} analysis result(s)`,
12089
+ unreachable: (count) => `Could not send to Harnessforce. ${count} analysis result(s) will be sent in the next run`,
12090
+ expiredUnsent: (count) => `Deleted ${count} unsent analysis result(s) that fell outside the sending range`,
12091
+ readOnly: (count) => `Could not send ${count} analysis result(s) because the Workspace is view-only. They will be sent in the next run`,
12092
+ invalidReports: (count, reasons) => `Deleted ${count} analysis result(s) without sending because their format is invalid (${reasons.join(", ")})`,
12093
+ rejected: (status, count) => `Harnessforce rejected the submission (HTTP ${status}). Deleted ${count} analysis result(s) without sending`,
12094
+ keyUnusable: (count) => `The ingest key cannot be used. Deleted ${count} unsent analysis result(s). Run \`harnessforce init\``,
12095
+ loginExpired: "Your login has expired. Run `harnessforce init`",
12096
+ unwritable: "Cannot write to `~/.harnessforce/tune/`, so the analysis results are not sent",
12097
+ recordFailed: "Could not record the suggestion",
12098
+ recordInvalid: (field) => `The suggestion record input is invalid: ${field}`,
12099
+ usage: "Usage: harnessforce tune [--all] [--no-send] [--show-report] [--json], harnessforce tune record, or harnessforce tune --purge",
12100
+ purged: "Deleted the analysis results, suggestions and unsent analysis results in `~/.harnessforce/tune/`",
12101
+ purgeFailed: "Could not delete some files in `~/.harnessforce/tune/`"
11374
12102
  };
11375
12103
  var Notices = class {
11376
12104
  entries = /* @__PURE__ */ new Map();
@@ -11411,7 +12139,7 @@ const tunePaths = (homeDir) => {
11411
12139
  };
11412
12140
  };
11413
12141
  const DIR_MODE = 448;
11414
- const FILE_MODE$1 = 384;
12142
+ const FILE_MODE = 384;
11415
12143
  const destinationKey = ({ workspaceId, endpoint }) => `${workspaceId} ${endpoint}`;
11416
12144
  async function readJsonFile(path) {
11417
12145
  try {
@@ -11431,10 +12159,10 @@ async function writeFileAtomic(path, content) {
11431
12159
  const temporary = `${path}.${randomUUID()}.tmp`;
11432
12160
  try {
11433
12161
  await writeFile(temporary, content, {
11434
- mode: FILE_MODE$1,
12162
+ mode: FILE_MODE,
11435
12163
  flag: "wx"
11436
12164
  });
11437
- await chmod(temporary, FILE_MODE$1);
12165
+ await chmod(temporary, FILE_MODE);
11438
12166
  await rename(temporary, path);
11439
12167
  } finally {
11440
12168
  await rm(temporary, { force: true });
@@ -11724,7 +12452,7 @@ async function readSendSetting(homeDir) {
11724
12452
  const DAY_MS = 864e5;
11725
12453
  const DEFAULT_RANGE_DAYS = 30;
11726
12454
  const SKIPPED_RATIO = .05;
11727
- const ACCESS_MESSAGES$1 = {
12455
+ const ACCESS_MESSAGES = {
11728
12456
  keychainUnavailable: TUNE_MESSAGES.keychainUnavailable,
11729
12457
  initRequired: TUNE_MESSAGES.initRequired,
11730
12458
  invalidUrl: TUNE_MESSAGES.invalidUrl,
@@ -11960,14 +12688,14 @@ async function analyzeUnderLock(options, access, paths, deps) {
11960
12688
  writeSessionConfigs(paths.sessionConfigs, configs)
11961
12689
  ]).then(() => true, () => false);
11962
12690
  if (!isWritten) notices.add("unwritable", TUNE_MESSAGES.unwritable, 1);
11963
- if (options.json) deps.stdout(`${JSON.stringify(output)}\n`);
12691
+ if (options.json) deps.stdout(`${asciiJson(output)}\n`);
11964
12692
  else {
11965
12693
  deps.stdout(renderAnalysis(analyzed.map((a) => a.report), analyzed.map((a) => a.tune.usage), {
11966
12694
  sessionCount: analyzed.length,
11967
12695
  analyzerVersion: ANALYZER_VERSION,
11968
12696
  parserVersion: PARSER_VERSION
11969
12697
  }));
11970
- if (options.showReport) deps.stdout(`${JSON.stringify(analyzed.filter((a) => a.sendable === true).map((a) => a.report), null, 2)}\n`);
12698
+ if (options.showReport) deps.stdout(`${asciiJson(analyzed.filter((a) => a.sendable === true).map((a) => a.report), 2)}\n`);
11971
12699
  }
11972
12700
  if (analyzed.length < 10) notices.add("insufficient", TUNE_MESSAGES.insufficient(analyzed.length));
11973
12701
  const lines = analyzed.reduce((sum, a) => sum + a.tune.lines, 0);
@@ -11990,7 +12718,7 @@ async function analyzeUnderLock(options, access, paths, deps) {
11990
12718
  }
11991
12719
  function analyzeCommand(options, deps) {
11992
12720
  return runUntilStop(async () => {
11993
- const access = await verifyCliAccess(deps, ACCESS_MESSAGES$1);
12721
+ const access = await verifyCliAccess(deps, ACCESS_MESSAGES);
11994
12722
  const paths = tunePaths(deps.homeDir);
11995
12723
  await ensureDir(paths.dir).catch(() => stopWith(TUNE_MESSAGES.unwritable));
11996
12724
  const release = await acquireTuneLock(paths.lock, deps).catch(() => stopWith(TUNE_MESSAGES.unwritable));
@@ -12119,7 +12847,7 @@ const sha256 = (text) => createHash("sha256").update(Buffer.from(text, "utf8")).
12119
12847
  function expectedHash(input, component) {
12120
12848
  if (component !== null && VALUE_KINDS.has(component.kind)) {
12121
12849
  if (!("value" in input)) return invalid("value");
12122
- return hashValue(input.value);
12850
+ return hashValue(hashInput(component.kind, input.value));
12123
12851
  }
12124
12852
  if (typeof input.content !== "string") return invalid("content");
12125
12853
  return hashFileContent(Buffer.from(input.content, "utf8"));
@@ -12226,6 +12954,10 @@ function parseArgs(args) {
12226
12954
  options
12227
12955
  };
12228
12956
  }
12957
+ function canTuneSend(args) {
12958
+ const parsed = parseArgs(args);
12959
+ return parsed?.kind === "analyze" && !parsed.options.noSend;
12960
+ }
12229
12961
  async function underLock(deps, failure, body) {
12230
12962
  return runUntilStop(async () => {
12231
12963
  const paths = tunePaths(deps.homeDir);
@@ -12279,464 +13011,14 @@ async function tuneCommand(args, deps) {
12279
13011
  return analyzeCommand(parsed.options, deps);
12280
13012
  }
12281
13013
  //#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
13014
  //#region src/main.ts
12734
13015
  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";
13016
+ const EXIT_CLIENT_UNSUPPORTED = 5;
12736
13017
  function parseInitArgs(args) {
12737
13018
  let url;
12738
13019
  let port;
12739
13020
  let sendContent = false;
13021
+ let noPluginAutoUpdate = false;
12740
13022
  for (let i = 0; i < args.length;) {
12741
13023
  if (args[i] === "--url") {
12742
13024
  if (url !== void 0 || args[i + 1] === void 0) return void 0;
@@ -12759,12 +13041,19 @@ function parseInitArgs(args) {
12759
13041
  i += 1;
12760
13042
  continue;
12761
13043
  }
13044
+ if (args[i] === "--no-plugin-auto-update") {
13045
+ if (noPluginAutoUpdate) return void 0;
13046
+ noPluginAutoUpdate = true;
13047
+ i += 1;
13048
+ continue;
13049
+ }
12762
13050
  return;
12763
13051
  }
12764
13052
  return {
12765
13053
  url,
12766
13054
  port,
12767
- sendContent
13055
+ sendContent,
13056
+ noPluginAutoUpdate
12768
13057
  };
12769
13058
  }
12770
13059
  function parseRunArgs(args) {
@@ -12777,55 +13066,94 @@ function parseRunArgs(args) {
12777
13066
  } : void 0;
12778
13067
  }
12779
13068
  async function run(argv, deps) {
13069
+ const help = resolveHelp(argv);
13070
+ if (help !== void 0) {
13071
+ deps.stdout(help);
13072
+ return 0;
13073
+ }
12780
13074
  const [command, ...rest] = argv;
12781
13075
  if (command === "--version" && rest.length === 0) {
12782
13076
  deps.stdout(`${version}\n`);
12783
13077
  return 0;
12784
13078
  }
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, {
13079
+ const client = {
13080
+ component: "cli",
13081
+ version,
13082
+ dev: deps.devBuild
13083
+ };
13084
+ const cli = {
12792
13085
  ...deps,
12793
- git: deps.importGit,
12794
- now: () => deps.now().getTime()
12795
- });
13086
+ fetch: recordingFetch(deps.fetch, client, updateStatePath(deps.homeDir), deps.now)
13087
+ };
13088
+ const updateDeps = {
13089
+ ...cli,
13090
+ client
13091
+ };
13092
+ const unsupported = async () => (await readCliUpdateState(deps.homeDir, version))?.status === "unsupported";
13093
+ const guarded = async (command) => {
13094
+ if (await isSendingBlocked(updateDeps)) {
13095
+ await noticeUpdate(updateDeps);
13096
+ return 1;
13097
+ }
13098
+ const code = await command();
13099
+ await noticeUpdate(updateDeps);
13100
+ return code;
13101
+ };
13102
+ if (command === "otel-headers" && rest.length === 0) {
13103
+ if (await unsupported()) return EXIT_CLIENT_UNSUPPORTED;
13104
+ return otelHeaders(cli.env, cli.managedDir, cli.keychain, cli.stdout, cli.stderr, cli.endStdout);
13105
+ }
13106
+ if (command === "import" && rest.length === 0) return guarded(() => importCommand({
13107
+ ...cli,
13108
+ git: cli.importGit,
13109
+ now: () => cli.now().getTime()
13110
+ }));
13111
+ if (command === "tune") return guarded(() => tuneCommand(rest, {
13112
+ ...cli,
13113
+ git: cli.importGit,
13114
+ now: () => cli.now().getTime()
13115
+ }));
12796
13116
  if (command === "hook") {
12797
13117
  if (rest.length !== 1 || rest[0] !== "session-start") {
12798
- deps.stderr(HOOK_USAGE);
13118
+ cli.stderr(HOOK_USAGE);
12799
13119
  return 1;
12800
13120
  }
12801
13121
  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
13122
+ env: cli.env,
13123
+ homeDir: cli.homeDir,
13124
+ now: cli.now,
13125
+ git: cli.hookGit,
13126
+ fetch: cli.fetch,
13127
+ keychain: cli.keychain,
13128
+ readStdin: cli.readStdin,
13129
+ stdout: cli.stdout,
13130
+ stderr: cli.stderr,
13131
+ client,
13132
+ defaultUrl: cli.defaultUrl
12811
13133
  });
12812
13134
  }
12813
13135
  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
13136
+ homeDir: cli.homeDir,
13137
+ managedDir: cli.managedDir,
13138
+ env: cli.env,
13139
+ stdout: cli.stdout,
13140
+ stderr: cli.stderr,
13141
+ now: () => cli.now().getTime(),
13142
+ git: cli.importGit,
13143
+ top: cli.top
12822
13144
  });
12823
- if (command === "usage-limits") return usageLimitsCommand(rest, deps);
12824
13145
  const initArgs = command === "init" ? parseInitArgs(rest) : void 0;
12825
- if (initArgs) return init(initArgs, deps);
13146
+ if (initArgs) return guarded(() => init(initArgs, cli));
12826
13147
  const runArgs = command === "run" ? parseRunArgs(rest) : void 0;
12827
- if (runArgs) return runIssue(runArgs, deps);
12828
- deps.stderr(USAGE);
13148
+ if (runArgs) {
13149
+ if (await isSendingBlocked(updateDeps)) {
13150
+ await noticeUpdate(updateDeps);
13151
+ return runWithoutHarnessforce(runArgs, cli);
13152
+ }
13153
+ await noticeUpdate(updateDeps);
13154
+ return runIssue(runArgs, cli);
13155
+ }
13156
+ cli.stderr(UNKNOWN_COMMAND_HINT);
12829
13157
  return 1;
12830
13158
  }
12831
13159
  //#endregion
@@ -12836,9 +13164,9 @@ const SENDING_COMMANDS = /* @__PURE__ */ new Set([
12836
13164
  "import",
12837
13165
  "hook"
12838
13166
  ]);
12839
- const isSending = (argv) => SENDING_COMMANDS.has(argv[0] ?? "") || argv[0] === "usage-limits" && argv[1] !== "statusline";
13167
+ const isSending = (argv) => SENDING_COMMANDS.has(argv[0] ?? "") || argv[0] === "tune" && canTuneSend(argv.slice(1));
12840
13168
  const HOOK_RESTART_FAILED = "harnessforce: session registration skipped (restart failed)";
12841
- const RESTART_FAILED = "harnessforceを起動し直せませんでした";
13169
+ const RESTART_FAILED = "Could not restart harnessforce";
12842
13170
  async function relaunchHf(argv, deps) {
12843
13171
  if (!isSending(argv)) return void 0;
12844
13172
  const outcome = await relaunchWithoutRuntimeVariables({
@@ -13000,43 +13328,9 @@ function createProcessIo(proc = process) {
13000
13328
  };
13001
13329
  }
13002
13330
  //#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
13331
  //#region src/bin.ts
13038
13332
  const require = createRequire(import.meta.url);
13039
- const { url: defaultUrl } = require("./build-config.json");
13333
+ const { url: defaultUrl, isDefault } = require("./build-config.json");
13040
13334
  const CALLBACK_TIMEOUT_MS = 3e5;
13041
13335
  const argv = process.argv.slice(2);
13042
13336
  const restoredEnv = takeStashedRuntimeEnv(process.env, process.platform);
@@ -13059,6 +13353,7 @@ process.exitCode = relaunched ?? await run(argv, {
13059
13353
  }),
13060
13354
  stdout: (text) => process.stdout.write(text),
13061
13355
  stderr: (text) => process.stderr.write(text),
13356
+ endStdout: () => new Promise((resolve) => process.stdout.end(() => resolve())),
13062
13357
  fetch: (url, init) => fetch(url, init),
13063
13358
  openBrowser: (url) => openBrowser(url, {
13064
13359
  platform: process.platform,
@@ -13070,6 +13365,7 @@ process.exitCode = relaunched ?? await run(argv, {
13070
13365
  homeDir: homedir(),
13071
13366
  managedDir: managedDirFor(process.platform),
13072
13367
  defaultUrl,
13368
+ devBuild: !isDefault,
13073
13369
  callbackTimeoutMs: CALLBACK_TIMEOUT_MS,
13074
13370
  cwd: process.cwd(),
13075
13371
  git: createGitRunner({
@@ -13096,19 +13392,6 @@ process.exitCode = relaunched ?? await run(argv, {
13096
13392
  }),
13097
13393
  sleep: (ms) => setTimeout$1(ms),
13098
13394
  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
13395
  readStdin: async (maxBytes) => {
13113
13396
  const chunks = [];
13114
13397
  let size = 0;