@harnessforce/cli 0.1.0 → 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 +43 -34
  2. package/dist/bin.js +1386 -835
  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;
@@ -414,7 +413,54 @@ function takeStashedRuntimeEnv(env, platform) {
414
413
  return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string" && isRuntimeVariable(entry[0], platform)));
415
414
  }
416
415
  //#endregion
416
+ //#region src/shared/messages.ts
417
+ const INIT_MESSAGES = {
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
+ };
444
+ const codexNotes = (configPath) => [
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
+ ].join("\n");
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;
452
+ //#endregion
417
453
  //#region src/credentials/keychain.ts
454
+ async function openSingleStore(store) {
455
+ if (!await store.isAvailable()) return void 0;
456
+ return {
457
+ isKernelKeyring: false,
458
+ items: await store.list(),
459
+ set: (account, secret) => store.set(account, secret),
460
+ delete: (account) => store.delete(account),
461
+ deleteElsewhere: async () => {}
462
+ };
463
+ }
418
464
  const KEYCHAIN_SERVICE = "harnessforce";
419
465
  const INGEST_KEY_SUFFIX = ":ingest-key";
420
466
  const API_TOKEN_SUFFIX = ":api-token";
@@ -422,27 +468,200 @@ const ingestKeyAccount = (workspaceId) => `${workspaceId}${INGEST_KEY_SUFFIX}`;
422
468
  const apiTokenAccount = (workspaceId) => `${workspaceId}${API_TOKEN_SUFFIX}`;
423
469
  const ingestOriginAccount = (workspaceId) => `${workspaceId}:ingest-origin`;
424
470
  const urlOriginAccount = (workspaceId) => `${workspaceId}:url-origin`;
471
+ const workspaceAccounts = (workspaceId) => [
472
+ ingestKeyAccount(workspaceId),
473
+ apiTokenAccount(workspaceId),
474
+ ingestOriginAccount(workspaceId),
475
+ urlOriginAccount(workspaceId)
476
+ ];
425
477
  const isIngestKeyAccount = (account) => account.endsWith(INGEST_KEY_SUFFIX);
426
478
  const apiTokenWorkspaceId = (account) => account.endsWith(API_TOKEN_SUFFIX) ? account.slice(0, -10) : void 0;
427
479
  //#endregion
480
+ //#region src/credentials/linux-keychain.ts
481
+ const SECRET_SERVICE_TIMEOUT_MS = 6e4;
482
+ const WAITING_NOTICE_DELAY_MS = 2e3;
483
+ const PROBE_ACCOUNT$1 = "availability-probe";
484
+ const PROBE_VALUE = "harnessforce";
485
+ const MIGRATION_PROBE_TIMEOUT_MS = 200;
486
+ const MIGRATION_BUDGET_MS = 1e3;
487
+ const CLEANUP_TIMEOUT_MS = 200;
488
+ const succeeds = (operation) => operation().then(() => true, () => false);
489
+ const workspaceOf = (account) => account.slice(0, account.lastIndexOf(":"));
490
+ function createLinuxKeychain(options) {
491
+ return build(options, SECRET_SERVICE_TIMEOUT_MS);
492
+ }
493
+ function build(options, timeoutMs) {
494
+ const { kernel, secretService } = options;
495
+ const locations = /* @__PURE__ */ new Map();
496
+ const locate = (workspaceId) => {
497
+ let located = locations.get(workspaceId);
498
+ if (!located) {
499
+ located = kernel.get(ingestKeyAccount(workspaceId), timeoutMs).then((key) => ({
500
+ location: key ? "kernel" : "secret-service",
501
+ isKernelBroken: false
502
+ }), () => ({
503
+ location: "secret-service",
504
+ isKernelBroken: true
505
+ }));
506
+ locations.set(workspaceId, located);
507
+ }
508
+ return located;
509
+ };
510
+ const storeAt = (location) => location === "kernel" ? kernel : secretService;
511
+ return {
512
+ unavailableHint: options.unavailableHint,
513
+ async isAvailable() {
514
+ return await succeeds(() => kernel.get(PROBE_ACCOUNT$1, timeoutMs)) || await succeeds(() => secretService.get(PROBE_ACCOUNT$1, timeoutMs));
515
+ },
516
+ async get(account) {
517
+ const { location, isKernelBroken } = await locate(workspaceOf(account));
518
+ if (location === "kernel") return kernel.get(account, timeoutMs);
519
+ try {
520
+ return await secretService.get(account, timeoutMs);
521
+ } catch (error) {
522
+ if (isKernelBroken) throw error;
523
+ return;
524
+ }
525
+ },
526
+ async set(account, secret) {
527
+ const { location } = await locate(workspaceOf(account));
528
+ await storeAt(location).set(account, secret, timeoutMs);
529
+ },
530
+ async delete(account) {
531
+ const { location } = await locate(workspaceOf(account));
532
+ await storeAt(location).delete(account, timeoutMs);
533
+ },
534
+ async migrateFromKernel(workspaceId) {
535
+ const { location } = await locate(workspaceId);
536
+ if (location !== "kernel") return;
537
+ await migrate(options, workspaceId).catch(() => {});
538
+ },
539
+ withTimeout: (ms) => build(options, ms),
540
+ openForInit: (init) => openForInit(options, timeoutMs, init)
541
+ };
542
+ }
543
+ async function openForInit({ kernel, secretService }, timeoutMs, { previousWorkspaceId, onWaiting }) {
544
+ const canWrite = (store) => succeeds(async () => {
545
+ await store.set(PROBE_ACCOUNT$1, PROBE_VALUE, timeoutMs);
546
+ await store.delete(PROBE_ACCOUNT$1, timeoutMs);
547
+ });
548
+ const notice = setTimeout(onWaiting, WAITING_NOTICE_DELAY_MS);
549
+ const usesSecretService = await canWrite(secretService).finally(() => clearTimeout(notice));
550
+ const readKernel = async () => {
551
+ if (!previousWorkspaceId) return [];
552
+ const accounts = [ingestKeyAccount(previousWorkspaceId), apiTokenAccount(previousWorkspaceId)];
553
+ const items = [];
554
+ for (const account of accounts) {
555
+ const secret = await kernel.get(account, timeoutMs);
556
+ if (secret !== void 0) items.push({
557
+ account,
558
+ secret
559
+ });
560
+ }
561
+ return items;
562
+ };
563
+ if (usesSecretService) return {
564
+ isKernelKeyring: false,
565
+ items: [...await secretService.list(timeoutMs), ...await readKernel().catch(() => [])],
566
+ set: (account, secret) => secretService.set(account, secret, timeoutMs),
567
+ delete: (account) => secretService.delete(account, timeoutMs),
568
+ async deleteElsewhere(workspaceId) {
569
+ for (const account of workspaceAccounts(workspaceId)) await kernel.delete(account, timeoutMs).catch(() => {});
570
+ }
571
+ };
572
+ if (!await canWrite(kernel)) return void 0;
573
+ return {
574
+ isKernelKeyring: true,
575
+ items: await readKernel(),
576
+ set: (account, secret) => kernel.set(account, secret, timeoutMs),
577
+ delete: (account) => kernel.delete(account, timeoutMs),
578
+ deleteElsewhere: async () => {}
579
+ };
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
+ }
634
+ //#endregion
428
635
  //#region src/credentials/os-keychain.ts
429
- const SUPPORTED_PLATFORMS = /* @__PURE__ */ new Set([
430
- "darwin",
431
- "linux",
432
- "win32"
433
- ]);
434
- const ENTRY_OPTIONS = { linux: { store: "secret-service" } };
435
636
  const PROBE_ACCOUNT = "availability-probe";
436
- function createOsKeychain({ platform, load }) {
637
+ function createOsKeychain({ platform, load, secretService }) {
437
638
  let loaded;
438
639
  const keyring = () => {
439
640
  loaded ??= load();
440
641
  return loaded;
441
642
  };
442
- const entry = async (account) => new (await (keyring())).Entry(KEYCHAIN_SERVICE, account, ENTRY_OPTIONS);
643
+ if (platform === "linux") return createLinuxKeychain({
644
+ kernel: kernelKeyring(keyring),
645
+ secretService,
646
+ unavailableHint: INIT_MESSAGES.linuxKeychainUnavailable
647
+ });
648
+ if (platform === "darwin" || platform === "win32") return singleStore(keyring);
649
+ return unsupported;
650
+ }
651
+ function kernelKeyring(keyring) {
652
+ const entry = async (account) => new (await (keyring())).Entry(KEYCHAIN_SERVICE, account, { linux: { store: "keyutils" } });
443
653
  return {
654
+ get: async (account) => (await entry(account)).getPassword() ?? void 0,
655
+ set: async (account, secret) => (await entry(account)).setPassword(secret),
656
+ delete: async (account) => {
657
+ (await entry(account)).deleteCredential();
658
+ }
659
+ };
660
+ }
661
+ function singleStore(keyring) {
662
+ const entry = async (account) => new (await (keyring())).Entry(KEYCHAIN_SERVICE, account);
663
+ const keychain = {
444
664
  async isAvailable() {
445
- if (!SUPPORTED_PLATFORMS.has(platform)) return false;
446
665
  try {
447
666
  (await entry(PROBE_ACCOUNT)).getPassword();
448
667
  return true;
@@ -450,17 +669,121 @@ function createOsKeychain({ platform, load }) {
450
669
  return false;
451
670
  }
452
671
  },
453
- async get(account) {
454
- return (await entry(account)).getPassword() ?? void 0;
672
+ get: async (account) => (await entry(account)).getPassword() ?? void 0,
673
+ set: async (account, secret) => (await entry(account)).setPassword(secret),
674
+ delete: async (account) => {
675
+ (await entry(account)).deleteCredential();
455
676
  },
456
- async set(account, secret) {
457
- (await entry(account)).setPassword(secret);
677
+ withTimeout: (ms) => limitedSingleStore(keychain, keyring, ms),
678
+ openForInit: () => openSingleStore({
679
+ ...keychain,
680
+ list: async () => (await keyring()).findCredentials(KEYCHAIN_SERVICE).map(({ account, password }) => ({
681
+ account,
682
+ secret: password
683
+ }))
684
+ })
685
+ };
686
+ return keychain;
687
+ }
688
+ function limitedSingleStore(base, keyring, ms) {
689
+ const within = (operation) => async (account) => {
690
+ const entry = new (await (keyring())).AsyncEntry(KEYCHAIN_SERVICE, account);
691
+ let timer;
692
+ const timeout = new Promise((_, reject) => {
693
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("keychain timed out")), ms);
694
+ });
695
+ return Promise.race([operation(entry), timeout]).finally(() => clearTimeout(timer));
696
+ };
697
+ return {
698
+ ...base,
699
+ get: async (account) => await within((entry) => entry.getPassword())(account) ?? void 0,
700
+ set: (account, secret) => within((entry) => entry.setPassword(secret))(account),
701
+ delete: async (account) => {
702
+ await within((entry) => entry.deleteCredential())(account);
703
+ }
704
+ };
705
+ }
706
+ const unsupportedError = async () => {
707
+ throw new Error("no keychain on this platform");
708
+ };
709
+ const unsupported = {
710
+ isAvailable: async () => false,
711
+ get: unsupportedError,
712
+ set: unsupportedError,
713
+ delete: unsupportedError,
714
+ withTimeout: () => unsupported,
715
+ openForInit: async () => void 0
716
+ };
717
+ //#endregion
718
+ //#region src/credentials/secret-service-process.ts
719
+ const CHILD_SCRIPT = `
720
+ const [op, service, account, timeoutMs, modulePath] = process.argv.slice(1);
721
+ setTimeout(() => process.kill(process.pid, "SIGKILL"), Number(timeoutMs));
722
+ const chunks = [];
723
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
724
+ process.stdin.on("end", async () => {
725
+ try {
726
+ const keyring = require(modulePath);
727
+ const options = { linux: { store: "secret-service" } };
728
+ let result = null;
729
+ if (op === "list") {
730
+ result = await keyring.findCredentialsAsync(service);
731
+ } else {
732
+ const entry = new keyring.AsyncEntry(service, account, options);
733
+ if (op === "get") result = (await entry.getPassword()) ?? null;
734
+ if (op === "set") await entry.setPassword(Buffer.concat(chunks).toString("utf8"));
735
+ if (op === "delete") await entry.deleteCredential();
736
+ }
737
+ process.stdout.write(JSON.stringify({ result }), () => process.exit(0));
738
+ } catch {
739
+ process.exit(1);
740
+ }
741
+ });
742
+ `;
743
+ function createSecretServiceProcess({ execPath, resolveKeyring }) {
744
+ const run = (op, account, timeoutMs, input = "") => new Promise((resolve, reject) => {
745
+ const child = spawn(execPath, [
746
+ "-e",
747
+ CHILD_SCRIPT,
748
+ op,
749
+ KEYCHAIN_SERVICE,
750
+ account,
751
+ String(timeoutMs),
752
+ resolveKeyring()
753
+ ], { stdio: [
754
+ "pipe",
755
+ "pipe",
756
+ "ignore"
757
+ ] });
758
+ const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
759
+ const chunks = [];
760
+ child.stdout.on("data", (chunk) => chunks.push(chunk));
761
+ child.on("error", reject);
762
+ child.on("close", (code) => {
763
+ clearTimeout(timer);
764
+ if (code !== 0) return reject(/* @__PURE__ */ new Error(`Secret Service ${op} failed`));
765
+ try {
766
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")).result);
767
+ } catch (error) {
768
+ reject(error);
769
+ }
770
+ });
771
+ child.stdin.on("error", () => {});
772
+ child.stdin.end(input);
773
+ });
774
+ return {
775
+ async get(account, timeoutMs) {
776
+ const secret = await run("get", account, timeoutMs);
777
+ return typeof secret === "string" ? secret : void 0;
458
778
  },
459
- async delete(account) {
460
- (await entry(account)).deleteCredential();
779
+ async set(account, secret, timeoutMs) {
780
+ await run("set", account, timeoutMs, secret);
781
+ },
782
+ async delete(account, timeoutMs) {
783
+ await run("delete", account, timeoutMs);
461
784
  },
462
- async list() {
463
- return (await keyring()).findCredentials(KEYCHAIN_SERVICE).map(({ account, password }) => ({
785
+ async list(timeoutMs) {
786
+ return (await run("list", "", timeoutMs)).map(({ account, password }) => ({
464
787
  account,
465
788
  secret: password
466
789
  }));
@@ -519,9 +842,9 @@ async function openBrowser(url, { platform, env, cwd, restoredEnv, spawn: spawnI
519
842
  //#region src/init/loopback.ts
520
843
  const HOST = "127.0.0.1";
521
844
  const CALLBACK_PATH = "/callback";
522
- const page = (message) => `<!doctype html><html lang="ja"><meta charset="utf-8"><title>Harnessforce</title><p>${message}</p></html>`;
523
- const SUCCESS_PAGE = page("ターミナルに戻ってください");
524
- 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");
525
848
  async function startLoopback(options) {
526
849
  let settle = () => {};
527
850
  const callback = new Promise((resolve) => {
@@ -569,11 +892,6 @@ async function startLoopback(options) {
569
892
  };
570
893
  }
571
894
  //#endregion
572
- //#region ../agent-core/src/hook-warning.ts
573
- const RESTART_FAILED_WARNING = "Harnessforceのhookを起動し直せませんでした";
574
- const INVALID_ENDPOINT_WARNING = "Harnessforceの送信先が不正です。`harnessforce init`を実行し直してください";
575
- const notConfiguredWarning = (agent) => agent === "claude_code" ? "Harnessforceの送信が設定されていません。`/harnessforce:setup`を実行してください" : "Harnessforceの送信が設定されていません。`harnessforce init`を実行してください";
576
- //#endregion
577
895
  //#region ../agent-core/src/url.ts
578
896
  const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
579
897
  "localhost",
@@ -597,8 +915,168 @@ function underBase(base, path) {
597
915
  }
598
916
  const withoutExtras = (url) => `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
599
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
600
1078
  //#region ../agent-core/src/ingest.ts
601
- const SEND_TIMEOUT_MS$1 = 2e3;
1079
+ const SEND_TIMEOUT_MS = 2e3;
602
1080
  function ingestBaseFrom(endpoint) {
603
1081
  const base = parseAllowedUrl(endpoint);
604
1082
  if (!base) return void 0;
@@ -606,8 +1084,8 @@ function ingestBaseFrom(endpoint) {
606
1084
  ingestBase.pathname = base.pathname.replace(/\/+$/, "");
607
1085
  return ingestBase;
608
1086
  }
609
- const postItem = (destination, path, item, fetchImpl, timeoutMs = SEND_TIMEOUT_MS$1) => postItems(destination, path, [item], fetchImpl, timeoutMs);
610
- 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) {
611
1089
  try {
612
1090
  const response = await fetchImpl(underBase(destination.ingestBase, path), {
613
1091
  method: "POST",
@@ -633,68 +1111,37 @@ async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_T
633
1111
  }
634
1112
  }
635
1113
  //#endregion
636
- //#region ../agent-core/src/issue.ts
637
- const isIssueIdentifier = (value) => isToken(value, 300);
638
- //#endregion
639
- //#region ../agent-core/src/using-harnessforce.ts
640
- 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();
641
- //#endregion
642
- //#region ../agent-core/src/session-context.ts
643
- const sessionContextLine = (sessionId) => `harnessforce session_id: ${sessionId}`;
644
- const sessionContext = (sessionId) => ({ hookSpecificOutput: {
645
- hookEventName: "SessionStart",
646
- additionalContext: `${sessionContextLine(sessionId)}\n\n${usingHarnessforceBody}`
647
- } });
648
- //#endregion
649
- //#region ../agent-core/src/vcs.ts
650
- async function selectRemote(cwd, git) {
651
- if (await git(cwd, ["rev-parse", "--is-inside-work-tree"]) !== "true") return void 0;
652
- const remotes = (await git(cwd, ["remote"]))?.split("\n") ?? [];
653
- return remotes.includes("origin") ? "origin" : remotes[0];
654
- }
655
- async function resolveVcs(cwd, git) {
656
- const remote = await selectRemote(cwd, git);
657
- if (!remote) return void 0;
658
- const [url, branch, commit] = await Promise.all([
659
- git(cwd, [
660
- "remote",
661
- "get-url",
662
- remote
663
- ]),
664
- git(cwd, [
665
- "symbolic-ref",
666
- "--quiet",
667
- "--short",
668
- "HEAD"
669
- ]),
670
- git(cwd, [
671
- "rev-parse",
672
- "--verify",
673
- "--quiet",
674
- "HEAD"
675
- ])
676
- ]);
677
- const repository = url === void 0 ? void 0 : normalizeRepository(url);
678
- return {
679
- ...repository === void 0 ? {} : { repository },
680
- ...branch === void 0 ? {} : { branch },
681
- ...commit === void 0 ? {} : { commit }
682
- };
683
- }
684
- async function resolveRepository(cwd, git) {
685
- if (!isAbsolute(cwd)) return void 0;
686
- const remote = await selectRemote(cwd, git);
687
- if (!remote) return void 0;
688
- const url = await git(cwd, [
689
- "remote",
690
- "get-url",
691
- remote
692
- ]);
693
- 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;
694
1123
  }
695
- async function resolveProjectRoot(cwd, git) {
696
- const topLevel = await git(cwd, ["rev-parse", "--show-toplevel"]);
697
- 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;
698
1145
  }
699
1146
  //#endregion
700
1147
  //#region ../agent-core/src/config/scope.ts
@@ -724,7 +1171,7 @@ var Sink = class {
724
1171
  });
725
1172
  return {
726
1173
  guard: this.guard,
727
- addValue: (kind, id, value) => add(kind, id, hashValue(value)),
1174
+ addValue: (kind, id, value) => add(kind, id, hashValue(hashInput(kind, value))),
728
1175
  addFile: async (kind, id, path) => {
729
1176
  const content = await readFileIfExists(path, this.guard);
730
1177
  if (content) add(kind, id, hashFileContent(content));
@@ -754,6 +1201,9 @@ function collectMcpServers$1(s, servers) {
754
1201
  for (const [name, value] of Object.entries(servers)) if (isObject(value)) s.addValue("mcp_server", name, value);
755
1202
  }
756
1203
  //#endregion
1204
+ //#region ../agent-core/src/plugin-auto-update.ts
1205
+ const MARKETPLACE_NAME = "harnessforce-agent";
1206
+ //#endregion
757
1207
  //#region src/shared/settings.ts
758
1208
  const OTEL_HEADERS_HELPER = "harnessforce otel-headers";
759
1209
  const PLUGIN_ID$1 = "harnessforce@harnessforce-agent";
@@ -782,6 +1232,37 @@ async function readUserSettings(path) {
782
1232
  return { kind: "invalid" };
783
1233
  }
784
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
+ }
785
1266
  function mergeUserSettings(settings, env) {
786
1267
  return {
787
1268
  ...settings,
@@ -834,6 +1315,399 @@ async function resolveCliDestinations(env, settingsPath, defaultUrl) {
834
1315
  };
835
1316
  }
836
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
837
1711
  //#region src/credentials/user-destination.ts
838
1712
  async function resolveUserDestination(deps) {
839
1713
  const { workspaceId, ingestEndpoint } = await resolveCliDestinations(deps.env, userSettingsPath(deps.env, deps.homeDir), "");
@@ -849,7 +1723,7 @@ async function resolveUserDestination(deps) {
849
1723
  };
850
1724
  if (!await deps.keychain.isAvailable()) return {
851
1725
  kind: "skipped",
852
- reason: "no ingest key"
1726
+ reason: "keychain unreadable"
853
1727
  };
854
1728
  const key = await deps.keychain.get(ingestKeyAccount(workspaceId));
855
1729
  if (!key) return {
@@ -872,9 +1746,10 @@ async function resolveUserDestination(deps) {
872
1746
  //#endregion
873
1747
  //#region src/hook/session-start.ts
874
1748
  const HOOK_USAGE = "Usage: harnessforce hook session-start\n";
875
- const MAX_INPUT_BYTES$1 = 1048576;
1749
+ const MAX_INPUT_BYTES = 1048576;
876
1750
  const REGISTERING_SOURCES = /* @__PURE__ */ new Set(["startup", "clear"]);
877
- const REVOKED_MESSAGE = "送信キーが失効しています。`harnessforce init`を実行してください";
1751
+ const KEYCHAIN_READ_TIMEOUT_MS = 1e3;
1752
+ const REVOKED_MESSAGE = "Your ingest key has been revoked. Run `harnessforce init`";
878
1753
  function parseInput(raw) {
879
1754
  let fields;
880
1755
  try {
@@ -891,17 +1766,40 @@ function parseInput(raw) {
891
1766
  source: typeof source === "string" ? source : void 0
892
1767
  };
893
1768
  }
894
- const report$1 = (deps, detail) => deps.stderr(`harnessforce: session registration ${detail}\n`);
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
+ };
1777
+ function resolveWithinLimit(deps) {
1778
+ let timer;
1779
+ const slow = new Promise((resolve) => {
1780
+ timer = setTimeout(() => resolve({
1781
+ kind: "skipped",
1782
+ reason: "keychain unreadable"
1783
+ }), KEYCHAIN_READ_TIMEOUT_MS);
1784
+ });
1785
+ return Promise.race([resolveUserDestination({
1786
+ ...deps,
1787
+ keychain: deps.keychain.withTimeout(KEYCHAIN_READ_TIMEOUT_MS)
1788
+ }).catch(() => ({
1789
+ kind: "skipped",
1790
+ reason: "keychain unreadable"
1791
+ })), slow]).finally(() => clearTimeout(timer));
1792
+ }
895
1793
  async function resolveDestination(deps) {
896
- const resolved = await resolveUserDestination(deps);
1794
+ const resolved = await resolveWithinLimit(deps);
897
1795
  if (resolved.kind === "ok") return { destination: resolved.destination };
898
1796
  if (resolved.kind === "none") {
899
1797
  report$1(deps, "skipped (not configured)");
900
1798
  return { warning: notConfiguredWarning("codex") };
901
1799
  }
902
- report$1(deps, `skipped (${resolved.reason})`);
903
- if (resolved.reason === "not configured") return { warning: notConfiguredWarning("codex") };
904
- 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] };
905
1803
  }
906
1804
  function registrationSource(env) {
907
1805
  const issue = env.HARNESSFORCE_ISSUE;
@@ -926,20 +1824,32 @@ async function register(input, deps) {
926
1824
  if (outcome.kind === "failed") report$1(deps, `failed (${outcome.reason})`);
927
1825
  return { outcome };
928
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
+ }
929
1838
  async function sessionStart(deps) {
930
- 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(() => ""));
931
1840
  if (!input) return 0;
932
- 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) => {
933
1844
  report$1(deps, `failed (${error instanceof Error ? error.name : "unknown"})`);
934
1845
  return {};
935
1846
  }) : {};
936
- const warning = registered.outcome?.kind === "unauthorized" ? REVOKED_MESSAGE : registered.warning ?? (registered.outcome?.kind === "failed" ? "Harnessforceへのsession登録に失敗しました。通信を確かめてください" : void 0);
937
- if (warning !== void 0) {
938
- deps.stderr(`${warning}\n`);
939
- deps.stdout(`${sessionContextLine(input.sessionId)}\n${usingHarnessforceBody}\n${warning}\n`);
940
- return 0;
941
- }
942
- 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`);
943
1853
  return 0;
944
1854
  }
945
1855
  //#endregion
@@ -998,7 +1908,7 @@ function parseStoredApiToken(secret, workspaceId) {
998
1908
  async function verifyCliAccess(deps, messages) {
999
1909
  const { keychain } = deps;
1000
1910
  const stop = (message) => stopWith(messages[message]);
1001
- if (!await keychain.isAvailable().catch(() => false)) stop("keychainUnavailable");
1911
+ if (!await keychain.isAvailable().catch(() => false)) stopWith(keychainUnavailableMessage(keychain, messages.keychainUnavailable));
1002
1912
  const read = (account) => keychain.get(account).catch(() => stop("keychainUnavailable"));
1003
1913
  const destinations = await resolveCliDestinations(deps.env, userSettingsPath(deps.env, deps.homeDir), deps.defaultUrl);
1004
1914
  const workspaceId = destinations.workspaceId ?? stop("initRequired");
@@ -1072,7 +1982,7 @@ const LOCK_WAIT_MS$1 = 3e4;
1072
1982
  const STALE_LOCK_MS$1 = 6e4;
1073
1983
  const RETRY_INTERVAL_MS$1 = 100;
1074
1984
  const DIR_MODE$2 = 448;
1075
- const FILE_MODE$5 = 384;
1985
+ const FILE_MODE$4 = 384;
1076
1986
  const isAlreadyExists$1 = (error) => error?.code === "EEXIST";
1077
1987
  async function removeIfStale$1(path, now) {
1078
1988
  const created = await stat(path).catch(() => void 0);
@@ -1087,7 +1997,7 @@ async function acquireTokenLock(path, deps) {
1087
1997
  const deadline = deps.now() + LOCK_WAIT_MS$1;
1088
1998
  while (deps.now() < deadline) {
1089
1999
  try {
1090
- await (await open(path, "wx", FILE_MODE$5)).close();
2000
+ await (await open(path, "wx", FILE_MODE$4)).close();
1091
2001
  return () => rm(path, { force: true });
1092
2002
  } catch (error) {
1093
2003
  if (!isAlreadyExists$1(error)) throw error;
@@ -1099,6 +2009,7 @@ async function acquireTokenLock(path, deps) {
1099
2009
  //#endregion
1100
2010
  //#region src/credentials/api-token-session.ts
1101
2011
  const CLIENT_ID$1 = "harnessforce-cli";
2012
+ const LOCKED_KEYCHAIN_TIMEOUT_MS = 1e4;
1102
2013
  const isPast = (instant, now) => now >= Date.parse(instant);
1103
2014
  async function requestRefresh(endpoint, refreshToken, workspaceId, fetchImpl) {
1104
2015
  try {
@@ -1127,11 +2038,11 @@ async function requestRefresh(endpoint, refreshToken, workspaceId, fetchImpl) {
1127
2038
  return { kind: "failed" };
1128
2039
  }
1129
2040
  }
1130
- function tokenStore(deps) {
2041
+ function tokenStore(deps, keychain) {
1131
2042
  const account = apiTokenAccount(deps.workspaceId);
1132
2043
  return {
1133
- read: async () => parseStoredApiToken(await deps.keychain.get(account).catch(() => void 0), deps.workspaceId),
1134
- save: (token) => deps.keychain.set(account, serializeApiToken(token)).then(() => true).catch(() => false)
2044
+ read: async () => parseStoredApiToken(await keychain.get(account).catch(() => void 0), deps.workspaceId),
2045
+ save: (token) => keychain.set(account, serializeApiToken(token)).then(() => true).catch(() => false)
1135
2046
  };
1136
2047
  }
1137
2048
  async function exchange(stored, deps, store) {
@@ -1144,7 +2055,8 @@ async function exchange(stored, deps, store) {
1144
2055
  const reread = await store.read();
1145
2056
  return reread && reread.refreshToken !== stored.refreshToken ? reread : void 0;
1146
2057
  }
1147
- async function refreshUnderLock(used, deps, store) {
2058
+ async function refreshUnderLock(used, deps) {
2059
+ const store = tokenStore(deps, deps.keychain.withTimeout(LOCKED_KEYCHAIN_TIMEOUT_MS));
1148
2060
  const release = await acquireTokenLock(tokenLockPath(deps.homeDir), {
1149
2061
  now: deps.now,
1150
2062
  sleep: deps.sleep
@@ -1159,8 +2071,8 @@ async function refreshUnderLock(used, deps, store) {
1159
2071
  }
1160
2072
  }
1161
2073
  function createApiTokenSession(deps) {
1162
- const store = tokenStore(deps);
1163
- const refresh = (used) => refreshUnderLock(used, deps, store);
2074
+ const store = tokenStore(deps, deps.keychain);
2075
+ const refresh = (used) => refreshUnderLock(used, deps);
1164
2076
  let current;
1165
2077
  async function withToken(request, isUnauthorized) {
1166
2078
  let token = current ?? await store.read();
@@ -1206,37 +2118,6 @@ function createApiTokenSession(deps) {
1206
2118
  };
1207
2119
  }
1208
2120
  //#endregion
1209
- //#region src/shared/messages.ts
1210
- const INIT_MESSAGES = {
1211
- success: "Harnessforceに接続しました。Claude Codeを再起動すると設定が反映されます",
1212
- invalidUrl: "接続先のURLが不正です",
1213
- keychainUnavailable: "OSのキーチェーンを利用できないため、送信キーを保存できません",
1214
- tooManyKeys: "keychainにある送信キーが多すぎるため、`harnessforce init`を実行できません",
1215
- tooManyCredentials: "keychainにある送信キーまたはログインの情報が多すぎるため、`harnessforce init`を実行できません",
1216
- settingsUnreadable: "Claude Codeのuser settingsを読めません",
1217
- network: "Harnessforceとの通信に失敗しました。もう一度`harnessforce init`を実行してください",
1218
- listenFailed: "ログインの待ち受けを開始できませんでした",
1219
- timeout: "ログインが時間内に完了しませんでした。もう一度`harnessforce init`を実行してください",
1220
- accessDenied: "ログインが拒否されました",
1221
- noWorkspace: "参加しているWorkspaceがありません。Workspaceを作成するか招待を受諾してから、もう一度`harnessforce init`を実行してください",
1222
- loginFailed: "ログインに失敗しました。もう一度`harnessforce init`を実行してください",
1223
- viewer: "閲覧のみのロールでは送信キーを発行できません。Workspaceの管理者に連絡してください",
1224
- limitOwnerAdmin: "有効な送信キーの数が上限に達しました。送信キーの画面で使っていないキーを失効してください",
1225
- limitMember: "送信キーの上限に達しています。Workspaceの管理者に連絡してください",
1226
- gateUnavailable: "受信側へ反映できませんでした。変更はしていません。もう一度お試しください",
1227
- ingestUnavailable: "この環境はテレメトリを受信しないため、送信キーを発行できません",
1228
- saveFailed: "送信キーを保存できませんでした。もう一度`harnessforce init`を実行してください",
1229
- codexSaveFailed: "Codexの設定を書き込めませんでした。もう一度`harnessforce init`を実行してください",
1230
- contentEnabled: "入力した指示の本文を送る設定を有効にしました。パターンに一致した秘密値と個人情報を伏せ字にしますが、すべてを検出できるとは限りません。保存した本文は、Workspaceのメンバーの中ではあなただけが読め、本文データの保持期間(最大30日)が過ぎると削除されます",
1231
- contentNotOptedIn: "このWorkspaceは本文データをopt-inしていないため、本文を送る設定は有効にしませんでした。Workspaceの設定のデータの保持でopt-inしてから、もう一度`harnessforce init --send-content`を実行してください"
1232
- };
1233
- const codexNotes = (configPath) => [
1234
- `Codexの設定を書き込みました(${configPath})。Codexを再起動すると反映されます`,
1235
- "Codexのhookは、Codexの/hooksで内容をtrustするまで実行されません。/hooksでharnessforceのSessionStartのhookをtrustしてください",
1236
- "HarnessforceのMCPを使う場合は、`codex mcp login harnessforce`でログインしてください"
1237
- ].join("\n");
1238
- const listenFailedMessage = (port) => `${INIT_MESSAGES.listenFailed}。ポート ${port}を使っているプロセスを終えるか、\`harnessforce init --port\`で別のポートを指定してください`;
1239
- //#endregion
1240
2121
  //#region src/import/read-api.ts
1241
2122
  const FAILED$1 = { kind: "failed" };
1242
2123
  async function getJson(url, apiToken, fetchImpl) {
@@ -1333,7 +2214,7 @@ async function recordBase(deps) {
1333
2214
  //#region src/import/state.ts
1334
2215
  const STATE_VERSION = 1;
1335
2216
  const DIR_MODE$1 = 448;
1336
- const FILE_MODE$4 = 384;
2217
+ const FILE_MODE$3 = 384;
1337
2218
  const sessionStateKey = (session) => session.agent === "claude_code" ? session.session_id : `${session.agent}:${session.session_id}`;
1338
2219
  const importStatePath = (homeDir) => join(homeDir, ".harnessforce", "import-state.json");
1339
2220
  const keyOf = ({ workspaceId, endpoint }) => `${workspaceId} ${endpoint}`;
@@ -1378,10 +2259,10 @@ async function recordSentSessions(path, destination, sessionIds) {
1378
2259
  const temporary = `${path}.${randomUUID()}.tmp`;
1379
2260
  try {
1380
2261
  await writeFile(temporary, `${JSON.stringify(next)}\n`, {
1381
- mode: FILE_MODE$4,
2262
+ mode: FILE_MODE$3,
1382
2263
  flag: "wx"
1383
2264
  });
1384
- await chmod(temporary, FILE_MODE$4);
2265
+ await chmod(temporary, FILE_MODE$3);
1385
2266
  await rename(temporary, path);
1386
2267
  } finally {
1387
2268
  await rm(temporary, { force: true });
@@ -1719,7 +2600,7 @@ async function parseRollout(path) {
1719
2600
  const accumulator = new Accumulator$1();
1720
2601
  let skippedLines = 0;
1721
2602
  try {
1722
- const lines = createInterface$1({
2603
+ const lines = createInterface({
1723
2604
  input: createReadStream(path, { encoding: "utf8" }),
1724
2605
  crlfDelay: Number.POSITIVE_INFINITY
1725
2606
  });
@@ -1948,7 +2829,7 @@ async function readTranscript(path, accumulator) {
1948
2829
  let lines = 0;
1949
2830
  let skippedLines = 0;
1950
2831
  try {
1951
- const reader = createInterface$1({
2832
+ const reader = createInterface({
1952
2833
  input: createReadStream(path, { encoding: "utf8" }),
1953
2834
  crlfDelay: Number.POSITIVE_INFINITY
1954
2835
  });
@@ -2023,6 +2904,11 @@ async function parseTranscriptUsage(path) {
2023
2904
  }
2024
2905
  //#endregion
2025
2906
  //#region src/import/sessions.ts
2907
+ const noneNotImported = () => ({
2908
+ outOfRange: 0,
2909
+ outsideGit: 0,
2910
+ notConnected: 0
2911
+ });
2026
2912
  const readDir$1 = (dir) => readdir(dir, { withFileTypes: true }).catch(() => []);
2027
2913
  async function listTranscripts(projectsDir) {
2028
2914
  const projects = (await readDir$1(projectsDir)).filter((entry) => entry.isDirectory());
@@ -2049,33 +2935,49 @@ function toSessionImport(session, repository) {
2049
2935
  };
2050
2936
  }
2051
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));
2052
- async function scanSessions(options) {
2938
+ const cachedRepositoryOf = (git) => {
2053
2939
  const repositories = /* @__PURE__ */ new Map();
2054
- const repositoryOf = (cwd) => {
2055
- const cached = repositories.get(cwd) ?? resolveRepository(cwd, options.git);
2940
+ return (cwd) => {
2941
+ const cached = repositories.get(cwd) ?? resolveRepository(cwd, git);
2056
2942
  repositories.set(cwd, cached);
2057
2943
  return cached;
2058
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);
2059
2957
  const sessions = /* @__PURE__ */ new Map();
2958
+ const notImported = noneNotImported();
2060
2959
  let skippedLines = 0;
2061
2960
  let skippedFiles = 0;
2062
2961
  for (const file of await listTranscripts(options.projectsDir)) {
2063
2962
  const modifiedMs = (await stat(file).catch(() => void 0))?.mtimeMs;
2064
- if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) continue;
2963
+ if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) {
2964
+ notImported.outOfRange += 1;
2965
+ continue;
2966
+ }
2065
2967
  const result = await parseTranscript(file);
2066
2968
  skippedLines += result.skippedLines;
2067
2969
  if (result.kind === "unreadable") skippedFiles += 1;
2068
2970
  if (result.kind !== "session") continue;
2069
2971
  const { session } = result;
2070
- if (Date.parse(session.endedAt) < options.sinceMs || !session.cwd) continue;
2071
- const repository = await repositoryOf(session.cwd);
2072
- if (repository === void 0 || !options.connected.has(repository)) continue;
2073
- 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));
2074
2975
  }
2075
2976
  return {
2076
2977
  sessions: sortedSessions(sessions),
2077
2978
  skippedLines,
2078
- skippedFiles
2979
+ skippedFiles,
2980
+ notImported
2079
2981
  };
2080
2982
  }
2081
2983
  const numericName = (name, length) => name.length === length && /^\d+$/.test(name) ? Number(name) : void 0;
@@ -2125,43 +3027,39 @@ function toCodexSessionImport(session, repository) {
2125
3027
  };
2126
3028
  }
2127
3029
  async function scanCodexSessions(options) {
2128
- const repositories = /* @__PURE__ */ new Map();
2129
- const repositoryOf = (cwd) => {
2130
- const cached = repositories.get(cwd) ?? resolveRepository(cwd, options.git);
2131
- repositories.set(cwd, cached);
2132
- return cached;
2133
- };
2134
- const isSent = async (cwd) => {
2135
- if (!cwd) return void 0;
2136
- const repository = await repositoryOf(cwd);
2137
- return repository !== void 0 && options.connected.has(repository) ? repository : void 0;
2138
- };
3030
+ const repositoryOf = cachedRepositoryOf(options.git);
2139
3031
  const { files, compressed } = await listRollouts$1(join(options.codexDir, "sessions"), localDayKey(options.sinceMs));
2140
3032
  const sessions = /* @__PURE__ */ new Map();
3033
+ const notImported = noneNotImported();
2141
3034
  let skippedLines = 0;
2142
3035
  let skippedFiles = 0;
2143
3036
  let unextractable = 0;
2144
3037
  for (const file of files) {
2145
3038
  const modifiedMs = (await stat(file).catch(() => void 0))?.mtimeMs;
2146
- if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) continue;
3039
+ if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) {
3040
+ notImported.outOfRange += 1;
3041
+ continue;
3042
+ }
2147
3043
  const result = await parseRollout(file);
2148
3044
  skippedLines += result.skippedLines;
2149
3045
  if (result.kind === "unreadable") skippedFiles += 1;
2150
3046
  if (result.kind === "unextractable") {
2151
- 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;
2152
3050
  continue;
2153
3051
  }
2154
3052
  if (result.kind !== "session") continue;
2155
3053
  const { session } = result;
2156
- if (Date.parse(session.endedAt) < options.sinceMs) continue;
2157
- const repository = await isSent(session.cwd);
2158
- if (repository === void 0) continue;
2159
- 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));
2160
3057
  }
2161
3058
  return {
2162
3059
  sessions: sortedSessions(sessions),
2163
3060
  skippedLines,
2164
3061
  skippedFiles,
3062
+ notImported,
2165
3063
  unextractable,
2166
3064
  compressed
2167
3065
  };
@@ -2169,49 +3067,78 @@ async function scanCodexSessions(options) {
2169
3067
  //#endregion
2170
3068
  //#region src/import/command.ts
2171
3069
  const DAY_MS$1 = 864e5;
2172
- const MESSAGES$1 = {
2173
- runInit: "`harnessforce init`を実行してください",
2174
- invalidUrl: "接続先のURLが不正です",
3070
+ const MESSAGES = {
3071
+ runInit: "Run `harnessforce init`",
3072
+ invalidUrl: "The connection URL is invalid",
2175
3073
  keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
2176
- readFailed: "Harnessforceとの通信に失敗しました。もう一度`harnessforce import`を実行してください",
2177
- sendFailed: "Harnessforceとの通信に失敗しました。もう一度`harnessforce import`を実行すると続きから取り込みます",
2178
- revoked: "送信キーが失効しています。`harnessforce init`を実行してください",
2179
- loginExpired: "ログインの有効期限が切れました。`harnessforce init`を実行してください",
2180
- 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"
2181
3082
  };
2182
3083
  const limitMessage = {
2183
- monthly_event_limit: (count) => `月間イベント数の上限に達したため、${count}件のセッションを取り込めませんでした。上限が解除された後に\`harnessforce import\`を再実行すると続きから取り込みます`,
2184
- 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`
2185
3086
  };
2186
- const ACCESS_MESSAGES$3 = {
2187
- keychainUnavailable: MESSAGES$1.keychainUnavailable,
2188
- initRequired: MESSAGES$1.runInit,
2189
- invalidUrl: MESSAGES$1.invalidUrl,
2190
- 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
2191
3092
  };
2192
3093
  function unwrap(outcome) {
2193
3094
  if (outcome.kind === "ok") return outcome.value;
2194
- return stopWith(outcome.kind === "unauthorized" ? MESSAGES$1.loginExpired : MESSAGES$1.readFailed);
3095
+ return stopWith(outcome.kind === "unauthorized" ? MESSAGES.loginExpired : MESSAGES.readFailed);
2195
3096
  }
2196
- 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) {
2197
3115
  switch (result.kind) {
2198
3116
  case "done":
2199
- deps.stdout(`${result.imported}件のセッションを取り込みました\n`);
2200
- 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);
2201
3123
  return 0;
2202
3124
  case "limited":
2203
3125
  deps.stderr(`${limitMessage[result.reason](result.notImported)}\n`);
2204
3126
  return 1;
2205
3127
  case "unauthorized":
2206
- deps.stderr(`${MESSAGES$1.revoked}\n`);
3128
+ deps.stderr(`${MESSAGES.revoked}\n`);
2207
3129
  return 1;
2208
3130
  case "failed":
2209
- deps.stderr(`${MESSAGES$1.sendFailed}\n`);
3131
+ deps.stderr(`${MESSAGES.sendFailed}\n`);
2210
3132
  return 1;
2211
3133
  }
2212
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
+ });
2213
3140
  async function runImport(deps) {
2214
- const access = await verifyCliAccess(deps, ACCESS_MESSAGES$3);
3141
+ const access = await verifyCliAccess(deps, ACCESS_MESSAGES$2);
2215
3142
  const destination = {
2216
3143
  workspaceId: access.workspaceId,
2217
3144
  endpoint: withoutExtras(access.ingest)
@@ -2242,20 +3169,28 @@ async function runImport(deps) {
2242
3169
  });
2243
3170
  const skippedLines = claude.skippedLines + codex.skippedLines;
2244
3171
  const skippedFiles = claude.skippedFiles + codex.skippedFiles;
2245
- if (skippedLines > 0 || skippedFiles > 0) deps.stdout(`読めなかった${skippedLines}行と${skippedFiles}個のファイルを読み飛ばしました\n`);
2246
- if (codex.unextractable > 0) deps.stdout(`modelまたはtoken数を読めないCodexのセッション${codex.unextractable}個は取り込みませんでした\n`);
2247
- 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`);
2248
3175
  const scanned = [...claude.sessions, ...codex.sessions];
2249
3176
  const statePath = importStatePath(deps.homeDir);
2250
3177
  const sent = await readSentSessions(statePath, destination);
3178
+ const unsent = scanned.filter((s) => !sent.has(sessionStateKey(s)));
2251
3179
  return report(await sendSessions({
2252
3180
  endpoint: access.ingest,
2253
3181
  ingestKey: access.ingestKey,
2254
- sessions: scanned.filter((s) => !sent.has(sessionStateKey(s))),
3182
+ sessions: unsent,
2255
3183
  fetch: deps.fetch,
2256
3184
  sleep: deps.sleep,
2257
- record: (ids) => recordSentSessions(statePath, destination, ids).catch(() => stopWith(MESSAGES$1.stateFailed))
2258
- }), 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);
2259
3194
  }
2260
3195
  function importCommand(deps) {
2261
3196
  return runUntilStop(() => runImport(deps), deps.stderr);
@@ -8735,7 +9670,7 @@ function Ko(e, t) {
8735
9670
  const HOOK_COMMAND = "harnessforce hook session-start";
8736
9671
  const HOOK_COMMAND_WINDOWS = "harnessforce.cmd hook session-start";
8737
9672
  const HOOK_TIMEOUT_SECONDS = 10;
8738
- const FILE_MODE$3 = 384;
9673
+ const FILE_MODE$2 = 384;
8739
9674
  function codexConfigPath(env, homeDir) {
8740
9675
  return join(absoluteEnv(env.CODEX_HOME) ?? join(homeDir, ".codex"), "config.toml");
8741
9676
  }
@@ -8810,10 +9745,10 @@ async function writeCodexConfig(path, text) {
8810
9745
  const temporary = `${path}.${randomUUID()}.tmp`;
8811
9746
  try {
8812
9747
  await writeFile(temporary, text, {
8813
- mode: FILE_MODE$3,
9748
+ mode: FILE_MODE$2,
8814
9749
  flag: "wx"
8815
9750
  });
8816
- await chmod(temporary, FILE_MODE$3);
9751
+ await chmod(temporary, FILE_MODE$2);
8817
9752
  await rename(temporary, path);
8818
9753
  } finally {
8819
9754
  await rm(temporary, { force: true });
@@ -8898,8 +9833,15 @@ const stop$1 = (message) => stopWith(INIT_MESSAGES[message]);
8898
9833
  const sha256Hex = (value) => createHash("sha256").update(value, "utf8").digest("hex");
8899
9834
  function init(args, deps) {
8900
9835
  return runUntilStop(async () => {
8901
- const codexPath = await runInit(args, deps);
8902
- deps.stdout(`${INIT_MESSAGES.success}\n${codexNotes(codexPath)}\n`);
9836
+ const { codexPath, connection, isKernelKeyring, isPluginAutoUpdateOn } = await runInit(args, deps);
9837
+ if (connection !== withoutExtras(new URL(deps.defaultUrl))) deps.stdout(`${nonDefaultConnectionMessage(connection)}\n`);
9838
+ const lines = [
9839
+ INIT_MESSAGES.success,
9840
+ ...isKernelKeyring ? [INIT_MESSAGES.savedToKernelKeyring] : [],
9841
+ ...isPluginAutoUpdateOn ? [INIT_MESSAGES.pluginAutoUpdateOn] : [],
9842
+ codexNotes(codexPath)
9843
+ ];
9844
+ deps.stdout(`${lines.join("\n")}\n`);
8903
9845
  return 0;
8904
9846
  }, deps.stderr);
8905
9847
  }
@@ -8907,9 +9849,11 @@ async function runInit(args, deps) {
8907
9849
  const port = args.port ?? DEFAULT_PORT;
8908
9850
  const base = parseAllowedUrl(args.url ?? deps.defaultUrl) ?? stop$1("invalidUrl");
8909
9851
  const recordedUrl = withoutExtras(base);
8910
- const revoke = await readRevokeHashes(deps.keychain);
8911
9852
  const settingsPath = userSettingsPath(deps.env, deps.homeDir);
8912
- if ((await readUserSettings(settingsPath)).kind === "invalid") stop$1("settingsUnreadable");
9853
+ const store = await openKeychain(deps, (await readSettingsEnv$1(settingsPath)).HARNESSFORCE_WORKSPACE_ID);
9854
+ const revoke = revokeHashes(store);
9855
+ const settings = await readUserSettings(settingsPath);
9856
+ if (settings.kind === "invalid" || !hasWritableMarketplaces(settings.settings)) stop$1("settingsUnreadable");
8913
9857
  const authorizationEndpoint = await discoverAuthorizationEndpoint(base, deps.fetch) ?? stop$1("network");
8914
9858
  const secrets = createLoginSecrets();
8915
9859
  const { code, redirectUri } = await logIn(authorizationEndpoint, secrets, port, deps);
@@ -8921,18 +9865,27 @@ async function runInit(args, deps) {
8921
9865
  revoke_key_hashes: revoke.keyHashes,
8922
9866
  revoke_api_token_hashes: revoke.apiTokenHashes
8923
9867
  }, deps.fetch));
8924
- await save(issued, recordedUrl, settingsPath, args.sendContent, deps).catch((error) => {
9868
+ const isPluginAutoUpdateOn = await save(issued, recordedUrl, settingsPath, args, store).catch((error) => {
8925
9869
  if (isStop(error)) throw error;
8926
9870
  return stop$1("saveFailed");
8927
9871
  });
8928
9872
  const codexPath = codexConfigPath(deps.env, deps.homeDir);
8929
9873
  await saveCodexConfig(issued, recordedUrl, codexPath, args.sendContent).catch(() => stop$1("codexSaveFailed"));
8930
9874
  if (args.sendContent) deps.stdout(`${issued.contentOptIn ? INIT_MESSAGES.contentEnabled : INIT_MESSAGES.contentNotOptedIn}\n`);
8931
- return codexPath;
9875
+ return {
9876
+ codexPath,
9877
+ connection: recordedUrl,
9878
+ isKernelKeyring: store.isKernelKeyring,
9879
+ isPluginAutoUpdateOn
9880
+ };
9881
+ }
9882
+ async function openKeychain(deps, previousWorkspaceId) {
9883
+ return await deps.keychain.openForInit({
9884
+ previousWorkspaceId,
9885
+ onWaiting: () => deps.stderr(`${INIT_MESSAGES.waitingForSecretService}\n`)
9886
+ }).catch(() => stop$1("keychainUnavailable")) ?? stopWith(keychainUnavailableMessage(deps.keychain));
8932
9887
  }
8933
- async function readRevokeHashes(keychain) {
8934
- if (!await keychain.isAvailable()) stop$1("keychainUnavailable");
8935
- const items = await keychain.list().catch(() => void 0) ?? stop$1("keychainUnavailable");
9888
+ function revokeHashes({ items }) {
8936
9889
  const keyHashes = items.filter((item) => isIngestKeyAccount(item.account)).map((item) => sha256Hex(item.secret));
8937
9890
  if (keyHashes.length > MAX_REVOKE_HASHES) stop$1("tooManyKeys");
8938
9891
  const apiTokenHashes = items.flatMap((item) => {
@@ -8985,15 +9938,16 @@ function issuedOrStop(outcome) {
8985
9938
  case "unexpected": return stop$1("network");
8986
9939
  }
8987
9940
  }
8988
- async function save(issued, connection, settingsPath, sendContent, deps) {
8989
- await deps.keychain.delete(ingestOriginAccount(issued.workspaceId));
8990
- await deps.keychain.delete(urlOriginAccount(issued.workspaceId));
8991
- await deps.keychain.set(ingestKeyAccount(issued.workspaceId), issued.ingestKey);
8992
- await deps.keychain.set(apiTokenAccount(issued.workspaceId), serializeApiToken(issued.apiToken));
8993
- await deps.keychain.set(ingestOriginAccount(issued.workspaceId), new URL(issued.ingestEndpoint).origin);
8994
- await deps.keychain.set(urlOriginAccount(issued.workspaceId), new URL(connection).origin);
9941
+ async function save(issued, connection, settingsPath, args, store) {
9942
+ await store.delete(ingestOriginAccount(issued.workspaceId));
9943
+ await store.delete(urlOriginAccount(issued.workspaceId));
9944
+ await store.set(ingestKeyAccount(issued.workspaceId), issued.ingestKey);
9945
+ await store.set(apiTokenAccount(issued.workspaceId), serializeApiToken(issued.apiToken));
9946
+ await store.set(ingestOriginAccount(issued.workspaceId), new URL(issued.ingestEndpoint).origin);
9947
+ await store.set(urlOriginAccount(issued.workspaceId), new URL(connection).origin);
9948
+ await store.deleteElsewhere(issued.workspaceId);
8995
9949
  const current = await readUserSettings(settingsPath);
8996
- if (current.kind === "invalid") return stop$1("saveFailed");
9950
+ if (current.kind === "invalid" || !hasWritableMarketplaces(current.settings)) return stop$1("saveFailed");
8997
9951
  const env = {
8998
9952
  HARNESSFORCE_URL: connection,
8999
9953
  HARNESSFORCE_ENDPOINT: issued.ingestEndpoint,
@@ -9007,8 +9961,10 @@ async function save(issued, connection, settingsPath, sendContent, deps) {
9007
9961
  OTEL_TRACES_EXPORTER: "otlp",
9008
9962
  OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf"
9009
9963
  };
9010
- if (sendContent && issued.contentOptIn) env.OTEL_LOG_USER_PROMPTS = "1";
9011
- 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;
9012
9968
  }
9013
9969
  async function saveCodexConfig(issued, connection, path, sendContent) {
9014
9970
  await writeCodexConfig(path, mergeCodexConfig(await readCodexConfig(path), {
@@ -9028,27 +9984,46 @@ const DESTINATION_VARIABLES = [
9028
9984
  "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"
9029
9985
  ];
9030
9986
  const WITHHELD = "harnessforce: user key withheld (destination not verified)\n";
9031
- async function otelHeaders(env, managedDir, keychain, stdout, stderr) {
9032
- const selection = await selectKey(env, managedDir, keychain).catch(() => ({ kind: "none" }));
9033
- if (selection.kind === "withheld") stderr(WITHHELD);
9034
- 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;
9035
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
+ }
9036
10004
  return 0;
9037
10005
  }
9038
- async function selectKey(env, managedDir, keychain) {
10006
+ const HOOK_READ_TIMEOUT_MS = 1e3;
10007
+ async function selectKey(env, managedDir, unlimited) {
9039
10008
  const managed = await readManagedEnv(managedDir, ["HARNESSFORCE_INGEST_KEY"]);
9040
10009
  if (managed.HARNESSFORCE_INGEST_KEY) return {
9041
10010
  kind: "key",
9042
10011
  key: managed.HARNESSFORCE_INGEST_KEY
9043
10012
  };
9044
10013
  const workspaceId = env.HARNESSFORCE_WORKSPACE_ID;
9045
- if (!workspaceId || !await keychain.isAvailable()) return { kind: "none" };
10014
+ if (!workspaceId) return { kind: "missing" };
10015
+ const keychain = unlimited.withTimeout(HOOK_READ_TIMEOUT_MS);
10016
+ if (!await keychain.isAvailable()) return { kind: "failed" };
9046
10017
  const key = await keychain.get(ingestKeyAccount(workspaceId));
9047
- if (!key) return { kind: "none" };
10018
+ if (!key) return { kind: "missing" };
9048
10019
  const pinnedOrigin = await keychain.get(ingestOriginAccount(workspaceId));
9049
10020
  return pinnedOrigin && isPinnedDestination(env, pinnedOrigin) ? {
9050
10021
  kind: "key",
9051
- key
10022
+ key,
10023
+ user: {
10024
+ keychain,
10025
+ workspaceId
10026
+ }
9052
10027
  } : { kind: "withheld" };
9053
10028
  }
9054
10029
  function isPinnedDestination(env, pinnedOrigin) {
@@ -9379,16 +10354,16 @@ function buildLaunch(request) {
9379
10354
  //#region src/run/messages.ts
9380
10355
  const RUN_MESSAGES = {
9381
10356
  keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
9382
- initRequired: "`harnessforce init`を実行してください",
10357
+ initRequired: "Run `harnessforce init`",
9383
10358
  invalidUrl: INIT_MESSAGES.invalidUrl,
9384
- invalidIssue: "Issueの識別子が不正です",
9385
- issueInitRequired: "Issueを解決できませんでした。`harnessforce init`を実行してください",
9386
- loginExpired: "ログインの有効期限が切れました。`harnessforce init`を実行してください",
9387
- issueFailed: "Issueを解決できず、候補も取得できませんでした",
9388
- noMatch: "一致するIssueがありません",
9389
- 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:"
9390
10365
  };
9391
- const launchFailedMessage = (agent) => `${agent}を起動できませんでした`;
10366
+ const launchFailedMessage = (agent) => `Could not start ${agent}`;
9392
10367
  //#endregion
9393
10368
  //#region src/run/run.ts
9394
10369
  const stop = (message) => stopWith(RUN_MESSAGES[message]);
@@ -9397,14 +10372,14 @@ function candidateList(candidates) {
9397
10372
  const lines = candidates.map(({ identifier, title }) => `${printable(identifier)} ${printable(title)}`);
9398
10373
  return [RUN_MESSAGES.candidatesHeader, ...lines].join("\n");
9399
10374
  }
9400
- const ACCESS_MESSAGES$2 = {
10375
+ const ACCESS_MESSAGES$1 = {
9401
10376
  keychainUnavailable: RUN_MESSAGES.keychainUnavailable,
9402
10377
  initRequired: RUN_MESSAGES.initRequired,
9403
10378
  invalidUrl: RUN_MESSAGES.invalidUrl,
9404
10379
  apiTokenMissing: RUN_MESSAGES.issueInitRequired
9405
10380
  };
9406
10381
  async function prepareLaunch(args, deps) {
9407
- const { workspaceId, ingestEndpoint, readApiBase, accessToken } = await verifyCliAccess(deps, ACCESS_MESSAGES$2);
10382
+ const { workspaceId, ingestEndpoint, readApiBase, accessToken } = await verifyCliAccess(deps, ACCESS_MESSAGES$1);
9408
10383
  if (!isIssueIdentifier(args.issue)) stop("invalidIssue");
9409
10384
  const readFetch = createApiTokenSession({
9410
10385
  keychain: deps.keychain,
@@ -9455,6 +10430,20 @@ function runIssue(args, deps) {
9455
10430
  return 1;
9456
10431
  }, deps.stderr);
9457
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
+ }
9458
10447
  //#endregion
9459
10448
  //#region src/top/args.ts
9460
10449
  const TOP_USAGE = "Usage: harnessforce top [--once] [--json] [--ascii] [--theme <auto|light|dark|ansi>]\n";
@@ -9690,7 +10679,7 @@ function renderList(sessions, options) {
9690
10679
  const active = sessions.filter((s) => sessionState(s.lastEventAtMs, options.nowMs) === "active").length;
9691
10680
  return [clip(`${sessions.length} sessions, ${active} active`, options)];
9692
10681
  }
9693
- 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);
9694
10683
  return [
9695
10684
  header(sessions, options),
9696
10685
  ...body,
@@ -9751,7 +10740,7 @@ function renderDetail(row, options) {
9751
10740
  clip(options.painter.dim(DETAIL_HINT), options)
9752
10741
  ];
9753
10742
  }
9754
- 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)`] : [];
9755
10744
  //#endregion
9756
10745
  //#region src/top/codex-session.ts
9757
10746
  const DETACHED = "HEAD";
@@ -10366,7 +11355,7 @@ const DEFAULT_SETTINGS = {
10366
11355
  };
10367
11356
  const unreadable = {
10368
11357
  settings: DEFAULT_SETTINGS,
10369
- notice: "端末の設定(~/.harnessforce/config.json)を読めないため、既定の表示にします"
11358
+ notice: "Cannot read the device settings (`~/.harnessforce/config.json`), so the default display is used"
10370
11359
  };
10371
11360
  async function readTopSettings(homeDir) {
10372
11361
  let text;
@@ -10856,7 +11845,9 @@ const median = (values) => {
10856
11845
  const middle = Math.floor(sorted.length / 2);
10857
11846
  return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
10858
11847
  };
10859
- 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`;
10860
11851
  function summarize(reports, group, kind, countField, medianField) {
10861
11852
  const values = reports.map((r) => r[group][kind]).filter((v) => v?.measurement === "measured");
10862
11853
  if (values.length === 0) return void 0;
@@ -10869,16 +11860,16 @@ function summarize(reports, group, kind, countField, medianField) {
10869
11860
  };
10870
11861
  }
10871
11862
  function renderAnalysis(reports, usages, header) {
10872
- 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):"];
10873
11864
  for (const kind of INTERVENTION_KINDS) {
10874
11865
  const s = summarize(reports, "interventions", kind, "count", "wait_seconds_median");
10875
- 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)}`);
10876
11867
  }
10877
- lines.push("ループにできる繰り返し(回数、その間の介入、所要時間の中央値の中央値):");
11868
+ lines.push("Repeatable loops (count, interventions during them, median of median duration):");
10878
11869
  for (const kind of LOOP_KINDS) {
10879
11870
  const s = summarize(reports, "loops", kind, "occurrences", "duration_seconds_median");
10880
11871
  const interventions = s?.values.reduce((sum, v) => sum + (v.interventions ?? 0), 0) ?? 0;
10881
- 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)}`);
10882
11873
  }
10883
11874
  const servers = /* @__PURE__ */ new Map();
10884
11875
  for (const report of reports) for (const s of report.mcp_servers) {
@@ -10893,19 +11884,19 @@ function renderAnalysis(reports, usages, header) {
10893
11884
  configured: current.configured + (s.configured ? 1 : 0)
10894
11885
  });
10895
11886
  }
10896
- lines.push("MCP server(呼び出し、失敗、設定されていたsession):");
10897
- if (servers.size === 0) lines.push(" なし");
10898
- 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)`);
10899
11890
  lines.push(...renderUsage(usages));
10900
11891
  return `${lines.join("\n")}\n`;
10901
11892
  }
10902
11893
  const percent = (share) => `${(share * 100).toFixed(1)}%`;
10903
11894
  function renderUsage(usages) {
10904
- const lines = ["使い方(端末だけの値。送信しません):"];
11895
+ const lines = ["Usage (this device only; not sent):"];
10905
11896
  for (const kind of ["frequent_compaction", "low_cache_reuse"]) lines.push(` ${kind}: ${usages.filter((u) => u.kinds.includes(kind)).length} session`);
10906
11897
  const shares = modelShares(usages);
10907
- const parts = shares && [...shares.main.map((m) => `本体 ${m.model} ${percent(m.share)}`), ...shares.subagent.map((m) => `subagent ${m.model} ${percent(m.share)}`)];
10908
- 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"}`);
10909
11900
  return lines;
10910
11901
  }
10911
11902
  function localIso(ms) {
@@ -11026,7 +12017,7 @@ const LOCK_WAIT_MS = 3e4;
11026
12017
  const STALE_LOCK_MS = 6e4;
11027
12018
  const HEARTBEAT_MS = 1e4;
11028
12019
  const RETRY_INTERVAL_MS = 100;
11029
- const FILE_MODE$2 = 384;
12020
+ const FILE_MODE$1 = 384;
11030
12021
  const isAlreadyExists = (error) => error?.code === "EEXIST";
11031
12022
  async function removeIfStale(path, now) {
11032
12023
  const current = await stat(path).catch(() => void 0);
@@ -11036,7 +12027,7 @@ async function acquireTuneLock(path, deps) {
11036
12027
  const deadline = deps.now() + LOCK_WAIT_MS;
11037
12028
  while (deps.now() < deadline) {
11038
12029
  try {
11039
- const handle = await open(path, "wx", FILE_MODE$2);
12030
+ const handle = await open(path, "wx", FILE_MODE$1);
11040
12031
  const token = randomUUID();
11041
12032
  await handle.writeFile(token);
11042
12033
  await handle.close();
@@ -11081,33 +12072,33 @@ const NOTICE_ORDER = [
11081
12072
  ];
11082
12073
  const TUNE_MESSAGES = {
11083
12074
  keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
11084
- initRequired: "`harnessforce init`を実行してください。Viewerのロールでは`harnessforce tune`を利用できません",
11085
- invalidUrl: "接続先のURLが不正です",
11086
- locked: "別の`harnessforce tune`が実行中のため、実行できません",
11087
- insufficient: (count) => `データ不足: 分析したセッションは${count}件です。提案には10件以上が必要です(あと${10 - count}件)`,
11088
- outOfRange: (days, count) => `送信の範囲外: 開始が${days}日より前の${count}件のセッションは送信しません`,
11089
- notConnected: (count) => `接続済みでないリポジトリの${count}件のセッションは送信しません`,
11090
- partlyUnreadable: (lines, skipped) => `記録の一部を読めていない: 読んだ${lines}行のうち${skipped}行を読み飛ばしました`,
11091
- savedList: (fetchedAt) => `接続済みのリポジトリの一覧を取得できないため、${fetchedAt}に保存した一覧を使います`,
11092
- noList: "接続済みのリポジトリを確認できないため送信しません",
11093
- noSend: "送信しない設定のため、分析結果を送信しません",
11094
- settingUnreadable: "端末の設定(`~/.harnessforce/config.json`)を読めないため、分析結果を送信しません",
11095
- importLimited: (count) => `月間イベント数の上限または閲覧のみのため、${count}件のセッションを取り込めませんでした。分析結果は送信し、取り込みは解除の後の実行でやり直します`,
11096
- importFailed: "送信の前のセッションの取り込みに失敗しました。分析結果は送信し、取り込みは次の実行でやり直します",
11097
- sent: (count) => `${count}件の分析結果を送信しました`,
11098
- unreachable: (count) => `Harnessforceへ送信できませんでした。${count}件の分析結果は次の実行で送ります`,
11099
- expiredUnsent: (count) => `送信の範囲外になった未送信の分析結果${count}件を削除しました`,
11100
- readOnly: (count) => `Workspaceが閲覧のみのため、${count}件の分析結果を送信できませんでした。次の実行で送ります`,
11101
- invalidReports: (count, reasons) => `形式が不正なため${count}件の分析結果を送信せずに削除しました(${reasons.join("、")})`,
11102
- rejected: (status, count) => `Harnessforceが送信を拒否しました(HTTP ${status})。${count}件の分析結果を送信せずに削除しました`,
11103
- keyUnusable: (count) => `送信キーを使えません。未送信の分析結果${count}件を削除しました。\`harnessforce init\`を実行してください`,
11104
- loginExpired: "ログインの有効期限が切れました。`harnessforce init`を実行してください",
11105
- unwritable: "`~/.harnessforce/tune/`に書き込めないため、分析結果を送信しません",
11106
- recordFailed: "提案を記録できませんでした",
11107
- recordInvalid: (field) => `提案の記録の入力が不正です: ${field}`,
11108
- usage: "使い方: harnessforce tune [--all] [--no-send] [--show-report] [--json] | harnessforce tune record | harnessforce tune --purge",
11109
- purged: "`~/.harnessforce/tune/`の分析結果、提案、未送信の分析結果を削除しました",
11110
- 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/`"
11111
12102
  };
11112
12103
  var Notices = class {
11113
12104
  entries = /* @__PURE__ */ new Map();
@@ -11148,7 +12139,7 @@ const tunePaths = (homeDir) => {
11148
12139
  };
11149
12140
  };
11150
12141
  const DIR_MODE = 448;
11151
- const FILE_MODE$1 = 384;
12142
+ const FILE_MODE = 384;
11152
12143
  const destinationKey = ({ workspaceId, endpoint }) => `${workspaceId} ${endpoint}`;
11153
12144
  async function readJsonFile(path) {
11154
12145
  try {
@@ -11168,10 +12159,10 @@ async function writeFileAtomic(path, content) {
11168
12159
  const temporary = `${path}.${randomUUID()}.tmp`;
11169
12160
  try {
11170
12161
  await writeFile(temporary, content, {
11171
- mode: FILE_MODE$1,
12162
+ mode: FILE_MODE,
11172
12163
  flag: "wx"
11173
12164
  });
11174
- await chmod(temporary, FILE_MODE$1);
12165
+ await chmod(temporary, FILE_MODE);
11175
12166
  await rename(temporary, path);
11176
12167
  } finally {
11177
12168
  await rm(temporary, { force: true });
@@ -11461,7 +12452,7 @@ async function readSendSetting(homeDir) {
11461
12452
  const DAY_MS = 864e5;
11462
12453
  const DEFAULT_RANGE_DAYS = 30;
11463
12454
  const SKIPPED_RATIO = .05;
11464
- const ACCESS_MESSAGES$1 = {
12455
+ const ACCESS_MESSAGES = {
11465
12456
  keychainUnavailable: TUNE_MESSAGES.keychainUnavailable,
11466
12457
  initRequired: TUNE_MESSAGES.initRequired,
11467
12458
  invalidUrl: TUNE_MESSAGES.invalidUrl,
@@ -11697,14 +12688,14 @@ async function analyzeUnderLock(options, access, paths, deps) {
11697
12688
  writeSessionConfigs(paths.sessionConfigs, configs)
11698
12689
  ]).then(() => true, () => false);
11699
12690
  if (!isWritten) notices.add("unwritable", TUNE_MESSAGES.unwritable, 1);
11700
- if (options.json) deps.stdout(`${JSON.stringify(output)}\n`);
12691
+ if (options.json) deps.stdout(`${asciiJson(output)}\n`);
11701
12692
  else {
11702
12693
  deps.stdout(renderAnalysis(analyzed.map((a) => a.report), analyzed.map((a) => a.tune.usage), {
11703
12694
  sessionCount: analyzed.length,
11704
12695
  analyzerVersion: ANALYZER_VERSION,
11705
12696
  parserVersion: PARSER_VERSION
11706
12697
  }));
11707
- 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`);
11708
12699
  }
11709
12700
  if (analyzed.length < 10) notices.add("insufficient", TUNE_MESSAGES.insufficient(analyzed.length));
11710
12701
  const lines = analyzed.reduce((sum, a) => sum + a.tune.lines, 0);
@@ -11727,7 +12718,7 @@ async function analyzeUnderLock(options, access, paths, deps) {
11727
12718
  }
11728
12719
  function analyzeCommand(options, deps) {
11729
12720
  return runUntilStop(async () => {
11730
- const access = await verifyCliAccess(deps, ACCESS_MESSAGES$1);
12721
+ const access = await verifyCliAccess(deps, ACCESS_MESSAGES);
11731
12722
  const paths = tunePaths(deps.homeDir);
11732
12723
  await ensureDir(paths.dir).catch(() => stopWith(TUNE_MESSAGES.unwritable));
11733
12724
  const release = await acquireTuneLock(paths.lock, deps).catch(() => stopWith(TUNE_MESSAGES.unwritable));
@@ -11856,7 +12847,7 @@ const sha256 = (text) => createHash("sha256").update(Buffer.from(text, "utf8")).
11856
12847
  function expectedHash(input, component) {
11857
12848
  if (component !== null && VALUE_KINDS.has(component.kind)) {
11858
12849
  if (!("value" in input)) return invalid("value");
11859
- return hashValue(input.value);
12850
+ return hashValue(hashInput(component.kind, input.value));
11860
12851
  }
11861
12852
  if (typeof input.content !== "string") return invalid("content");
11862
12853
  return hashFileContent(Buffer.from(input.content, "utf8"));
@@ -11963,6 +12954,10 @@ function parseArgs(args) {
11963
12954
  options
11964
12955
  };
11965
12956
  }
12957
+ function canTuneSend(args) {
12958
+ const parsed = parseArgs(args);
12959
+ return parsed?.kind === "analyze" && !parsed.options.noSend;
12960
+ }
11966
12961
  async function underLock(deps, failure, body) {
11967
12962
  return runUntilStop(async () => {
11968
12963
  const paths = tunePaths(deps.homeDir);
@@ -12016,464 +13011,14 @@ async function tuneCommand(args, deps) {
12016
13011
  return analyzeCommand(parsed.options, deps);
12017
13012
  }
12018
13013
  //#endregion
12019
- //#region src/usage-limits/consent.ts
12020
- const consentUrl = (base) => underBase(base, "api/v1/cli/usage-limits/consent");
12021
- async function call(base, fetchImpl, method, body) {
12022
- try {
12023
- return await fetchImpl(consentUrl(base), {
12024
- method,
12025
- headers: {
12026
- accept: "application/json",
12027
- ...body ? { "content-type": "application/json" } : {}
12028
- },
12029
- ...body ? { body: JSON.stringify(body) } : {},
12030
- redirect: "manual",
12031
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
12032
- });
12033
- } catch {
12034
- return;
12035
- }
12036
- }
12037
- async function readConsent(base, fetchImpl) {
12038
- const response = await call(base, fetchImpl, "GET");
12039
- if (response?.status === 401) return { kind: "unauthorized" };
12040
- if (response?.status !== 200) return { kind: "failed" };
12041
- const body = await readJsonObjectBody(response);
12042
- if (typeof body?.workspace_opted_in !== "boolean" || typeof body.shared !== "boolean" || typeof body.current_text_version !== "number") return { kind: "failed" };
12043
- return {
12044
- kind: "ok",
12045
- state: {
12046
- workspaceOptedIn: body.workspace_opted_in,
12047
- shared: body.shared,
12048
- currentTextVersion: body.current_text_version
12049
- }
12050
- };
12051
- }
12052
- async function recordConsent(base, fetchImpl, textVersion) {
12053
- const response = await call(base, fetchImpl, "PUT", { text_version: textVersion });
12054
- if (!response) return { kind: "failed" };
12055
- if (response.status === 204) return { kind: "ok" };
12056
- if (response.status === 401) return { kind: "unauthorized" };
12057
- const body = await readJsonObjectBody(response);
12058
- if (response.status === 400 && body?.code === "invalid_text_version") return { kind: "invalid_text_version" };
12059
- if (response.status === 403) return body?.code === "workspace_read_only" ? { kind: "read_only" } : { kind: "viewer" };
12060
- return { kind: "failed" };
12061
- }
12062
- async function revokeConsent(base, fetchImpl) {
12063
- const response = await call(base, fetchImpl, "DELETE");
12064
- if (response?.status === 204) return { kind: "ok" };
12065
- return response?.status === 401 ? { kind: "unauthorized" } : { kind: "failed" };
12066
- }
12067
- //#endregion
12068
- //#region src/usage-limits/files.ts
12069
- const DIRECTORY_MODE = 448;
12070
- const FILE_MODE = 384;
12071
- const markPath = (homeDir) => join(homeDir, ".harnessforce", "usage-limits.json");
12072
- const statePath = (homeDir) => join(homeDir, ".harnessforce", "usage-limits-state.json");
12073
- async function readJson(path) {
12074
- try {
12075
- return JSON.parse(await readFile(path, "utf8"));
12076
- } catch {
12077
- return;
12078
- }
12079
- }
12080
- async function readMark(homeDir) {
12081
- const value = await readJson(markPath(homeDir));
12082
- if (!isObject(value)) return void 0;
12083
- const { consented, text_version: textVersion, original } = value;
12084
- if (typeof consented !== "boolean" || typeof textVersion !== "number" || !(original === null || isObject(original))) return void 0;
12085
- return {
12086
- consented,
12087
- text_version: textVersion,
12088
- original
12089
- };
12090
- }
12091
- async function readSendState(homeDir) {
12092
- const value = await readJson(statePath(homeDir));
12093
- if (!isObject(value) || typeof value.sent_at !== "number") return void 0;
12094
- const resets = value.resets_at;
12095
- if (!isObject(resets)) return void 0;
12096
- const entries = Object.entries(resets);
12097
- if (entries.some(([, at]) => typeof at !== "number")) return void 0;
12098
- return {
12099
- sent_at: value.sent_at,
12100
- resets_at: Object.fromEntries(entries)
12101
- };
12102
- }
12103
- async function writeOwnerOnly(path, value) {
12104
- await mkdir(dirname(path), {
12105
- recursive: true,
12106
- mode: DIRECTORY_MODE
12107
- });
12108
- const temporary = `${path}.${randomUUID()}.tmp`;
12109
- try {
12110
- await writeFile(temporary, `${JSON.stringify(value)}\n`, {
12111
- mode: FILE_MODE,
12112
- flag: "wx"
12113
- });
12114
- await chmod(temporary, FILE_MODE);
12115
- await rename(temporary, path);
12116
- } finally {
12117
- await rm(temporary, { force: true });
12118
- }
12119
- }
12120
- const writeSendState = (homeDir, state) => writeOwnerOnly(statePath(homeDir), state);
12121
- const writeMark = (homeDir, mark) => writeOwnerOnly(markPath(homeDir), mark);
12122
- async function removeMarkAndState(homeDir) {
12123
- await rm(markPath(homeDir), { force: true });
12124
- await rm(statePath(homeDir), { force: true });
12125
- }
12126
- //#endregion
12127
- //#region src/usage-limits/messages.ts
12128
- const CONSENT_TEXT = {
12129
- ja: "あなたの名前とともに、あなたの利用枠(5時間・7日などの窓の使用率とリセット時刻だけ)を、このWorkspaceのメンバー全員が見られます。プロンプトなどの本文は送りません。いつでもやめられ、やめると保存済みの値も削除します。メンバーの順位づけには使いません。",
12130
- 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."
12131
- };
12132
- const CONSENT_PROMPT = {
12133
- ja: "同意しますか? [y/N] ",
12134
- en: "Do you agree? [y/N] "
12135
- };
12136
- const MESSAGES = {
12137
- windows: "この版のWindowsでは、利用枠の共有を設定できません",
12138
- settingsUnreadable: "Claude Codeのuser settingsを読めません",
12139
- statusLineUnsupported: "このstatusLineは組み込めません",
12140
- noUserKey: "この端末には利用者用の送信キーがありません。`harnessforce init`を実行してください",
12141
- keychainUnavailable: "OSのキーチェーンを利用できないため、利用枠の共有を設定できません",
12142
- invalidUrl: "接続先のURLが不正です",
12143
- loginExpired: "ログインの有効期限が切れました。`harnessforce init`をもう一度実行してください",
12144
- notAgreed: "同意されなかったため、何も変えませんでした",
12145
- invalidTextVersion: "このCLIの利用枠の文面が古いか新しすぎます。`harnessforce`を更新してください",
12146
- viewer: "閲覧のみのロールでは、利用枠を共有できません。Workspaceの管理者に連絡してください",
12147
- readOnly: "このWorkspaceは閲覧のみのため、利用枠の共有を設定できません",
12148
- failed: "Harnessforceとの通信に失敗しました。もう一度お試しください",
12149
- saveFailed: "端末の設定を書き込めなかったため、同意を取り消しました。もう一度お試しください",
12150
- revokeFailedAfterSave: "端末の設定を書き込めず、同意の取り消しにも失敗しました。`harnessforce usage-limits status`で状態を確かめ、`harnessforce usage-limits off`を実行してください",
12151
- notOptedIn: "同意を記録しました。Workspaceが利用枠の記録をopt-inするまで、値は保存されません",
12152
- enabled: "利用枠の共有を有効にしました。Claude Codeを再起動すると反映されます",
12153
- disabled: "利用枠の共有をやめ、保存済みの値の削除を依頼しました",
12154
- revokedButNotRestored: "同意は撤回しましたが、statusLineを元に戻せませんでした。`harnessforce usage-limits off`をもう一度実行してください",
12155
- usage: "Usage: harnessforce usage-limits on [--yes] | harnessforce usage-limits off | harnessforce usage-limits status | harnessforce usage-limits statusline\n"
12156
- };
12157
- const STATUS_LABELS = {
12158
- consent: {
12159
- shared: "同意している",
12160
- notShared: "同意していない",
12161
- outdated: "文面が古い",
12162
- unknown: "確認できません"
12163
- },
12164
- optIn: {
12165
- on: "有効",
12166
- off: "無効",
12167
- unknown: "確認できません"
12168
- },
12169
- statusLine: {
12170
- embeddedWithOriginal: "組み込まれている(元のcommandあり)",
12171
- embeddedWithoutOriginal: "組み込まれている(元のcommandなし)",
12172
- notEmbedded: "組み込まれていない"
12173
- }
12174
- };
12175
- //#endregion
12176
- //#region src/usage-limits/statusline-settings.ts
12177
- const STATUSLINE_COMMAND = "harnessforce usage-limits statusline";
12178
- const isEmbedded = (statusLine) => isObject(statusLine) && statusLine.command === STATUSLINE_COMMAND;
12179
- function canEmbed(settings) {
12180
- const { statusLine } = settings;
12181
- if (statusLine === void 0) return true;
12182
- return isObject(statusLine) && statusLine.type === "command" && typeof statusLine.command === "string";
12183
- }
12184
- function originalFor(settings, mark) {
12185
- const { statusLine } = settings;
12186
- if (isEmbedded(statusLine)) return mark ? mark.original : null;
12187
- return isObject(statusLine) ? statusLine : null;
12188
- }
12189
- function embedStatusLine(settings) {
12190
- const { statusLine } = settings;
12191
- if (isEmbedded(statusLine)) return settings;
12192
- return {
12193
- ...settings,
12194
- statusLine: isObject(statusLine) ? {
12195
- ...statusLine,
12196
- command: STATUSLINE_COMMAND
12197
- } : {
12198
- type: "command",
12199
- command: STATUSLINE_COMMAND
12200
- }
12201
- };
12202
- }
12203
- function restoreStatusLine(settings, original) {
12204
- if (!isEmbedded(settings.statusLine)) return settings;
12205
- const { statusLine: _embedded, ...rest } = settings;
12206
- return original === null ? rest : {
12207
- ...settings,
12208
- statusLine: original
12209
- };
12210
- }
12211
- const isStatusLineEmbedded = (settings) => isEmbedded(settings.statusLine);
12212
- //#endregion
12213
- //#region src/usage-limits/manage.ts
12214
- const ACCESS_MESSAGES = {
12215
- keychainUnavailable: MESSAGES.keychainUnavailable,
12216
- initRequired: MESSAGES.noUserKey,
12217
- invalidUrl: MESSAGES.invalidUrl,
12218
- apiTokenMissing: MESSAGES.noUserKey
12219
- };
12220
- function language(env) {
12221
- return (env.LC_ALL || env.LC_MESSAGES || env.LANG || "").toLowerCase().startsWith("ja") ? "ja" : "en";
12222
- }
12223
- function consentClient(deps, access) {
12224
- const authorized = createApiTokenSession({
12225
- keychain: deps.keychain,
12226
- workspaceId: access.workspaceId,
12227
- readBase: access.readApiBase,
12228
- fetch: deps.fetch,
12229
- now: () => deps.now().getTime(),
12230
- sleep: deps.sleep,
12231
- homeDir: deps.homeDir
12232
- }).authorizedFetch(deps.fetch);
12233
- const base = access.readApiBase;
12234
- return {
12235
- read: () => readConsent(base, authorized),
12236
- record: () => recordConsent(base, authorized, 1),
12237
- revoke: () => revokeConsent(base, authorized)
12238
- };
12239
- }
12240
- const settingsPathOf = (deps) => userSettingsPath(deps.env, deps.homeDir);
12241
- async function runOn(yes, deps) {
12242
- if (deps.platform === "win32") return stopWith(MESSAGES.windows);
12243
- const client = consentClient(deps, await verifyCliAccess(deps, ACCESS_MESSAGES));
12244
- const read = await readUserSettings(settingsPathOf(deps));
12245
- if (read.kind === "invalid") return stopWith(MESSAGES.settingsUnreadable);
12246
- if (!canEmbed(read.settings)) return stopWith(MESSAGES.statusLineUnsupported);
12247
- const settings = read.settings;
12248
- const before = await client.read();
12249
- const lang = language(deps.env);
12250
- deps.stdout(`${CONSENT_TEXT[lang]}\n`);
12251
- if (!yes && (await deps.ask(CONSENT_PROMPT[lang])).trim() !== "y") return stopWith(MESSAGES.notAgreed);
12252
- const recorded = await client.record();
12253
- if (recorded.kind === "unauthorized") return stopWith(MESSAGES.loginExpired);
12254
- if (recorded.kind === "invalid_text_version") return stopWith(MESSAGES.invalidTextVersion);
12255
- if (recorded.kind === "viewer") return stopWith(MESSAGES.viewer);
12256
- if (recorded.kind === "read_only") return stopWith(MESSAGES.readOnly);
12257
- if (recorded.kind === "failed") return stopWith(MESSAGES.failed);
12258
- const mark = await readMark(deps.homeDir);
12259
- const undo = async () => {
12260
- await (mark ? writeMark(deps.homeDir, {
12261
- ...mark,
12262
- consented: false
12263
- }) : removeMarkAndState(deps.homeDir)).catch(() => {});
12264
- const revoked = await client.revoke();
12265
- return stopWith(revoked.kind === "ok" ? MESSAGES.saveFailed : MESSAGES.revokeFailedAfterSave);
12266
- };
12267
- try {
12268
- await writeMark(deps.homeDir, {
12269
- consented: true,
12270
- text_version: 1,
12271
- original: originalFor(settings, mark)
12272
- });
12273
- } catch {
12274
- return undo();
12275
- }
12276
- try {
12277
- await writeUserSettings(settingsPathOf(deps), embedStatusLine(settings));
12278
- } catch {
12279
- return undo();
12280
- }
12281
- const optedOut = before.kind === "ok" && !before.state.workspaceOptedIn;
12282
- deps.stdout(`${optedOut ? MESSAGES.notOptedIn : MESSAGES.enabled}\n`);
12283
- return 0;
12284
- }
12285
- async function runOff(deps) {
12286
- const revoked = await consentClient(deps, await verifyCliAccess(deps, ACCESS_MESSAGES)).revoke();
12287
- if (revoked.kind === "unauthorized") return stopWith(MESSAGES.loginExpired);
12288
- if (revoked.kind === "failed") return stopWith(MESSAGES.failed);
12289
- const mark = await readMark(deps.homeDir);
12290
- const read = await readUserSettings(settingsPathOf(deps));
12291
- const keepWithoutConsent = async () => {
12292
- if (mark) await writeMark(deps.homeDir, {
12293
- ...mark,
12294
- consented: false
12295
- }).catch(() => {});
12296
- return stopWith(MESSAGES.revokedButNotRestored);
12297
- };
12298
- if (read.kind === "invalid") return keepWithoutConsent();
12299
- if (isStatusLineEmbedded(read.settings)) try {
12300
- await writeUserSettings(settingsPathOf(deps), restoreStatusLine(read.settings, mark?.original ?? null));
12301
- } catch {
12302
- return keepWithoutConsent();
12303
- }
12304
- await removeMarkAndState(deps.homeDir).catch(() => {});
12305
- deps.stdout(`${MESSAGES.disabled}\n`);
12306
- return 0;
12307
- }
12308
- function consentLabel(result, mark) {
12309
- if (result.kind !== "ok") return STATUS_LABELS.consent.unknown;
12310
- if (result.state.shared) return STATUS_LABELS.consent.shared;
12311
- return mark?.consented === true && mark.text_version !== result.state.currentTextVersion ? STATUS_LABELS.consent.outdated : STATUS_LABELS.consent.notShared;
12312
- }
12313
- async function runStatus(deps) {
12314
- const mark = await readMark(deps.homeDir);
12315
- const read = await readUserSettings(settingsPathOf(deps));
12316
- const statusLine = !(read.kind === "ok" && isStatusLineEmbedded(read.settings)) ? STATUS_LABELS.statusLine.notEmbedded : mark?.original ? STATUS_LABELS.statusLine.embeddedWithOriginal : STATUS_LABELS.statusLine.embeddedWithoutOriginal;
12317
- const result = await readRemote(deps);
12318
- deps.stdout([
12319
- `本人の同意: ${consentLabel(result, mark)}`,
12320
- `Workspaceのopt-in: ${result.kind !== "ok" ? STATUS_LABELS.optIn.unknown : result.state.workspaceOptedIn ? STATUS_LABELS.optIn.on : STATUS_LABELS.optIn.off}`,
12321
- `statusLineの組み込み: ${statusLine}`,
12322
- ""
12323
- ].join("\n"));
12324
- return 0;
12325
- }
12326
- async function readRemote(deps) {
12327
- try {
12328
- return await consentClient(deps, await verifyCliAccess(deps, ACCESS_MESSAGES)).read();
12329
- } catch (error) {
12330
- if (isStop(error)) return { kind: "failed" };
12331
- throw error;
12332
- }
12333
- }
12334
- function onCommand(yes, deps) {
12335
- return runUntilStop(() => runOn(yes, deps), deps.stderr);
12336
- }
12337
- function offCommand(deps) {
12338
- return runUntilStop(() => runOff(deps), deps.stderr);
12339
- }
12340
- const statusCommand = runStatus;
12341
- //#endregion
12342
- //#region src/usage-limits/summary.ts
12343
- const WINDOWS = [{
12344
- name: "five_hour",
12345
- minutes: 300
12346
- }, {
12347
- name: "seven_day",
12348
- minutes: 10080
12349
- }];
12350
- const MIN_SEND_INTERVAL_MS = 3e5;
12351
- const instant = (ms) => new Date(ms).toISOString().replace(".000Z", "Z");
12352
- function windowFrom(value, minutes, nowMs) {
12353
- if (!isObject(value)) return void 0;
12354
- const { used_percentage: used, resets_at: resetsAt } = value;
12355
- if (typeof used !== "number" || !(used >= 0 && used <= 100)) return void 0;
12356
- if (typeof resetsAt !== "number" || !Number.isFinite(resetsAt)) return void 0;
12357
- const resetsAtMs = Math.floor(resetsAt) * 1e3;
12358
- if (resetsAtMs <= nowMs) return void 0;
12359
- return {
12360
- window_minutes: minutes,
12361
- used_percent: used,
12362
- resets_at: instant(resetsAtMs)
12363
- };
12364
- }
12365
- function summaryFrom(stdin, nowMs) {
12366
- let json;
12367
- try {
12368
- json = JSON.parse(stdin);
12369
- } catch {
12370
- return;
12371
- }
12372
- const limits = isObject(json) ? json.rate_limits : void 0;
12373
- if (!isObject(limits)) return void 0;
12374
- const windows = WINDOWS.flatMap(({ name, minutes }) => {
12375
- const window = windowFrom(limits[name], minutes, nowMs);
12376
- return window ? [window] : [];
12377
- });
12378
- return windows.length === 0 ? void 0 : {
12379
- agent: "claude_code",
12380
- observed_at: instant(Math.floor(nowMs / 1e3) * 1e3),
12381
- windows
12382
- };
12383
- }
12384
- const resetSeconds = (window) => Date.parse(window.resets_at) / 1e3;
12385
- function isDue(summary, state, nowMs) {
12386
- if (!state) return true;
12387
- return summary.windows.some((window) => state.resets_at[String(window.window_minutes)] !== resetSeconds(window)) || nowMs - state.sent_at >= MIN_SEND_INTERVAL_MS;
12388
- }
12389
- const stateFor = (summary, nowMs) => ({
12390
- sent_at: nowMs,
12391
- resets_at: Object.fromEntries(summary.windows.map((window) => [String(window.window_minutes), resetSeconds(window)]))
12392
- });
12393
- function parseSummary(raw) {
12394
- let value;
12395
- try {
12396
- value = JSON.parse(raw);
12397
- } catch {
12398
- return;
12399
- }
12400
- if (!isObject(value) || value.agent !== "claude_code") return void 0;
12401
- const { observed_at: observedAt, windows } = value;
12402
- if (typeof observedAt !== "string" || !Array.isArray(windows)) return void 0;
12403
- const rebuilt = windows.flatMap((window) => {
12404
- if (!isObject(window)) return [];
12405
- const { window_minutes: minutes, used_percent: used } = window;
12406
- const resetsAt = window.resets_at;
12407
- 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)) ? [{
12408
- window_minutes: minutes,
12409
- used_percent: used,
12410
- resets_at: resetsAt
12411
- }] : [];
12412
- });
12413
- return rebuilt.length > 0 && rebuilt.length === windows.length ? {
12414
- agent: "claude_code",
12415
- observed_at: observedAt,
12416
- windows: rebuilt
12417
- } : void 0;
12418
- }
12419
- //#endregion
12420
- //#region src/usage-limits/send.ts
12421
- const SEND_TIMEOUT_MS = 5e3;
12422
- async function sendUsageLimits(raw, deps) {
12423
- const summary = parseSummary(raw);
12424
- if (!summary) return 0;
12425
- const mark = await readMark(deps.homeDir);
12426
- if (!mark?.consented || mark.text_version !== 1) return 0;
12427
- const resolved = await resolveUserDestination(deps).catch(() => void 0);
12428
- if (resolved?.kind !== "ok") return 0;
12429
- await postItem(resolved.destination, "v1/usage-limits", summary, deps.fetch, SEND_TIMEOUT_MS);
12430
- return 0;
12431
- }
12432
- //#endregion
12433
- //#region src/usage-limits/statusline.ts
12434
- const MAX_INPUT_BYTES = Number.MAX_SAFE_INTEGER;
12435
- function originalCommand(original) {
12436
- const command = original?.command;
12437
- return isObject(original) && typeof command === "string" && command !== "" ? command : void 0;
12438
- }
12439
- async function statusline(deps) {
12440
- const input = await deps.readStdin(MAX_INPUT_BYTES).catch(() => Buffer.alloc(0));
12441
- const mark = await readMark(deps.homeDir);
12442
- const command = originalCommand(mark?.original ?? null);
12443
- const code = command ? await deps.runOriginal(command, input) : void 0;
12444
- await sendIfDue(deps, input, mark).catch(() => {});
12445
- return code ?? 0;
12446
- }
12447
- async function sendIfDue(deps, input, mark) {
12448
- if (!mark?.consented || mark.text_version !== 1) return;
12449
- const nowMs = deps.now().getTime();
12450
- const summary = summaryFrom(input.toString("utf8"), nowMs);
12451
- if (!summary) return;
12452
- if (!isDue(summary, await readSendState(deps.homeDir), nowMs)) return;
12453
- await writeSendState(deps.homeDir, stateFor(summary, nowMs));
12454
- deps.spawnSender(JSON.stringify(summary));
12455
- }
12456
- //#endregion
12457
- //#region src/usage-limits/command.ts
12458
- async function usageLimitsCommand(args, deps) {
12459
- const [subcommand, payload] = args;
12460
- if (subcommand === "statusline" && args.length === 1) return statusline(deps);
12461
- if (subcommand === "send" && payload !== void 0 && args.length === 2) return sendUsageLimits(payload, deps);
12462
- const isYes = args[1] === "--yes";
12463
- if (subcommand === "on" && (args.length === 1 || args.length === 2 && isYes)) return onCommand(isYes, deps);
12464
- if (subcommand === "off" && args.length === 1) return offCommand(deps);
12465
- if (subcommand === "status" && args.length === 1) return statusCommand(deps);
12466
- deps.stderr(MESSAGES.usage);
12467
- return 1;
12468
- }
12469
- //#endregion
12470
13014
  //#region src/main.ts
12471
13015
  const { version } = createRequire(import.meta.url)("../package.json");
12472
- 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;
12473
13017
  function parseInitArgs(args) {
12474
13018
  let url;
12475
13019
  let port;
12476
13020
  let sendContent = false;
13021
+ let noPluginAutoUpdate = false;
12477
13022
  for (let i = 0; i < args.length;) {
12478
13023
  if (args[i] === "--url") {
12479
13024
  if (url !== void 0 || args[i + 1] === void 0) return void 0;
@@ -12496,12 +13041,19 @@ function parseInitArgs(args) {
12496
13041
  i += 1;
12497
13042
  continue;
12498
13043
  }
13044
+ if (args[i] === "--no-plugin-auto-update") {
13045
+ if (noPluginAutoUpdate) return void 0;
13046
+ noPluginAutoUpdate = true;
13047
+ i += 1;
13048
+ continue;
13049
+ }
12499
13050
  return;
12500
13051
  }
12501
13052
  return {
12502
13053
  url,
12503
13054
  port,
12504
- sendContent
13055
+ sendContent,
13056
+ noPluginAutoUpdate
12505
13057
  };
12506
13058
  }
12507
13059
  function parseRunArgs(args) {
@@ -12514,55 +13066,94 @@ function parseRunArgs(args) {
12514
13066
  } : void 0;
12515
13067
  }
12516
13068
  async function run(argv, deps) {
13069
+ const help = resolveHelp(argv);
13070
+ if (help !== void 0) {
13071
+ deps.stdout(help);
13072
+ return 0;
13073
+ }
12517
13074
  const [command, ...rest] = argv;
12518
13075
  if (command === "--version" && rest.length === 0) {
12519
13076
  deps.stdout(`${version}\n`);
12520
13077
  return 0;
12521
13078
  }
12522
- if (command === "otel-headers" && rest.length === 0) return otelHeaders(deps.env, deps.managedDir, deps.keychain, deps.stdout, deps.stderr);
12523
- if (command === "import" && rest.length === 0) return importCommand({
12524
- ...deps,
12525
- git: deps.importGit,
12526
- now: () => deps.now().getTime()
12527
- });
12528
- if (command === "tune") return tuneCommand(rest, {
13079
+ const client = {
13080
+ component: "cli",
13081
+ version,
13082
+ dev: deps.devBuild
13083
+ };
13084
+ const cli = {
12529
13085
  ...deps,
12530
- git: deps.importGit,
12531
- now: () => deps.now().getTime()
12532
- });
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
+ }));
12533
13116
  if (command === "hook") {
12534
13117
  if (rest.length !== 1 || rest[0] !== "session-start") {
12535
- deps.stderr(HOOK_USAGE);
13118
+ cli.stderr(HOOK_USAGE);
12536
13119
  return 1;
12537
13120
  }
12538
13121
  return sessionStart({
12539
- env: deps.env,
12540
- homeDir: deps.homeDir,
12541
- now: deps.now,
12542
- git: deps.hookGit,
12543
- fetch: deps.fetch,
12544
- keychain: deps.keychain,
12545
- readStdin: deps.readStdin,
12546
- stdout: deps.stdout,
12547
- 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
12548
13133
  });
12549
13134
  }
12550
13135
  if (command === "top") return topCommand(rest, {
12551
- homeDir: deps.homeDir,
12552
- managedDir: deps.managedDir,
12553
- env: deps.env,
12554
- stdout: deps.stdout,
12555
- stderr: deps.stderr,
12556
- now: () => deps.now().getTime(),
12557
- git: deps.importGit,
12558
- 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
12559
13144
  });
12560
- if (command === "usage-limits") return usageLimitsCommand(rest, deps);
12561
13145
  const initArgs = command === "init" ? parseInitArgs(rest) : void 0;
12562
- if (initArgs) return init(initArgs, deps);
13146
+ if (initArgs) return guarded(() => init(initArgs, cli));
12563
13147
  const runArgs = command === "run" ? parseRunArgs(rest) : void 0;
12564
- if (runArgs) return runIssue(runArgs, deps);
12565
- 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);
12566
13157
  return 1;
12567
13158
  }
12568
13159
  //#endregion
@@ -12573,9 +13164,9 @@ const SENDING_COMMANDS = /* @__PURE__ */ new Set([
12573
13164
  "import",
12574
13165
  "hook"
12575
13166
  ]);
12576
- 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));
12577
13168
  const HOOK_RESTART_FAILED = "harnessforce: session registration skipped (restart failed)";
12578
- const RESTART_FAILED = "harnessforceを起動し直せませんでした";
13169
+ const RESTART_FAILED = "Could not restart harnessforce";
12579
13170
  async function relaunchHf(argv, deps) {
12580
13171
  if (!isSending(argv)) return void 0;
12581
13172
  const outcome = await relaunchWithoutRuntimeVariables({
@@ -12737,42 +13328,9 @@ function createProcessIo(proc = process) {
12737
13328
  };
12738
13329
  }
12739
13330
  //#endregion
12740
- //#region src/usage-limits/original.ts
12741
- function runOriginalCommand(command, input) {
12742
- return new Promise((resolve) => {
12743
- let child;
12744
- try {
12745
- child = spawn("sh", ["-c", command], { stdio: [
12746
- "pipe",
12747
- "inherit",
12748
- "inherit"
12749
- ] });
12750
- } catch {
12751
- return resolve(void 0);
12752
- }
12753
- child.once("error", () => resolve(void 0));
12754
- child.once("exit", (code, signal) => resolve(code ?? 128 + (signal ? constants.signals[signal] : 0)));
12755
- child.stdin?.on("error", () => {});
12756
- child.stdin?.end(input);
12757
- });
12758
- }
12759
- //#endregion
12760
- //#region src/usage-limits/spawn-sender.ts
12761
- function spawnUsageSender(payload) {
12762
- spawn(process.execPath, [
12763
- ...process.execArgv,
12764
- ...process.argv.slice(1, 2),
12765
- "usage-limits",
12766
- "send",
12767
- payload
12768
- ], {
12769
- detached: true,
12770
- stdio: "ignore"
12771
- }).unref();
12772
- }
12773
- //#endregion
12774
13331
  //#region src/bin.ts
12775
- const { url: defaultUrl } = createRequire(import.meta.url)("./build-config.json");
13332
+ const require = createRequire(import.meta.url);
13333
+ const { url: defaultUrl, isDefault } = require("./build-config.json");
12776
13334
  const CALLBACK_TIMEOUT_MS = 3e5;
12777
13335
  const argv = process.argv.slice(2);
12778
13336
  const restoredEnv = takeStashedRuntimeEnv(process.env, process.platform);
@@ -12787,10 +13345,15 @@ process.exitCode = relaunched ?? await run(argv, {
12787
13345
  env: process.env,
12788
13346
  keychain: createOsKeychain({
12789
13347
  platform: process.platform,
12790
- load: () => import("@napi-rs/keyring")
13348
+ load: () => import("@napi-rs/keyring"),
13349
+ secretService: createSecretServiceProcess({
13350
+ execPath: process.execPath,
13351
+ resolveKeyring: () => require.resolve("@napi-rs/keyring")
13352
+ })
12791
13353
  }),
12792
13354
  stdout: (text) => process.stdout.write(text),
12793
13355
  stderr: (text) => process.stderr.write(text),
13356
+ endStdout: () => new Promise((resolve) => process.stdout.end(() => resolve())),
12794
13357
  fetch: (url, init) => fetch(url, init),
12795
13358
  openBrowser: (url) => openBrowser(url, {
12796
13359
  platform: process.platform,
@@ -12802,6 +13365,7 @@ process.exitCode = relaunched ?? await run(argv, {
12802
13365
  homeDir: homedir(),
12803
13366
  managedDir: managedDirFor(process.platform),
12804
13367
  defaultUrl,
13368
+ devBuild: !isDefault,
12805
13369
  callbackTimeoutMs: CALLBACK_TIMEOUT_MS,
12806
13370
  cwd: process.cwd(),
12807
13371
  git: createGitRunner({
@@ -12828,19 +13392,6 @@ process.exitCode = relaunched ?? await run(argv, {
12828
13392
  }),
12829
13393
  sleep: (ms) => setTimeout$1(ms),
12830
13394
  top: createProcessIo(),
12831
- runOriginal: runOriginalCommand,
12832
- spawnSender: spawnUsageSender,
12833
- ask: async (question) => {
12834
- const lines = createInterface({
12835
- input: process.stdin,
12836
- output: process.stdout
12837
- });
12838
- try {
12839
- return await lines.question(question);
12840
- } finally {
12841
- lines.close();
12842
- }
12843
- },
12844
13395
  readStdin: async (maxBytes) => {
12845
13396
  const chunks = [];
12846
13397
  let size = 0;