@harnessforce/cli 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -28
- package/dist/bin.js +1080 -797
- 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
|
|
12
|
+
import { createInterface } from "node:readline";
|
|
14
13
|
import { isDeepStrictEqual } from "node:util";
|
|
15
14
|
//#region ../agent-core/src/config/canonical.ts
|
|
16
15
|
const byCodeUnit$1 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -416,38 +415,40 @@ function takeStashedRuntimeEnv(env, platform) {
|
|
|
416
415
|
//#endregion
|
|
417
416
|
//#region src/shared/messages.ts
|
|
418
417
|
const INIT_MESSAGES = {
|
|
419
|
-
success: "Harnessforce
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
418
|
+
success: "Connected to Harnessforce. Restart Claude Code to apply the settings",
|
|
419
|
+
pluginAutoUpdateOn: "Automatic updates for the Harnessforce plugin are on. To turn them off, run `harnessforce init --no-plugin-auto-update` or use /plugin",
|
|
420
|
+
invalidUrl: "The connection URL is invalid",
|
|
421
|
+
keychainUnavailable: "The OS keychain is unavailable, so the ingest key cannot be saved",
|
|
422
|
+
linuxKeychainUnavailable: "Neither the Secret Service (GNOME Keyring or KWallet) nor the kernel keyring is available. In containers and CI, ask an Owner or Admin to distribute a Workspace ingest key through managed settings",
|
|
423
|
+
waitingForSecretService: "Waiting for the Secret Service unlock prompt (up to 60 seconds)",
|
|
424
|
+
savedToKernelKeyring: "The Secret Service is unavailable, so the ingest key was saved in the kernel keyring. It is lost on reboot or after a period of disuse. If it disappears, run `harnessforce init` again",
|
|
425
|
+
tooManyKeys: "There are too many ingest keys in the keychain, so `harnessforce init` cannot run",
|
|
426
|
+
tooManyCredentials: "There are too many ingest keys or login entries in the keychain, so `harnessforce init` cannot run",
|
|
427
|
+
settingsUnreadable: "Cannot read the Claude Code user settings",
|
|
428
|
+
network: "Communication with Harnessforce failed. Run `harnessforce init` again",
|
|
429
|
+
listenFailed: "Could not start waiting for the login",
|
|
430
|
+
timeout: "The login did not complete in time. Run `harnessforce init` again",
|
|
431
|
+
accessDenied: "The login was denied",
|
|
432
|
+
noWorkspace: "You are not a member of any Workspace. Create a Workspace or accept an invitation, then run `harnessforce init` again",
|
|
433
|
+
loginFailed: "The login failed. Run `harnessforce init` again",
|
|
434
|
+
viewer: "The view-only role cannot issue an ingest key. Contact a Workspace administrator",
|
|
435
|
+
limitOwnerAdmin: "The number of active ingest keys has reached the limit. Revoke unused keys on the ingest keys page",
|
|
436
|
+
limitMember: "The ingest key limit has been reached. Contact a Workspace administrator",
|
|
437
|
+
gateUnavailable: "Could not apply the change on the receiving side. Nothing was changed. Try again",
|
|
438
|
+
ingestUnavailable: "This environment does not receive telemetry, so an ingest key cannot be issued",
|
|
439
|
+
saveFailed: "Could not save the ingest key. Run `harnessforce init` again",
|
|
440
|
+
codexSaveFailed: "Could not write the Codex configuration. Run `harnessforce init` again",
|
|
441
|
+
contentEnabled: "Sending the prompts you type is now enabled. Values that match known patterns for secrets and personal information are masked, but not everything can be detected. Within the workspace, only you can read your stored content, and it is deleted after the content retention period (at most 30 days)",
|
|
442
|
+
contentNotOptedIn: "This workspace has not opted in to prompts you typed, so sending content was not enabled. Opt in under Data retention in the workspace settings, then run `harnessforce init --send-content` again"
|
|
443
443
|
};
|
|
444
444
|
const codexNotes = (configPath) => [
|
|
445
|
-
`Codex
|
|
446
|
-
"Codex
|
|
447
|
-
"Harnessforce
|
|
445
|
+
`Wrote the Codex configuration (${configPath}). Restart Codex to apply it`,
|
|
446
|
+
"Codex hooks do not run until you trust them in Codex's /hooks. Trust the harnessforce SessionStart hook in /hooks",
|
|
447
|
+
"To use the Harnessforce MCP server, log in with `codex mcp login harnessforce`"
|
|
448
448
|
].join("\n");
|
|
449
|
-
const
|
|
450
|
-
const
|
|
449
|
+
const nonDefaultConnectionMessage = (url) => `Connecting to ${url} (not the default production endpoint)`;
|
|
450
|
+
const listenFailedMessage = (port) => `${INIT_MESSAGES.listenFailed}. Stop the process using port ${port}, or specify another port with \`harnessforce init --port\``;
|
|
451
|
+
const keychainUnavailableMessage = (keychain, message = INIT_MESSAGES.keychainUnavailable) => keychain.unavailableHint ? `${message}. ${keychain.unavailableHint}` : message;
|
|
451
452
|
//#endregion
|
|
452
453
|
//#region src/credentials/keychain.ts
|
|
453
454
|
async function openSingleStore(store) {
|
|
@@ -481,6 +482,9 @@ const SECRET_SERVICE_TIMEOUT_MS = 6e4;
|
|
|
481
482
|
const WAITING_NOTICE_DELAY_MS = 2e3;
|
|
482
483
|
const PROBE_ACCOUNT$1 = "availability-probe";
|
|
483
484
|
const PROBE_VALUE = "harnessforce";
|
|
485
|
+
const MIGRATION_PROBE_TIMEOUT_MS = 200;
|
|
486
|
+
const MIGRATION_BUDGET_MS = 1e3;
|
|
487
|
+
const CLEANUP_TIMEOUT_MS = 200;
|
|
484
488
|
const succeeds = (operation) => operation().then(() => true, () => false);
|
|
485
489
|
const workspaceOf = (account) => account.slice(0, account.lastIndexOf(":"));
|
|
486
490
|
function createLinuxKeychain(options) {
|
|
@@ -527,6 +531,11 @@ function build(options, timeoutMs) {
|
|
|
527
531
|
const { location } = await locate(workspaceOf(account));
|
|
528
532
|
await storeAt(location).delete(account, timeoutMs);
|
|
529
533
|
},
|
|
534
|
+
async migrateFromKernel(workspaceId) {
|
|
535
|
+
const { location } = await locate(workspaceId);
|
|
536
|
+
if (location !== "kernel") return;
|
|
537
|
+
await migrate(options, workspaceId).catch(() => {});
|
|
538
|
+
},
|
|
530
539
|
withTimeout: (ms) => build(options, ms),
|
|
531
540
|
openForInit: (init) => openForInit(options, timeoutMs, init)
|
|
532
541
|
};
|
|
@@ -569,6 +578,59 @@ async function openForInit({ kernel, secretService }, timeoutMs, { previousWorks
|
|
|
569
578
|
deleteElsewhere: async () => {}
|
|
570
579
|
};
|
|
571
580
|
}
|
|
581
|
+
var MigrationExpired = class extends Error {};
|
|
582
|
+
const MIGRATION_ORDER = (workspaceId) => [
|
|
583
|
+
ingestOriginAccount(workspaceId),
|
|
584
|
+
urlOriginAccount(workspaceId),
|
|
585
|
+
apiTokenAccount(workspaceId),
|
|
586
|
+
ingestKeyAccount(workspaceId)
|
|
587
|
+
];
|
|
588
|
+
async function migrate({ kernel, secretService }, workspaceId) {
|
|
589
|
+
let isExpired = false;
|
|
590
|
+
const deadline = Date.now() + MIGRATION_BUDGET_MS;
|
|
591
|
+
const remaining = () => {
|
|
592
|
+
if (isExpired) throw new MigrationExpired();
|
|
593
|
+
return Math.max(deadline - Date.now(), 1);
|
|
594
|
+
};
|
|
595
|
+
const written = [];
|
|
596
|
+
let isVerified = false;
|
|
597
|
+
const move = async () => {
|
|
598
|
+
await secretService.get(PROBE_ACCOUNT$1, MIGRATION_PROBE_TIMEOUT_MS);
|
|
599
|
+
const items = [];
|
|
600
|
+
for (const account of MIGRATION_ORDER(workspaceId)) {
|
|
601
|
+
const secret = await kernel.get(account, remaining());
|
|
602
|
+
if (secret !== void 0) items.push({
|
|
603
|
+
account,
|
|
604
|
+
secret
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
for (const account of workspaceAccounts(workspaceId)) await secretService.delete(account, remaining());
|
|
608
|
+
for (const { account, secret } of items) {
|
|
609
|
+
written.push(account);
|
|
610
|
+
await secretService.set(account, secret, remaining());
|
|
611
|
+
}
|
|
612
|
+
for (const { account, secret } of items) if (await secretService.get(account, remaining()) !== secret) throw new Error("the Secret Service returned a different value");
|
|
613
|
+
remaining();
|
|
614
|
+
isVerified = true;
|
|
615
|
+
for (const { account } of [...items].reverse()) await kernel.delete(account, remaining());
|
|
616
|
+
};
|
|
617
|
+
let timer;
|
|
618
|
+
const expiry = new Promise((_, reject) => {
|
|
619
|
+
timer = setTimeout(() => {
|
|
620
|
+
isExpired = true;
|
|
621
|
+
reject(new MigrationExpired());
|
|
622
|
+
}, MIGRATION_BUDGET_MS);
|
|
623
|
+
});
|
|
624
|
+
try {
|
|
625
|
+
await Promise.race([move(), expiry]);
|
|
626
|
+
} catch (error) {
|
|
627
|
+
if (isVerified) throw error;
|
|
628
|
+
await Promise.allSettled(written.map((account) => secretService.delete(account, CLEANUP_TIMEOUT_MS)));
|
|
629
|
+
throw error;
|
|
630
|
+
} finally {
|
|
631
|
+
clearTimeout(timer);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
572
634
|
//#endregion
|
|
573
635
|
//#region src/credentials/os-keychain.ts
|
|
574
636
|
const PROBE_ACCOUNT = "availability-probe";
|
|
@@ -780,9 +842,9 @@ async function openBrowser(url, { platform, env, cwd, restoredEnv, spawn: spawnI
|
|
|
780
842
|
//#region src/init/loopback.ts
|
|
781
843
|
const HOST = "127.0.0.1";
|
|
782
844
|
const CALLBACK_PATH = "/callback";
|
|
783
|
-
const page = (message) => `<!doctype html><html lang="
|
|
784
|
-
const SUCCESS_PAGE = page("
|
|
785
|
-
const FAILURE_PAGE = page("
|
|
845
|
+
const page = (message) => `<!doctype html><html lang="en"><meta charset="utf-8"><title>Harnessforce</title><p>${message}</p></html>`;
|
|
846
|
+
const SUCCESS_PAGE = page("Return to the terminal");
|
|
847
|
+
const FAILURE_PAGE = page("The login failed. Check the terminal");
|
|
786
848
|
async function startLoopback(options) {
|
|
787
849
|
let settle = () => {};
|
|
788
850
|
const callback = new Promise((resolve) => {
|
|
@@ -830,11 +892,6 @@ async function startLoopback(options) {
|
|
|
830
892
|
};
|
|
831
893
|
}
|
|
832
894
|
//#endregion
|
|
833
|
-
//#region ../agent-core/src/hook-warning.ts
|
|
834
|
-
const RESTART_FAILED_WARNING = "Harnessforceのhookを起動し直せませんでした";
|
|
835
|
-
const INVALID_ENDPOINT_WARNING = "Harnessforceの送信先が不正です。`harnessforce init`を実行し直してください";
|
|
836
|
-
const notConfiguredWarning = (agent) => agent === "claude_code" ? "Harnessforceの送信が設定されていません。`/harnessforce:setup`を実行してください" : "Harnessforceの送信が設定されていません。`harnessforce init`を実行してください";
|
|
837
|
-
//#endregion
|
|
838
895
|
//#region ../agent-core/src/url.ts
|
|
839
896
|
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
840
897
|
"localhost",
|
|
@@ -858,8 +915,168 @@ function underBase(base, path) {
|
|
|
858
915
|
}
|
|
859
916
|
const withoutExtras = (url) => `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
|
|
860
917
|
//#endregion
|
|
918
|
+
//#region ../agent-core/src/client-update.ts
|
|
919
|
+
const CLIENT_UPDATE_HEADER = "harnessforce-client-update";
|
|
920
|
+
const UPDATE_COMMANDS = {
|
|
921
|
+
cli: "npm install -g @harnessforce/cli@latest",
|
|
922
|
+
"claude-code-plugin": "claude plugin update harnessforce@harnessforce-agent"
|
|
923
|
+
};
|
|
924
|
+
const DISPLAY_NAMES = {
|
|
925
|
+
cli: "CLI",
|
|
926
|
+
"claude-code-plugin": "plugin"
|
|
927
|
+
};
|
|
928
|
+
const DAY_MS$2 = 864e5;
|
|
929
|
+
const RECHECK_TIMEOUT_MS = 2e3;
|
|
930
|
+
const userAgentOf = (client) => `harnessforce-${client.component}${client.dev ? "-dev" : ""}/${client.version}`;
|
|
931
|
+
const STATUSES = /* @__PURE__ */ new Set([
|
|
932
|
+
"current",
|
|
933
|
+
"outdated",
|
|
934
|
+
"deprecated",
|
|
935
|
+
"unsupported"
|
|
936
|
+
]);
|
|
937
|
+
const REASONS = /* @__PURE__ */ new Set(["security", "compatibility"]);
|
|
938
|
+
function parseClientUpdateHeader(value) {
|
|
939
|
+
if (!value) return void 0;
|
|
940
|
+
const members = /* @__PURE__ */ new Map();
|
|
941
|
+
for (const member of value.split(",")) {
|
|
942
|
+
const [key, raw] = member.trim().split("=", 2);
|
|
943
|
+
if (key && raw !== void 0) members.set(key, raw);
|
|
944
|
+
}
|
|
945
|
+
const status = members.get("status");
|
|
946
|
+
if (!status || !STATUSES.has(status)) return void 0;
|
|
947
|
+
const update = { status };
|
|
948
|
+
const latest = /^"([^"\\]*)"$/.exec(members.get("latest") ?? "")?.[1];
|
|
949
|
+
if (latest) update.latest = latest;
|
|
950
|
+
const reason = members.get("reason");
|
|
951
|
+
if (reason && REASONS.has(reason)) update.reason = reason;
|
|
952
|
+
const seconds = /^@(\d+)$/.exec(members.get("deadline") ?? "")?.[1];
|
|
953
|
+
if (seconds) update.deadline = (/* @__PURE__ */ new Date(Number(seconds) * 1e3)).toISOString();
|
|
954
|
+
return isComplete(update) ? update : void 0;
|
|
955
|
+
}
|
|
956
|
+
function isComplete(update) {
|
|
957
|
+
switch (update.status) {
|
|
958
|
+
case "outdated": return Boolean(update.latest);
|
|
959
|
+
case "deprecated": return Boolean(update.reason) && !Number.isNaN(Date.parse(update.deadline ?? ""));
|
|
960
|
+
case "unsupported": return Boolean(update.reason);
|
|
961
|
+
default: return true;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function stateFrom(value, version) {
|
|
965
|
+
if (!isObject(value) || value.version !== version) return void 0;
|
|
966
|
+
if (typeof value.receivedAt !== "string") return void 0;
|
|
967
|
+
if (typeof value.status !== "string" || !STATUSES.has(value.status)) return void 0;
|
|
968
|
+
const state = value;
|
|
969
|
+
return isComplete(state) ? state : void 0;
|
|
970
|
+
}
|
|
971
|
+
async function readUpdateState(path, version) {
|
|
972
|
+
const saved = await readSavedUpdateState(path);
|
|
973
|
+
return saved?.version === version ? saved : void 0;
|
|
974
|
+
}
|
|
975
|
+
async function readSavedUpdateState(path) {
|
|
976
|
+
try {
|
|
977
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
978
|
+
return isObject(value) && typeof value.version === "string" ? stateFrom(value, value.version) : void 0;
|
|
979
|
+
} catch {
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
async function writeUpdateState(path, state) {
|
|
984
|
+
await mkdir(dirname(path), {
|
|
985
|
+
recursive: true,
|
|
986
|
+
mode: 448
|
|
987
|
+
});
|
|
988
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
989
|
+
await writeFile(temporary, `${JSON.stringify(state)}\n`, { mode: 384 });
|
|
990
|
+
await rename(temporary, path);
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Harnessforceへの要求に`User-Agent`を付け、応答の`Harnessforce-Client-Update`を状態のfileへ保存するfetch。
|
|
994
|
+
* 保存に失敗しても要求の結果は変えない。statePathが無い(pluginのdata directoryが無い)場合は版の申告だけを行う。
|
|
995
|
+
*/
|
|
996
|
+
function recordingFetch(fetchImpl, client, statePath, now) {
|
|
997
|
+
return async (url, init) => {
|
|
998
|
+
const headers = {
|
|
999
|
+
...Object.fromEntries(new Headers(init.headers)),
|
|
1000
|
+
"user-agent": userAgentOf(client)
|
|
1001
|
+
};
|
|
1002
|
+
const response = await fetchImpl(url, {
|
|
1003
|
+
...init,
|
|
1004
|
+
headers
|
|
1005
|
+
});
|
|
1006
|
+
const update = parseClientUpdateHeader(response.headers.get(CLIENT_UPDATE_HEADER));
|
|
1007
|
+
if (update && statePath) {
|
|
1008
|
+
const previous = await readUpdateState(statePath, client.version);
|
|
1009
|
+
await writeUpdateState(statePath, {
|
|
1010
|
+
...update,
|
|
1011
|
+
version: client.version,
|
|
1012
|
+
receivedAt: now().toISOString(),
|
|
1013
|
+
...previous?.outdatedShownAt ? { outdatedShownAt: previous.outdatedShownAt } : {}
|
|
1014
|
+
}).catch(() => {});
|
|
1015
|
+
}
|
|
1016
|
+
return response;
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
const utcDate = (instant) => new Intl.DateTimeFormat("en-CA", {
|
|
1020
|
+
timeZone: "UTC",
|
|
1021
|
+
year: "numeric",
|
|
1022
|
+
month: "2-digit",
|
|
1023
|
+
day: "2-digit"
|
|
1024
|
+
}).format(new Date(instant));
|
|
1025
|
+
const unsupportedLine = (component) => `This Harnessforce ${DISPLAY_NAMES[component]} version is no longer supported, so nothing is being sent to Harnessforce. Update it: \`${UPDATE_COMMANDS[component]}\``;
|
|
1026
|
+
function updateLine(component, update) {
|
|
1027
|
+
const name = DISPLAY_NAMES[component];
|
|
1028
|
+
const command = UPDATE_COMMANDS[component];
|
|
1029
|
+
switch (update.status) {
|
|
1030
|
+
case "unsupported": return unsupportedLine(component);
|
|
1031
|
+
case "deprecated": {
|
|
1032
|
+
const stops = `stops sending on ${utcDate(update.deadline ?? "")} (00:00 UTC)`;
|
|
1033
|
+
return update.reason === "security" ? `A security update for Harnessforce ${name} is available. This version ${stops}. Update it: \`${command}\`` : `This Harnessforce ${name} version ${stops}. Update it: \`${command}\``;
|
|
1034
|
+
}
|
|
1035
|
+
case "outdated": return `Harnessforce ${name} ${update.latest} is available. Update it: \`${command}\``;
|
|
1036
|
+
default: return;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function shouldShowOutdated(state, now, env) {
|
|
1040
|
+
if (state.status !== "outdated" || env.NO_UPDATE_NOTIFIER || env.CI) return false;
|
|
1041
|
+
const shownAt = state.outdatedShownAt ? Date.parse(state.outdatedShownAt) : NaN;
|
|
1042
|
+
return Number.isNaN(shownAt) || now.getTime() - shownAt >= DAY_MS$2;
|
|
1043
|
+
}
|
|
1044
|
+
const markOutdatedShown = (path, state, now) => writeUpdateState(path, {
|
|
1045
|
+
...state,
|
|
1046
|
+
outdatedShownAt: now.toISOString()
|
|
1047
|
+
});
|
|
1048
|
+
/**
|
|
1049
|
+
* 表示する1行。outdatedは24時間に1回までとし、表示する場合はその時刻を書く。isSuppressedは、pluginを
|
|
1050
|
+
* Claude Codeが自動更新する間など、呼び出し側がoutdatedを示さない条件。
|
|
1051
|
+
*/
|
|
1052
|
+
async function noticeLine(component, statePath, state, options) {
|
|
1053
|
+
if (state.status === "outdated") {
|
|
1054
|
+
if (!shouldShowOutdated(state, options.now, options.env) || await options.isSuppressed?.()) return void 0;
|
|
1055
|
+
await markOutdatedShown(statePath, state, options.now).catch(() => {});
|
|
1056
|
+
}
|
|
1057
|
+
return updateLine(component, state);
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* unsupportedを受けてから24時間以上経っていれば、資格情報を送らずに`GET /v1/client-status`で確かめ直す
|
|
1061
|
+
* (client-updates.md「送信の停止」)。失敗した場合は状態を変えない。
|
|
1062
|
+
*/
|
|
1063
|
+
async function recheckClientStatus(options) {
|
|
1064
|
+
const { state } = options;
|
|
1065
|
+
if (state?.status !== "unsupported" || options.now().getTime() - Date.parse(state.receivedAt) < DAY_MS$2) return state;
|
|
1066
|
+
try {
|
|
1067
|
+
await (await options.fetch(underBase(options.ingestBase, "v1/client-status"), {
|
|
1068
|
+
method: "GET",
|
|
1069
|
+
redirect: "error",
|
|
1070
|
+
signal: AbortSignal.timeout(RECHECK_TIMEOUT_MS)
|
|
1071
|
+
})).body?.cancel();
|
|
1072
|
+
} catch {
|
|
1073
|
+
return state;
|
|
1074
|
+
}
|
|
1075
|
+
return await readUpdateState(options.statePath, options.version) ?? state;
|
|
1076
|
+
}
|
|
1077
|
+
//#endregion
|
|
861
1078
|
//#region ../agent-core/src/ingest.ts
|
|
862
|
-
const SEND_TIMEOUT_MS
|
|
1079
|
+
const SEND_TIMEOUT_MS = 2e3;
|
|
863
1080
|
function ingestBaseFrom(endpoint) {
|
|
864
1081
|
const base = parseAllowedUrl(endpoint);
|
|
865
1082
|
if (!base) return void 0;
|
|
@@ -867,8 +1084,8 @@ function ingestBaseFrom(endpoint) {
|
|
|
867
1084
|
ingestBase.pathname = base.pathname.replace(/\/+$/, "");
|
|
868
1085
|
return ingestBase;
|
|
869
1086
|
}
|
|
870
|
-
const postItem = (destination, path, item, fetchImpl, timeoutMs = SEND_TIMEOUT_MS
|
|
871
|
-
async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_TIMEOUT_MS
|
|
1087
|
+
const postItem = (destination, path, item, fetchImpl, timeoutMs = SEND_TIMEOUT_MS) => postItems(destination, path, [item], fetchImpl, timeoutMs);
|
|
1088
|
+
async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_TIMEOUT_MS) {
|
|
872
1089
|
try {
|
|
873
1090
|
const response = await fetchImpl(underBase(destination.ingestBase, path), {
|
|
874
1091
|
method: "POST",
|
|
@@ -894,68 +1111,37 @@ async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_T
|
|
|
894
1111
|
}
|
|
895
1112
|
}
|
|
896
1113
|
//#endregion
|
|
897
|
-
//#region ../agent-core/src/
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
hookEventName: "SessionStart",
|
|
907
|
-
additionalContext: `${sessionContextLine(sessionId)}\n\n${usingHarnessforceBody}`
|
|
908
|
-
} });
|
|
909
|
-
//#endregion
|
|
910
|
-
//#region ../agent-core/src/vcs.ts
|
|
911
|
-
async function selectRemote(cwd, git) {
|
|
912
|
-
if (await git(cwd, ["rev-parse", "--is-inside-work-tree"]) !== "true") return void 0;
|
|
913
|
-
const remotes = (await git(cwd, ["remote"]))?.split("\n") ?? [];
|
|
914
|
-
return remotes.includes("origin") ? "origin" : remotes[0];
|
|
915
|
-
}
|
|
916
|
-
async function resolveVcs(cwd, git) {
|
|
917
|
-
const remote = await selectRemote(cwd, git);
|
|
918
|
-
if (!remote) return void 0;
|
|
919
|
-
const [url, branch, commit] = await Promise.all([
|
|
920
|
-
git(cwd, [
|
|
921
|
-
"remote",
|
|
922
|
-
"get-url",
|
|
923
|
-
remote
|
|
924
|
-
]),
|
|
925
|
-
git(cwd, [
|
|
926
|
-
"symbolic-ref",
|
|
927
|
-
"--quiet",
|
|
928
|
-
"--short",
|
|
929
|
-
"HEAD"
|
|
930
|
-
]),
|
|
931
|
-
git(cwd, [
|
|
932
|
-
"rev-parse",
|
|
933
|
-
"--verify",
|
|
934
|
-
"--quiet",
|
|
935
|
-
"HEAD"
|
|
936
|
-
])
|
|
937
|
-
]);
|
|
938
|
-
const repository = url === void 0 ? void 0 : normalizeRepository(url);
|
|
939
|
-
return {
|
|
940
|
-
...repository === void 0 ? {} : { repository },
|
|
941
|
-
...branch === void 0 ? {} : { branch },
|
|
942
|
-
...commit === void 0 ? {} : { commit }
|
|
943
|
-
};
|
|
944
|
-
}
|
|
945
|
-
async function resolveRepository(cwd, git) {
|
|
946
|
-
if (!isAbsolute(cwd)) return void 0;
|
|
947
|
-
const remote = await selectRemote(cwd, git);
|
|
948
|
-
if (!remote) return void 0;
|
|
949
|
-
const url = await git(cwd, [
|
|
950
|
-
"remote",
|
|
951
|
-
"get-url",
|
|
952
|
-
remote
|
|
953
|
-
]);
|
|
954
|
-
return url === void 0 ? void 0 : normalizeRepository(url);
|
|
1114
|
+
//#region ../agent-core/src/config/hash-input.ts
|
|
1115
|
+
function withoutSecrets(entry) {
|
|
1116
|
+
const { env, headers, url, args: _args, ...rest } = entry;
|
|
1117
|
+
const result = rest;
|
|
1118
|
+
if (isObject(env)) result.env = Object.keys(env).sort(byCodeUnit$1);
|
|
1119
|
+
if (isObject(headers)) result.headers = Object.keys(headers).sort(byCodeUnit$1);
|
|
1120
|
+
const strippedUrl = typeof url === "string" ? withoutCredentials(url) : void 0;
|
|
1121
|
+
if (strippedUrl !== void 0) result.url = strippedUrl;
|
|
1122
|
+
return result;
|
|
955
1123
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1124
|
+
function withoutCredentials(url) {
|
|
1125
|
+
let parsed;
|
|
1126
|
+
try {
|
|
1127
|
+
parsed = new URL(url);
|
|
1128
|
+
} catch {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
parsed.username = "";
|
|
1132
|
+
parsed.password = "";
|
|
1133
|
+
parsed.search = "";
|
|
1134
|
+
parsed.hash = "";
|
|
1135
|
+
return parsed.href;
|
|
1136
|
+
}
|
|
1137
|
+
const reduceHookGroup = (group) => isObject(group) && Array.isArray(group.hooks) ? {
|
|
1138
|
+
...group,
|
|
1139
|
+
hooks: group.hooks.map((handler) => isObject(handler) ? withoutSecrets(handler) : handler)
|
|
1140
|
+
} : group;
|
|
1141
|
+
function hashInput(kind, value) {
|
|
1142
|
+
if (kind === "mcp_server" && isObject(value)) return withoutSecrets(value);
|
|
1143
|
+
if (kind === "hook" && Array.isArray(value)) return value.map(reduceHookGroup);
|
|
1144
|
+
return value;
|
|
959
1145
|
}
|
|
960
1146
|
//#endregion
|
|
961
1147
|
//#region ../agent-core/src/config/scope.ts
|
|
@@ -985,7 +1171,7 @@ var Sink = class {
|
|
|
985
1171
|
});
|
|
986
1172
|
return {
|
|
987
1173
|
guard: this.guard,
|
|
988
|
-
addValue: (kind, id, value) => add(kind, id, hashValue(value)),
|
|
1174
|
+
addValue: (kind, id, value) => add(kind, id, hashValue(hashInput(kind, value))),
|
|
989
1175
|
addFile: async (kind, id, path) => {
|
|
990
1176
|
const content = await readFileIfExists(path, this.guard);
|
|
991
1177
|
if (content) add(kind, id, hashFileContent(content));
|
|
@@ -1015,6 +1201,9 @@ function collectMcpServers$1(s, servers) {
|
|
|
1015
1201
|
for (const [name, value] of Object.entries(servers)) if (isObject(value)) s.addValue("mcp_server", name, value);
|
|
1016
1202
|
}
|
|
1017
1203
|
//#endregion
|
|
1204
|
+
//#region ../agent-core/src/plugin-auto-update.ts
|
|
1205
|
+
const MARKETPLACE_NAME = "harnessforce-agent";
|
|
1206
|
+
//#endregion
|
|
1018
1207
|
//#region src/shared/settings.ts
|
|
1019
1208
|
const OTEL_HEADERS_HELPER = "harnessforce otel-headers";
|
|
1020
1209
|
const PLUGIN_ID$1 = "harnessforce@harnessforce-agent";
|
|
@@ -1043,6 +1232,37 @@ async function readUserSettings(path) {
|
|
|
1043
1232
|
return { kind: "invalid" };
|
|
1044
1233
|
}
|
|
1045
1234
|
}
|
|
1235
|
+
const MARKETPLACE_KEYS = ["extraKnownMarketplaces", "additionalMarketplaces"];
|
|
1236
|
+
const MARKETPLACE_SOURCE = {
|
|
1237
|
+
source: "github",
|
|
1238
|
+
repo: "lambda-script/harnessforce-agent"
|
|
1239
|
+
};
|
|
1240
|
+
const isMarketplacesValid = (value) => value === void 0 || isObject(value) && (value["harnessforce-agent"] === void 0 || isObject(value["harnessforce-agent"]));
|
|
1241
|
+
const hasWritableMarketplaces = (settings) => MARKETPLACE_KEYS.every((key) => isMarketplacesValid(settings[key]));
|
|
1242
|
+
/**
|
|
1243
|
+
* marketplaceの登録の鍵の`harnessforce-agent`に、sourceと自動更新を書く。sourceと、利用者が切り替えた真偽値は残し、
|
|
1244
|
+
* turnOffのときだけ偽にする。自動更新が有効になったかも返す。
|
|
1245
|
+
*/
|
|
1246
|
+
function withPluginAutoUpdate(settings, turnOff) {
|
|
1247
|
+
const key = settings.additionalMarketplaces !== void 0 && settings.extraKnownMarketplaces === void 0 ? "additionalMarketplaces" : "extraKnownMarketplaces";
|
|
1248
|
+
const marketplaces = settings[key] ?? {};
|
|
1249
|
+
const entry = marketplaces["harnessforce-agent"] ?? {};
|
|
1250
|
+
const autoUpdate = turnOff ? false : typeof entry.autoUpdate === "boolean" ? entry.autoUpdate : true;
|
|
1251
|
+
return {
|
|
1252
|
+
settings: {
|
|
1253
|
+
...settings,
|
|
1254
|
+
[key]: {
|
|
1255
|
+
...marketplaces,
|
|
1256
|
+
[MARKETPLACE_NAME]: {
|
|
1257
|
+
...entry,
|
|
1258
|
+
source: entry.source ?? MARKETPLACE_SOURCE,
|
|
1259
|
+
autoUpdate
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
},
|
|
1263
|
+
isOn: autoUpdate
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1046
1266
|
function mergeUserSettings(settings, env) {
|
|
1047
1267
|
return {
|
|
1048
1268
|
...settings,
|
|
@@ -1095,6 +1315,399 @@ async function resolveCliDestinations(env, settingsPath, defaultUrl) {
|
|
|
1095
1315
|
};
|
|
1096
1316
|
}
|
|
1097
1317
|
//#endregion
|
|
1318
|
+
//#region src/client-update.ts
|
|
1319
|
+
const updateStatePath = (homeDir) => join(homeDir, ".harnessforce", "update-status.json");
|
|
1320
|
+
const readCliUpdateState = (homeDir, version) => readUpdateState(updateStatePath(homeDir), version);
|
|
1321
|
+
/**
|
|
1322
|
+
* 送信を伴うcommandの前に呼ぶ。unsupportedを受けてから24時間以上経っていれば資格情報を送らずに確かめ直し、
|
|
1323
|
+
* それでもunsupportedなら真を返す(client-updates.md「送信の停止」)。
|
|
1324
|
+
*/
|
|
1325
|
+
async function isSendingBlocked(deps) {
|
|
1326
|
+
const statePath = updateStatePath(deps.homeDir);
|
|
1327
|
+
const state = await readUpdateState(statePath, deps.client.version);
|
|
1328
|
+
if (state?.status !== "unsupported") return false;
|
|
1329
|
+
const { ingestEndpoint } = await resolveCliDestinations(deps.env, userSettingsPath(deps.env, deps.homeDir), deps.defaultUrl);
|
|
1330
|
+
const ingestBase = ingestBaseFrom(ingestEndpoint);
|
|
1331
|
+
return (ingestBase ? await recheckClientStatus({
|
|
1332
|
+
state,
|
|
1333
|
+
ingestBase,
|
|
1334
|
+
fetch: deps.fetch,
|
|
1335
|
+
statePath,
|
|
1336
|
+
version: deps.client.version,
|
|
1337
|
+
now: deps.now
|
|
1338
|
+
}) : state)?.status === "unsupported";
|
|
1339
|
+
}
|
|
1340
|
+
/** commandの出力の後に、更新の状態の1行をstderrへ書く。outdatedは24時間に1回までとする。 */
|
|
1341
|
+
async function noticeUpdate(deps) {
|
|
1342
|
+
const statePath = updateStatePath(deps.homeDir);
|
|
1343
|
+
const state = await readUpdateState(statePath, deps.client.version);
|
|
1344
|
+
if (!state) return;
|
|
1345
|
+
const line = await noticeLine("cli", statePath, state, {
|
|
1346
|
+
now: deps.now(),
|
|
1347
|
+
env: deps.env
|
|
1348
|
+
});
|
|
1349
|
+
if (line) deps.stderr(`${line}\n`);
|
|
1350
|
+
}
|
|
1351
|
+
//#endregion
|
|
1352
|
+
//#region src/help.ts
|
|
1353
|
+
const SENDS_NOTHING_LOCAL = "Nothing. It reads local files only and makes no network request.";
|
|
1354
|
+
const COMMANDS$1 = {
|
|
1355
|
+
init: {
|
|
1356
|
+
summary: "Connect this machine to a Harnessforce Workspace",
|
|
1357
|
+
text: `Usage: harnessforce init [--port <port>] [--send-content] [--no-plugin-auto-update]
|
|
1358
|
+
|
|
1359
|
+
When to use:
|
|
1360
|
+
Once per machine, before anything else, and again whenever the login expires.
|
|
1361
|
+
|
|
1362
|
+
What it does:
|
|
1363
|
+
Opens your browser to log in to a Workspace, then stores a user ingest key and
|
|
1364
|
+
an API token in the OS keychain. It configures Claude Code (user settings) and
|
|
1365
|
+
Codex (config.toml) to export telemetry to the Workspace.
|
|
1366
|
+
|
|
1367
|
+
Options:
|
|
1368
|
+
--port <port> Loopback port for the login callback, 1-65535 (default 8080).
|
|
1369
|
+
--send-content Also send the text of the prompts you type. Takes effect only
|
|
1370
|
+
when the Workspace has opted in. Off by default.
|
|
1371
|
+
--no-plugin-auto-update
|
|
1372
|
+
Turn off automatic updates of the Harnessforce plugin in
|
|
1373
|
+
Claude Code. On by default; a value you set is kept.
|
|
1374
|
+
|
|
1375
|
+
Sends:
|
|
1376
|
+
The login exchange with the Workspace. After init, the agents you run send
|
|
1377
|
+
telemetry (token counts, tool names, timings) with that key.
|
|
1378
|
+
|
|
1379
|
+
Does not send:
|
|
1380
|
+
Prompt text (unless --send-content and the Workspace opt-in), model responses,
|
|
1381
|
+
tool input or output, code diffs, and secrets. The key is never written to a
|
|
1382
|
+
plain file.
|
|
1383
|
+
|
|
1384
|
+
Exit codes:
|
|
1385
|
+
0 Connected and configured.
|
|
1386
|
+
1 Failed (login cancelled or timed out, keychain unavailable, or a write
|
|
1387
|
+
failed). The reason is printed to stderr.
|
|
1388
|
+
|
|
1389
|
+
Examples:
|
|
1390
|
+
harnessforce init
|
|
1391
|
+
harnessforce init --send-content
|
|
1392
|
+
harnessforce init --port 8081
|
|
1393
|
+
`
|
|
1394
|
+
},
|
|
1395
|
+
import: {
|
|
1396
|
+
summary: "Send past local sessions to the Workspace",
|
|
1397
|
+
text: `Usage: harnessforce import
|
|
1398
|
+
|
|
1399
|
+
When to use:
|
|
1400
|
+
Right after init, to backfill sessions that ran before Harnessforce was
|
|
1401
|
+
connected. It is safe to run again; sessions already sent are skipped.
|
|
1402
|
+
|
|
1403
|
+
What it does:
|
|
1404
|
+
Reads Claude Code and Codex session records on this machine, keeps the
|
|
1405
|
+
sessions of repositories connected to the Workspace and inside its import
|
|
1406
|
+
window, and sends a summary of each. When it imports nothing, it prints why
|
|
1407
|
+
and what to do next.
|
|
1408
|
+
|
|
1409
|
+
Sends:
|
|
1410
|
+
Per session: agent, session ID, repository, branch, commit, start and end
|
|
1411
|
+
time, model, input and output token counts, and tool call counts.
|
|
1412
|
+
|
|
1413
|
+
Does not send:
|
|
1414
|
+
Prompt text, model responses, tool input or output, and code diffs. Sessions
|
|
1415
|
+
of repositories that are not connected are not sent.
|
|
1416
|
+
|
|
1417
|
+
Exit codes:
|
|
1418
|
+
0 Finished (including "nothing to import").
|
|
1419
|
+
1 Not logged in, keychain unavailable, a limit was reached, or sending failed.
|
|
1420
|
+
|
|
1421
|
+
Examples:
|
|
1422
|
+
harnessforce import
|
|
1423
|
+
`
|
|
1424
|
+
},
|
|
1425
|
+
"otel-headers": {
|
|
1426
|
+
summary: "Print the telemetry auth header (used by Claude Code)",
|
|
1427
|
+
text: `Usage: harnessforce otel-headers
|
|
1428
|
+
|
|
1429
|
+
When to use:
|
|
1430
|
+
You normally never run it. Claude Code calls it as its otelHeadersHelper.
|
|
1431
|
+
|
|
1432
|
+
What it does:
|
|
1433
|
+
Reads the ingest key from the keychain and prints {"Authorization": "Bearer <key>"}
|
|
1434
|
+
as JSON on stdout.
|
|
1435
|
+
|
|
1436
|
+
Sends:
|
|
1437
|
+
${SENDS_NOTHING_LOCAL}
|
|
1438
|
+
|
|
1439
|
+
Does not send:
|
|
1440
|
+
Anything. Do not paste its output into chats or logs: it contains the key.
|
|
1441
|
+
|
|
1442
|
+
Exit codes:
|
|
1443
|
+
0 Header printed.
|
|
1444
|
+
1 No usable key (not logged in, keychain unavailable, or the destination was
|
|
1445
|
+
not verified). Nothing is printed to stdout.
|
|
1446
|
+
|
|
1447
|
+
Examples:
|
|
1448
|
+
harnessforce otel-headers
|
|
1449
|
+
`
|
|
1450
|
+
},
|
|
1451
|
+
run: {
|
|
1452
|
+
summary: "Start an agent tied to an Issue",
|
|
1453
|
+
text: `Usage: harnessforce run --issue <identifier> -- <agent> [args]
|
|
1454
|
+
|
|
1455
|
+
When to use:
|
|
1456
|
+
When the branch name does not contain the Issue identifier and you want the
|
|
1457
|
+
work tied to the Issue anyway.
|
|
1458
|
+
|
|
1459
|
+
What it does:
|
|
1460
|
+
Starts <agent> (for example claude or codex) with your arguments, with the
|
|
1461
|
+
Issue identifier added to the telemetry it exports. Everything after "--" is
|
|
1462
|
+
passed to the agent unchanged, including "--help". The agent's exit code is
|
|
1463
|
+
returned.
|
|
1464
|
+
|
|
1465
|
+
Options:
|
|
1466
|
+
--issue <identifier> The Issue identifier, for example ABC-123.
|
|
1467
|
+
|
|
1468
|
+
Sends:
|
|
1469
|
+
The same telemetry as an agent started directly, plus the Issue identifier.
|
|
1470
|
+
|
|
1471
|
+
Does not send:
|
|
1472
|
+
Anything beyond what init configured. Prompt text is sent only if you chose
|
|
1473
|
+
--send-content at init.
|
|
1474
|
+
|
|
1475
|
+
Exit codes:
|
|
1476
|
+
The agent's own exit code when it ran.
|
|
1477
|
+
1 Could not start: not logged in, bad arguments, or the agent was not found.
|
|
1478
|
+
|
|
1479
|
+
Examples:
|
|
1480
|
+
harnessforce run --issue ABC-123 -- claude
|
|
1481
|
+
harnessforce run --issue ABC-123 -- codex --model gpt-5
|
|
1482
|
+
`
|
|
1483
|
+
},
|
|
1484
|
+
tune: {
|
|
1485
|
+
summary: "Analyze your past sessions and report counts",
|
|
1486
|
+
text: `Usage: harnessforce tune [--all] [--no-send] [--show-report] [--json]
|
|
1487
|
+
harnessforce tune record
|
|
1488
|
+
harnessforce tune --purge
|
|
1489
|
+
|
|
1490
|
+
When to use:
|
|
1491
|
+
Periodically (or through /harnessforce:tune) to see where human intervention,
|
|
1492
|
+
repeated work, and MCP or tool usage could improve. It needs at least 10
|
|
1493
|
+
sessions.
|
|
1494
|
+
|
|
1495
|
+
What it does:
|
|
1496
|
+
Imports sessions that were not sent yet, analyzes the local session records,
|
|
1497
|
+
and prints the result. Files it writes live under ~/.harnessforce/tune/.
|
|
1498
|
+
|
|
1499
|
+
Options:
|
|
1500
|
+
--all Analyze sessions of repositories not connected too (they are
|
|
1501
|
+
never sent).
|
|
1502
|
+
--no-send Analyze only; send nothing.
|
|
1503
|
+
--show-report Print the exact values that would be sent.
|
|
1504
|
+
--json Print the result as one JSON object on stdout. Messages go to
|
|
1505
|
+
stderr.
|
|
1506
|
+
record Read one proposal as JSON from stdin and store it locally.
|
|
1507
|
+
--purge Delete the analysis, proposals, and unsent results.
|
|
1508
|
+
|
|
1509
|
+
Sends:
|
|
1510
|
+
Counts computed by public rules (no text), for connected repositories only.
|
|
1511
|
+
It also runs the same session import as "harnessforce import".
|
|
1512
|
+
|
|
1513
|
+
Does not send:
|
|
1514
|
+
Prompt text, model responses, tool input or output, and code diffs. Usage
|
|
1515
|
+
statistics and proposals stay on this machine.
|
|
1516
|
+
|
|
1517
|
+
Exit codes:
|
|
1518
|
+
0 Done (including "not enough data" and "nothing sent").
|
|
1519
|
+
1 Not logged in, keychain unavailable, another tune is running, or a local
|
|
1520
|
+
write failed.
|
|
1521
|
+
2 Bad option, or invalid input to "record".
|
|
1522
|
+
3 Analysis shown, but sending is not possible until you run "harnessforce init".
|
|
1523
|
+
|
|
1524
|
+
Examples:
|
|
1525
|
+
harnessforce tune
|
|
1526
|
+
harnessforce tune --no-send --json
|
|
1527
|
+
harnessforce tune --purge
|
|
1528
|
+
`
|
|
1529
|
+
},
|
|
1530
|
+
top: {
|
|
1531
|
+
summary: "Show running and recent sessions on this machine",
|
|
1532
|
+
text: `Usage: harnessforce top [--once] [--json] [--ascii] [--theme <auto|light|dark|ansi>]
|
|
1533
|
+
|
|
1534
|
+
When to use:
|
|
1535
|
+
To watch what your agents are doing right now. In a terminal it is a live
|
|
1536
|
+
view; with --once or --json it prints a snapshot and exits.
|
|
1537
|
+
|
|
1538
|
+
What it does:
|
|
1539
|
+
Reads Claude Code and Codex session records on this machine and shows each
|
|
1540
|
+
session's state, repository, branch, tokens, and tool calls.
|
|
1541
|
+
|
|
1542
|
+
Options:
|
|
1543
|
+
--once Print one snapshot and exit.
|
|
1544
|
+
--json Print one snapshot as JSON and exit.
|
|
1545
|
+
--ascii Use plain ASCII instead of box-drawing characters.
|
|
1546
|
+
--theme Color theme: auto (default), light, dark, or ansi.
|
|
1547
|
+
|
|
1548
|
+
Sends:
|
|
1549
|
+
${SENDS_NOTHING_LOCAL}
|
|
1550
|
+
|
|
1551
|
+
Does not send:
|
|
1552
|
+
Anything. It never changes any file.
|
|
1553
|
+
|
|
1554
|
+
Exit codes:
|
|
1555
|
+
0 Done (including quitting the live view).
|
|
1556
|
+
1 Bad arguments.
|
|
1557
|
+
|
|
1558
|
+
Examples:
|
|
1559
|
+
harnessforce top
|
|
1560
|
+
harnessforce top --once --json
|
|
1561
|
+
`
|
|
1562
|
+
},
|
|
1563
|
+
hook: {
|
|
1564
|
+
summary: "Session-start hook (used by Codex)",
|
|
1565
|
+
text: `Usage: harnessforce hook session-start
|
|
1566
|
+
|
|
1567
|
+
When to use:
|
|
1568
|
+
You normally never run it. Codex runs it when a session starts.
|
|
1569
|
+
|
|
1570
|
+
What it does:
|
|
1571
|
+
Reads the hook input from stdin, registers the session with the Workspace, and
|
|
1572
|
+
prints context for the agent (the session ID, plus a one-line warning if
|
|
1573
|
+
registration failed).
|
|
1574
|
+
|
|
1575
|
+
Sends:
|
|
1576
|
+
Session registration: agent, session ID, repository, branch, commit, and
|
|
1577
|
+
start time.
|
|
1578
|
+
|
|
1579
|
+
Does not send:
|
|
1580
|
+
Prompt text, model responses, tool input or output, and code diffs.
|
|
1581
|
+
|
|
1582
|
+
Exit codes:
|
|
1583
|
+
0 Always, even on failure: a hook must never stop your session. A failure is
|
|
1584
|
+
shown as a one-line warning.
|
|
1585
|
+
1 Wrong arguments.
|
|
1586
|
+
|
|
1587
|
+
Examples:
|
|
1588
|
+
harnessforce hook session-start < hook-input.json
|
|
1589
|
+
`
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
const ORDER = [
|
|
1593
|
+
"init",
|
|
1594
|
+
"import",
|
|
1595
|
+
"run",
|
|
1596
|
+
"tune",
|
|
1597
|
+
"top",
|
|
1598
|
+
"otel-headers",
|
|
1599
|
+
"hook"
|
|
1600
|
+
];
|
|
1601
|
+
const NAME_WIDTH = Math.max(...ORDER.map((name) => name.length)) + 2;
|
|
1602
|
+
const OVERVIEW = `Usage: harnessforce <command> [options]
|
|
1603
|
+
|
|
1604
|
+
Connect your AI coding agents to a Harnessforce Workspace.
|
|
1605
|
+
|
|
1606
|
+
Commands:
|
|
1607
|
+
${ORDER.map((name) => ` ${name.padEnd(NAME_WIDTH)}${COMMANDS$1[name].summary}`).join("\n")}
|
|
1608
|
+
|
|
1609
|
+
Run "harnessforce <command> --help" for what a command does, what it sends and
|
|
1610
|
+
does not send, and its exit codes.
|
|
1611
|
+
|
|
1612
|
+
Options:
|
|
1613
|
+
-h, --help Show this help.
|
|
1614
|
+
--version Show the version.
|
|
1615
|
+
`;
|
|
1616
|
+
const UNKNOWN_COMMAND_HINT = `Usage: harnessforce <command> [options]
|
|
1617
|
+
Run "harnessforce --help" to list the commands.
|
|
1618
|
+
`;
|
|
1619
|
+
const isCommand = (name) => name !== void 0 && Object.hasOwn(COMMANDS$1, name);
|
|
1620
|
+
const isHelpFlag = (arg) => arg === "--help" || arg === "-h";
|
|
1621
|
+
function resolveHelp(argv) {
|
|
1622
|
+
const [command, ...rest] = argv;
|
|
1623
|
+
if (command === void 0) return void 0;
|
|
1624
|
+
if (isHelpFlag(command) && rest.length === 0) return OVERVIEW;
|
|
1625
|
+
if (command === "help") {
|
|
1626
|
+
if (rest.length === 0) return OVERVIEW;
|
|
1627
|
+
if (rest.length === 1 && isCommand(rest[0])) return COMMANDS$1[rest[0]].text;
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
if (!isCommand(command)) return void 0;
|
|
1631
|
+
const separator = rest.indexOf("--");
|
|
1632
|
+
return (separator === -1 ? rest : rest.slice(0, separator)).some(isHelpFlag) ? COMMANDS$1[command].text : void 0;
|
|
1633
|
+
}
|
|
1634
|
+
//#endregion
|
|
1635
|
+
//#region ../agent-core/src/hook-warning.ts
|
|
1636
|
+
const RESTART_FAILED_WARNING = "Could not restart the Harnessforce hook";
|
|
1637
|
+
const INVALID_ENDPOINT_WARNING = "The Harnessforce destination is invalid. Run `harnessforce init` again";
|
|
1638
|
+
const notConfiguredWarning = (agent) => agent === "claude_code" ? "Harnessforce sending is not configured. Run `/harnessforce:setup`" : "Harnessforce sending is not configured. Run `harnessforce init`";
|
|
1639
|
+
const stopped = (agent) => agent === "claude_code" ? "telemetry is not being sent" : "sessions are not being registered";
|
|
1640
|
+
const keyMissingWarning = (agent) => `Harnessforce cannot find your ingest key, so ${stopped(agent)}. Run \`harnessforce init\``;
|
|
1641
|
+
const keyUnreadableWarning = (agent) => `Harnessforce could not read your ingest key from the OS keychain, so ${agent === "claude_code" ? "telemetry may not be sent" : "sessions may not be registered"}. Unlock the keychain or run \`harnessforce init\``;
|
|
1642
|
+
const DESTINATION_MISMATCH_WARNING = {
|
|
1643
|
+
claude_code: "Telemetry is not being sent because the Harnessforce destination differs from the one saved by `harnessforce init`. Check this repository's settings or run `harnessforce init` again",
|
|
1644
|
+
codex: "Sessions are not being registered because the Harnessforce destination differs from the one saved by `harnessforce init`. Run `harnessforce init` again"
|
|
1645
|
+
};
|
|
1646
|
+
//#endregion
|
|
1647
|
+
//#region ../agent-core/src/issue.ts
|
|
1648
|
+
const isIssueIdentifier = (value) => isToken(value, 300);
|
|
1649
|
+
//#endregion
|
|
1650
|
+
//#region ../agent-core/src/using-harnessforce.ts
|
|
1651
|
+
const usingHarnessforceBody = "---\nname: using-harnessforce\ndescription: Use when starting a session in an environment with the Harnessforce hook, when starting work on an Issue, or when asked what is recorded or collected. The hook injects this text into the context at session start.\n---\n\n# Using Harnessforce\n\nThis session has the Harnessforce hook installed. Harnessforce links the work of people and AI to Issues and records it. Follow these rules.\n\n## What is recorded\n\n- Session registration: agent, session ID, repository, branch, commit, start time, and the first prompt ID.\n- Config snapshot: the structure of the configuration in use (Claude Code only).\n- OTel telemetry: what the user chooses to send.\n- For work on an Issue, `record-run` registers the session as a Run and closes it. It records no Plan, Decision, or Definition of Done.\n\n## What is not collected\n\n- Prompts, responses, tool input and output, and the body of code diffs arrive only when the user opts in with `--send-content` and the Workspace has opted in. They are not collected by default.\n- Never put these contents, secrets, or environment variable values in MCP tool input or in a record.\n\n## Which hook sends what\n\n- SessionStart: session registration and the config snapshot (Codex sends session registration only).\n- UserPromptSubmit: resends on the first prompt (Claude Code only).\n- A failing hook never stops the session. When the hook cannot send, it shows a one-line warning. Tell the user when a warning appears.\n\n## Which skill and tool to use, and when\n\n- For work on a specified Issue ID, or when the environment variable `HARNESSFORCE_ISSUE` is set, follow `record-run` and use `start_run`, `get_issue`, and `complete_run`. An agent that cannot load the skill calls the tools in the same order.\n- Pass the value of the `harnessforce session_id:` line in the context to `start_run`. If there are several, use the last one. If the line is missing, do not guess the session ID, and do not call `start_run` or `complete_run`.\n- Use `propose-improvements` for the analysis of `/harnessforce:tune`.\n- When no Issue is specified, do not start recording a Run.\n\n## Red Flags\n\nStop when one of these thoughts comes up.\n\n| Thought | Reality |\n| --- | --- |\n| An Issue was specified, but the work is small, so recording is unnecessary | Work on a specified Issue is recorded regardless of its size |\n| The session ID line is missing, so I will guess it and call `start_run` | Do not guess. The Run would no longer be linked to the session |\n| Summarizing the content before recording would help | Do not pass content to the tool. It goes beyond what is collected |\n| A warning appeared, but I can ignore it | Sending is not working. Tell the user |\n\n## Priority\n\n1. The user's instructions (CLAUDE.md, AGENTS.md, direct requests). If the user tells you to stop recording, stop recording the Run.\n2. This introduction.\n3. Default behavior.\n\n## Subagents\n\nA subagent that was delegated only a specific task by its parent does not register a Run by following this introduction. The parent registers the Run.\n".replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---\n+/, "").trimEnd();
|
|
1652
|
+
//#endregion
|
|
1653
|
+
//#region ../agent-core/src/session-context.ts
|
|
1654
|
+
const sessionContextLine = (sessionId) => `harnessforce session_id: ${sessionId}`;
|
|
1655
|
+
const sessionContext = (sessionId) => ({ hookSpecificOutput: {
|
|
1656
|
+
hookEventName: "SessionStart",
|
|
1657
|
+
additionalContext: `${sessionContextLine(sessionId)}\n\n${usingHarnessforceBody}`
|
|
1658
|
+
} });
|
|
1659
|
+
//#endregion
|
|
1660
|
+
//#region ../agent-core/src/vcs.ts
|
|
1661
|
+
async function selectRemote(cwd, git) {
|
|
1662
|
+
if (await git(cwd, ["rev-parse", "--is-inside-work-tree"]) !== "true") return void 0;
|
|
1663
|
+
const remotes = (await git(cwd, ["remote"]))?.split("\n") ?? [];
|
|
1664
|
+
return remotes.includes("origin") ? "origin" : remotes[0];
|
|
1665
|
+
}
|
|
1666
|
+
async function resolveVcs(cwd, git) {
|
|
1667
|
+
const remote = await selectRemote(cwd, git);
|
|
1668
|
+
if (!remote) return void 0;
|
|
1669
|
+
const [url, branch, commit] = await Promise.all([
|
|
1670
|
+
git(cwd, [
|
|
1671
|
+
"remote",
|
|
1672
|
+
"get-url",
|
|
1673
|
+
remote
|
|
1674
|
+
]),
|
|
1675
|
+
git(cwd, [
|
|
1676
|
+
"symbolic-ref",
|
|
1677
|
+
"--quiet",
|
|
1678
|
+
"--short",
|
|
1679
|
+
"HEAD"
|
|
1680
|
+
]),
|
|
1681
|
+
git(cwd, [
|
|
1682
|
+
"rev-parse",
|
|
1683
|
+
"--verify",
|
|
1684
|
+
"--quiet",
|
|
1685
|
+
"HEAD"
|
|
1686
|
+
])
|
|
1687
|
+
]);
|
|
1688
|
+
const repository = url === void 0 ? void 0 : normalizeRepository(url);
|
|
1689
|
+
return {
|
|
1690
|
+
...repository === void 0 ? {} : { repository },
|
|
1691
|
+
...branch === void 0 ? {} : { branch },
|
|
1692
|
+
...commit === void 0 ? {} : { commit }
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
async function resolveRepository(cwd, git) {
|
|
1696
|
+
if (!isAbsolute(cwd)) return void 0;
|
|
1697
|
+
const remote = await selectRemote(cwd, git);
|
|
1698
|
+
if (!remote) return void 0;
|
|
1699
|
+
const url = await git(cwd, [
|
|
1700
|
+
"remote",
|
|
1701
|
+
"get-url",
|
|
1702
|
+
remote
|
|
1703
|
+
]);
|
|
1704
|
+
return url === void 0 ? void 0 : normalizeRepository(url);
|
|
1705
|
+
}
|
|
1706
|
+
async function resolveProjectRoot(cwd, git) {
|
|
1707
|
+
const topLevel = await git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
1708
|
+
return topLevel && isAbsolute(topLevel) ? topLevel : cwd;
|
|
1709
|
+
}
|
|
1710
|
+
//#endregion
|
|
1098
1711
|
//#region src/credentials/user-destination.ts
|
|
1099
1712
|
async function resolveUserDestination(deps) {
|
|
1100
1713
|
const { workspaceId, ingestEndpoint } = await resolveCliDestinations(deps.env, userSettingsPath(deps.env, deps.homeDir), "");
|
|
@@ -1110,7 +1723,7 @@ async function resolveUserDestination(deps) {
|
|
|
1110
1723
|
};
|
|
1111
1724
|
if (!await deps.keychain.isAvailable()) return {
|
|
1112
1725
|
kind: "skipped",
|
|
1113
|
-
reason: "
|
|
1726
|
+
reason: "keychain unreadable"
|
|
1114
1727
|
};
|
|
1115
1728
|
const key = await deps.keychain.get(ingestKeyAccount(workspaceId));
|
|
1116
1729
|
if (!key) return {
|
|
@@ -1133,10 +1746,10 @@ async function resolveUserDestination(deps) {
|
|
|
1133
1746
|
//#endregion
|
|
1134
1747
|
//#region src/hook/session-start.ts
|
|
1135
1748
|
const HOOK_USAGE = "Usage: harnessforce hook session-start\n";
|
|
1136
|
-
const MAX_INPUT_BYTES
|
|
1749
|
+
const MAX_INPUT_BYTES = 1048576;
|
|
1137
1750
|
const REGISTERING_SOURCES = /* @__PURE__ */ new Set(["startup", "clear"]);
|
|
1138
1751
|
const KEYCHAIN_READ_TIMEOUT_MS = 1e3;
|
|
1139
|
-
const REVOKED_MESSAGE = "
|
|
1752
|
+
const REVOKED_MESSAGE = "Your ingest key has been revoked. Run `harnessforce init`";
|
|
1140
1753
|
function parseInput(raw) {
|
|
1141
1754
|
let fields;
|
|
1142
1755
|
try {
|
|
@@ -1154,18 +1767,28 @@ function parseInput(raw) {
|
|
|
1154
1767
|
};
|
|
1155
1768
|
}
|
|
1156
1769
|
const report$1 = (deps, detail) => deps.stderr(`harnessforce: session registration ${detail}\n`);
|
|
1770
|
+
const SKIPPED_WARNINGS = {
|
|
1771
|
+
"not configured": notConfiguredWarning("codex"),
|
|
1772
|
+
"invalid endpoint": INVALID_ENDPOINT_WARNING,
|
|
1773
|
+
"no ingest key": keyMissingWarning("codex"),
|
|
1774
|
+
"keychain unreadable": keyUnreadableWarning("codex"),
|
|
1775
|
+
"destination not verified": DESTINATION_MISMATCH_WARNING.codex
|
|
1776
|
+
};
|
|
1157
1777
|
function resolveWithinLimit(deps) {
|
|
1158
1778
|
let timer;
|
|
1159
1779
|
const slow = new Promise((resolve) => {
|
|
1160
1780
|
timer = setTimeout(() => resolve({
|
|
1161
1781
|
kind: "skipped",
|
|
1162
|
-
reason: "
|
|
1782
|
+
reason: "keychain unreadable"
|
|
1163
1783
|
}), KEYCHAIN_READ_TIMEOUT_MS);
|
|
1164
1784
|
});
|
|
1165
1785
|
return Promise.race([resolveUserDestination({
|
|
1166
1786
|
...deps,
|
|
1167
1787
|
keychain: deps.keychain.withTimeout(KEYCHAIN_READ_TIMEOUT_MS)
|
|
1168
|
-
})
|
|
1788
|
+
}).catch(() => ({
|
|
1789
|
+
kind: "skipped",
|
|
1790
|
+
reason: "keychain unreadable"
|
|
1791
|
+
})), slow]).finally(() => clearTimeout(timer));
|
|
1169
1792
|
}
|
|
1170
1793
|
async function resolveDestination(deps) {
|
|
1171
1794
|
const resolved = await resolveWithinLimit(deps);
|
|
@@ -1174,9 +1797,9 @@ async function resolveDestination(deps) {
|
|
|
1174
1797
|
report$1(deps, "skipped (not configured)");
|
|
1175
1798
|
return { warning: notConfiguredWarning("codex") };
|
|
1176
1799
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
return
|
|
1800
|
+
const detail = resolved.reason === "keychain unreadable" ? "no ingest key" : resolved.reason;
|
|
1801
|
+
report$1(deps, `skipped (${detail})`);
|
|
1802
|
+
return { warning: SKIPPED_WARNINGS[resolved.reason] };
|
|
1180
1803
|
}
|
|
1181
1804
|
function registrationSource(env) {
|
|
1182
1805
|
const issue = env.HARNESSFORCE_ISSUE;
|
|
@@ -1201,20 +1824,32 @@ async function register(input, deps) {
|
|
|
1201
1824
|
if (outcome.kind === "failed") report$1(deps, `failed (${outcome.reason})`);
|
|
1202
1825
|
return { outcome };
|
|
1203
1826
|
}
|
|
1827
|
+
async function selectWarning(registered, deps) {
|
|
1828
|
+
if (registered.outcome?.kind === "unauthorized") return REVOKED_MESSAGE;
|
|
1829
|
+
const state = await readCliUpdateState(deps.homeDir, deps.client.version);
|
|
1830
|
+
if (state?.status === "unsupported") return updateLine("cli", state);
|
|
1831
|
+
const sending = registered.warning ?? (registered.outcome?.kind === "failed" ? "Harnessforce session registration failed. Check your network connection" : void 0);
|
|
1832
|
+
if (sending !== void 0 || !state) return sending;
|
|
1833
|
+
return noticeLine("cli", updateStatePath(deps.homeDir), state, {
|
|
1834
|
+
now: deps.now(),
|
|
1835
|
+
env: deps.env
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1204
1838
|
async function sessionStart(deps) {
|
|
1205
|
-
const input = parseInput(await deps.readStdin(MAX_INPUT_BYTES
|
|
1839
|
+
const input = parseInput(await deps.readStdin(MAX_INPUT_BYTES).then((buffer) => buffer.toString("utf8")).catch(() => ""));
|
|
1206
1840
|
if (!input) return 0;
|
|
1207
|
-
const
|
|
1841
|
+
const registers = input.source === void 0 || REGISTERING_SOURCES.has(input.source);
|
|
1842
|
+
const blocked = registers && await isSendingBlocked(deps);
|
|
1843
|
+
const registered = registers && !blocked ? await register(input, deps).catch((error) => {
|
|
1208
1844
|
report$1(deps, `failed (${error instanceof Error ? error.name : "unknown"})`);
|
|
1209
1845
|
return {};
|
|
1210
1846
|
}) : {};
|
|
1211
|
-
const warning =
|
|
1212
|
-
if (warning !== void 0) {
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
}
|
|
1217
|
-
deps.stdout(`${JSON.stringify(sessionContext(input.sessionId))}\n`);
|
|
1847
|
+
const warning = registers ? await selectWarning(registered, deps) : void 0;
|
|
1848
|
+
if (warning !== void 0) deps.stderr(`${warning}\n`);
|
|
1849
|
+
deps.stdout(`${JSON.stringify({
|
|
1850
|
+
...sessionContext(input.sessionId),
|
|
1851
|
+
...warning === void 0 ? {} : { systemMessage: warning }
|
|
1852
|
+
})}\n`);
|
|
1218
1853
|
return 0;
|
|
1219
1854
|
}
|
|
1220
1855
|
//#endregion
|
|
@@ -1347,7 +1982,7 @@ const LOCK_WAIT_MS$1 = 3e4;
|
|
|
1347
1982
|
const STALE_LOCK_MS$1 = 6e4;
|
|
1348
1983
|
const RETRY_INTERVAL_MS$1 = 100;
|
|
1349
1984
|
const DIR_MODE$2 = 448;
|
|
1350
|
-
const FILE_MODE$
|
|
1985
|
+
const FILE_MODE$4 = 384;
|
|
1351
1986
|
const isAlreadyExists$1 = (error) => error?.code === "EEXIST";
|
|
1352
1987
|
async function removeIfStale$1(path, now) {
|
|
1353
1988
|
const created = await stat(path).catch(() => void 0);
|
|
@@ -1362,7 +1997,7 @@ async function acquireTokenLock(path, deps) {
|
|
|
1362
1997
|
const deadline = deps.now() + LOCK_WAIT_MS$1;
|
|
1363
1998
|
while (deps.now() < deadline) {
|
|
1364
1999
|
try {
|
|
1365
|
-
await (await open(path, "wx", FILE_MODE$
|
|
2000
|
+
await (await open(path, "wx", FILE_MODE$4)).close();
|
|
1366
2001
|
return () => rm(path, { force: true });
|
|
1367
2002
|
} catch (error) {
|
|
1368
2003
|
if (!isAlreadyExists$1(error)) throw error;
|
|
@@ -1579,7 +2214,7 @@ async function recordBase(deps) {
|
|
|
1579
2214
|
//#region src/import/state.ts
|
|
1580
2215
|
const STATE_VERSION = 1;
|
|
1581
2216
|
const DIR_MODE$1 = 448;
|
|
1582
|
-
const FILE_MODE$
|
|
2217
|
+
const FILE_MODE$3 = 384;
|
|
1583
2218
|
const sessionStateKey = (session) => session.agent === "claude_code" ? session.session_id : `${session.agent}:${session.session_id}`;
|
|
1584
2219
|
const importStatePath = (homeDir) => join(homeDir, ".harnessforce", "import-state.json");
|
|
1585
2220
|
const keyOf = ({ workspaceId, endpoint }) => `${workspaceId} ${endpoint}`;
|
|
@@ -1624,10 +2259,10 @@ async function recordSentSessions(path, destination, sessionIds) {
|
|
|
1624
2259
|
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
1625
2260
|
try {
|
|
1626
2261
|
await writeFile(temporary, `${JSON.stringify(next)}\n`, {
|
|
1627
|
-
mode: FILE_MODE$
|
|
2262
|
+
mode: FILE_MODE$3,
|
|
1628
2263
|
flag: "wx"
|
|
1629
2264
|
});
|
|
1630
|
-
await chmod(temporary, FILE_MODE$
|
|
2265
|
+
await chmod(temporary, FILE_MODE$3);
|
|
1631
2266
|
await rename(temporary, path);
|
|
1632
2267
|
} finally {
|
|
1633
2268
|
await rm(temporary, { force: true });
|
|
@@ -1965,7 +2600,7 @@ async function parseRollout(path) {
|
|
|
1965
2600
|
const accumulator = new Accumulator$1();
|
|
1966
2601
|
let skippedLines = 0;
|
|
1967
2602
|
try {
|
|
1968
|
-
const lines = createInterface
|
|
2603
|
+
const lines = createInterface({
|
|
1969
2604
|
input: createReadStream(path, { encoding: "utf8" }),
|
|
1970
2605
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
1971
2606
|
});
|
|
@@ -2194,7 +2829,7 @@ async function readTranscript(path, accumulator) {
|
|
|
2194
2829
|
let lines = 0;
|
|
2195
2830
|
let skippedLines = 0;
|
|
2196
2831
|
try {
|
|
2197
|
-
const reader = createInterface
|
|
2832
|
+
const reader = createInterface({
|
|
2198
2833
|
input: createReadStream(path, { encoding: "utf8" }),
|
|
2199
2834
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
2200
2835
|
});
|
|
@@ -2269,6 +2904,11 @@ async function parseTranscriptUsage(path) {
|
|
|
2269
2904
|
}
|
|
2270
2905
|
//#endregion
|
|
2271
2906
|
//#region src/import/sessions.ts
|
|
2907
|
+
const noneNotImported = () => ({
|
|
2908
|
+
outOfRange: 0,
|
|
2909
|
+
outsideGit: 0,
|
|
2910
|
+
notConnected: 0
|
|
2911
|
+
});
|
|
2272
2912
|
const readDir$1 = (dir) => readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2273
2913
|
async function listTranscripts(projectsDir) {
|
|
2274
2914
|
const projects = (await readDir$1(projectsDir)).filter((entry) => entry.isDirectory());
|
|
@@ -2295,33 +2935,49 @@ function toSessionImport(session, repository) {
|
|
|
2295
2935
|
};
|
|
2296
2936
|
}
|
|
2297
2937
|
const sortedSessions = (sessions) => [...sessions.values()].sort((a, b) => Date.parse(a.started_at) - Date.parse(b.started_at) || (a.session_id < b.session_id ? -1 : a.session_id > b.session_id ? 1 : 0));
|
|
2298
|
-
|
|
2938
|
+
const cachedRepositoryOf = (git) => {
|
|
2299
2939
|
const repositories = /* @__PURE__ */ new Map();
|
|
2300
|
-
|
|
2301
|
-
const cached = repositories.get(cwd) ?? resolveRepository(cwd,
|
|
2940
|
+
return (cwd) => {
|
|
2941
|
+
const cached = repositories.get(cwd) ?? resolveRepository(cwd, git);
|
|
2302
2942
|
repositories.set(cwd, cached);
|
|
2303
2943
|
return cached;
|
|
2304
2944
|
};
|
|
2945
|
+
};
|
|
2946
|
+
async function judge(session, options, repositoryOf) {
|
|
2947
|
+
if (Date.parse(session.endedAt) < options.sinceMs) return { kind: "outOfRange" };
|
|
2948
|
+
const repository = session.cwd ? await repositoryOf(session.cwd) : void 0;
|
|
2949
|
+
if (repository === void 0) return { kind: "outsideGit" };
|
|
2950
|
+
return options.connected.has(repository) ? {
|
|
2951
|
+
kind: "import",
|
|
2952
|
+
repository
|
|
2953
|
+
} : { kind: "notConnected" };
|
|
2954
|
+
}
|
|
2955
|
+
async function scanSessions(options) {
|
|
2956
|
+
const repositoryOf = cachedRepositoryOf(options.git);
|
|
2305
2957
|
const sessions = /* @__PURE__ */ new Map();
|
|
2958
|
+
const notImported = noneNotImported();
|
|
2306
2959
|
let skippedLines = 0;
|
|
2307
2960
|
let skippedFiles = 0;
|
|
2308
2961
|
for (const file of await listTranscripts(options.projectsDir)) {
|
|
2309
2962
|
const modifiedMs = (await stat(file).catch(() => void 0))?.mtimeMs;
|
|
2310
|
-
if (modifiedMs !== void 0 && modifiedMs < options.sinceMs)
|
|
2963
|
+
if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) {
|
|
2964
|
+
notImported.outOfRange += 1;
|
|
2965
|
+
continue;
|
|
2966
|
+
}
|
|
2311
2967
|
const result = await parseTranscript(file);
|
|
2312
2968
|
skippedLines += result.skippedLines;
|
|
2313
2969
|
if (result.kind === "unreadable") skippedFiles += 1;
|
|
2314
2970
|
if (result.kind !== "session") continue;
|
|
2315
2971
|
const { session } = result;
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
if (
|
|
2319
|
-
if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, toSessionImport(session, repository));
|
|
2972
|
+
const verdict = await judge(session, options, repositoryOf);
|
|
2973
|
+
if (verdict.kind !== "import") notImported[verdict.kind] += 1;
|
|
2974
|
+
else if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, toSessionImport(session, verdict.repository));
|
|
2320
2975
|
}
|
|
2321
2976
|
return {
|
|
2322
2977
|
sessions: sortedSessions(sessions),
|
|
2323
2978
|
skippedLines,
|
|
2324
|
-
skippedFiles
|
|
2979
|
+
skippedFiles,
|
|
2980
|
+
notImported
|
|
2325
2981
|
};
|
|
2326
2982
|
}
|
|
2327
2983
|
const numericName = (name, length) => name.length === length && /^\d+$/.test(name) ? Number(name) : void 0;
|
|
@@ -2371,43 +3027,39 @@ function toCodexSessionImport(session, repository) {
|
|
|
2371
3027
|
};
|
|
2372
3028
|
}
|
|
2373
3029
|
async function scanCodexSessions(options) {
|
|
2374
|
-
const
|
|
2375
|
-
const repositoryOf = (cwd) => {
|
|
2376
|
-
const cached = repositories.get(cwd) ?? resolveRepository(cwd, options.git);
|
|
2377
|
-
repositories.set(cwd, cached);
|
|
2378
|
-
return cached;
|
|
2379
|
-
};
|
|
2380
|
-
const isSent = async (cwd) => {
|
|
2381
|
-
if (!cwd) return void 0;
|
|
2382
|
-
const repository = await repositoryOf(cwd);
|
|
2383
|
-
return repository !== void 0 && options.connected.has(repository) ? repository : void 0;
|
|
2384
|
-
};
|
|
3030
|
+
const repositoryOf = cachedRepositoryOf(options.git);
|
|
2385
3031
|
const { files, compressed } = await listRollouts$1(join(options.codexDir, "sessions"), localDayKey(options.sinceMs));
|
|
2386
3032
|
const sessions = /* @__PURE__ */ new Map();
|
|
3033
|
+
const notImported = noneNotImported();
|
|
2387
3034
|
let skippedLines = 0;
|
|
2388
3035
|
let skippedFiles = 0;
|
|
2389
3036
|
let unextractable = 0;
|
|
2390
3037
|
for (const file of files) {
|
|
2391
3038
|
const modifiedMs = (await stat(file).catch(() => void 0))?.mtimeMs;
|
|
2392
|
-
if (modifiedMs !== void 0 && modifiedMs < options.sinceMs)
|
|
3039
|
+
if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) {
|
|
3040
|
+
notImported.outOfRange += 1;
|
|
3041
|
+
continue;
|
|
3042
|
+
}
|
|
2393
3043
|
const result = await parseRollout(file);
|
|
2394
3044
|
skippedLines += result.skippedLines;
|
|
2395
3045
|
if (result.kind === "unreadable") skippedFiles += 1;
|
|
2396
3046
|
if (result.kind === "unextractable") {
|
|
2397
|
-
|
|
3047
|
+
const verdict = await judge(result, options, repositoryOf);
|
|
3048
|
+
if (verdict.kind === "import") unextractable += 1;
|
|
3049
|
+
else notImported[verdict.kind] += 1;
|
|
2398
3050
|
continue;
|
|
2399
3051
|
}
|
|
2400
3052
|
if (result.kind !== "session") continue;
|
|
2401
3053
|
const { session } = result;
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
if (
|
|
2405
|
-
if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, toCodexSessionImport(session, repository));
|
|
3054
|
+
const verdict = await judge(session, options, repositoryOf);
|
|
3055
|
+
if (verdict.kind !== "import") notImported[verdict.kind] += 1;
|
|
3056
|
+
else if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, toCodexSessionImport(session, verdict.repository));
|
|
2406
3057
|
}
|
|
2407
3058
|
return {
|
|
2408
3059
|
sessions: sortedSessions(sessions),
|
|
2409
3060
|
skippedLines,
|
|
2410
3061
|
skippedFiles,
|
|
3062
|
+
notImported,
|
|
2411
3063
|
unextractable,
|
|
2412
3064
|
compressed
|
|
2413
3065
|
};
|
|
@@ -2415,49 +3067,78 @@ async function scanCodexSessions(options) {
|
|
|
2415
3067
|
//#endregion
|
|
2416
3068
|
//#region src/import/command.ts
|
|
2417
3069
|
const DAY_MS$1 = 864e5;
|
|
2418
|
-
const MESSAGES
|
|
2419
|
-
runInit: "`harnessforce init
|
|
2420
|
-
invalidUrl: "
|
|
3070
|
+
const MESSAGES = {
|
|
3071
|
+
runInit: "Run `harnessforce init`",
|
|
3072
|
+
invalidUrl: "The connection URL is invalid",
|
|
2421
3073
|
keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
|
|
2422
|
-
readFailed: "Harnessforce
|
|
2423
|
-
sendFailed: "Harnessforce
|
|
2424
|
-
revoked: "
|
|
2425
|
-
loginExpired: "
|
|
2426
|
-
stateFailed: "
|
|
3074
|
+
readFailed: "Communication with Harnessforce failed. Run `harnessforce import` again",
|
|
3075
|
+
sendFailed: "Communication with Harnessforce failed. Run `harnessforce import` again to resume importing",
|
|
3076
|
+
revoked: "Your ingest key has been revoked. Run `harnessforce init`",
|
|
3077
|
+
loginExpired: "Your login has expired. Run `harnessforce init`",
|
|
3078
|
+
stateFailed: "Could not save the import state",
|
|
3079
|
+
connectNone: "No GitHub repository is connected to the Workspace. Connect the GitHub App in Harnessforce to import sessions with `harnessforce import`",
|
|
3080
|
+
connectOther: "The session's repository is not connected to the Workspace. Connect that repository to the GitHub App in Harnessforce to import it with `harnessforce import`",
|
|
3081
|
+
nothingFound: "No session records to import were found"
|
|
2427
3082
|
};
|
|
2428
3083
|
const limitMessage = {
|
|
2429
|
-
monthly_event_limit: (count) =>
|
|
2430
|
-
workspace_read_only: (count) => `
|
|
3084
|
+
monthly_event_limit: (count) => `Could not import ${count} session(s) because the monthly event limit was reached. Run \`harnessforce import\` again after the limit is lifted to resume importing`,
|
|
3085
|
+
workspace_read_only: (count) => `Could not import ${count} session(s) because the Workspace is view-only. Run \`harnessforce import\` again after view-only mode is lifted to resume importing`
|
|
2431
3086
|
};
|
|
2432
|
-
const ACCESS_MESSAGES$
|
|
2433
|
-
keychainUnavailable: MESSAGES
|
|
2434
|
-
initRequired: MESSAGES
|
|
2435
|
-
invalidUrl: MESSAGES
|
|
2436
|
-
apiTokenMissing: MESSAGES
|
|
3087
|
+
const ACCESS_MESSAGES$2 = {
|
|
3088
|
+
keychainUnavailable: MESSAGES.keychainUnavailable,
|
|
3089
|
+
initRequired: MESSAGES.runInit,
|
|
3090
|
+
invalidUrl: MESSAGES.invalidUrl,
|
|
3091
|
+
apiTokenMissing: MESSAGES.runInit
|
|
2437
3092
|
};
|
|
2438
3093
|
function unwrap(outcome) {
|
|
2439
3094
|
if (outcome.kind === "ok") return outcome.value;
|
|
2440
|
-
return stopWith(outcome.kind === "unauthorized" ? MESSAGES
|
|
3095
|
+
return stopWith(outcome.kind === "unauthorized" ? MESSAGES.loginExpired : MESSAGES.readFailed);
|
|
2441
3096
|
}
|
|
2442
|
-
|
|
3097
|
+
const NOT_IMPORTED_LABELS = [
|
|
3098
|
+
["alreadySent", "already sent"],
|
|
3099
|
+
["outOfRange", "outside the period"],
|
|
3100
|
+
["outsideGit", "outside a git repository"],
|
|
3101
|
+
["notConnected", "in a repository not connected to the Workspace"]
|
|
3102
|
+
];
|
|
3103
|
+
function breakdownLine(breakdown) {
|
|
3104
|
+
const parts = NOT_IMPORTED_LABELS.filter(([key]) => breakdown[key] > 0).map(([key, label]) => `${label} ${breakdown[key]}`);
|
|
3105
|
+
return parts.length > 0 ? `Sessions not imported: ${parts.join(", ")}` : void 0;
|
|
3106
|
+
}
|
|
3107
|
+
function reportNotImported(imported, notes, deps) {
|
|
3108
|
+
const line = breakdownLine(notes.breakdown);
|
|
3109
|
+
if (line) deps.stdout(`${line}\n`);
|
|
3110
|
+
if (imported > 0) return;
|
|
3111
|
+
if (notes.breakdown.notConnected > 0) deps.stdout(`${notes.connectedCount === 0 ? MESSAGES.connectNone : MESSAGES.connectOther}\n`);
|
|
3112
|
+
else if (!line && !notes.otherNoticeShown) deps.stdout(`${MESSAGES.nothingFound}\n`);
|
|
3113
|
+
}
|
|
3114
|
+
function report(result, notes, deps) {
|
|
2443
3115
|
switch (result.kind) {
|
|
2444
3116
|
case "done":
|
|
2445
|
-
deps.stdout(
|
|
2446
|
-
if (result.invalid > 0) deps.stdout(
|
|
3117
|
+
deps.stdout(`Sessions imported: ${result.imported}\n`);
|
|
3118
|
+
if (result.invalid > 0) deps.stdout(`Could not import ${result.invalid} session(s) because their format is invalid\n`);
|
|
3119
|
+
reportNotImported(result.imported, {
|
|
3120
|
+
...notes,
|
|
3121
|
+
otherNoticeShown: notes.otherNoticeShown || result.invalid > 0
|
|
3122
|
+
}, deps);
|
|
2447
3123
|
return 0;
|
|
2448
3124
|
case "limited":
|
|
2449
3125
|
deps.stderr(`${limitMessage[result.reason](result.notImported)}\n`);
|
|
2450
3126
|
return 1;
|
|
2451
3127
|
case "unauthorized":
|
|
2452
|
-
deps.stderr(`${MESSAGES
|
|
3128
|
+
deps.stderr(`${MESSAGES.revoked}\n`);
|
|
2453
3129
|
return 1;
|
|
2454
3130
|
case "failed":
|
|
2455
|
-
deps.stderr(`${MESSAGES
|
|
3131
|
+
deps.stderr(`${MESSAGES.sendFailed}\n`);
|
|
2456
3132
|
return 1;
|
|
2457
3133
|
}
|
|
2458
3134
|
}
|
|
3135
|
+
const sumNotImported = (a, b) => ({
|
|
3136
|
+
outOfRange: a.outOfRange + b.outOfRange,
|
|
3137
|
+
outsideGit: a.outsideGit + b.outsideGit,
|
|
3138
|
+
notConnected: a.notConnected + b.notConnected
|
|
3139
|
+
});
|
|
2459
3140
|
async function runImport(deps) {
|
|
2460
|
-
const access = await verifyCliAccess(deps, ACCESS_MESSAGES$
|
|
3141
|
+
const access = await verifyCliAccess(deps, ACCESS_MESSAGES$2);
|
|
2461
3142
|
const destination = {
|
|
2462
3143
|
workspaceId: access.workspaceId,
|
|
2463
3144
|
endpoint: withoutExtras(access.ingest)
|
|
@@ -2488,20 +3169,28 @@ async function runImport(deps) {
|
|
|
2488
3169
|
});
|
|
2489
3170
|
const skippedLines = claude.skippedLines + codex.skippedLines;
|
|
2490
3171
|
const skippedFiles = claude.skippedFiles + codex.skippedFiles;
|
|
2491
|
-
if (skippedLines > 0 || skippedFiles > 0) deps.stdout(
|
|
2492
|
-
if (codex.unextractable > 0) deps.stdout(`
|
|
2493
|
-
if (codex.compressed > 0) deps.stdout(
|
|
3172
|
+
if (skippedLines > 0 || skippedFiles > 0) deps.stdout(`Skipped ${skippedLines} unreadable line(s) and ${skippedFiles} unreadable file(s)\n`);
|
|
3173
|
+
if (codex.unextractable > 0) deps.stdout(`Did not import ${codex.unextractable} Codex session(s) whose model or token count could not be read\n`);
|
|
3174
|
+
if (codex.compressed > 0) deps.stdout(`Did not import ${codex.compressed} compressed Codex session record(s) because they cannot be read\n`);
|
|
2494
3175
|
const scanned = [...claude.sessions, ...codex.sessions];
|
|
2495
3176
|
const statePath = importStatePath(deps.homeDir);
|
|
2496
3177
|
const sent = await readSentSessions(statePath, destination);
|
|
3178
|
+
const unsent = scanned.filter((s) => !sent.has(sessionStateKey(s)));
|
|
2497
3179
|
return report(await sendSessions({
|
|
2498
3180
|
endpoint: access.ingest,
|
|
2499
3181
|
ingestKey: access.ingestKey,
|
|
2500
|
-
sessions:
|
|
3182
|
+
sessions: unsent,
|
|
2501
3183
|
fetch: deps.fetch,
|
|
2502
3184
|
sleep: deps.sleep,
|
|
2503
|
-
record: (ids) => recordSentSessions(statePath, destination, ids).catch(() => stopWith(MESSAGES
|
|
2504
|
-
}),
|
|
3185
|
+
record: (ids) => recordSentSessions(statePath, destination, ids).catch(() => stopWith(MESSAGES.stateFailed))
|
|
3186
|
+
}), {
|
|
3187
|
+
breakdown: {
|
|
3188
|
+
alreadySent: scanned.length - unsent.length,
|
|
3189
|
+
...sumNotImported(claude.notImported, codex.notImported)
|
|
3190
|
+
},
|
|
3191
|
+
otherNoticeShown: skippedLines > 0 || skippedFiles > 0 || codex.unextractable > 0 || codex.compressed > 0,
|
|
3192
|
+
connectedCount: connected.size
|
|
3193
|
+
}, deps);
|
|
2505
3194
|
}
|
|
2506
3195
|
function importCommand(deps) {
|
|
2507
3196
|
return runUntilStop(() => runImport(deps), deps.stderr);
|
|
@@ -8981,7 +9670,7 @@ function Ko(e, t) {
|
|
|
8981
9670
|
const HOOK_COMMAND = "harnessforce hook session-start";
|
|
8982
9671
|
const HOOK_COMMAND_WINDOWS = "harnessforce.cmd hook session-start";
|
|
8983
9672
|
const HOOK_TIMEOUT_SECONDS = 10;
|
|
8984
|
-
const FILE_MODE$
|
|
9673
|
+
const FILE_MODE$2 = 384;
|
|
8985
9674
|
function codexConfigPath(env, homeDir) {
|
|
8986
9675
|
return join(absoluteEnv(env.CODEX_HOME) ?? join(homeDir, ".codex"), "config.toml");
|
|
8987
9676
|
}
|
|
@@ -9056,10 +9745,10 @@ async function writeCodexConfig(path, text) {
|
|
|
9056
9745
|
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
9057
9746
|
try {
|
|
9058
9747
|
await writeFile(temporary, text, {
|
|
9059
|
-
mode: FILE_MODE$
|
|
9748
|
+
mode: FILE_MODE$2,
|
|
9060
9749
|
flag: "wx"
|
|
9061
9750
|
});
|
|
9062
|
-
await chmod(temporary, FILE_MODE$
|
|
9751
|
+
await chmod(temporary, FILE_MODE$2);
|
|
9063
9752
|
await rename(temporary, path);
|
|
9064
9753
|
} finally {
|
|
9065
9754
|
await rm(temporary, { force: true });
|
|
@@ -9144,10 +9833,12 @@ const stop$1 = (message) => stopWith(INIT_MESSAGES[message]);
|
|
|
9144
9833
|
const sha256Hex = (value) => createHash("sha256").update(value, "utf8").digest("hex");
|
|
9145
9834
|
function init(args, deps) {
|
|
9146
9835
|
return runUntilStop(async () => {
|
|
9147
|
-
const { codexPath, isKernelKeyring } = await runInit(args, deps);
|
|
9836
|
+
const { codexPath, connection, isKernelKeyring, isPluginAutoUpdateOn } = await runInit(args, deps);
|
|
9837
|
+
if (connection !== withoutExtras(new URL(deps.defaultUrl))) deps.stdout(`${nonDefaultConnectionMessage(connection)}\n`);
|
|
9148
9838
|
const lines = [
|
|
9149
9839
|
INIT_MESSAGES.success,
|
|
9150
9840
|
...isKernelKeyring ? [INIT_MESSAGES.savedToKernelKeyring] : [],
|
|
9841
|
+
...isPluginAutoUpdateOn ? [INIT_MESSAGES.pluginAutoUpdateOn] : [],
|
|
9151
9842
|
codexNotes(codexPath)
|
|
9152
9843
|
];
|
|
9153
9844
|
deps.stdout(`${lines.join("\n")}\n`);
|
|
@@ -9161,7 +9852,8 @@ async function runInit(args, deps) {
|
|
|
9161
9852
|
const settingsPath = userSettingsPath(deps.env, deps.homeDir);
|
|
9162
9853
|
const store = await openKeychain(deps, (await readSettingsEnv$1(settingsPath)).HARNESSFORCE_WORKSPACE_ID);
|
|
9163
9854
|
const revoke = revokeHashes(store);
|
|
9164
|
-
|
|
9855
|
+
const settings = await readUserSettings(settingsPath);
|
|
9856
|
+
if (settings.kind === "invalid" || !hasWritableMarketplaces(settings.settings)) stop$1("settingsUnreadable");
|
|
9165
9857
|
const authorizationEndpoint = await discoverAuthorizationEndpoint(base, deps.fetch) ?? stop$1("network");
|
|
9166
9858
|
const secrets = createLoginSecrets();
|
|
9167
9859
|
const { code, redirectUri } = await logIn(authorizationEndpoint, secrets, port, deps);
|
|
@@ -9173,7 +9865,7 @@ async function runInit(args, deps) {
|
|
|
9173
9865
|
revoke_key_hashes: revoke.keyHashes,
|
|
9174
9866
|
revoke_api_token_hashes: revoke.apiTokenHashes
|
|
9175
9867
|
}, deps.fetch));
|
|
9176
|
-
await save(issued, recordedUrl, settingsPath, args
|
|
9868
|
+
const isPluginAutoUpdateOn = await save(issued, recordedUrl, settingsPath, args, store).catch((error) => {
|
|
9177
9869
|
if (isStop(error)) throw error;
|
|
9178
9870
|
return stop$1("saveFailed");
|
|
9179
9871
|
});
|
|
@@ -9182,7 +9874,9 @@ async function runInit(args, deps) {
|
|
|
9182
9874
|
if (args.sendContent) deps.stdout(`${issued.contentOptIn ? INIT_MESSAGES.contentEnabled : INIT_MESSAGES.contentNotOptedIn}\n`);
|
|
9183
9875
|
return {
|
|
9184
9876
|
codexPath,
|
|
9185
|
-
|
|
9877
|
+
connection: recordedUrl,
|
|
9878
|
+
isKernelKeyring: store.isKernelKeyring,
|
|
9879
|
+
isPluginAutoUpdateOn
|
|
9186
9880
|
};
|
|
9187
9881
|
}
|
|
9188
9882
|
async function openKeychain(deps, previousWorkspaceId) {
|
|
@@ -9244,7 +9938,7 @@ function issuedOrStop(outcome) {
|
|
|
9244
9938
|
case "unexpected": return stop$1("network");
|
|
9245
9939
|
}
|
|
9246
9940
|
}
|
|
9247
|
-
async function save(issued, connection, settingsPath,
|
|
9941
|
+
async function save(issued, connection, settingsPath, args, store) {
|
|
9248
9942
|
await store.delete(ingestOriginAccount(issued.workspaceId));
|
|
9249
9943
|
await store.delete(urlOriginAccount(issued.workspaceId));
|
|
9250
9944
|
await store.set(ingestKeyAccount(issued.workspaceId), issued.ingestKey);
|
|
@@ -9253,7 +9947,7 @@ async function save(issued, connection, settingsPath, sendContent, store) {
|
|
|
9253
9947
|
await store.set(urlOriginAccount(issued.workspaceId), new URL(connection).origin);
|
|
9254
9948
|
await store.deleteElsewhere(issued.workspaceId);
|
|
9255
9949
|
const current = await readUserSettings(settingsPath);
|
|
9256
|
-
if (current.kind === "invalid") return stop$1("saveFailed");
|
|
9950
|
+
if (current.kind === "invalid" || !hasWritableMarketplaces(current.settings)) return stop$1("saveFailed");
|
|
9257
9951
|
const env = {
|
|
9258
9952
|
HARNESSFORCE_URL: connection,
|
|
9259
9953
|
HARNESSFORCE_ENDPOINT: issued.ingestEndpoint,
|
|
@@ -9267,8 +9961,10 @@ async function save(issued, connection, settingsPath, sendContent, store) {
|
|
|
9267
9961
|
OTEL_TRACES_EXPORTER: "otlp",
|
|
9268
9962
|
OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf"
|
|
9269
9963
|
};
|
|
9270
|
-
if (sendContent && issued.contentOptIn) env.OTEL_LOG_USER_PROMPTS = "1";
|
|
9271
|
-
|
|
9964
|
+
if (args.sendContent && issued.contentOptIn) env.OTEL_LOG_USER_PROMPTS = "1";
|
|
9965
|
+
const { settings, isOn } = withPluginAutoUpdate(mergeUserSettings(current.settings, env), args.noPluginAutoUpdate);
|
|
9966
|
+
await writeUserSettings(settingsPath, settings);
|
|
9967
|
+
return isOn;
|
|
9272
9968
|
}
|
|
9273
9969
|
async function saveCodexConfig(issued, connection, path, sendContent) {
|
|
9274
9970
|
await writeCodexConfig(path, mergeCodexConfig(await readCodexConfig(path), {
|
|
@@ -9288,11 +9984,23 @@ const DESTINATION_VARIABLES = [
|
|
|
9288
9984
|
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"
|
|
9289
9985
|
];
|
|
9290
9986
|
const WITHHELD = "harnessforce: user key withheld (destination not verified)\n";
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9987
|
+
const EXIT_KEY_MISSING = 3;
|
|
9988
|
+
const EXIT_WITHHELD = 4;
|
|
9989
|
+
const EXIT_FAILED = 1;
|
|
9990
|
+
async function otelHeaders(env, managedDir, keychain, stdout, stderr, endStdout) {
|
|
9991
|
+
const selection = await selectKey(env, managedDir, keychain).catch(() => ({ kind: "failed" }));
|
|
9992
|
+
if (selection.kind === "withheld") {
|
|
9993
|
+
stderr(WITHHELD);
|
|
9994
|
+
return EXIT_WITHHELD;
|
|
9995
|
+
}
|
|
9996
|
+
if (selection.kind === "missing") return EXIT_KEY_MISSING;
|
|
9997
|
+
if (selection.kind === "failed") return EXIT_FAILED;
|
|
9295
9998
|
stdout(`${JSON.stringify({ Authorization: `Bearer ${selection.key}` })}\n`);
|
|
9999
|
+
const { user } = selection;
|
|
10000
|
+
if (user && !env.HARNESSFORCE_NO_MIGRATE) {
|
|
10001
|
+
await endStdout();
|
|
10002
|
+
await user.keychain.migrateFromKernel?.(user.workspaceId).catch(() => {});
|
|
10003
|
+
}
|
|
9296
10004
|
return 0;
|
|
9297
10005
|
}
|
|
9298
10006
|
const HOOK_READ_TIMEOUT_MS = 1e3;
|
|
@@ -9303,15 +10011,19 @@ async function selectKey(env, managedDir, unlimited) {
|
|
|
9303
10011
|
key: managed.HARNESSFORCE_INGEST_KEY
|
|
9304
10012
|
};
|
|
9305
10013
|
const workspaceId = env.HARNESSFORCE_WORKSPACE_ID;
|
|
9306
|
-
if (!workspaceId) return { kind: "
|
|
10014
|
+
if (!workspaceId) return { kind: "missing" };
|
|
9307
10015
|
const keychain = unlimited.withTimeout(HOOK_READ_TIMEOUT_MS);
|
|
9308
|
-
if (!await keychain.isAvailable()) return { kind: "
|
|
10016
|
+
if (!await keychain.isAvailable()) return { kind: "failed" };
|
|
9309
10017
|
const key = await keychain.get(ingestKeyAccount(workspaceId));
|
|
9310
|
-
if (!key) return { kind: "
|
|
10018
|
+
if (!key) return { kind: "missing" };
|
|
9311
10019
|
const pinnedOrigin = await keychain.get(ingestOriginAccount(workspaceId));
|
|
9312
10020
|
return pinnedOrigin && isPinnedDestination(env, pinnedOrigin) ? {
|
|
9313
10021
|
kind: "key",
|
|
9314
|
-
key
|
|
10022
|
+
key,
|
|
10023
|
+
user: {
|
|
10024
|
+
keychain,
|
|
10025
|
+
workspaceId
|
|
10026
|
+
}
|
|
9315
10027
|
} : { kind: "withheld" };
|
|
9316
10028
|
}
|
|
9317
10029
|
function isPinnedDestination(env, pinnedOrigin) {
|
|
@@ -9642,16 +10354,16 @@ function buildLaunch(request) {
|
|
|
9642
10354
|
//#region src/run/messages.ts
|
|
9643
10355
|
const RUN_MESSAGES = {
|
|
9644
10356
|
keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
|
|
9645
|
-
initRequired: "`harnessforce init
|
|
10357
|
+
initRequired: "Run `harnessforce init`",
|
|
9646
10358
|
invalidUrl: INIT_MESSAGES.invalidUrl,
|
|
9647
|
-
invalidIssue: "Issue
|
|
9648
|
-
issueInitRequired: "Issue
|
|
9649
|
-
loginExpired: "
|
|
9650
|
-
issueFailed: "Issue
|
|
9651
|
-
noMatch: "
|
|
9652
|
-
candidatesHeader: "Issue
|
|
10359
|
+
invalidIssue: "The Issue identifier is invalid",
|
|
10360
|
+
issueInitRequired: "Could not resolve the Issue. Run `harnessforce init`",
|
|
10361
|
+
loginExpired: "Your login has expired. Run `harnessforce init`",
|
|
10362
|
+
issueFailed: "Could not resolve the Issue or fetch candidates",
|
|
10363
|
+
noMatch: "No matching Issue",
|
|
10364
|
+
candidatesHeader: "Could not resolve the Issue. Candidates:"
|
|
9653
10365
|
};
|
|
9654
|
-
const launchFailedMessage = (agent) =>
|
|
10366
|
+
const launchFailedMessage = (agent) => `Could not start ${agent}`;
|
|
9655
10367
|
//#endregion
|
|
9656
10368
|
//#region src/run/run.ts
|
|
9657
10369
|
const stop = (message) => stopWith(RUN_MESSAGES[message]);
|
|
@@ -9660,14 +10372,14 @@ function candidateList(candidates) {
|
|
|
9660
10372
|
const lines = candidates.map(({ identifier, title }) => `${printable(identifier)} ${printable(title)}`);
|
|
9661
10373
|
return [RUN_MESSAGES.candidatesHeader, ...lines].join("\n");
|
|
9662
10374
|
}
|
|
9663
|
-
const ACCESS_MESSAGES$
|
|
10375
|
+
const ACCESS_MESSAGES$1 = {
|
|
9664
10376
|
keychainUnavailable: RUN_MESSAGES.keychainUnavailable,
|
|
9665
10377
|
initRequired: RUN_MESSAGES.initRequired,
|
|
9666
10378
|
invalidUrl: RUN_MESSAGES.invalidUrl,
|
|
9667
10379
|
apiTokenMissing: RUN_MESSAGES.issueInitRequired
|
|
9668
10380
|
};
|
|
9669
10381
|
async function prepareLaunch(args, deps) {
|
|
9670
|
-
const { workspaceId, ingestEndpoint, readApiBase, accessToken } = await verifyCliAccess(deps, ACCESS_MESSAGES$
|
|
10382
|
+
const { workspaceId, ingestEndpoint, readApiBase, accessToken } = await verifyCliAccess(deps, ACCESS_MESSAGES$1);
|
|
9671
10383
|
if (!isIssueIdentifier(args.issue)) stop("invalidIssue");
|
|
9672
10384
|
const readFetch = createApiTokenSession({
|
|
9673
10385
|
keychain: deps.keychain,
|
|
@@ -9718,6 +10430,20 @@ function runIssue(args, deps) {
|
|
|
9718
10430
|
return 1;
|
|
9719
10431
|
}, deps.stderr);
|
|
9720
10432
|
}
|
|
10433
|
+
async function runWithoutHarnessforce(args, deps) {
|
|
10434
|
+
const env = Object.fromEntries(Object.entries({
|
|
10435
|
+
...deps.env,
|
|
10436
|
+
...deps.restoredEnv
|
|
10437
|
+
}).filter((entry) => entry[1] !== void 0));
|
|
10438
|
+
const outcome = await deps.launch({
|
|
10439
|
+
command: args.agent,
|
|
10440
|
+
args: [...args.agentArgs],
|
|
10441
|
+
env
|
|
10442
|
+
});
|
|
10443
|
+
if (outcome.kind === "exited") return outcome.code;
|
|
10444
|
+
deps.stderr(`${launchFailedMessage(args.agent)}\n`);
|
|
10445
|
+
return 1;
|
|
10446
|
+
}
|
|
9721
10447
|
//#endregion
|
|
9722
10448
|
//#region src/top/args.ts
|
|
9723
10449
|
const TOP_USAGE = "Usage: harnessforce top [--once] [--json] [--ascii] [--theme <auto|light|dark|ansi>]\n";
|
|
@@ -9953,7 +10679,7 @@ function renderList(sessions, options) {
|
|
|
9953
10679
|
const active = sessions.filter((s) => sessionState(s.lastEventAtMs, options.nowMs) === "active").length;
|
|
9954
10680
|
return [clip(`${sessions.length} sessions, ${active} active`, options)];
|
|
9955
10681
|
}
|
|
9956
|
-
const body = sessions.length === 0 ? ["
|
|
10682
|
+
const body = sessions.length === 0 ? ["No sessions recorded in the last 24 hours"].map((t) => clip(t, options)) : options.width >= COMPACT_WIDTH ? table(sessions, options, options.width >= FULL_WIDTH) : shortRows(sessions, options);
|
|
9957
10683
|
return [
|
|
9958
10684
|
header(sessions, options),
|
|
9959
10685
|
...body,
|
|
@@ -10014,7 +10740,7 @@ function renderDetail(row, options) {
|
|
|
10014
10740
|
clip(options.painter.dim(DETAIL_HINT), options)
|
|
10015
10741
|
];
|
|
10016
10742
|
}
|
|
10017
|
-
const skippedNotice = (snapshot) => snapshot.skippedLines > 0 || snapshot.skippedFiles > 0 ? [
|
|
10743
|
+
const skippedNotice = (snapshot) => snapshot.skippedLines > 0 || snapshot.skippedFiles > 0 ? [`Skipped ${snapshot.skippedLines} unreadable line(s) and ${snapshot.skippedFiles} unreadable file(s)`] : [];
|
|
10018
10744
|
//#endregion
|
|
10019
10745
|
//#region src/top/codex-session.ts
|
|
10020
10746
|
const DETACHED = "HEAD";
|
|
@@ -10629,7 +11355,7 @@ const DEFAULT_SETTINGS = {
|
|
|
10629
11355
|
};
|
|
10630
11356
|
const unreadable = {
|
|
10631
11357
|
settings: DEFAULT_SETTINGS,
|
|
10632
|
-
notice: "
|
|
11358
|
+
notice: "Cannot read the device settings (`~/.harnessforce/config.json`), so the default display is used"
|
|
10633
11359
|
};
|
|
10634
11360
|
async function readTopSettings(homeDir) {
|
|
10635
11361
|
let text;
|
|
@@ -11119,7 +11845,9 @@ const median = (values) => {
|
|
|
11119
11845
|
const middle = Math.floor(sorted.length / 2);
|
|
11120
11846
|
return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
11121
11847
|
};
|
|
11122
|
-
const
|
|
11848
|
+
const escapeForTerminal = (text) => text.replace(/[^\x20-\x7e]/gu, (c) => `\\u{${c.codePointAt(0).toString(16)}}`);
|
|
11849
|
+
const asciiJson = (value, space) => JSON.stringify(value, null, space).replace(/[^\n\x20-\x7e]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
11850
|
+
const secondsText = (value) => value === null ? "—" : `${value.toFixed(1)}s`;
|
|
11123
11851
|
function summarize(reports, group, kind, countField, medianField) {
|
|
11124
11852
|
const values = reports.map((r) => r[group][kind]).filter((v) => v?.measurement === "measured");
|
|
11125
11853
|
if (values.length === 0) return void 0;
|
|
@@ -11132,16 +11860,16 @@ function summarize(reports, group, kind, countField, medianField) {
|
|
|
11132
11860
|
};
|
|
11133
11861
|
}
|
|
11134
11862
|
function renderAnalysis(reports, usages, header) {
|
|
11135
|
-
const lines = [
|
|
11863
|
+
const lines = [`Sessions analyzed: ${header.sessionCount} (analyzer ${header.analyzerVersion}, parser ${header.parserVersion})`, "Human interventions (count, median of per-session median wait time):"];
|
|
11136
11864
|
for (const kind of INTERVENTION_KINDS) {
|
|
11137
11865
|
const s = summarize(reports, "interventions", kind, "count", "wait_seconds_median");
|
|
11138
|
-
lines.push(s === void 0 ? ` ${kind}:
|
|
11866
|
+
lines.push(s === void 0 ? ` ${kind}: not measured` : ` ${kind}: ${s.total} time(s), ${secondsText(s.median)}`);
|
|
11139
11867
|
}
|
|
11140
|
-
lines.push("
|
|
11868
|
+
lines.push("Repeatable loops (count, interventions during them, median of median duration):");
|
|
11141
11869
|
for (const kind of LOOP_KINDS) {
|
|
11142
11870
|
const s = summarize(reports, "loops", kind, "occurrences", "duration_seconds_median");
|
|
11143
11871
|
const interventions = s?.values.reduce((sum, v) => sum + (v.interventions ?? 0), 0) ?? 0;
|
|
11144
|
-
lines.push(s === void 0 ? ` ${kind}:
|
|
11872
|
+
lines.push(s === void 0 ? ` ${kind}: not measured` : ` ${kind}: ${s.total} time(s), ${interventions} intervention(s), ${secondsText(s.median)}`);
|
|
11145
11873
|
}
|
|
11146
11874
|
const servers = /* @__PURE__ */ new Map();
|
|
11147
11875
|
for (const report of reports) for (const s of report.mcp_servers) {
|
|
@@ -11156,19 +11884,19 @@ function renderAnalysis(reports, usages, header) {
|
|
|
11156
11884
|
configured: current.configured + (s.configured ? 1 : 0)
|
|
11157
11885
|
});
|
|
11158
11886
|
}
|
|
11159
|
-
lines.push("MCP
|
|
11160
|
-
if (servers.size === 0) lines.push("
|
|
11161
|
-
for (const [server, s] of [...servers].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) lines.push(` ${server}: ${s.calls === null ? "
|
|
11887
|
+
lines.push("MCP servers (calls, failures, sessions where configured):");
|
|
11888
|
+
if (servers.size === 0) lines.push(" none");
|
|
11889
|
+
for (const [server, s] of [...servers].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) lines.push(` ${escapeForTerminal(server)}: ${s.calls === null ? "not measured" : `${s.calls} call(s)`}, ${s.failures === null ? "not measured" : `${s.failures} failure(s)`}, ${s.configured} session(s)`);
|
|
11162
11890
|
lines.push(...renderUsage(usages));
|
|
11163
11891
|
return `${lines.join("\n")}\n`;
|
|
11164
11892
|
}
|
|
11165
11893
|
const percent = (share) => `${(share * 100).toFixed(1)}%`;
|
|
11166
11894
|
function renderUsage(usages) {
|
|
11167
|
-
const lines = ["
|
|
11895
|
+
const lines = ["Usage (this device only; not sent):"];
|
|
11168
11896
|
for (const kind of ["frequent_compaction", "low_cache_reuse"]) lines.push(` ${kind}: ${usages.filter((u) => u.kinds.includes(kind)).length} session`);
|
|
11169
11897
|
const shares = modelShares(usages);
|
|
11170
|
-
const parts = shares && [...shares.main.map((m) =>
|
|
11171
|
-
lines.push(` model
|
|
11898
|
+
const parts = shares && [...shares.main.map((m) => `main ${escapeForTerminal(m.model)} ${percent(m.share)}`), ...shares.subagent.map((m) => `subagent ${escapeForTerminal(m.model)} ${percent(m.share)}`)];
|
|
11899
|
+
lines.push(` model (share of output_tokens): ${parts ? parts.join(", ") : "none"}`);
|
|
11172
11900
|
return lines;
|
|
11173
11901
|
}
|
|
11174
11902
|
function localIso(ms) {
|
|
@@ -11289,7 +12017,7 @@ const LOCK_WAIT_MS = 3e4;
|
|
|
11289
12017
|
const STALE_LOCK_MS = 6e4;
|
|
11290
12018
|
const HEARTBEAT_MS = 1e4;
|
|
11291
12019
|
const RETRY_INTERVAL_MS = 100;
|
|
11292
|
-
const FILE_MODE$
|
|
12020
|
+
const FILE_MODE$1 = 384;
|
|
11293
12021
|
const isAlreadyExists = (error) => error?.code === "EEXIST";
|
|
11294
12022
|
async function removeIfStale(path, now) {
|
|
11295
12023
|
const current = await stat(path).catch(() => void 0);
|
|
@@ -11299,7 +12027,7 @@ async function acquireTuneLock(path, deps) {
|
|
|
11299
12027
|
const deadline = deps.now() + LOCK_WAIT_MS;
|
|
11300
12028
|
while (deps.now() < deadline) {
|
|
11301
12029
|
try {
|
|
11302
|
-
const handle = await open(path, "wx", FILE_MODE$
|
|
12030
|
+
const handle = await open(path, "wx", FILE_MODE$1);
|
|
11303
12031
|
const token = randomUUID();
|
|
11304
12032
|
await handle.writeFile(token);
|
|
11305
12033
|
await handle.close();
|
|
@@ -11344,33 +12072,33 @@ const NOTICE_ORDER = [
|
|
|
11344
12072
|
];
|
|
11345
12073
|
const TUNE_MESSAGES = {
|
|
11346
12074
|
keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
|
|
11347
|
-
initRequired: "`harnessforce init
|
|
11348
|
-
invalidUrl: "
|
|
11349
|
-
locked: "
|
|
11350
|
-
insufficient: (count) =>
|
|
11351
|
-
outOfRange: (days, count) =>
|
|
11352
|
-
notConnected: (count) =>
|
|
11353
|
-
partlyUnreadable: (lines, skipped) =>
|
|
11354
|
-
savedList: (fetchedAt) =>
|
|
11355
|
-
noList: "
|
|
11356
|
-
noSend: "
|
|
11357
|
-
settingUnreadable: "
|
|
11358
|
-
importLimited: (count) =>
|
|
11359
|
-
importFailed: "
|
|
11360
|
-
sent: (count) =>
|
|
11361
|
-
unreachable: (count) => `Harnessforce
|
|
11362
|
-
expiredUnsent: (count) =>
|
|
11363
|
-
readOnly: (count) => `
|
|
11364
|
-
invalidReports: (count, reasons) =>
|
|
11365
|
-
rejected: (status, count) => `Harnessforce
|
|
11366
|
-
keyUnusable: (count) =>
|
|
11367
|
-
loginExpired: "
|
|
11368
|
-
unwritable: "`~/.harnessforce/tune
|
|
11369
|
-
recordFailed: "
|
|
11370
|
-
recordInvalid: (field) =>
|
|
11371
|
-
usage: "
|
|
11372
|
-
purged: "`~/.harnessforce/tune
|
|
11373
|
-
purgeFailed: "`~/.harnessforce/tune
|
|
12075
|
+
initRequired: "Run `harnessforce init`. The Viewer role cannot use `harnessforce tune`",
|
|
12076
|
+
invalidUrl: "The connection URL is invalid",
|
|
12077
|
+
locked: "Another `harnessforce tune` is running, so this run cannot start",
|
|
12078
|
+
insufficient: (count) => `Not enough data: ${count} session(s) analyzed. Suggestions need at least 10 (${10 - count} more needed)`,
|
|
12079
|
+
outOfRange: (days, count) => `Outside the sending range: ${count} session(s) that started more than ${days} days ago will not be sent`,
|
|
12080
|
+
notConnected: (count) => `${count} session(s) in repositories that are not connected will not be sent`,
|
|
12081
|
+
partlyUnreadable: (lines, skipped) => `Some records could not be read: skipped ${skipped} of ${lines} lines read`,
|
|
12082
|
+
savedList: (fetchedAt) => `Could not fetch the list of connected repositories, so the list saved at ${fetchedAt} is used`,
|
|
12083
|
+
noList: "Could not verify the connected repositories, so nothing is sent",
|
|
12084
|
+
noSend: "Sending is turned off, so the analysis results are not sent",
|
|
12085
|
+
settingUnreadable: "Cannot read the device settings (`~/.harnessforce/config.json`), so the analysis results are not sent",
|
|
12086
|
+
importLimited: (count) => `Could not import ${count} session(s) because of the monthly event limit or view-only mode. The analysis results are sent; importing is retried in a run after the restriction is lifted`,
|
|
12087
|
+
importFailed: "Importing sessions before sending failed. The analysis results are sent; importing is retried in the next run",
|
|
12088
|
+
sent: (count) => `Sent ${count} analysis result(s)`,
|
|
12089
|
+
unreachable: (count) => `Could not send to Harnessforce. ${count} analysis result(s) will be sent in the next run`,
|
|
12090
|
+
expiredUnsent: (count) => `Deleted ${count} unsent analysis result(s) that fell outside the sending range`,
|
|
12091
|
+
readOnly: (count) => `Could not send ${count} analysis result(s) because the Workspace is view-only. They will be sent in the next run`,
|
|
12092
|
+
invalidReports: (count, reasons) => `Deleted ${count} analysis result(s) without sending because their format is invalid (${reasons.join(", ")})`,
|
|
12093
|
+
rejected: (status, count) => `Harnessforce rejected the submission (HTTP ${status}). Deleted ${count} analysis result(s) without sending`,
|
|
12094
|
+
keyUnusable: (count) => `The ingest key cannot be used. Deleted ${count} unsent analysis result(s). Run \`harnessforce init\``,
|
|
12095
|
+
loginExpired: "Your login has expired. Run `harnessforce init`",
|
|
12096
|
+
unwritable: "Cannot write to `~/.harnessforce/tune/`, so the analysis results are not sent",
|
|
12097
|
+
recordFailed: "Could not record the suggestion",
|
|
12098
|
+
recordInvalid: (field) => `The suggestion record input is invalid: ${field}`,
|
|
12099
|
+
usage: "Usage: harnessforce tune [--all] [--no-send] [--show-report] [--json], harnessforce tune record, or harnessforce tune --purge",
|
|
12100
|
+
purged: "Deleted the analysis results, suggestions and unsent analysis results in `~/.harnessforce/tune/`",
|
|
12101
|
+
purgeFailed: "Could not delete some files in `~/.harnessforce/tune/`"
|
|
11374
12102
|
};
|
|
11375
12103
|
var Notices = class {
|
|
11376
12104
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -11411,7 +12139,7 @@ const tunePaths = (homeDir) => {
|
|
|
11411
12139
|
};
|
|
11412
12140
|
};
|
|
11413
12141
|
const DIR_MODE = 448;
|
|
11414
|
-
const FILE_MODE
|
|
12142
|
+
const FILE_MODE = 384;
|
|
11415
12143
|
const destinationKey = ({ workspaceId, endpoint }) => `${workspaceId} ${endpoint}`;
|
|
11416
12144
|
async function readJsonFile(path) {
|
|
11417
12145
|
try {
|
|
@@ -11431,10 +12159,10 @@ async function writeFileAtomic(path, content) {
|
|
|
11431
12159
|
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
11432
12160
|
try {
|
|
11433
12161
|
await writeFile(temporary, content, {
|
|
11434
|
-
mode: FILE_MODE
|
|
12162
|
+
mode: FILE_MODE,
|
|
11435
12163
|
flag: "wx"
|
|
11436
12164
|
});
|
|
11437
|
-
await chmod(temporary, FILE_MODE
|
|
12165
|
+
await chmod(temporary, FILE_MODE);
|
|
11438
12166
|
await rename(temporary, path);
|
|
11439
12167
|
} finally {
|
|
11440
12168
|
await rm(temporary, { force: true });
|
|
@@ -11724,7 +12452,7 @@ async function readSendSetting(homeDir) {
|
|
|
11724
12452
|
const DAY_MS = 864e5;
|
|
11725
12453
|
const DEFAULT_RANGE_DAYS = 30;
|
|
11726
12454
|
const SKIPPED_RATIO = .05;
|
|
11727
|
-
const ACCESS_MESSAGES
|
|
12455
|
+
const ACCESS_MESSAGES = {
|
|
11728
12456
|
keychainUnavailable: TUNE_MESSAGES.keychainUnavailable,
|
|
11729
12457
|
initRequired: TUNE_MESSAGES.initRequired,
|
|
11730
12458
|
invalidUrl: TUNE_MESSAGES.invalidUrl,
|
|
@@ -11960,14 +12688,14 @@ async function analyzeUnderLock(options, access, paths, deps) {
|
|
|
11960
12688
|
writeSessionConfigs(paths.sessionConfigs, configs)
|
|
11961
12689
|
]).then(() => true, () => false);
|
|
11962
12690
|
if (!isWritten) notices.add("unwritable", TUNE_MESSAGES.unwritable, 1);
|
|
11963
|
-
if (options.json) deps.stdout(`${
|
|
12691
|
+
if (options.json) deps.stdout(`${asciiJson(output)}\n`);
|
|
11964
12692
|
else {
|
|
11965
12693
|
deps.stdout(renderAnalysis(analyzed.map((a) => a.report), analyzed.map((a) => a.tune.usage), {
|
|
11966
12694
|
sessionCount: analyzed.length,
|
|
11967
12695
|
analyzerVersion: ANALYZER_VERSION,
|
|
11968
12696
|
parserVersion: PARSER_VERSION
|
|
11969
12697
|
}));
|
|
11970
|
-
if (options.showReport) deps.stdout(`${
|
|
12698
|
+
if (options.showReport) deps.stdout(`${asciiJson(analyzed.filter((a) => a.sendable === true).map((a) => a.report), 2)}\n`);
|
|
11971
12699
|
}
|
|
11972
12700
|
if (analyzed.length < 10) notices.add("insufficient", TUNE_MESSAGES.insufficient(analyzed.length));
|
|
11973
12701
|
const lines = analyzed.reduce((sum, a) => sum + a.tune.lines, 0);
|
|
@@ -11990,7 +12718,7 @@ async function analyzeUnderLock(options, access, paths, deps) {
|
|
|
11990
12718
|
}
|
|
11991
12719
|
function analyzeCommand(options, deps) {
|
|
11992
12720
|
return runUntilStop(async () => {
|
|
11993
|
-
const access = await verifyCliAccess(deps, ACCESS_MESSAGES
|
|
12721
|
+
const access = await verifyCliAccess(deps, ACCESS_MESSAGES);
|
|
11994
12722
|
const paths = tunePaths(deps.homeDir);
|
|
11995
12723
|
await ensureDir(paths.dir).catch(() => stopWith(TUNE_MESSAGES.unwritable));
|
|
11996
12724
|
const release = await acquireTuneLock(paths.lock, deps).catch(() => stopWith(TUNE_MESSAGES.unwritable));
|
|
@@ -12119,7 +12847,7 @@ const sha256 = (text) => createHash("sha256").update(Buffer.from(text, "utf8")).
|
|
|
12119
12847
|
function expectedHash(input, component) {
|
|
12120
12848
|
if (component !== null && VALUE_KINDS.has(component.kind)) {
|
|
12121
12849
|
if (!("value" in input)) return invalid("value");
|
|
12122
|
-
return hashValue(input.value);
|
|
12850
|
+
return hashValue(hashInput(component.kind, input.value));
|
|
12123
12851
|
}
|
|
12124
12852
|
if (typeof input.content !== "string") return invalid("content");
|
|
12125
12853
|
return hashFileContent(Buffer.from(input.content, "utf8"));
|
|
@@ -12226,6 +12954,10 @@ function parseArgs(args) {
|
|
|
12226
12954
|
options
|
|
12227
12955
|
};
|
|
12228
12956
|
}
|
|
12957
|
+
function canTuneSend(args) {
|
|
12958
|
+
const parsed = parseArgs(args);
|
|
12959
|
+
return parsed?.kind === "analyze" && !parsed.options.noSend;
|
|
12960
|
+
}
|
|
12229
12961
|
async function underLock(deps, failure, body) {
|
|
12230
12962
|
return runUntilStop(async () => {
|
|
12231
12963
|
const paths = tunePaths(deps.homeDir);
|
|
@@ -12279,464 +13011,14 @@ async function tuneCommand(args, deps) {
|
|
|
12279
13011
|
return analyzeCommand(parsed.options, deps);
|
|
12280
13012
|
}
|
|
12281
13013
|
//#endregion
|
|
12282
|
-
//#region src/usage-limits/consent.ts
|
|
12283
|
-
const consentUrl = (base) => underBase(base, "api/v1/cli/usage-limits/consent");
|
|
12284
|
-
async function call(base, fetchImpl, method, body) {
|
|
12285
|
-
try {
|
|
12286
|
-
return await fetchImpl(consentUrl(base), {
|
|
12287
|
-
method,
|
|
12288
|
-
headers: {
|
|
12289
|
-
accept: "application/json",
|
|
12290
|
-
...body ? { "content-type": "application/json" } : {}
|
|
12291
|
-
},
|
|
12292
|
-
...body ? { body: JSON.stringify(body) } : {},
|
|
12293
|
-
redirect: "manual",
|
|
12294
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
12295
|
-
});
|
|
12296
|
-
} catch {
|
|
12297
|
-
return;
|
|
12298
|
-
}
|
|
12299
|
-
}
|
|
12300
|
-
async function readConsent(base, fetchImpl) {
|
|
12301
|
-
const response = await call(base, fetchImpl, "GET");
|
|
12302
|
-
if (response?.status === 401) return { kind: "unauthorized" };
|
|
12303
|
-
if (response?.status !== 200) return { kind: "failed" };
|
|
12304
|
-
const body = await readJsonObjectBody(response);
|
|
12305
|
-
if (typeof body?.workspace_opted_in !== "boolean" || typeof body.shared !== "boolean" || typeof body.current_text_version !== "number") return { kind: "failed" };
|
|
12306
|
-
return {
|
|
12307
|
-
kind: "ok",
|
|
12308
|
-
state: {
|
|
12309
|
-
workspaceOptedIn: body.workspace_opted_in,
|
|
12310
|
-
shared: body.shared,
|
|
12311
|
-
currentTextVersion: body.current_text_version
|
|
12312
|
-
}
|
|
12313
|
-
};
|
|
12314
|
-
}
|
|
12315
|
-
async function recordConsent(base, fetchImpl, textVersion) {
|
|
12316
|
-
const response = await call(base, fetchImpl, "PUT", { text_version: textVersion });
|
|
12317
|
-
if (!response) return { kind: "failed" };
|
|
12318
|
-
if (response.status === 204) return { kind: "ok" };
|
|
12319
|
-
if (response.status === 401) return { kind: "unauthorized" };
|
|
12320
|
-
const body = await readJsonObjectBody(response);
|
|
12321
|
-
if (response.status === 400 && body?.code === "invalid_text_version") return { kind: "invalid_text_version" };
|
|
12322
|
-
if (response.status === 403) return body?.code === "workspace_read_only" ? { kind: "read_only" } : { kind: "viewer" };
|
|
12323
|
-
return { kind: "failed" };
|
|
12324
|
-
}
|
|
12325
|
-
async function revokeConsent(base, fetchImpl) {
|
|
12326
|
-
const response = await call(base, fetchImpl, "DELETE");
|
|
12327
|
-
if (response?.status === 204) return { kind: "ok" };
|
|
12328
|
-
return response?.status === 401 ? { kind: "unauthorized" } : { kind: "failed" };
|
|
12329
|
-
}
|
|
12330
|
-
//#endregion
|
|
12331
|
-
//#region src/usage-limits/files.ts
|
|
12332
|
-
const DIRECTORY_MODE = 448;
|
|
12333
|
-
const FILE_MODE = 384;
|
|
12334
|
-
const markPath = (homeDir) => join(homeDir, ".harnessforce", "usage-limits.json");
|
|
12335
|
-
const statePath = (homeDir) => join(homeDir, ".harnessforce", "usage-limits-state.json");
|
|
12336
|
-
async function readJson(path) {
|
|
12337
|
-
try {
|
|
12338
|
-
return JSON.parse(await readFile(path, "utf8"));
|
|
12339
|
-
} catch {
|
|
12340
|
-
return;
|
|
12341
|
-
}
|
|
12342
|
-
}
|
|
12343
|
-
async function readMark(homeDir) {
|
|
12344
|
-
const value = await readJson(markPath(homeDir));
|
|
12345
|
-
if (!isObject(value)) return void 0;
|
|
12346
|
-
const { consented, text_version: textVersion, original } = value;
|
|
12347
|
-
if (typeof consented !== "boolean" || typeof textVersion !== "number" || !(original === null || isObject(original))) return void 0;
|
|
12348
|
-
return {
|
|
12349
|
-
consented,
|
|
12350
|
-
text_version: textVersion,
|
|
12351
|
-
original
|
|
12352
|
-
};
|
|
12353
|
-
}
|
|
12354
|
-
async function readSendState(homeDir) {
|
|
12355
|
-
const value = await readJson(statePath(homeDir));
|
|
12356
|
-
if (!isObject(value) || typeof value.sent_at !== "number") return void 0;
|
|
12357
|
-
const resets = value.resets_at;
|
|
12358
|
-
if (!isObject(resets)) return void 0;
|
|
12359
|
-
const entries = Object.entries(resets);
|
|
12360
|
-
if (entries.some(([, at]) => typeof at !== "number")) return void 0;
|
|
12361
|
-
return {
|
|
12362
|
-
sent_at: value.sent_at,
|
|
12363
|
-
resets_at: Object.fromEntries(entries)
|
|
12364
|
-
};
|
|
12365
|
-
}
|
|
12366
|
-
async function writeOwnerOnly(path, value) {
|
|
12367
|
-
await mkdir(dirname(path), {
|
|
12368
|
-
recursive: true,
|
|
12369
|
-
mode: DIRECTORY_MODE
|
|
12370
|
-
});
|
|
12371
|
-
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
12372
|
-
try {
|
|
12373
|
-
await writeFile(temporary, `${JSON.stringify(value)}\n`, {
|
|
12374
|
-
mode: FILE_MODE,
|
|
12375
|
-
flag: "wx"
|
|
12376
|
-
});
|
|
12377
|
-
await chmod(temporary, FILE_MODE);
|
|
12378
|
-
await rename(temporary, path);
|
|
12379
|
-
} finally {
|
|
12380
|
-
await rm(temporary, { force: true });
|
|
12381
|
-
}
|
|
12382
|
-
}
|
|
12383
|
-
const writeSendState = (homeDir, state) => writeOwnerOnly(statePath(homeDir), state);
|
|
12384
|
-
const writeMark = (homeDir, mark) => writeOwnerOnly(markPath(homeDir), mark);
|
|
12385
|
-
async function removeMarkAndState(homeDir) {
|
|
12386
|
-
await rm(markPath(homeDir), { force: true });
|
|
12387
|
-
await rm(statePath(homeDir), { force: true });
|
|
12388
|
-
}
|
|
12389
|
-
//#endregion
|
|
12390
|
-
//#region src/usage-limits/messages.ts
|
|
12391
|
-
const CONSENT_TEXT = {
|
|
12392
|
-
ja: "あなたの名前とともに、あなたの利用枠(5時間・7日などの窓の使用率とリセット時刻だけ)を、このWorkspaceのメンバー全員が見られます。プロンプトなどの本文は送りません。いつでもやめられ、やめると保存済みの値も削除します。メンバーの順位づけには使いません。",
|
|
12393
|
-
en: "Everyone in this workspace can see your usage limits together with your name (only the usage percentage and reset time of each window, such as 5 hours and 7 days). No prompt or other content is sent. You can stop at any time, and stopping deletes the stored values. They are not used to rank members."
|
|
12394
|
-
};
|
|
12395
|
-
const CONSENT_PROMPT = {
|
|
12396
|
-
ja: "同意しますか? [y/N] ",
|
|
12397
|
-
en: "Do you agree? [y/N] "
|
|
12398
|
-
};
|
|
12399
|
-
const MESSAGES = {
|
|
12400
|
-
windows: "この版のWindowsでは、利用枠の共有を設定できません",
|
|
12401
|
-
settingsUnreadable: "Claude Codeのuser settingsを読めません",
|
|
12402
|
-
statusLineUnsupported: "このstatusLineは組み込めません",
|
|
12403
|
-
noUserKey: "この端末には利用者用の送信キーがありません。`harnessforce init`を実行してください",
|
|
12404
|
-
keychainUnavailable: "OSのキーチェーンを利用できないため、利用枠の共有を設定できません",
|
|
12405
|
-
invalidUrl: "接続先のURLが不正です",
|
|
12406
|
-
loginExpired: "ログインの有効期限が切れました。`harnessforce init`をもう一度実行してください",
|
|
12407
|
-
notAgreed: "同意されなかったため、何も変えませんでした",
|
|
12408
|
-
invalidTextVersion: "このCLIの利用枠の文面が古いか新しすぎます。`harnessforce`を更新してください",
|
|
12409
|
-
viewer: "閲覧のみのロールでは、利用枠を共有できません。Workspaceの管理者に連絡してください",
|
|
12410
|
-
readOnly: "このWorkspaceは閲覧のみのため、利用枠の共有を設定できません",
|
|
12411
|
-
failed: "Harnessforceとの通信に失敗しました。もう一度お試しください",
|
|
12412
|
-
saveFailed: "端末の設定を書き込めなかったため、同意を取り消しました。もう一度お試しください",
|
|
12413
|
-
revokeFailedAfterSave: "端末の設定を書き込めず、同意の取り消しにも失敗しました。`harnessforce usage-limits status`で状態を確かめ、`harnessforce usage-limits off`を実行してください",
|
|
12414
|
-
notOptedIn: "同意を記録しました。Workspaceが利用枠の記録をopt-inするまで、値は保存されません",
|
|
12415
|
-
enabled: "利用枠の共有を有効にしました。Claude Codeを再起動すると反映されます",
|
|
12416
|
-
disabled: "利用枠の共有をやめ、保存済みの値の削除を依頼しました",
|
|
12417
|
-
revokedButNotRestored: "同意は撤回しましたが、statusLineを元に戻せませんでした。`harnessforce usage-limits off`をもう一度実行してください",
|
|
12418
|
-
usage: "Usage: harnessforce usage-limits on [--yes] | harnessforce usage-limits off | harnessforce usage-limits status | harnessforce usage-limits statusline\n"
|
|
12419
|
-
};
|
|
12420
|
-
const STATUS_LABELS = {
|
|
12421
|
-
consent: {
|
|
12422
|
-
shared: "同意している",
|
|
12423
|
-
notShared: "同意していない",
|
|
12424
|
-
outdated: "文面が古い",
|
|
12425
|
-
unknown: "確認できません"
|
|
12426
|
-
},
|
|
12427
|
-
optIn: {
|
|
12428
|
-
on: "有効",
|
|
12429
|
-
off: "無効",
|
|
12430
|
-
unknown: "確認できません"
|
|
12431
|
-
},
|
|
12432
|
-
statusLine: {
|
|
12433
|
-
embeddedWithOriginal: "組み込まれている(元のcommandあり)",
|
|
12434
|
-
embeddedWithoutOriginal: "組み込まれている(元のcommandなし)",
|
|
12435
|
-
notEmbedded: "組み込まれていない"
|
|
12436
|
-
}
|
|
12437
|
-
};
|
|
12438
|
-
//#endregion
|
|
12439
|
-
//#region src/usage-limits/statusline-settings.ts
|
|
12440
|
-
const STATUSLINE_COMMAND = "harnessforce usage-limits statusline";
|
|
12441
|
-
const isEmbedded = (statusLine) => isObject(statusLine) && statusLine.command === STATUSLINE_COMMAND;
|
|
12442
|
-
function canEmbed(settings) {
|
|
12443
|
-
const { statusLine } = settings;
|
|
12444
|
-
if (statusLine === void 0) return true;
|
|
12445
|
-
return isObject(statusLine) && statusLine.type === "command" && typeof statusLine.command === "string";
|
|
12446
|
-
}
|
|
12447
|
-
function originalFor(settings, mark) {
|
|
12448
|
-
const { statusLine } = settings;
|
|
12449
|
-
if (isEmbedded(statusLine)) return mark ? mark.original : null;
|
|
12450
|
-
return isObject(statusLine) ? statusLine : null;
|
|
12451
|
-
}
|
|
12452
|
-
function embedStatusLine(settings) {
|
|
12453
|
-
const { statusLine } = settings;
|
|
12454
|
-
if (isEmbedded(statusLine)) return settings;
|
|
12455
|
-
return {
|
|
12456
|
-
...settings,
|
|
12457
|
-
statusLine: isObject(statusLine) ? {
|
|
12458
|
-
...statusLine,
|
|
12459
|
-
command: STATUSLINE_COMMAND
|
|
12460
|
-
} : {
|
|
12461
|
-
type: "command",
|
|
12462
|
-
command: STATUSLINE_COMMAND
|
|
12463
|
-
}
|
|
12464
|
-
};
|
|
12465
|
-
}
|
|
12466
|
-
function restoreStatusLine(settings, original) {
|
|
12467
|
-
if (!isEmbedded(settings.statusLine)) return settings;
|
|
12468
|
-
const { statusLine: _embedded, ...rest } = settings;
|
|
12469
|
-
return original === null ? rest : {
|
|
12470
|
-
...settings,
|
|
12471
|
-
statusLine: original
|
|
12472
|
-
};
|
|
12473
|
-
}
|
|
12474
|
-
const isStatusLineEmbedded = (settings) => isEmbedded(settings.statusLine);
|
|
12475
|
-
//#endregion
|
|
12476
|
-
//#region src/usage-limits/manage.ts
|
|
12477
|
-
const ACCESS_MESSAGES = {
|
|
12478
|
-
keychainUnavailable: MESSAGES.keychainUnavailable,
|
|
12479
|
-
initRequired: MESSAGES.noUserKey,
|
|
12480
|
-
invalidUrl: MESSAGES.invalidUrl,
|
|
12481
|
-
apiTokenMissing: MESSAGES.noUserKey
|
|
12482
|
-
};
|
|
12483
|
-
function language(env) {
|
|
12484
|
-
return (env.LC_ALL || env.LC_MESSAGES || env.LANG || "").toLowerCase().startsWith("ja") ? "ja" : "en";
|
|
12485
|
-
}
|
|
12486
|
-
function consentClient(deps, access) {
|
|
12487
|
-
const authorized = createApiTokenSession({
|
|
12488
|
-
keychain: deps.keychain,
|
|
12489
|
-
workspaceId: access.workspaceId,
|
|
12490
|
-
readBase: access.readApiBase,
|
|
12491
|
-
fetch: deps.fetch,
|
|
12492
|
-
now: () => deps.now().getTime(),
|
|
12493
|
-
sleep: deps.sleep,
|
|
12494
|
-
homeDir: deps.homeDir
|
|
12495
|
-
}).authorizedFetch(deps.fetch);
|
|
12496
|
-
const base = access.readApiBase;
|
|
12497
|
-
return {
|
|
12498
|
-
read: () => readConsent(base, authorized),
|
|
12499
|
-
record: () => recordConsent(base, authorized, 1),
|
|
12500
|
-
revoke: () => revokeConsent(base, authorized)
|
|
12501
|
-
};
|
|
12502
|
-
}
|
|
12503
|
-
const settingsPathOf = (deps) => userSettingsPath(deps.env, deps.homeDir);
|
|
12504
|
-
async function runOn(yes, deps) {
|
|
12505
|
-
if (deps.platform === "win32") return stopWith(MESSAGES.windows);
|
|
12506
|
-
const client = consentClient(deps, await verifyCliAccess(deps, ACCESS_MESSAGES));
|
|
12507
|
-
const read = await readUserSettings(settingsPathOf(deps));
|
|
12508
|
-
if (read.kind === "invalid") return stopWith(MESSAGES.settingsUnreadable);
|
|
12509
|
-
if (!canEmbed(read.settings)) return stopWith(MESSAGES.statusLineUnsupported);
|
|
12510
|
-
const settings = read.settings;
|
|
12511
|
-
const before = await client.read();
|
|
12512
|
-
const lang = language(deps.env);
|
|
12513
|
-
deps.stdout(`${CONSENT_TEXT[lang]}\n`);
|
|
12514
|
-
if (!yes && (await deps.ask(CONSENT_PROMPT[lang])).trim() !== "y") return stopWith(MESSAGES.notAgreed);
|
|
12515
|
-
const recorded = await client.record();
|
|
12516
|
-
if (recorded.kind === "unauthorized") return stopWith(MESSAGES.loginExpired);
|
|
12517
|
-
if (recorded.kind === "invalid_text_version") return stopWith(MESSAGES.invalidTextVersion);
|
|
12518
|
-
if (recorded.kind === "viewer") return stopWith(MESSAGES.viewer);
|
|
12519
|
-
if (recorded.kind === "read_only") return stopWith(MESSAGES.readOnly);
|
|
12520
|
-
if (recorded.kind === "failed") return stopWith(MESSAGES.failed);
|
|
12521
|
-
const mark = await readMark(deps.homeDir);
|
|
12522
|
-
const undo = async () => {
|
|
12523
|
-
await (mark ? writeMark(deps.homeDir, {
|
|
12524
|
-
...mark,
|
|
12525
|
-
consented: false
|
|
12526
|
-
}) : removeMarkAndState(deps.homeDir)).catch(() => {});
|
|
12527
|
-
const revoked = await client.revoke();
|
|
12528
|
-
return stopWith(revoked.kind === "ok" ? MESSAGES.saveFailed : MESSAGES.revokeFailedAfterSave);
|
|
12529
|
-
};
|
|
12530
|
-
try {
|
|
12531
|
-
await writeMark(deps.homeDir, {
|
|
12532
|
-
consented: true,
|
|
12533
|
-
text_version: 1,
|
|
12534
|
-
original: originalFor(settings, mark)
|
|
12535
|
-
});
|
|
12536
|
-
} catch {
|
|
12537
|
-
return undo();
|
|
12538
|
-
}
|
|
12539
|
-
try {
|
|
12540
|
-
await writeUserSettings(settingsPathOf(deps), embedStatusLine(settings));
|
|
12541
|
-
} catch {
|
|
12542
|
-
return undo();
|
|
12543
|
-
}
|
|
12544
|
-
const optedOut = before.kind === "ok" && !before.state.workspaceOptedIn;
|
|
12545
|
-
deps.stdout(`${optedOut ? MESSAGES.notOptedIn : MESSAGES.enabled}\n`);
|
|
12546
|
-
return 0;
|
|
12547
|
-
}
|
|
12548
|
-
async function runOff(deps) {
|
|
12549
|
-
const revoked = await consentClient(deps, await verifyCliAccess(deps, ACCESS_MESSAGES)).revoke();
|
|
12550
|
-
if (revoked.kind === "unauthorized") return stopWith(MESSAGES.loginExpired);
|
|
12551
|
-
if (revoked.kind === "failed") return stopWith(MESSAGES.failed);
|
|
12552
|
-
const mark = await readMark(deps.homeDir);
|
|
12553
|
-
const read = await readUserSettings(settingsPathOf(deps));
|
|
12554
|
-
const keepWithoutConsent = async () => {
|
|
12555
|
-
if (mark) await writeMark(deps.homeDir, {
|
|
12556
|
-
...mark,
|
|
12557
|
-
consented: false
|
|
12558
|
-
}).catch(() => {});
|
|
12559
|
-
return stopWith(MESSAGES.revokedButNotRestored);
|
|
12560
|
-
};
|
|
12561
|
-
if (read.kind === "invalid") return keepWithoutConsent();
|
|
12562
|
-
if (isStatusLineEmbedded(read.settings)) try {
|
|
12563
|
-
await writeUserSettings(settingsPathOf(deps), restoreStatusLine(read.settings, mark?.original ?? null));
|
|
12564
|
-
} catch {
|
|
12565
|
-
return keepWithoutConsent();
|
|
12566
|
-
}
|
|
12567
|
-
await removeMarkAndState(deps.homeDir).catch(() => {});
|
|
12568
|
-
deps.stdout(`${MESSAGES.disabled}\n`);
|
|
12569
|
-
return 0;
|
|
12570
|
-
}
|
|
12571
|
-
function consentLabel(result, mark) {
|
|
12572
|
-
if (result.kind !== "ok") return STATUS_LABELS.consent.unknown;
|
|
12573
|
-
if (result.state.shared) return STATUS_LABELS.consent.shared;
|
|
12574
|
-
return mark?.consented === true && mark.text_version !== result.state.currentTextVersion ? STATUS_LABELS.consent.outdated : STATUS_LABELS.consent.notShared;
|
|
12575
|
-
}
|
|
12576
|
-
async function runStatus(deps) {
|
|
12577
|
-
const mark = await readMark(deps.homeDir);
|
|
12578
|
-
const read = await readUserSettings(settingsPathOf(deps));
|
|
12579
|
-
const statusLine = !(read.kind === "ok" && isStatusLineEmbedded(read.settings)) ? STATUS_LABELS.statusLine.notEmbedded : mark?.original ? STATUS_LABELS.statusLine.embeddedWithOriginal : STATUS_LABELS.statusLine.embeddedWithoutOriginal;
|
|
12580
|
-
const result = await readRemote(deps);
|
|
12581
|
-
deps.stdout([
|
|
12582
|
-
`本人の同意: ${consentLabel(result, mark)}`,
|
|
12583
|
-
`Workspaceのopt-in: ${result.kind !== "ok" ? STATUS_LABELS.optIn.unknown : result.state.workspaceOptedIn ? STATUS_LABELS.optIn.on : STATUS_LABELS.optIn.off}`,
|
|
12584
|
-
`statusLineの組み込み: ${statusLine}`,
|
|
12585
|
-
""
|
|
12586
|
-
].join("\n"));
|
|
12587
|
-
return 0;
|
|
12588
|
-
}
|
|
12589
|
-
async function readRemote(deps) {
|
|
12590
|
-
try {
|
|
12591
|
-
return await consentClient(deps, await verifyCliAccess(deps, ACCESS_MESSAGES)).read();
|
|
12592
|
-
} catch (error) {
|
|
12593
|
-
if (isStop(error)) return { kind: "failed" };
|
|
12594
|
-
throw error;
|
|
12595
|
-
}
|
|
12596
|
-
}
|
|
12597
|
-
function onCommand(yes, deps) {
|
|
12598
|
-
return runUntilStop(() => runOn(yes, deps), deps.stderr);
|
|
12599
|
-
}
|
|
12600
|
-
function offCommand(deps) {
|
|
12601
|
-
return runUntilStop(() => runOff(deps), deps.stderr);
|
|
12602
|
-
}
|
|
12603
|
-
const statusCommand = runStatus;
|
|
12604
|
-
//#endregion
|
|
12605
|
-
//#region src/usage-limits/summary.ts
|
|
12606
|
-
const WINDOWS = [{
|
|
12607
|
-
name: "five_hour",
|
|
12608
|
-
minutes: 300
|
|
12609
|
-
}, {
|
|
12610
|
-
name: "seven_day",
|
|
12611
|
-
minutes: 10080
|
|
12612
|
-
}];
|
|
12613
|
-
const MIN_SEND_INTERVAL_MS = 3e5;
|
|
12614
|
-
const instant = (ms) => new Date(ms).toISOString().replace(".000Z", "Z");
|
|
12615
|
-
function windowFrom(value, minutes, nowMs) {
|
|
12616
|
-
if (!isObject(value)) return void 0;
|
|
12617
|
-
const { used_percentage: used, resets_at: resetsAt } = value;
|
|
12618
|
-
if (typeof used !== "number" || !(used >= 0 && used <= 100)) return void 0;
|
|
12619
|
-
if (typeof resetsAt !== "number" || !Number.isFinite(resetsAt)) return void 0;
|
|
12620
|
-
const resetsAtMs = Math.floor(resetsAt) * 1e3;
|
|
12621
|
-
if (resetsAtMs <= nowMs) return void 0;
|
|
12622
|
-
return {
|
|
12623
|
-
window_minutes: minutes,
|
|
12624
|
-
used_percent: used,
|
|
12625
|
-
resets_at: instant(resetsAtMs)
|
|
12626
|
-
};
|
|
12627
|
-
}
|
|
12628
|
-
function summaryFrom(stdin, nowMs) {
|
|
12629
|
-
let json;
|
|
12630
|
-
try {
|
|
12631
|
-
json = JSON.parse(stdin);
|
|
12632
|
-
} catch {
|
|
12633
|
-
return;
|
|
12634
|
-
}
|
|
12635
|
-
const limits = isObject(json) ? json.rate_limits : void 0;
|
|
12636
|
-
if (!isObject(limits)) return void 0;
|
|
12637
|
-
const windows = WINDOWS.flatMap(({ name, minutes }) => {
|
|
12638
|
-
const window = windowFrom(limits[name], minutes, nowMs);
|
|
12639
|
-
return window ? [window] : [];
|
|
12640
|
-
});
|
|
12641
|
-
return windows.length === 0 ? void 0 : {
|
|
12642
|
-
agent: "claude_code",
|
|
12643
|
-
observed_at: instant(Math.floor(nowMs / 1e3) * 1e3),
|
|
12644
|
-
windows
|
|
12645
|
-
};
|
|
12646
|
-
}
|
|
12647
|
-
const resetSeconds = (window) => Date.parse(window.resets_at) / 1e3;
|
|
12648
|
-
function isDue(summary, state, nowMs) {
|
|
12649
|
-
if (!state) return true;
|
|
12650
|
-
return summary.windows.some((window) => state.resets_at[String(window.window_minutes)] !== resetSeconds(window)) || nowMs - state.sent_at >= MIN_SEND_INTERVAL_MS;
|
|
12651
|
-
}
|
|
12652
|
-
const stateFor = (summary, nowMs) => ({
|
|
12653
|
-
sent_at: nowMs,
|
|
12654
|
-
resets_at: Object.fromEntries(summary.windows.map((window) => [String(window.window_minutes), resetSeconds(window)]))
|
|
12655
|
-
});
|
|
12656
|
-
function parseSummary(raw) {
|
|
12657
|
-
let value;
|
|
12658
|
-
try {
|
|
12659
|
-
value = JSON.parse(raw);
|
|
12660
|
-
} catch {
|
|
12661
|
-
return;
|
|
12662
|
-
}
|
|
12663
|
-
if (!isObject(value) || value.agent !== "claude_code") return void 0;
|
|
12664
|
-
const { observed_at: observedAt, windows } = value;
|
|
12665
|
-
if (typeof observedAt !== "string" || !Array.isArray(windows)) return void 0;
|
|
12666
|
-
const rebuilt = windows.flatMap((window) => {
|
|
12667
|
-
if (!isObject(window)) return [];
|
|
12668
|
-
const { window_minutes: minutes, used_percent: used } = window;
|
|
12669
|
-
const resetsAt = window.resets_at;
|
|
12670
|
-
return WINDOWS.some((w) => w.minutes === minutes) && typeof minutes === "number" && typeof used === "number" && used >= 0 && used <= 100 && typeof resetsAt === "string" && !Number.isNaN(Date.parse(resetsAt)) ? [{
|
|
12671
|
-
window_minutes: minutes,
|
|
12672
|
-
used_percent: used,
|
|
12673
|
-
resets_at: resetsAt
|
|
12674
|
-
}] : [];
|
|
12675
|
-
});
|
|
12676
|
-
return rebuilt.length > 0 && rebuilt.length === windows.length ? {
|
|
12677
|
-
agent: "claude_code",
|
|
12678
|
-
observed_at: observedAt,
|
|
12679
|
-
windows: rebuilt
|
|
12680
|
-
} : void 0;
|
|
12681
|
-
}
|
|
12682
|
-
//#endregion
|
|
12683
|
-
//#region src/usage-limits/send.ts
|
|
12684
|
-
const SEND_TIMEOUT_MS = 5e3;
|
|
12685
|
-
async function sendUsageLimits(raw, deps) {
|
|
12686
|
-
const summary = parseSummary(raw);
|
|
12687
|
-
if (!summary) return 0;
|
|
12688
|
-
const mark = await readMark(deps.homeDir);
|
|
12689
|
-
if (!mark?.consented || mark.text_version !== 1) return 0;
|
|
12690
|
-
const resolved = await resolveUserDestination(deps).catch(() => void 0);
|
|
12691
|
-
if (resolved?.kind !== "ok") return 0;
|
|
12692
|
-
await postItem(resolved.destination, "v1/usage-limits", summary, deps.fetch, SEND_TIMEOUT_MS);
|
|
12693
|
-
return 0;
|
|
12694
|
-
}
|
|
12695
|
-
//#endregion
|
|
12696
|
-
//#region src/usage-limits/statusline.ts
|
|
12697
|
-
const MAX_INPUT_BYTES = Number.MAX_SAFE_INTEGER;
|
|
12698
|
-
function originalCommand(original) {
|
|
12699
|
-
const command = original?.command;
|
|
12700
|
-
return isObject(original) && typeof command === "string" && command !== "" ? command : void 0;
|
|
12701
|
-
}
|
|
12702
|
-
async function statusline(deps) {
|
|
12703
|
-
const input = await deps.readStdin(MAX_INPUT_BYTES).catch(() => Buffer.alloc(0));
|
|
12704
|
-
const mark = await readMark(deps.homeDir);
|
|
12705
|
-
const command = originalCommand(mark?.original ?? null);
|
|
12706
|
-
const code = command ? await deps.runOriginal(command, input) : void 0;
|
|
12707
|
-
await sendIfDue(deps, input, mark).catch(() => {});
|
|
12708
|
-
return code ?? 0;
|
|
12709
|
-
}
|
|
12710
|
-
async function sendIfDue(deps, input, mark) {
|
|
12711
|
-
if (!mark?.consented || mark.text_version !== 1) return;
|
|
12712
|
-
const nowMs = deps.now().getTime();
|
|
12713
|
-
const summary = summaryFrom(input.toString("utf8"), nowMs);
|
|
12714
|
-
if (!summary) return;
|
|
12715
|
-
if (!isDue(summary, await readSendState(deps.homeDir), nowMs)) return;
|
|
12716
|
-
await writeSendState(deps.homeDir, stateFor(summary, nowMs));
|
|
12717
|
-
deps.spawnSender(JSON.stringify(summary));
|
|
12718
|
-
}
|
|
12719
|
-
//#endregion
|
|
12720
|
-
//#region src/usage-limits/command.ts
|
|
12721
|
-
async function usageLimitsCommand(args, deps) {
|
|
12722
|
-
const [subcommand, payload] = args;
|
|
12723
|
-
if (subcommand === "statusline" && args.length === 1) return statusline(deps);
|
|
12724
|
-
if (subcommand === "send" && payload !== void 0 && args.length === 2) return sendUsageLimits(payload, deps);
|
|
12725
|
-
const isYes = args[1] === "--yes";
|
|
12726
|
-
if (subcommand === "on" && (args.length === 1 || args.length === 2 && isYes)) return onCommand(isYes, deps);
|
|
12727
|
-
if (subcommand === "off" && args.length === 1) return offCommand(deps);
|
|
12728
|
-
if (subcommand === "status" && args.length === 1) return statusCommand(deps);
|
|
12729
|
-
deps.stderr(MESSAGES.usage);
|
|
12730
|
-
return 1;
|
|
12731
|
-
}
|
|
12732
|
-
//#endregion
|
|
12733
13014
|
//#region src/main.ts
|
|
12734
13015
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
12735
|
-
const
|
|
13016
|
+
const EXIT_CLIENT_UNSUPPORTED = 5;
|
|
12736
13017
|
function parseInitArgs(args) {
|
|
12737
13018
|
let url;
|
|
12738
13019
|
let port;
|
|
12739
13020
|
let sendContent = false;
|
|
13021
|
+
let noPluginAutoUpdate = false;
|
|
12740
13022
|
for (let i = 0; i < args.length;) {
|
|
12741
13023
|
if (args[i] === "--url") {
|
|
12742
13024
|
if (url !== void 0 || args[i + 1] === void 0) return void 0;
|
|
@@ -12759,12 +13041,19 @@ function parseInitArgs(args) {
|
|
|
12759
13041
|
i += 1;
|
|
12760
13042
|
continue;
|
|
12761
13043
|
}
|
|
13044
|
+
if (args[i] === "--no-plugin-auto-update") {
|
|
13045
|
+
if (noPluginAutoUpdate) return void 0;
|
|
13046
|
+
noPluginAutoUpdate = true;
|
|
13047
|
+
i += 1;
|
|
13048
|
+
continue;
|
|
13049
|
+
}
|
|
12762
13050
|
return;
|
|
12763
13051
|
}
|
|
12764
13052
|
return {
|
|
12765
13053
|
url,
|
|
12766
13054
|
port,
|
|
12767
|
-
sendContent
|
|
13055
|
+
sendContent,
|
|
13056
|
+
noPluginAutoUpdate
|
|
12768
13057
|
};
|
|
12769
13058
|
}
|
|
12770
13059
|
function parseRunArgs(args) {
|
|
@@ -12777,55 +13066,94 @@ function parseRunArgs(args) {
|
|
|
12777
13066
|
} : void 0;
|
|
12778
13067
|
}
|
|
12779
13068
|
async function run(argv, deps) {
|
|
13069
|
+
const help = resolveHelp(argv);
|
|
13070
|
+
if (help !== void 0) {
|
|
13071
|
+
deps.stdout(help);
|
|
13072
|
+
return 0;
|
|
13073
|
+
}
|
|
12780
13074
|
const [command, ...rest] = argv;
|
|
12781
13075
|
if (command === "--version" && rest.length === 0) {
|
|
12782
13076
|
deps.stdout(`${version}\n`);
|
|
12783
13077
|
return 0;
|
|
12784
13078
|
}
|
|
12785
|
-
|
|
12786
|
-
|
|
12787
|
-
|
|
12788
|
-
|
|
12789
|
-
|
|
12790
|
-
|
|
12791
|
-
if (command === "tune") return tuneCommand(rest, {
|
|
13079
|
+
const client = {
|
|
13080
|
+
component: "cli",
|
|
13081
|
+
version,
|
|
13082
|
+
dev: deps.devBuild
|
|
13083
|
+
};
|
|
13084
|
+
const cli = {
|
|
12792
13085
|
...deps,
|
|
12793
|
-
|
|
12794
|
-
|
|
12795
|
-
|
|
13086
|
+
fetch: recordingFetch(deps.fetch, client, updateStatePath(deps.homeDir), deps.now)
|
|
13087
|
+
};
|
|
13088
|
+
const updateDeps = {
|
|
13089
|
+
...cli,
|
|
13090
|
+
client
|
|
13091
|
+
};
|
|
13092
|
+
const unsupported = async () => (await readCliUpdateState(deps.homeDir, version))?.status === "unsupported";
|
|
13093
|
+
const guarded = async (command) => {
|
|
13094
|
+
if (await isSendingBlocked(updateDeps)) {
|
|
13095
|
+
await noticeUpdate(updateDeps);
|
|
13096
|
+
return 1;
|
|
13097
|
+
}
|
|
13098
|
+
const code = await command();
|
|
13099
|
+
await noticeUpdate(updateDeps);
|
|
13100
|
+
return code;
|
|
13101
|
+
};
|
|
13102
|
+
if (command === "otel-headers" && rest.length === 0) {
|
|
13103
|
+
if (await unsupported()) return EXIT_CLIENT_UNSUPPORTED;
|
|
13104
|
+
return otelHeaders(cli.env, cli.managedDir, cli.keychain, cli.stdout, cli.stderr, cli.endStdout);
|
|
13105
|
+
}
|
|
13106
|
+
if (command === "import" && rest.length === 0) return guarded(() => importCommand({
|
|
13107
|
+
...cli,
|
|
13108
|
+
git: cli.importGit,
|
|
13109
|
+
now: () => cli.now().getTime()
|
|
13110
|
+
}));
|
|
13111
|
+
if (command === "tune") return guarded(() => tuneCommand(rest, {
|
|
13112
|
+
...cli,
|
|
13113
|
+
git: cli.importGit,
|
|
13114
|
+
now: () => cli.now().getTime()
|
|
13115
|
+
}));
|
|
12796
13116
|
if (command === "hook") {
|
|
12797
13117
|
if (rest.length !== 1 || rest[0] !== "session-start") {
|
|
12798
|
-
|
|
13118
|
+
cli.stderr(HOOK_USAGE);
|
|
12799
13119
|
return 1;
|
|
12800
13120
|
}
|
|
12801
13121
|
return sessionStart({
|
|
12802
|
-
env:
|
|
12803
|
-
homeDir:
|
|
12804
|
-
now:
|
|
12805
|
-
git:
|
|
12806
|
-
fetch:
|
|
12807
|
-
keychain:
|
|
12808
|
-
readStdin:
|
|
12809
|
-
stdout:
|
|
12810
|
-
stderr:
|
|
13122
|
+
env: cli.env,
|
|
13123
|
+
homeDir: cli.homeDir,
|
|
13124
|
+
now: cli.now,
|
|
13125
|
+
git: cli.hookGit,
|
|
13126
|
+
fetch: cli.fetch,
|
|
13127
|
+
keychain: cli.keychain,
|
|
13128
|
+
readStdin: cli.readStdin,
|
|
13129
|
+
stdout: cli.stdout,
|
|
13130
|
+
stderr: cli.stderr,
|
|
13131
|
+
client,
|
|
13132
|
+
defaultUrl: cli.defaultUrl
|
|
12811
13133
|
});
|
|
12812
13134
|
}
|
|
12813
13135
|
if (command === "top") return topCommand(rest, {
|
|
12814
|
-
homeDir:
|
|
12815
|
-
managedDir:
|
|
12816
|
-
env:
|
|
12817
|
-
stdout:
|
|
12818
|
-
stderr:
|
|
12819
|
-
now: () =>
|
|
12820
|
-
git:
|
|
12821
|
-
top:
|
|
13136
|
+
homeDir: cli.homeDir,
|
|
13137
|
+
managedDir: cli.managedDir,
|
|
13138
|
+
env: cli.env,
|
|
13139
|
+
stdout: cli.stdout,
|
|
13140
|
+
stderr: cli.stderr,
|
|
13141
|
+
now: () => cli.now().getTime(),
|
|
13142
|
+
git: cli.importGit,
|
|
13143
|
+
top: cli.top
|
|
12822
13144
|
});
|
|
12823
|
-
if (command === "usage-limits") return usageLimitsCommand(rest, deps);
|
|
12824
13145
|
const initArgs = command === "init" ? parseInitArgs(rest) : void 0;
|
|
12825
|
-
if (initArgs) return init(initArgs,
|
|
13146
|
+
if (initArgs) return guarded(() => init(initArgs, cli));
|
|
12826
13147
|
const runArgs = command === "run" ? parseRunArgs(rest) : void 0;
|
|
12827
|
-
if (runArgs)
|
|
12828
|
-
|
|
13148
|
+
if (runArgs) {
|
|
13149
|
+
if (await isSendingBlocked(updateDeps)) {
|
|
13150
|
+
await noticeUpdate(updateDeps);
|
|
13151
|
+
return runWithoutHarnessforce(runArgs, cli);
|
|
13152
|
+
}
|
|
13153
|
+
await noticeUpdate(updateDeps);
|
|
13154
|
+
return runIssue(runArgs, cli);
|
|
13155
|
+
}
|
|
13156
|
+
cli.stderr(UNKNOWN_COMMAND_HINT);
|
|
12829
13157
|
return 1;
|
|
12830
13158
|
}
|
|
12831
13159
|
//#endregion
|
|
@@ -12836,9 +13164,9 @@ const SENDING_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
12836
13164
|
"import",
|
|
12837
13165
|
"hook"
|
|
12838
13166
|
]);
|
|
12839
|
-
const isSending = (argv) => SENDING_COMMANDS.has(argv[0] ?? "") || argv[0] === "
|
|
13167
|
+
const isSending = (argv) => SENDING_COMMANDS.has(argv[0] ?? "") || argv[0] === "tune" && canTuneSend(argv.slice(1));
|
|
12840
13168
|
const HOOK_RESTART_FAILED = "harnessforce: session registration skipped (restart failed)";
|
|
12841
|
-
const RESTART_FAILED = "harnessforce
|
|
13169
|
+
const RESTART_FAILED = "Could not restart harnessforce";
|
|
12842
13170
|
async function relaunchHf(argv, deps) {
|
|
12843
13171
|
if (!isSending(argv)) return void 0;
|
|
12844
13172
|
const outcome = await relaunchWithoutRuntimeVariables({
|
|
@@ -13000,43 +13328,9 @@ function createProcessIo(proc = process) {
|
|
|
13000
13328
|
};
|
|
13001
13329
|
}
|
|
13002
13330
|
//#endregion
|
|
13003
|
-
//#region src/usage-limits/original.ts
|
|
13004
|
-
function runOriginalCommand(command, input) {
|
|
13005
|
-
return new Promise((resolve) => {
|
|
13006
|
-
let child;
|
|
13007
|
-
try {
|
|
13008
|
-
child = spawn("sh", ["-c", command], { stdio: [
|
|
13009
|
-
"pipe",
|
|
13010
|
-
"inherit",
|
|
13011
|
-
"inherit"
|
|
13012
|
-
] });
|
|
13013
|
-
} catch {
|
|
13014
|
-
return resolve(void 0);
|
|
13015
|
-
}
|
|
13016
|
-
child.once("error", () => resolve(void 0));
|
|
13017
|
-
child.once("exit", (code, signal) => resolve(code ?? 128 + (signal ? constants.signals[signal] : 0)));
|
|
13018
|
-
child.stdin?.on("error", () => {});
|
|
13019
|
-
child.stdin?.end(input);
|
|
13020
|
-
});
|
|
13021
|
-
}
|
|
13022
|
-
//#endregion
|
|
13023
|
-
//#region src/usage-limits/spawn-sender.ts
|
|
13024
|
-
function spawnUsageSender(payload) {
|
|
13025
|
-
spawn(process.execPath, [
|
|
13026
|
-
...process.execArgv,
|
|
13027
|
-
...process.argv.slice(1, 2),
|
|
13028
|
-
"usage-limits",
|
|
13029
|
-
"send",
|
|
13030
|
-
payload
|
|
13031
|
-
], {
|
|
13032
|
-
detached: true,
|
|
13033
|
-
stdio: "ignore"
|
|
13034
|
-
}).unref();
|
|
13035
|
-
}
|
|
13036
|
-
//#endregion
|
|
13037
13331
|
//#region src/bin.ts
|
|
13038
13332
|
const require = createRequire(import.meta.url);
|
|
13039
|
-
const { url: defaultUrl } = require("./build-config.json");
|
|
13333
|
+
const { url: defaultUrl, isDefault } = require("./build-config.json");
|
|
13040
13334
|
const CALLBACK_TIMEOUT_MS = 3e5;
|
|
13041
13335
|
const argv = process.argv.slice(2);
|
|
13042
13336
|
const restoredEnv = takeStashedRuntimeEnv(process.env, process.platform);
|
|
@@ -13059,6 +13353,7 @@ process.exitCode = relaunched ?? await run(argv, {
|
|
|
13059
13353
|
}),
|
|
13060
13354
|
stdout: (text) => process.stdout.write(text),
|
|
13061
13355
|
stderr: (text) => process.stderr.write(text),
|
|
13356
|
+
endStdout: () => new Promise((resolve) => process.stdout.end(() => resolve())),
|
|
13062
13357
|
fetch: (url, init) => fetch(url, init),
|
|
13063
13358
|
openBrowser: (url) => openBrowser(url, {
|
|
13064
13359
|
platform: process.platform,
|
|
@@ -13070,6 +13365,7 @@ process.exitCode = relaunched ?? await run(argv, {
|
|
|
13070
13365
|
homeDir: homedir(),
|
|
13071
13366
|
managedDir: managedDirFor(process.platform),
|
|
13072
13367
|
defaultUrl,
|
|
13368
|
+
devBuild: !isDefault,
|
|
13073
13369
|
callbackTimeoutMs: CALLBACK_TIMEOUT_MS,
|
|
13074
13370
|
cwd: process.cwd(),
|
|
13075
13371
|
git: createGitRunner({
|
|
@@ -13096,19 +13392,6 @@ process.exitCode = relaunched ?? await run(argv, {
|
|
|
13096
13392
|
}),
|
|
13097
13393
|
sleep: (ms) => setTimeout$1(ms),
|
|
13098
13394
|
top: createProcessIo(),
|
|
13099
|
-
runOriginal: runOriginalCommand,
|
|
13100
|
-
spawnSender: spawnUsageSender,
|
|
13101
|
-
ask: async (question) => {
|
|
13102
|
-
const lines = createInterface({
|
|
13103
|
-
input: process.stdin,
|
|
13104
|
-
output: process.stdout
|
|
13105
|
-
});
|
|
13106
|
-
try {
|
|
13107
|
-
return await lines.question(question);
|
|
13108
|
-
} finally {
|
|
13109
|
-
lines.close();
|
|
13110
|
-
}
|
|
13111
|
-
},
|
|
13112
13395
|
readStdin: async (maxBytes) => {
|
|
13113
13396
|
const chunks = [];
|
|
13114
13397
|
let size = 0;
|