@harnessforce/cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -34
- package/dist/bin.js +1386 -835
- 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;
|
|
@@ -414,7 +413,54 @@ function takeStashedRuntimeEnv(env, platform) {
|
|
|
414
413
|
return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string" && isRuntimeVariable(entry[0], platform)));
|
|
415
414
|
}
|
|
416
415
|
//#endregion
|
|
416
|
+
//#region src/shared/messages.ts
|
|
417
|
+
const INIT_MESSAGES = {
|
|
418
|
+
success: "Connected to Harnessforce. Restart Claude Code to apply the settings",
|
|
419
|
+
pluginAutoUpdateOn: "Automatic updates for the Harnessforce plugin are on. To turn them off, run `harnessforce init --no-plugin-auto-update` or use /plugin",
|
|
420
|
+
invalidUrl: "The connection URL is invalid",
|
|
421
|
+
keychainUnavailable: "The OS keychain is unavailable, so the ingest key cannot be saved",
|
|
422
|
+
linuxKeychainUnavailable: "Neither the Secret Service (GNOME Keyring or KWallet) nor the kernel keyring is available. In containers and CI, ask an Owner or Admin to distribute a Workspace ingest key through managed settings",
|
|
423
|
+
waitingForSecretService: "Waiting for the Secret Service unlock prompt (up to 60 seconds)",
|
|
424
|
+
savedToKernelKeyring: "The Secret Service is unavailable, so the ingest key was saved in the kernel keyring. It is lost on reboot or after a period of disuse. If it disappears, run `harnessforce init` again",
|
|
425
|
+
tooManyKeys: "There are too many ingest keys in the keychain, so `harnessforce init` cannot run",
|
|
426
|
+
tooManyCredentials: "There are too many ingest keys or login entries in the keychain, so `harnessforce init` cannot run",
|
|
427
|
+
settingsUnreadable: "Cannot read the Claude Code user settings",
|
|
428
|
+
network: "Communication with Harnessforce failed. Run `harnessforce init` again",
|
|
429
|
+
listenFailed: "Could not start waiting for the login",
|
|
430
|
+
timeout: "The login did not complete in time. Run `harnessforce init` again",
|
|
431
|
+
accessDenied: "The login was denied",
|
|
432
|
+
noWorkspace: "You are not a member of any Workspace. Create a Workspace or accept an invitation, then run `harnessforce init` again",
|
|
433
|
+
loginFailed: "The login failed. Run `harnessforce init` again",
|
|
434
|
+
viewer: "The view-only role cannot issue an ingest key. Contact a Workspace administrator",
|
|
435
|
+
limitOwnerAdmin: "The number of active ingest keys has reached the limit. Revoke unused keys on the ingest keys page",
|
|
436
|
+
limitMember: "The ingest key limit has been reached. Contact a Workspace administrator",
|
|
437
|
+
gateUnavailable: "Could not apply the change on the receiving side. Nothing was changed. Try again",
|
|
438
|
+
ingestUnavailable: "This environment does not receive telemetry, so an ingest key cannot be issued",
|
|
439
|
+
saveFailed: "Could not save the ingest key. Run `harnessforce init` again",
|
|
440
|
+
codexSaveFailed: "Could not write the Codex configuration. Run `harnessforce init` again",
|
|
441
|
+
contentEnabled: "Sending the prompts you type is now enabled. Values that match known patterns for secrets and personal information are masked, but not everything can be detected. Within the workspace, only you can read your stored content, and it is deleted after the content retention period (at most 30 days)",
|
|
442
|
+
contentNotOptedIn: "This workspace has not opted in to prompts you typed, so sending content was not enabled. Opt in under Data retention in the workspace settings, then run `harnessforce init --send-content` again"
|
|
443
|
+
};
|
|
444
|
+
const codexNotes = (configPath) => [
|
|
445
|
+
`Wrote the Codex configuration (${configPath}). Restart Codex to apply it`,
|
|
446
|
+
"Codex hooks do not run until you trust them in Codex's /hooks. Trust the harnessforce SessionStart hook in /hooks",
|
|
447
|
+
"To use the Harnessforce MCP server, log in with `codex mcp login harnessforce`"
|
|
448
|
+
].join("\n");
|
|
449
|
+
const nonDefaultConnectionMessage = (url) => `Connecting to ${url} (not the default production endpoint)`;
|
|
450
|
+
const listenFailedMessage = (port) => `${INIT_MESSAGES.listenFailed}. Stop the process using port ${port}, or specify another port with \`harnessforce init --port\``;
|
|
451
|
+
const keychainUnavailableMessage = (keychain, message = INIT_MESSAGES.keychainUnavailable) => keychain.unavailableHint ? `${message}. ${keychain.unavailableHint}` : message;
|
|
452
|
+
//#endregion
|
|
417
453
|
//#region src/credentials/keychain.ts
|
|
454
|
+
async function openSingleStore(store) {
|
|
455
|
+
if (!await store.isAvailable()) return void 0;
|
|
456
|
+
return {
|
|
457
|
+
isKernelKeyring: false,
|
|
458
|
+
items: await store.list(),
|
|
459
|
+
set: (account, secret) => store.set(account, secret),
|
|
460
|
+
delete: (account) => store.delete(account),
|
|
461
|
+
deleteElsewhere: async () => {}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
418
464
|
const KEYCHAIN_SERVICE = "harnessforce";
|
|
419
465
|
const INGEST_KEY_SUFFIX = ":ingest-key";
|
|
420
466
|
const API_TOKEN_SUFFIX = ":api-token";
|
|
@@ -422,27 +468,200 @@ const ingestKeyAccount = (workspaceId) => `${workspaceId}${INGEST_KEY_SUFFIX}`;
|
|
|
422
468
|
const apiTokenAccount = (workspaceId) => `${workspaceId}${API_TOKEN_SUFFIX}`;
|
|
423
469
|
const ingestOriginAccount = (workspaceId) => `${workspaceId}:ingest-origin`;
|
|
424
470
|
const urlOriginAccount = (workspaceId) => `${workspaceId}:url-origin`;
|
|
471
|
+
const workspaceAccounts = (workspaceId) => [
|
|
472
|
+
ingestKeyAccount(workspaceId),
|
|
473
|
+
apiTokenAccount(workspaceId),
|
|
474
|
+
ingestOriginAccount(workspaceId),
|
|
475
|
+
urlOriginAccount(workspaceId)
|
|
476
|
+
];
|
|
425
477
|
const isIngestKeyAccount = (account) => account.endsWith(INGEST_KEY_SUFFIX);
|
|
426
478
|
const apiTokenWorkspaceId = (account) => account.endsWith(API_TOKEN_SUFFIX) ? account.slice(0, -10) : void 0;
|
|
427
479
|
//#endregion
|
|
480
|
+
//#region src/credentials/linux-keychain.ts
|
|
481
|
+
const SECRET_SERVICE_TIMEOUT_MS = 6e4;
|
|
482
|
+
const WAITING_NOTICE_DELAY_MS = 2e3;
|
|
483
|
+
const PROBE_ACCOUNT$1 = "availability-probe";
|
|
484
|
+
const PROBE_VALUE = "harnessforce";
|
|
485
|
+
const MIGRATION_PROBE_TIMEOUT_MS = 200;
|
|
486
|
+
const MIGRATION_BUDGET_MS = 1e3;
|
|
487
|
+
const CLEANUP_TIMEOUT_MS = 200;
|
|
488
|
+
const succeeds = (operation) => operation().then(() => true, () => false);
|
|
489
|
+
const workspaceOf = (account) => account.slice(0, account.lastIndexOf(":"));
|
|
490
|
+
function createLinuxKeychain(options) {
|
|
491
|
+
return build(options, SECRET_SERVICE_TIMEOUT_MS);
|
|
492
|
+
}
|
|
493
|
+
function build(options, timeoutMs) {
|
|
494
|
+
const { kernel, secretService } = options;
|
|
495
|
+
const locations = /* @__PURE__ */ new Map();
|
|
496
|
+
const locate = (workspaceId) => {
|
|
497
|
+
let located = locations.get(workspaceId);
|
|
498
|
+
if (!located) {
|
|
499
|
+
located = kernel.get(ingestKeyAccount(workspaceId), timeoutMs).then((key) => ({
|
|
500
|
+
location: key ? "kernel" : "secret-service",
|
|
501
|
+
isKernelBroken: false
|
|
502
|
+
}), () => ({
|
|
503
|
+
location: "secret-service",
|
|
504
|
+
isKernelBroken: true
|
|
505
|
+
}));
|
|
506
|
+
locations.set(workspaceId, located);
|
|
507
|
+
}
|
|
508
|
+
return located;
|
|
509
|
+
};
|
|
510
|
+
const storeAt = (location) => location === "kernel" ? kernel : secretService;
|
|
511
|
+
return {
|
|
512
|
+
unavailableHint: options.unavailableHint,
|
|
513
|
+
async isAvailable() {
|
|
514
|
+
return await succeeds(() => kernel.get(PROBE_ACCOUNT$1, timeoutMs)) || await succeeds(() => secretService.get(PROBE_ACCOUNT$1, timeoutMs));
|
|
515
|
+
},
|
|
516
|
+
async get(account) {
|
|
517
|
+
const { location, isKernelBroken } = await locate(workspaceOf(account));
|
|
518
|
+
if (location === "kernel") return kernel.get(account, timeoutMs);
|
|
519
|
+
try {
|
|
520
|
+
return await secretService.get(account, timeoutMs);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
if (isKernelBroken) throw error;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
async set(account, secret) {
|
|
527
|
+
const { location } = await locate(workspaceOf(account));
|
|
528
|
+
await storeAt(location).set(account, secret, timeoutMs);
|
|
529
|
+
},
|
|
530
|
+
async delete(account) {
|
|
531
|
+
const { location } = await locate(workspaceOf(account));
|
|
532
|
+
await storeAt(location).delete(account, timeoutMs);
|
|
533
|
+
},
|
|
534
|
+
async migrateFromKernel(workspaceId) {
|
|
535
|
+
const { location } = await locate(workspaceId);
|
|
536
|
+
if (location !== "kernel") return;
|
|
537
|
+
await migrate(options, workspaceId).catch(() => {});
|
|
538
|
+
},
|
|
539
|
+
withTimeout: (ms) => build(options, ms),
|
|
540
|
+
openForInit: (init) => openForInit(options, timeoutMs, init)
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
async function openForInit({ kernel, secretService }, timeoutMs, { previousWorkspaceId, onWaiting }) {
|
|
544
|
+
const canWrite = (store) => succeeds(async () => {
|
|
545
|
+
await store.set(PROBE_ACCOUNT$1, PROBE_VALUE, timeoutMs);
|
|
546
|
+
await store.delete(PROBE_ACCOUNT$1, timeoutMs);
|
|
547
|
+
});
|
|
548
|
+
const notice = setTimeout(onWaiting, WAITING_NOTICE_DELAY_MS);
|
|
549
|
+
const usesSecretService = await canWrite(secretService).finally(() => clearTimeout(notice));
|
|
550
|
+
const readKernel = async () => {
|
|
551
|
+
if (!previousWorkspaceId) return [];
|
|
552
|
+
const accounts = [ingestKeyAccount(previousWorkspaceId), apiTokenAccount(previousWorkspaceId)];
|
|
553
|
+
const items = [];
|
|
554
|
+
for (const account of accounts) {
|
|
555
|
+
const secret = await kernel.get(account, timeoutMs);
|
|
556
|
+
if (secret !== void 0) items.push({
|
|
557
|
+
account,
|
|
558
|
+
secret
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return items;
|
|
562
|
+
};
|
|
563
|
+
if (usesSecretService) return {
|
|
564
|
+
isKernelKeyring: false,
|
|
565
|
+
items: [...await secretService.list(timeoutMs), ...await readKernel().catch(() => [])],
|
|
566
|
+
set: (account, secret) => secretService.set(account, secret, timeoutMs),
|
|
567
|
+
delete: (account) => secretService.delete(account, timeoutMs),
|
|
568
|
+
async deleteElsewhere(workspaceId) {
|
|
569
|
+
for (const account of workspaceAccounts(workspaceId)) await kernel.delete(account, timeoutMs).catch(() => {});
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
if (!await canWrite(kernel)) return void 0;
|
|
573
|
+
return {
|
|
574
|
+
isKernelKeyring: true,
|
|
575
|
+
items: await readKernel(),
|
|
576
|
+
set: (account, secret) => kernel.set(account, secret, timeoutMs),
|
|
577
|
+
delete: (account) => kernel.delete(account, timeoutMs),
|
|
578
|
+
deleteElsewhere: async () => {}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
var MigrationExpired = class extends Error {};
|
|
582
|
+
const MIGRATION_ORDER = (workspaceId) => [
|
|
583
|
+
ingestOriginAccount(workspaceId),
|
|
584
|
+
urlOriginAccount(workspaceId),
|
|
585
|
+
apiTokenAccount(workspaceId),
|
|
586
|
+
ingestKeyAccount(workspaceId)
|
|
587
|
+
];
|
|
588
|
+
async function migrate({ kernel, secretService }, workspaceId) {
|
|
589
|
+
let isExpired = false;
|
|
590
|
+
const deadline = Date.now() + MIGRATION_BUDGET_MS;
|
|
591
|
+
const remaining = () => {
|
|
592
|
+
if (isExpired) throw new MigrationExpired();
|
|
593
|
+
return Math.max(deadline - Date.now(), 1);
|
|
594
|
+
};
|
|
595
|
+
const written = [];
|
|
596
|
+
let isVerified = false;
|
|
597
|
+
const move = async () => {
|
|
598
|
+
await secretService.get(PROBE_ACCOUNT$1, MIGRATION_PROBE_TIMEOUT_MS);
|
|
599
|
+
const items = [];
|
|
600
|
+
for (const account of MIGRATION_ORDER(workspaceId)) {
|
|
601
|
+
const secret = await kernel.get(account, remaining());
|
|
602
|
+
if (secret !== void 0) items.push({
|
|
603
|
+
account,
|
|
604
|
+
secret
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
for (const account of workspaceAccounts(workspaceId)) await secretService.delete(account, remaining());
|
|
608
|
+
for (const { account, secret } of items) {
|
|
609
|
+
written.push(account);
|
|
610
|
+
await secretService.set(account, secret, remaining());
|
|
611
|
+
}
|
|
612
|
+
for (const { account, secret } of items) if (await secretService.get(account, remaining()) !== secret) throw new Error("the Secret Service returned a different value");
|
|
613
|
+
remaining();
|
|
614
|
+
isVerified = true;
|
|
615
|
+
for (const { account } of [...items].reverse()) await kernel.delete(account, remaining());
|
|
616
|
+
};
|
|
617
|
+
let timer;
|
|
618
|
+
const expiry = new Promise((_, reject) => {
|
|
619
|
+
timer = setTimeout(() => {
|
|
620
|
+
isExpired = true;
|
|
621
|
+
reject(new MigrationExpired());
|
|
622
|
+
}, MIGRATION_BUDGET_MS);
|
|
623
|
+
});
|
|
624
|
+
try {
|
|
625
|
+
await Promise.race([move(), expiry]);
|
|
626
|
+
} catch (error) {
|
|
627
|
+
if (isVerified) throw error;
|
|
628
|
+
await Promise.allSettled(written.map((account) => secretService.delete(account, CLEANUP_TIMEOUT_MS)));
|
|
629
|
+
throw error;
|
|
630
|
+
} finally {
|
|
631
|
+
clearTimeout(timer);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
//#endregion
|
|
428
635
|
//#region src/credentials/os-keychain.ts
|
|
429
|
-
const SUPPORTED_PLATFORMS = /* @__PURE__ */ new Set([
|
|
430
|
-
"darwin",
|
|
431
|
-
"linux",
|
|
432
|
-
"win32"
|
|
433
|
-
]);
|
|
434
|
-
const ENTRY_OPTIONS = { linux: { store: "secret-service" } };
|
|
435
636
|
const PROBE_ACCOUNT = "availability-probe";
|
|
436
|
-
function createOsKeychain({ platform, load }) {
|
|
637
|
+
function createOsKeychain({ platform, load, secretService }) {
|
|
437
638
|
let loaded;
|
|
438
639
|
const keyring = () => {
|
|
439
640
|
loaded ??= load();
|
|
440
641
|
return loaded;
|
|
441
642
|
};
|
|
442
|
-
|
|
643
|
+
if (platform === "linux") return createLinuxKeychain({
|
|
644
|
+
kernel: kernelKeyring(keyring),
|
|
645
|
+
secretService,
|
|
646
|
+
unavailableHint: INIT_MESSAGES.linuxKeychainUnavailable
|
|
647
|
+
});
|
|
648
|
+
if (platform === "darwin" || platform === "win32") return singleStore(keyring);
|
|
649
|
+
return unsupported;
|
|
650
|
+
}
|
|
651
|
+
function kernelKeyring(keyring) {
|
|
652
|
+
const entry = async (account) => new (await (keyring())).Entry(KEYCHAIN_SERVICE, account, { linux: { store: "keyutils" } });
|
|
443
653
|
return {
|
|
654
|
+
get: async (account) => (await entry(account)).getPassword() ?? void 0,
|
|
655
|
+
set: async (account, secret) => (await entry(account)).setPassword(secret),
|
|
656
|
+
delete: async (account) => {
|
|
657
|
+
(await entry(account)).deleteCredential();
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function singleStore(keyring) {
|
|
662
|
+
const entry = async (account) => new (await (keyring())).Entry(KEYCHAIN_SERVICE, account);
|
|
663
|
+
const keychain = {
|
|
444
664
|
async isAvailable() {
|
|
445
|
-
if (!SUPPORTED_PLATFORMS.has(platform)) return false;
|
|
446
665
|
try {
|
|
447
666
|
(await entry(PROBE_ACCOUNT)).getPassword();
|
|
448
667
|
return true;
|
|
@@ -450,17 +669,121 @@ function createOsKeychain({ platform, load }) {
|
|
|
450
669
|
return false;
|
|
451
670
|
}
|
|
452
671
|
},
|
|
453
|
-
async
|
|
454
|
-
|
|
672
|
+
get: async (account) => (await entry(account)).getPassword() ?? void 0,
|
|
673
|
+
set: async (account, secret) => (await entry(account)).setPassword(secret),
|
|
674
|
+
delete: async (account) => {
|
|
675
|
+
(await entry(account)).deleteCredential();
|
|
455
676
|
},
|
|
456
|
-
|
|
457
|
-
|
|
677
|
+
withTimeout: (ms) => limitedSingleStore(keychain, keyring, ms),
|
|
678
|
+
openForInit: () => openSingleStore({
|
|
679
|
+
...keychain,
|
|
680
|
+
list: async () => (await keyring()).findCredentials(KEYCHAIN_SERVICE).map(({ account, password }) => ({
|
|
681
|
+
account,
|
|
682
|
+
secret: password
|
|
683
|
+
}))
|
|
684
|
+
})
|
|
685
|
+
};
|
|
686
|
+
return keychain;
|
|
687
|
+
}
|
|
688
|
+
function limitedSingleStore(base, keyring, ms) {
|
|
689
|
+
const within = (operation) => async (account) => {
|
|
690
|
+
const entry = new (await (keyring())).AsyncEntry(KEYCHAIN_SERVICE, account);
|
|
691
|
+
let timer;
|
|
692
|
+
const timeout = new Promise((_, reject) => {
|
|
693
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("keychain timed out")), ms);
|
|
694
|
+
});
|
|
695
|
+
return Promise.race([operation(entry), timeout]).finally(() => clearTimeout(timer));
|
|
696
|
+
};
|
|
697
|
+
return {
|
|
698
|
+
...base,
|
|
699
|
+
get: async (account) => await within((entry) => entry.getPassword())(account) ?? void 0,
|
|
700
|
+
set: (account, secret) => within((entry) => entry.setPassword(secret))(account),
|
|
701
|
+
delete: async (account) => {
|
|
702
|
+
await within((entry) => entry.deleteCredential())(account);
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
const unsupportedError = async () => {
|
|
707
|
+
throw new Error("no keychain on this platform");
|
|
708
|
+
};
|
|
709
|
+
const unsupported = {
|
|
710
|
+
isAvailable: async () => false,
|
|
711
|
+
get: unsupportedError,
|
|
712
|
+
set: unsupportedError,
|
|
713
|
+
delete: unsupportedError,
|
|
714
|
+
withTimeout: () => unsupported,
|
|
715
|
+
openForInit: async () => void 0
|
|
716
|
+
};
|
|
717
|
+
//#endregion
|
|
718
|
+
//#region src/credentials/secret-service-process.ts
|
|
719
|
+
const CHILD_SCRIPT = `
|
|
720
|
+
const [op, service, account, timeoutMs, modulePath] = process.argv.slice(1);
|
|
721
|
+
setTimeout(() => process.kill(process.pid, "SIGKILL"), Number(timeoutMs));
|
|
722
|
+
const chunks = [];
|
|
723
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
724
|
+
process.stdin.on("end", async () => {
|
|
725
|
+
try {
|
|
726
|
+
const keyring = require(modulePath);
|
|
727
|
+
const options = { linux: { store: "secret-service" } };
|
|
728
|
+
let result = null;
|
|
729
|
+
if (op === "list") {
|
|
730
|
+
result = await keyring.findCredentialsAsync(service);
|
|
731
|
+
} else {
|
|
732
|
+
const entry = new keyring.AsyncEntry(service, account, options);
|
|
733
|
+
if (op === "get") result = (await entry.getPassword()) ?? null;
|
|
734
|
+
if (op === "set") await entry.setPassword(Buffer.concat(chunks).toString("utf8"));
|
|
735
|
+
if (op === "delete") await entry.deleteCredential();
|
|
736
|
+
}
|
|
737
|
+
process.stdout.write(JSON.stringify({ result }), () => process.exit(0));
|
|
738
|
+
} catch {
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
`;
|
|
743
|
+
function createSecretServiceProcess({ execPath, resolveKeyring }) {
|
|
744
|
+
const run = (op, account, timeoutMs, input = "") => new Promise((resolve, reject) => {
|
|
745
|
+
const child = spawn(execPath, [
|
|
746
|
+
"-e",
|
|
747
|
+
CHILD_SCRIPT,
|
|
748
|
+
op,
|
|
749
|
+
KEYCHAIN_SERVICE,
|
|
750
|
+
account,
|
|
751
|
+
String(timeoutMs),
|
|
752
|
+
resolveKeyring()
|
|
753
|
+
], { stdio: [
|
|
754
|
+
"pipe",
|
|
755
|
+
"pipe",
|
|
756
|
+
"ignore"
|
|
757
|
+
] });
|
|
758
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
|
|
759
|
+
const chunks = [];
|
|
760
|
+
child.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
761
|
+
child.on("error", reject);
|
|
762
|
+
child.on("close", (code) => {
|
|
763
|
+
clearTimeout(timer);
|
|
764
|
+
if (code !== 0) return reject(/* @__PURE__ */ new Error(`Secret Service ${op} failed`));
|
|
765
|
+
try {
|
|
766
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")).result);
|
|
767
|
+
} catch (error) {
|
|
768
|
+
reject(error);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
child.stdin.on("error", () => {});
|
|
772
|
+
child.stdin.end(input);
|
|
773
|
+
});
|
|
774
|
+
return {
|
|
775
|
+
async get(account, timeoutMs) {
|
|
776
|
+
const secret = await run("get", account, timeoutMs);
|
|
777
|
+
return typeof secret === "string" ? secret : void 0;
|
|
458
778
|
},
|
|
459
|
-
async
|
|
460
|
-
|
|
779
|
+
async set(account, secret, timeoutMs) {
|
|
780
|
+
await run("set", account, timeoutMs, secret);
|
|
781
|
+
},
|
|
782
|
+
async delete(account, timeoutMs) {
|
|
783
|
+
await run("delete", account, timeoutMs);
|
|
461
784
|
},
|
|
462
|
-
async list() {
|
|
463
|
-
return (await
|
|
785
|
+
async list(timeoutMs) {
|
|
786
|
+
return (await run("list", "", timeoutMs)).map(({ account, password }) => ({
|
|
464
787
|
account,
|
|
465
788
|
secret: password
|
|
466
789
|
}));
|
|
@@ -519,9 +842,9 @@ async function openBrowser(url, { platform, env, cwd, restoredEnv, spawn: spawnI
|
|
|
519
842
|
//#region src/init/loopback.ts
|
|
520
843
|
const HOST = "127.0.0.1";
|
|
521
844
|
const CALLBACK_PATH = "/callback";
|
|
522
|
-
const page = (message) => `<!doctype html><html lang="
|
|
523
|
-
const SUCCESS_PAGE = page("
|
|
524
|
-
const FAILURE_PAGE = page("
|
|
845
|
+
const page = (message) => `<!doctype html><html lang="en"><meta charset="utf-8"><title>Harnessforce</title><p>${message}</p></html>`;
|
|
846
|
+
const SUCCESS_PAGE = page("Return to the terminal");
|
|
847
|
+
const FAILURE_PAGE = page("The login failed. Check the terminal");
|
|
525
848
|
async function startLoopback(options) {
|
|
526
849
|
let settle = () => {};
|
|
527
850
|
const callback = new Promise((resolve) => {
|
|
@@ -569,11 +892,6 @@ async function startLoopback(options) {
|
|
|
569
892
|
};
|
|
570
893
|
}
|
|
571
894
|
//#endregion
|
|
572
|
-
//#region ../agent-core/src/hook-warning.ts
|
|
573
|
-
const RESTART_FAILED_WARNING = "Harnessforceのhookを起動し直せませんでした";
|
|
574
|
-
const INVALID_ENDPOINT_WARNING = "Harnessforceの送信先が不正です。`harnessforce init`を実行し直してください";
|
|
575
|
-
const notConfiguredWarning = (agent) => agent === "claude_code" ? "Harnessforceの送信が設定されていません。`/harnessforce:setup`を実行してください" : "Harnessforceの送信が設定されていません。`harnessforce init`を実行してください";
|
|
576
|
-
//#endregion
|
|
577
895
|
//#region ../agent-core/src/url.ts
|
|
578
896
|
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
579
897
|
"localhost",
|
|
@@ -597,8 +915,168 @@ function underBase(base, path) {
|
|
|
597
915
|
}
|
|
598
916
|
const withoutExtras = (url) => `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
|
|
599
917
|
//#endregion
|
|
918
|
+
//#region ../agent-core/src/client-update.ts
|
|
919
|
+
const CLIENT_UPDATE_HEADER = "harnessforce-client-update";
|
|
920
|
+
const UPDATE_COMMANDS = {
|
|
921
|
+
cli: "npm install -g @harnessforce/cli@latest",
|
|
922
|
+
"claude-code-plugin": "claude plugin update harnessforce@harnessforce-agent"
|
|
923
|
+
};
|
|
924
|
+
const DISPLAY_NAMES = {
|
|
925
|
+
cli: "CLI",
|
|
926
|
+
"claude-code-plugin": "plugin"
|
|
927
|
+
};
|
|
928
|
+
const DAY_MS$2 = 864e5;
|
|
929
|
+
const RECHECK_TIMEOUT_MS = 2e3;
|
|
930
|
+
const userAgentOf = (client) => `harnessforce-${client.component}${client.dev ? "-dev" : ""}/${client.version}`;
|
|
931
|
+
const STATUSES = /* @__PURE__ */ new Set([
|
|
932
|
+
"current",
|
|
933
|
+
"outdated",
|
|
934
|
+
"deprecated",
|
|
935
|
+
"unsupported"
|
|
936
|
+
]);
|
|
937
|
+
const REASONS = /* @__PURE__ */ new Set(["security", "compatibility"]);
|
|
938
|
+
function parseClientUpdateHeader(value) {
|
|
939
|
+
if (!value) return void 0;
|
|
940
|
+
const members = /* @__PURE__ */ new Map();
|
|
941
|
+
for (const member of value.split(",")) {
|
|
942
|
+
const [key, raw] = member.trim().split("=", 2);
|
|
943
|
+
if (key && raw !== void 0) members.set(key, raw);
|
|
944
|
+
}
|
|
945
|
+
const status = members.get("status");
|
|
946
|
+
if (!status || !STATUSES.has(status)) return void 0;
|
|
947
|
+
const update = { status };
|
|
948
|
+
const latest = /^"([^"\\]*)"$/.exec(members.get("latest") ?? "")?.[1];
|
|
949
|
+
if (latest) update.latest = latest;
|
|
950
|
+
const reason = members.get("reason");
|
|
951
|
+
if (reason && REASONS.has(reason)) update.reason = reason;
|
|
952
|
+
const seconds = /^@(\d+)$/.exec(members.get("deadline") ?? "")?.[1];
|
|
953
|
+
if (seconds) update.deadline = (/* @__PURE__ */ new Date(Number(seconds) * 1e3)).toISOString();
|
|
954
|
+
return isComplete(update) ? update : void 0;
|
|
955
|
+
}
|
|
956
|
+
function isComplete(update) {
|
|
957
|
+
switch (update.status) {
|
|
958
|
+
case "outdated": return Boolean(update.latest);
|
|
959
|
+
case "deprecated": return Boolean(update.reason) && !Number.isNaN(Date.parse(update.deadline ?? ""));
|
|
960
|
+
case "unsupported": return Boolean(update.reason);
|
|
961
|
+
default: return true;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function stateFrom(value, version) {
|
|
965
|
+
if (!isObject(value) || value.version !== version) return void 0;
|
|
966
|
+
if (typeof value.receivedAt !== "string") return void 0;
|
|
967
|
+
if (typeof value.status !== "string" || !STATUSES.has(value.status)) return void 0;
|
|
968
|
+
const state = value;
|
|
969
|
+
return isComplete(state) ? state : void 0;
|
|
970
|
+
}
|
|
971
|
+
async function readUpdateState(path, version) {
|
|
972
|
+
const saved = await readSavedUpdateState(path);
|
|
973
|
+
return saved?.version === version ? saved : void 0;
|
|
974
|
+
}
|
|
975
|
+
async function readSavedUpdateState(path) {
|
|
976
|
+
try {
|
|
977
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
978
|
+
return isObject(value) && typeof value.version === "string" ? stateFrom(value, value.version) : void 0;
|
|
979
|
+
} catch {
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
async function writeUpdateState(path, state) {
|
|
984
|
+
await mkdir(dirname(path), {
|
|
985
|
+
recursive: true,
|
|
986
|
+
mode: 448
|
|
987
|
+
});
|
|
988
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
989
|
+
await writeFile(temporary, `${JSON.stringify(state)}\n`, { mode: 384 });
|
|
990
|
+
await rename(temporary, path);
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Harnessforceへの要求に`User-Agent`を付け、応答の`Harnessforce-Client-Update`を状態のfileへ保存するfetch。
|
|
994
|
+
* 保存に失敗しても要求の結果は変えない。statePathが無い(pluginのdata directoryが無い)場合は版の申告だけを行う。
|
|
995
|
+
*/
|
|
996
|
+
function recordingFetch(fetchImpl, client, statePath, now) {
|
|
997
|
+
return async (url, init) => {
|
|
998
|
+
const headers = {
|
|
999
|
+
...Object.fromEntries(new Headers(init.headers)),
|
|
1000
|
+
"user-agent": userAgentOf(client)
|
|
1001
|
+
};
|
|
1002
|
+
const response = await fetchImpl(url, {
|
|
1003
|
+
...init,
|
|
1004
|
+
headers
|
|
1005
|
+
});
|
|
1006
|
+
const update = parseClientUpdateHeader(response.headers.get(CLIENT_UPDATE_HEADER));
|
|
1007
|
+
if (update && statePath) {
|
|
1008
|
+
const previous = await readUpdateState(statePath, client.version);
|
|
1009
|
+
await writeUpdateState(statePath, {
|
|
1010
|
+
...update,
|
|
1011
|
+
version: client.version,
|
|
1012
|
+
receivedAt: now().toISOString(),
|
|
1013
|
+
...previous?.outdatedShownAt ? { outdatedShownAt: previous.outdatedShownAt } : {}
|
|
1014
|
+
}).catch(() => {});
|
|
1015
|
+
}
|
|
1016
|
+
return response;
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
const utcDate = (instant) => new Intl.DateTimeFormat("en-CA", {
|
|
1020
|
+
timeZone: "UTC",
|
|
1021
|
+
year: "numeric",
|
|
1022
|
+
month: "2-digit",
|
|
1023
|
+
day: "2-digit"
|
|
1024
|
+
}).format(new Date(instant));
|
|
1025
|
+
const unsupportedLine = (component) => `This Harnessforce ${DISPLAY_NAMES[component]} version is no longer supported, so nothing is being sent to Harnessforce. Update it: \`${UPDATE_COMMANDS[component]}\``;
|
|
1026
|
+
function updateLine(component, update) {
|
|
1027
|
+
const name = DISPLAY_NAMES[component];
|
|
1028
|
+
const command = UPDATE_COMMANDS[component];
|
|
1029
|
+
switch (update.status) {
|
|
1030
|
+
case "unsupported": return unsupportedLine(component);
|
|
1031
|
+
case "deprecated": {
|
|
1032
|
+
const stops = `stops sending on ${utcDate(update.deadline ?? "")} (00:00 UTC)`;
|
|
1033
|
+
return update.reason === "security" ? `A security update for Harnessforce ${name} is available. This version ${stops}. Update it: \`${command}\`` : `This Harnessforce ${name} version ${stops}. Update it: \`${command}\``;
|
|
1034
|
+
}
|
|
1035
|
+
case "outdated": return `Harnessforce ${name} ${update.latest} is available. Update it: \`${command}\``;
|
|
1036
|
+
default: return;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function shouldShowOutdated(state, now, env) {
|
|
1040
|
+
if (state.status !== "outdated" || env.NO_UPDATE_NOTIFIER || env.CI) return false;
|
|
1041
|
+
const shownAt = state.outdatedShownAt ? Date.parse(state.outdatedShownAt) : NaN;
|
|
1042
|
+
return Number.isNaN(shownAt) || now.getTime() - shownAt >= DAY_MS$2;
|
|
1043
|
+
}
|
|
1044
|
+
const markOutdatedShown = (path, state, now) => writeUpdateState(path, {
|
|
1045
|
+
...state,
|
|
1046
|
+
outdatedShownAt: now.toISOString()
|
|
1047
|
+
});
|
|
1048
|
+
/**
|
|
1049
|
+
* 表示する1行。outdatedは24時間に1回までとし、表示する場合はその時刻を書く。isSuppressedは、pluginを
|
|
1050
|
+
* Claude Codeが自動更新する間など、呼び出し側がoutdatedを示さない条件。
|
|
1051
|
+
*/
|
|
1052
|
+
async function noticeLine(component, statePath, state, options) {
|
|
1053
|
+
if (state.status === "outdated") {
|
|
1054
|
+
if (!shouldShowOutdated(state, options.now, options.env) || await options.isSuppressed?.()) return void 0;
|
|
1055
|
+
await markOutdatedShown(statePath, state, options.now).catch(() => {});
|
|
1056
|
+
}
|
|
1057
|
+
return updateLine(component, state);
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* unsupportedを受けてから24時間以上経っていれば、資格情報を送らずに`GET /v1/client-status`で確かめ直す
|
|
1061
|
+
* (client-updates.md「送信の停止」)。失敗した場合は状態を変えない。
|
|
1062
|
+
*/
|
|
1063
|
+
async function recheckClientStatus(options) {
|
|
1064
|
+
const { state } = options;
|
|
1065
|
+
if (state?.status !== "unsupported" || options.now().getTime() - Date.parse(state.receivedAt) < DAY_MS$2) return state;
|
|
1066
|
+
try {
|
|
1067
|
+
await (await options.fetch(underBase(options.ingestBase, "v1/client-status"), {
|
|
1068
|
+
method: "GET",
|
|
1069
|
+
redirect: "error",
|
|
1070
|
+
signal: AbortSignal.timeout(RECHECK_TIMEOUT_MS)
|
|
1071
|
+
})).body?.cancel();
|
|
1072
|
+
} catch {
|
|
1073
|
+
return state;
|
|
1074
|
+
}
|
|
1075
|
+
return await readUpdateState(options.statePath, options.version) ?? state;
|
|
1076
|
+
}
|
|
1077
|
+
//#endregion
|
|
600
1078
|
//#region ../agent-core/src/ingest.ts
|
|
601
|
-
const SEND_TIMEOUT_MS
|
|
1079
|
+
const SEND_TIMEOUT_MS = 2e3;
|
|
602
1080
|
function ingestBaseFrom(endpoint) {
|
|
603
1081
|
const base = parseAllowedUrl(endpoint);
|
|
604
1082
|
if (!base) return void 0;
|
|
@@ -606,8 +1084,8 @@ function ingestBaseFrom(endpoint) {
|
|
|
606
1084
|
ingestBase.pathname = base.pathname.replace(/\/+$/, "");
|
|
607
1085
|
return ingestBase;
|
|
608
1086
|
}
|
|
609
|
-
const postItem = (destination, path, item, fetchImpl, timeoutMs = SEND_TIMEOUT_MS
|
|
610
|
-
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) {
|
|
611
1089
|
try {
|
|
612
1090
|
const response = await fetchImpl(underBase(destination.ingestBase, path), {
|
|
613
1091
|
method: "POST",
|
|
@@ -633,68 +1111,37 @@ async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_T
|
|
|
633
1111
|
}
|
|
634
1112
|
}
|
|
635
1113
|
//#endregion
|
|
636
|
-
//#region ../agent-core/src/
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
hookEventName: "SessionStart",
|
|
646
|
-
additionalContext: `${sessionContextLine(sessionId)}\n\n${usingHarnessforceBody}`
|
|
647
|
-
} });
|
|
648
|
-
//#endregion
|
|
649
|
-
//#region ../agent-core/src/vcs.ts
|
|
650
|
-
async function selectRemote(cwd, git) {
|
|
651
|
-
if (await git(cwd, ["rev-parse", "--is-inside-work-tree"]) !== "true") return void 0;
|
|
652
|
-
const remotes = (await git(cwd, ["remote"]))?.split("\n") ?? [];
|
|
653
|
-
return remotes.includes("origin") ? "origin" : remotes[0];
|
|
654
|
-
}
|
|
655
|
-
async function resolveVcs(cwd, git) {
|
|
656
|
-
const remote = await selectRemote(cwd, git);
|
|
657
|
-
if (!remote) return void 0;
|
|
658
|
-
const [url, branch, commit] = await Promise.all([
|
|
659
|
-
git(cwd, [
|
|
660
|
-
"remote",
|
|
661
|
-
"get-url",
|
|
662
|
-
remote
|
|
663
|
-
]),
|
|
664
|
-
git(cwd, [
|
|
665
|
-
"symbolic-ref",
|
|
666
|
-
"--quiet",
|
|
667
|
-
"--short",
|
|
668
|
-
"HEAD"
|
|
669
|
-
]),
|
|
670
|
-
git(cwd, [
|
|
671
|
-
"rev-parse",
|
|
672
|
-
"--verify",
|
|
673
|
-
"--quiet",
|
|
674
|
-
"HEAD"
|
|
675
|
-
])
|
|
676
|
-
]);
|
|
677
|
-
const repository = url === void 0 ? void 0 : normalizeRepository(url);
|
|
678
|
-
return {
|
|
679
|
-
...repository === void 0 ? {} : { repository },
|
|
680
|
-
...branch === void 0 ? {} : { branch },
|
|
681
|
-
...commit === void 0 ? {} : { commit }
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
async function resolveRepository(cwd, git) {
|
|
685
|
-
if (!isAbsolute(cwd)) return void 0;
|
|
686
|
-
const remote = await selectRemote(cwd, git);
|
|
687
|
-
if (!remote) return void 0;
|
|
688
|
-
const url = await git(cwd, [
|
|
689
|
-
"remote",
|
|
690
|
-
"get-url",
|
|
691
|
-
remote
|
|
692
|
-
]);
|
|
693
|
-
return url === void 0 ? void 0 : normalizeRepository(url);
|
|
1114
|
+
//#region ../agent-core/src/config/hash-input.ts
|
|
1115
|
+
function withoutSecrets(entry) {
|
|
1116
|
+
const { env, headers, url, args: _args, ...rest } = entry;
|
|
1117
|
+
const result = rest;
|
|
1118
|
+
if (isObject(env)) result.env = Object.keys(env).sort(byCodeUnit$1);
|
|
1119
|
+
if (isObject(headers)) result.headers = Object.keys(headers).sort(byCodeUnit$1);
|
|
1120
|
+
const strippedUrl = typeof url === "string" ? withoutCredentials(url) : void 0;
|
|
1121
|
+
if (strippedUrl !== void 0) result.url = strippedUrl;
|
|
1122
|
+
return result;
|
|
694
1123
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1124
|
+
function withoutCredentials(url) {
|
|
1125
|
+
let parsed;
|
|
1126
|
+
try {
|
|
1127
|
+
parsed = new URL(url);
|
|
1128
|
+
} catch {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
parsed.username = "";
|
|
1132
|
+
parsed.password = "";
|
|
1133
|
+
parsed.search = "";
|
|
1134
|
+
parsed.hash = "";
|
|
1135
|
+
return parsed.href;
|
|
1136
|
+
}
|
|
1137
|
+
const reduceHookGroup = (group) => isObject(group) && Array.isArray(group.hooks) ? {
|
|
1138
|
+
...group,
|
|
1139
|
+
hooks: group.hooks.map((handler) => isObject(handler) ? withoutSecrets(handler) : handler)
|
|
1140
|
+
} : group;
|
|
1141
|
+
function hashInput(kind, value) {
|
|
1142
|
+
if (kind === "mcp_server" && isObject(value)) return withoutSecrets(value);
|
|
1143
|
+
if (kind === "hook" && Array.isArray(value)) return value.map(reduceHookGroup);
|
|
1144
|
+
return value;
|
|
698
1145
|
}
|
|
699
1146
|
//#endregion
|
|
700
1147
|
//#region ../agent-core/src/config/scope.ts
|
|
@@ -724,7 +1171,7 @@ var Sink = class {
|
|
|
724
1171
|
});
|
|
725
1172
|
return {
|
|
726
1173
|
guard: this.guard,
|
|
727
|
-
addValue: (kind, id, value) => add(kind, id, hashValue(value)),
|
|
1174
|
+
addValue: (kind, id, value) => add(kind, id, hashValue(hashInput(kind, value))),
|
|
728
1175
|
addFile: async (kind, id, path) => {
|
|
729
1176
|
const content = await readFileIfExists(path, this.guard);
|
|
730
1177
|
if (content) add(kind, id, hashFileContent(content));
|
|
@@ -754,6 +1201,9 @@ function collectMcpServers$1(s, servers) {
|
|
|
754
1201
|
for (const [name, value] of Object.entries(servers)) if (isObject(value)) s.addValue("mcp_server", name, value);
|
|
755
1202
|
}
|
|
756
1203
|
//#endregion
|
|
1204
|
+
//#region ../agent-core/src/plugin-auto-update.ts
|
|
1205
|
+
const MARKETPLACE_NAME = "harnessforce-agent";
|
|
1206
|
+
//#endregion
|
|
757
1207
|
//#region src/shared/settings.ts
|
|
758
1208
|
const OTEL_HEADERS_HELPER = "harnessforce otel-headers";
|
|
759
1209
|
const PLUGIN_ID$1 = "harnessforce@harnessforce-agent";
|
|
@@ -782,6 +1232,37 @@ async function readUserSettings(path) {
|
|
|
782
1232
|
return { kind: "invalid" };
|
|
783
1233
|
}
|
|
784
1234
|
}
|
|
1235
|
+
const MARKETPLACE_KEYS = ["extraKnownMarketplaces", "additionalMarketplaces"];
|
|
1236
|
+
const MARKETPLACE_SOURCE = {
|
|
1237
|
+
source: "github",
|
|
1238
|
+
repo: "lambda-script/harnessforce-agent"
|
|
1239
|
+
};
|
|
1240
|
+
const isMarketplacesValid = (value) => value === void 0 || isObject(value) && (value["harnessforce-agent"] === void 0 || isObject(value["harnessforce-agent"]));
|
|
1241
|
+
const hasWritableMarketplaces = (settings) => MARKETPLACE_KEYS.every((key) => isMarketplacesValid(settings[key]));
|
|
1242
|
+
/**
|
|
1243
|
+
* marketplaceの登録の鍵の`harnessforce-agent`に、sourceと自動更新を書く。sourceと、利用者が切り替えた真偽値は残し、
|
|
1244
|
+
* turnOffのときだけ偽にする。自動更新が有効になったかも返す。
|
|
1245
|
+
*/
|
|
1246
|
+
function withPluginAutoUpdate(settings, turnOff) {
|
|
1247
|
+
const key = settings.additionalMarketplaces !== void 0 && settings.extraKnownMarketplaces === void 0 ? "additionalMarketplaces" : "extraKnownMarketplaces";
|
|
1248
|
+
const marketplaces = settings[key] ?? {};
|
|
1249
|
+
const entry = marketplaces["harnessforce-agent"] ?? {};
|
|
1250
|
+
const autoUpdate = turnOff ? false : typeof entry.autoUpdate === "boolean" ? entry.autoUpdate : true;
|
|
1251
|
+
return {
|
|
1252
|
+
settings: {
|
|
1253
|
+
...settings,
|
|
1254
|
+
[key]: {
|
|
1255
|
+
...marketplaces,
|
|
1256
|
+
[MARKETPLACE_NAME]: {
|
|
1257
|
+
...entry,
|
|
1258
|
+
source: entry.source ?? MARKETPLACE_SOURCE,
|
|
1259
|
+
autoUpdate
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
},
|
|
1263
|
+
isOn: autoUpdate
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
785
1266
|
function mergeUserSettings(settings, env) {
|
|
786
1267
|
return {
|
|
787
1268
|
...settings,
|
|
@@ -834,6 +1315,399 @@ async function resolveCliDestinations(env, settingsPath, defaultUrl) {
|
|
|
834
1315
|
};
|
|
835
1316
|
}
|
|
836
1317
|
//#endregion
|
|
1318
|
+
//#region src/client-update.ts
|
|
1319
|
+
const updateStatePath = (homeDir) => join(homeDir, ".harnessforce", "update-status.json");
|
|
1320
|
+
const readCliUpdateState = (homeDir, version) => readUpdateState(updateStatePath(homeDir), version);
|
|
1321
|
+
/**
|
|
1322
|
+
* 送信を伴うcommandの前に呼ぶ。unsupportedを受けてから24時間以上経っていれば資格情報を送らずに確かめ直し、
|
|
1323
|
+
* それでもunsupportedなら真を返す(client-updates.md「送信の停止」)。
|
|
1324
|
+
*/
|
|
1325
|
+
async function isSendingBlocked(deps) {
|
|
1326
|
+
const statePath = updateStatePath(deps.homeDir);
|
|
1327
|
+
const state = await readUpdateState(statePath, deps.client.version);
|
|
1328
|
+
if (state?.status !== "unsupported") return false;
|
|
1329
|
+
const { ingestEndpoint } = await resolveCliDestinations(deps.env, userSettingsPath(deps.env, deps.homeDir), deps.defaultUrl);
|
|
1330
|
+
const ingestBase = ingestBaseFrom(ingestEndpoint);
|
|
1331
|
+
return (ingestBase ? await recheckClientStatus({
|
|
1332
|
+
state,
|
|
1333
|
+
ingestBase,
|
|
1334
|
+
fetch: deps.fetch,
|
|
1335
|
+
statePath,
|
|
1336
|
+
version: deps.client.version,
|
|
1337
|
+
now: deps.now
|
|
1338
|
+
}) : state)?.status === "unsupported";
|
|
1339
|
+
}
|
|
1340
|
+
/** commandの出力の後に、更新の状態の1行をstderrへ書く。outdatedは24時間に1回までとする。 */
|
|
1341
|
+
async function noticeUpdate(deps) {
|
|
1342
|
+
const statePath = updateStatePath(deps.homeDir);
|
|
1343
|
+
const state = await readUpdateState(statePath, deps.client.version);
|
|
1344
|
+
if (!state) return;
|
|
1345
|
+
const line = await noticeLine("cli", statePath, state, {
|
|
1346
|
+
now: deps.now(),
|
|
1347
|
+
env: deps.env
|
|
1348
|
+
});
|
|
1349
|
+
if (line) deps.stderr(`${line}\n`);
|
|
1350
|
+
}
|
|
1351
|
+
//#endregion
|
|
1352
|
+
//#region src/help.ts
|
|
1353
|
+
const SENDS_NOTHING_LOCAL = "Nothing. It reads local files only and makes no network request.";
|
|
1354
|
+
const COMMANDS$1 = {
|
|
1355
|
+
init: {
|
|
1356
|
+
summary: "Connect this machine to a Harnessforce Workspace",
|
|
1357
|
+
text: `Usage: harnessforce init [--port <port>] [--send-content] [--no-plugin-auto-update]
|
|
1358
|
+
|
|
1359
|
+
When to use:
|
|
1360
|
+
Once per machine, before anything else, and again whenever the login expires.
|
|
1361
|
+
|
|
1362
|
+
What it does:
|
|
1363
|
+
Opens your browser to log in to a Workspace, then stores a user ingest key and
|
|
1364
|
+
an API token in the OS keychain. It configures Claude Code (user settings) and
|
|
1365
|
+
Codex (config.toml) to export telemetry to the Workspace.
|
|
1366
|
+
|
|
1367
|
+
Options:
|
|
1368
|
+
--port <port> Loopback port for the login callback, 1-65535 (default 8080).
|
|
1369
|
+
--send-content Also send the text of the prompts you type. Takes effect only
|
|
1370
|
+
when the Workspace has opted in. Off by default.
|
|
1371
|
+
--no-plugin-auto-update
|
|
1372
|
+
Turn off automatic updates of the Harnessforce plugin in
|
|
1373
|
+
Claude Code. On by default; a value you set is kept.
|
|
1374
|
+
|
|
1375
|
+
Sends:
|
|
1376
|
+
The login exchange with the Workspace. After init, the agents you run send
|
|
1377
|
+
telemetry (token counts, tool names, timings) with that key.
|
|
1378
|
+
|
|
1379
|
+
Does not send:
|
|
1380
|
+
Prompt text (unless --send-content and the Workspace opt-in), model responses,
|
|
1381
|
+
tool input or output, code diffs, and secrets. The key is never written to a
|
|
1382
|
+
plain file.
|
|
1383
|
+
|
|
1384
|
+
Exit codes:
|
|
1385
|
+
0 Connected and configured.
|
|
1386
|
+
1 Failed (login cancelled or timed out, keychain unavailable, or a write
|
|
1387
|
+
failed). The reason is printed to stderr.
|
|
1388
|
+
|
|
1389
|
+
Examples:
|
|
1390
|
+
harnessforce init
|
|
1391
|
+
harnessforce init --send-content
|
|
1392
|
+
harnessforce init --port 8081
|
|
1393
|
+
`
|
|
1394
|
+
},
|
|
1395
|
+
import: {
|
|
1396
|
+
summary: "Send past local sessions to the Workspace",
|
|
1397
|
+
text: `Usage: harnessforce import
|
|
1398
|
+
|
|
1399
|
+
When to use:
|
|
1400
|
+
Right after init, to backfill sessions that ran before Harnessforce was
|
|
1401
|
+
connected. It is safe to run again; sessions already sent are skipped.
|
|
1402
|
+
|
|
1403
|
+
What it does:
|
|
1404
|
+
Reads Claude Code and Codex session records on this machine, keeps the
|
|
1405
|
+
sessions of repositories connected to the Workspace and inside its import
|
|
1406
|
+
window, and sends a summary of each. When it imports nothing, it prints why
|
|
1407
|
+
and what to do next.
|
|
1408
|
+
|
|
1409
|
+
Sends:
|
|
1410
|
+
Per session: agent, session ID, repository, branch, commit, start and end
|
|
1411
|
+
time, model, input and output token counts, and tool call counts.
|
|
1412
|
+
|
|
1413
|
+
Does not send:
|
|
1414
|
+
Prompt text, model responses, tool input or output, and code diffs. Sessions
|
|
1415
|
+
of repositories that are not connected are not sent.
|
|
1416
|
+
|
|
1417
|
+
Exit codes:
|
|
1418
|
+
0 Finished (including "nothing to import").
|
|
1419
|
+
1 Not logged in, keychain unavailable, a limit was reached, or sending failed.
|
|
1420
|
+
|
|
1421
|
+
Examples:
|
|
1422
|
+
harnessforce import
|
|
1423
|
+
`
|
|
1424
|
+
},
|
|
1425
|
+
"otel-headers": {
|
|
1426
|
+
summary: "Print the telemetry auth header (used by Claude Code)",
|
|
1427
|
+
text: `Usage: harnessforce otel-headers
|
|
1428
|
+
|
|
1429
|
+
When to use:
|
|
1430
|
+
You normally never run it. Claude Code calls it as its otelHeadersHelper.
|
|
1431
|
+
|
|
1432
|
+
What it does:
|
|
1433
|
+
Reads the ingest key from the keychain and prints {"Authorization": "Bearer <key>"}
|
|
1434
|
+
as JSON on stdout.
|
|
1435
|
+
|
|
1436
|
+
Sends:
|
|
1437
|
+
${SENDS_NOTHING_LOCAL}
|
|
1438
|
+
|
|
1439
|
+
Does not send:
|
|
1440
|
+
Anything. Do not paste its output into chats or logs: it contains the key.
|
|
1441
|
+
|
|
1442
|
+
Exit codes:
|
|
1443
|
+
0 Header printed.
|
|
1444
|
+
1 No usable key (not logged in, keychain unavailable, or the destination was
|
|
1445
|
+
not verified). Nothing is printed to stdout.
|
|
1446
|
+
|
|
1447
|
+
Examples:
|
|
1448
|
+
harnessforce otel-headers
|
|
1449
|
+
`
|
|
1450
|
+
},
|
|
1451
|
+
run: {
|
|
1452
|
+
summary: "Start an agent tied to an Issue",
|
|
1453
|
+
text: `Usage: harnessforce run --issue <identifier> -- <agent> [args]
|
|
1454
|
+
|
|
1455
|
+
When to use:
|
|
1456
|
+
When the branch name does not contain the Issue identifier and you want the
|
|
1457
|
+
work tied to the Issue anyway.
|
|
1458
|
+
|
|
1459
|
+
What it does:
|
|
1460
|
+
Starts <agent> (for example claude or codex) with your arguments, with the
|
|
1461
|
+
Issue identifier added to the telemetry it exports. Everything after "--" is
|
|
1462
|
+
passed to the agent unchanged, including "--help". The agent's exit code is
|
|
1463
|
+
returned.
|
|
1464
|
+
|
|
1465
|
+
Options:
|
|
1466
|
+
--issue <identifier> The Issue identifier, for example ABC-123.
|
|
1467
|
+
|
|
1468
|
+
Sends:
|
|
1469
|
+
The same telemetry as an agent started directly, plus the Issue identifier.
|
|
1470
|
+
|
|
1471
|
+
Does not send:
|
|
1472
|
+
Anything beyond what init configured. Prompt text is sent only if you chose
|
|
1473
|
+
--send-content at init.
|
|
1474
|
+
|
|
1475
|
+
Exit codes:
|
|
1476
|
+
The agent's own exit code when it ran.
|
|
1477
|
+
1 Could not start: not logged in, bad arguments, or the agent was not found.
|
|
1478
|
+
|
|
1479
|
+
Examples:
|
|
1480
|
+
harnessforce run --issue ABC-123 -- claude
|
|
1481
|
+
harnessforce run --issue ABC-123 -- codex --model gpt-5
|
|
1482
|
+
`
|
|
1483
|
+
},
|
|
1484
|
+
tune: {
|
|
1485
|
+
summary: "Analyze your past sessions and report counts",
|
|
1486
|
+
text: `Usage: harnessforce tune [--all] [--no-send] [--show-report] [--json]
|
|
1487
|
+
harnessforce tune record
|
|
1488
|
+
harnessforce tune --purge
|
|
1489
|
+
|
|
1490
|
+
When to use:
|
|
1491
|
+
Periodically (or through /harnessforce:tune) to see where human intervention,
|
|
1492
|
+
repeated work, and MCP or tool usage could improve. It needs at least 10
|
|
1493
|
+
sessions.
|
|
1494
|
+
|
|
1495
|
+
What it does:
|
|
1496
|
+
Imports sessions that were not sent yet, analyzes the local session records,
|
|
1497
|
+
and prints the result. Files it writes live under ~/.harnessforce/tune/.
|
|
1498
|
+
|
|
1499
|
+
Options:
|
|
1500
|
+
--all Analyze sessions of repositories not connected too (they are
|
|
1501
|
+
never sent).
|
|
1502
|
+
--no-send Analyze only; send nothing.
|
|
1503
|
+
--show-report Print the exact values that would be sent.
|
|
1504
|
+
--json Print the result as one JSON object on stdout. Messages go to
|
|
1505
|
+
stderr.
|
|
1506
|
+
record Read one proposal as JSON from stdin and store it locally.
|
|
1507
|
+
--purge Delete the analysis, proposals, and unsent results.
|
|
1508
|
+
|
|
1509
|
+
Sends:
|
|
1510
|
+
Counts computed by public rules (no text), for connected repositories only.
|
|
1511
|
+
It also runs the same session import as "harnessforce import".
|
|
1512
|
+
|
|
1513
|
+
Does not send:
|
|
1514
|
+
Prompt text, model responses, tool input or output, and code diffs. Usage
|
|
1515
|
+
statistics and proposals stay on this machine.
|
|
1516
|
+
|
|
1517
|
+
Exit codes:
|
|
1518
|
+
0 Done (including "not enough data" and "nothing sent").
|
|
1519
|
+
1 Not logged in, keychain unavailable, another tune is running, or a local
|
|
1520
|
+
write failed.
|
|
1521
|
+
2 Bad option, or invalid input to "record".
|
|
1522
|
+
3 Analysis shown, but sending is not possible until you run "harnessforce init".
|
|
1523
|
+
|
|
1524
|
+
Examples:
|
|
1525
|
+
harnessforce tune
|
|
1526
|
+
harnessforce tune --no-send --json
|
|
1527
|
+
harnessforce tune --purge
|
|
1528
|
+
`
|
|
1529
|
+
},
|
|
1530
|
+
top: {
|
|
1531
|
+
summary: "Show running and recent sessions on this machine",
|
|
1532
|
+
text: `Usage: harnessforce top [--once] [--json] [--ascii] [--theme <auto|light|dark|ansi>]
|
|
1533
|
+
|
|
1534
|
+
When to use:
|
|
1535
|
+
To watch what your agents are doing right now. In a terminal it is a live
|
|
1536
|
+
view; with --once or --json it prints a snapshot and exits.
|
|
1537
|
+
|
|
1538
|
+
What it does:
|
|
1539
|
+
Reads Claude Code and Codex session records on this machine and shows each
|
|
1540
|
+
session's state, repository, branch, tokens, and tool calls.
|
|
1541
|
+
|
|
1542
|
+
Options:
|
|
1543
|
+
--once Print one snapshot and exit.
|
|
1544
|
+
--json Print one snapshot as JSON and exit.
|
|
1545
|
+
--ascii Use plain ASCII instead of box-drawing characters.
|
|
1546
|
+
--theme Color theme: auto (default), light, dark, or ansi.
|
|
1547
|
+
|
|
1548
|
+
Sends:
|
|
1549
|
+
${SENDS_NOTHING_LOCAL}
|
|
1550
|
+
|
|
1551
|
+
Does not send:
|
|
1552
|
+
Anything. It never changes any file.
|
|
1553
|
+
|
|
1554
|
+
Exit codes:
|
|
1555
|
+
0 Done (including quitting the live view).
|
|
1556
|
+
1 Bad arguments.
|
|
1557
|
+
|
|
1558
|
+
Examples:
|
|
1559
|
+
harnessforce top
|
|
1560
|
+
harnessforce top --once --json
|
|
1561
|
+
`
|
|
1562
|
+
},
|
|
1563
|
+
hook: {
|
|
1564
|
+
summary: "Session-start hook (used by Codex)",
|
|
1565
|
+
text: `Usage: harnessforce hook session-start
|
|
1566
|
+
|
|
1567
|
+
When to use:
|
|
1568
|
+
You normally never run it. Codex runs it when a session starts.
|
|
1569
|
+
|
|
1570
|
+
What it does:
|
|
1571
|
+
Reads the hook input from stdin, registers the session with the Workspace, and
|
|
1572
|
+
prints context for the agent (the session ID, plus a one-line warning if
|
|
1573
|
+
registration failed).
|
|
1574
|
+
|
|
1575
|
+
Sends:
|
|
1576
|
+
Session registration: agent, session ID, repository, branch, commit, and
|
|
1577
|
+
start time.
|
|
1578
|
+
|
|
1579
|
+
Does not send:
|
|
1580
|
+
Prompt text, model responses, tool input or output, and code diffs.
|
|
1581
|
+
|
|
1582
|
+
Exit codes:
|
|
1583
|
+
0 Always, even on failure: a hook must never stop your session. A failure is
|
|
1584
|
+
shown as a one-line warning.
|
|
1585
|
+
1 Wrong arguments.
|
|
1586
|
+
|
|
1587
|
+
Examples:
|
|
1588
|
+
harnessforce hook session-start < hook-input.json
|
|
1589
|
+
`
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
const ORDER = [
|
|
1593
|
+
"init",
|
|
1594
|
+
"import",
|
|
1595
|
+
"run",
|
|
1596
|
+
"tune",
|
|
1597
|
+
"top",
|
|
1598
|
+
"otel-headers",
|
|
1599
|
+
"hook"
|
|
1600
|
+
];
|
|
1601
|
+
const NAME_WIDTH = Math.max(...ORDER.map((name) => name.length)) + 2;
|
|
1602
|
+
const OVERVIEW = `Usage: harnessforce <command> [options]
|
|
1603
|
+
|
|
1604
|
+
Connect your AI coding agents to a Harnessforce Workspace.
|
|
1605
|
+
|
|
1606
|
+
Commands:
|
|
1607
|
+
${ORDER.map((name) => ` ${name.padEnd(NAME_WIDTH)}${COMMANDS$1[name].summary}`).join("\n")}
|
|
1608
|
+
|
|
1609
|
+
Run "harnessforce <command> --help" for what a command does, what it sends and
|
|
1610
|
+
does not send, and its exit codes.
|
|
1611
|
+
|
|
1612
|
+
Options:
|
|
1613
|
+
-h, --help Show this help.
|
|
1614
|
+
--version Show the version.
|
|
1615
|
+
`;
|
|
1616
|
+
const UNKNOWN_COMMAND_HINT = `Usage: harnessforce <command> [options]
|
|
1617
|
+
Run "harnessforce --help" to list the commands.
|
|
1618
|
+
`;
|
|
1619
|
+
const isCommand = (name) => name !== void 0 && Object.hasOwn(COMMANDS$1, name);
|
|
1620
|
+
const isHelpFlag = (arg) => arg === "--help" || arg === "-h";
|
|
1621
|
+
function resolveHelp(argv) {
|
|
1622
|
+
const [command, ...rest] = argv;
|
|
1623
|
+
if (command === void 0) return void 0;
|
|
1624
|
+
if (isHelpFlag(command) && rest.length === 0) return OVERVIEW;
|
|
1625
|
+
if (command === "help") {
|
|
1626
|
+
if (rest.length === 0) return OVERVIEW;
|
|
1627
|
+
if (rest.length === 1 && isCommand(rest[0])) return COMMANDS$1[rest[0]].text;
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
if (!isCommand(command)) return void 0;
|
|
1631
|
+
const separator = rest.indexOf("--");
|
|
1632
|
+
return (separator === -1 ? rest : rest.slice(0, separator)).some(isHelpFlag) ? COMMANDS$1[command].text : void 0;
|
|
1633
|
+
}
|
|
1634
|
+
//#endregion
|
|
1635
|
+
//#region ../agent-core/src/hook-warning.ts
|
|
1636
|
+
const RESTART_FAILED_WARNING = "Could not restart the Harnessforce hook";
|
|
1637
|
+
const INVALID_ENDPOINT_WARNING = "The Harnessforce destination is invalid. Run `harnessforce init` again";
|
|
1638
|
+
const notConfiguredWarning = (agent) => agent === "claude_code" ? "Harnessforce sending is not configured. Run `/harnessforce:setup`" : "Harnessforce sending is not configured. Run `harnessforce init`";
|
|
1639
|
+
const stopped = (agent) => agent === "claude_code" ? "telemetry is not being sent" : "sessions are not being registered";
|
|
1640
|
+
const keyMissingWarning = (agent) => `Harnessforce cannot find your ingest key, so ${stopped(agent)}. Run \`harnessforce init\``;
|
|
1641
|
+
const keyUnreadableWarning = (agent) => `Harnessforce could not read your ingest key from the OS keychain, so ${agent === "claude_code" ? "telemetry may not be sent" : "sessions may not be registered"}. Unlock the keychain or run \`harnessforce init\``;
|
|
1642
|
+
const DESTINATION_MISMATCH_WARNING = {
|
|
1643
|
+
claude_code: "Telemetry is not being sent because the Harnessforce destination differs from the one saved by `harnessforce init`. Check this repository's settings or run `harnessforce init` again",
|
|
1644
|
+
codex: "Sessions are not being registered because the Harnessforce destination differs from the one saved by `harnessforce init`. Run `harnessforce init` again"
|
|
1645
|
+
};
|
|
1646
|
+
//#endregion
|
|
1647
|
+
//#region ../agent-core/src/issue.ts
|
|
1648
|
+
const isIssueIdentifier = (value) => isToken(value, 300);
|
|
1649
|
+
//#endregion
|
|
1650
|
+
//#region ../agent-core/src/using-harnessforce.ts
|
|
1651
|
+
const usingHarnessforceBody = "---\nname: using-harnessforce\ndescription: Use when starting a session in an environment with the Harnessforce hook, when starting work on an Issue, or when asked what is recorded or collected. The hook injects this text into the context at session start.\n---\n\n# Using Harnessforce\n\nThis session has the Harnessforce hook installed. Harnessforce links the work of people and AI to Issues and records it. Follow these rules.\n\n## What is recorded\n\n- Session registration: agent, session ID, repository, branch, commit, start time, and the first prompt ID.\n- Config snapshot: the structure of the configuration in use (Claude Code only).\n- OTel telemetry: what the user chooses to send.\n- For work on an Issue, `record-run` registers the session as a Run and closes it. It records no Plan, Decision, or Definition of Done.\n\n## What is not collected\n\n- Prompts, responses, tool input and output, and the body of code diffs arrive only when the user opts in with `--send-content` and the Workspace has opted in. They are not collected by default.\n- Never put these contents, secrets, or environment variable values in MCP tool input or in a record.\n\n## Which hook sends what\n\n- SessionStart: session registration and the config snapshot (Codex sends session registration only).\n- UserPromptSubmit: resends on the first prompt (Claude Code only).\n- A failing hook never stops the session. When the hook cannot send, it shows a one-line warning. Tell the user when a warning appears.\n\n## Which skill and tool to use, and when\n\n- For work on a specified Issue ID, or when the environment variable `HARNESSFORCE_ISSUE` is set, follow `record-run` and use `start_run`, `get_issue`, and `complete_run`. An agent that cannot load the skill calls the tools in the same order.\n- Pass the value of the `harnessforce session_id:` line in the context to `start_run`. If there are several, use the last one. If the line is missing, do not guess the session ID, and do not call `start_run` or `complete_run`.\n- Use `propose-improvements` for the analysis of `/harnessforce:tune`.\n- When no Issue is specified, do not start recording a Run.\n\n## Red Flags\n\nStop when one of these thoughts comes up.\n\n| Thought | Reality |\n| --- | --- |\n| An Issue was specified, but the work is small, so recording is unnecessary | Work on a specified Issue is recorded regardless of its size |\n| The session ID line is missing, so I will guess it and call `start_run` | Do not guess. The Run would no longer be linked to the session |\n| Summarizing the content before recording would help | Do not pass content to the tool. It goes beyond what is collected |\n| A warning appeared, but I can ignore it | Sending is not working. Tell the user |\n\n## Priority\n\n1. The user's instructions (CLAUDE.md, AGENTS.md, direct requests). If the user tells you to stop recording, stop recording the Run.\n2. This introduction.\n3. Default behavior.\n\n## Subagents\n\nA subagent that was delegated only a specific task by its parent does not register a Run by following this introduction. The parent registers the Run.\n".replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---\n+/, "").trimEnd();
|
|
1652
|
+
//#endregion
|
|
1653
|
+
//#region ../agent-core/src/session-context.ts
|
|
1654
|
+
const sessionContextLine = (sessionId) => `harnessforce session_id: ${sessionId}`;
|
|
1655
|
+
const sessionContext = (sessionId) => ({ hookSpecificOutput: {
|
|
1656
|
+
hookEventName: "SessionStart",
|
|
1657
|
+
additionalContext: `${sessionContextLine(sessionId)}\n\n${usingHarnessforceBody}`
|
|
1658
|
+
} });
|
|
1659
|
+
//#endregion
|
|
1660
|
+
//#region ../agent-core/src/vcs.ts
|
|
1661
|
+
async function selectRemote(cwd, git) {
|
|
1662
|
+
if (await git(cwd, ["rev-parse", "--is-inside-work-tree"]) !== "true") return void 0;
|
|
1663
|
+
const remotes = (await git(cwd, ["remote"]))?.split("\n") ?? [];
|
|
1664
|
+
return remotes.includes("origin") ? "origin" : remotes[0];
|
|
1665
|
+
}
|
|
1666
|
+
async function resolveVcs(cwd, git) {
|
|
1667
|
+
const remote = await selectRemote(cwd, git);
|
|
1668
|
+
if (!remote) return void 0;
|
|
1669
|
+
const [url, branch, commit] = await Promise.all([
|
|
1670
|
+
git(cwd, [
|
|
1671
|
+
"remote",
|
|
1672
|
+
"get-url",
|
|
1673
|
+
remote
|
|
1674
|
+
]),
|
|
1675
|
+
git(cwd, [
|
|
1676
|
+
"symbolic-ref",
|
|
1677
|
+
"--quiet",
|
|
1678
|
+
"--short",
|
|
1679
|
+
"HEAD"
|
|
1680
|
+
]),
|
|
1681
|
+
git(cwd, [
|
|
1682
|
+
"rev-parse",
|
|
1683
|
+
"--verify",
|
|
1684
|
+
"--quiet",
|
|
1685
|
+
"HEAD"
|
|
1686
|
+
])
|
|
1687
|
+
]);
|
|
1688
|
+
const repository = url === void 0 ? void 0 : normalizeRepository(url);
|
|
1689
|
+
return {
|
|
1690
|
+
...repository === void 0 ? {} : { repository },
|
|
1691
|
+
...branch === void 0 ? {} : { branch },
|
|
1692
|
+
...commit === void 0 ? {} : { commit }
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
async function resolveRepository(cwd, git) {
|
|
1696
|
+
if (!isAbsolute(cwd)) return void 0;
|
|
1697
|
+
const remote = await selectRemote(cwd, git);
|
|
1698
|
+
if (!remote) return void 0;
|
|
1699
|
+
const url = await git(cwd, [
|
|
1700
|
+
"remote",
|
|
1701
|
+
"get-url",
|
|
1702
|
+
remote
|
|
1703
|
+
]);
|
|
1704
|
+
return url === void 0 ? void 0 : normalizeRepository(url);
|
|
1705
|
+
}
|
|
1706
|
+
async function resolveProjectRoot(cwd, git) {
|
|
1707
|
+
const topLevel = await git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
1708
|
+
return topLevel && isAbsolute(topLevel) ? topLevel : cwd;
|
|
1709
|
+
}
|
|
1710
|
+
//#endregion
|
|
837
1711
|
//#region src/credentials/user-destination.ts
|
|
838
1712
|
async function resolveUserDestination(deps) {
|
|
839
1713
|
const { workspaceId, ingestEndpoint } = await resolveCliDestinations(deps.env, userSettingsPath(deps.env, deps.homeDir), "");
|
|
@@ -849,7 +1723,7 @@ async function resolveUserDestination(deps) {
|
|
|
849
1723
|
};
|
|
850
1724
|
if (!await deps.keychain.isAvailable()) return {
|
|
851
1725
|
kind: "skipped",
|
|
852
|
-
reason: "
|
|
1726
|
+
reason: "keychain unreadable"
|
|
853
1727
|
};
|
|
854
1728
|
const key = await deps.keychain.get(ingestKeyAccount(workspaceId));
|
|
855
1729
|
if (!key) return {
|
|
@@ -872,9 +1746,10 @@ async function resolveUserDestination(deps) {
|
|
|
872
1746
|
//#endregion
|
|
873
1747
|
//#region src/hook/session-start.ts
|
|
874
1748
|
const HOOK_USAGE = "Usage: harnessforce hook session-start\n";
|
|
875
|
-
const MAX_INPUT_BYTES
|
|
1749
|
+
const MAX_INPUT_BYTES = 1048576;
|
|
876
1750
|
const REGISTERING_SOURCES = /* @__PURE__ */ new Set(["startup", "clear"]);
|
|
877
|
-
const
|
|
1751
|
+
const KEYCHAIN_READ_TIMEOUT_MS = 1e3;
|
|
1752
|
+
const REVOKED_MESSAGE = "Your ingest key has been revoked. Run `harnessforce init`";
|
|
878
1753
|
function parseInput(raw) {
|
|
879
1754
|
let fields;
|
|
880
1755
|
try {
|
|
@@ -891,17 +1766,40 @@ function parseInput(raw) {
|
|
|
891
1766
|
source: typeof source === "string" ? source : void 0
|
|
892
1767
|
};
|
|
893
1768
|
}
|
|
894
|
-
const report$1 = (deps, detail) => deps.stderr(`harnessforce: session registration ${detail}\n`);
|
|
1769
|
+
const report$1 = (deps, detail) => deps.stderr(`harnessforce: session registration ${detail}\n`);
|
|
1770
|
+
const SKIPPED_WARNINGS = {
|
|
1771
|
+
"not configured": notConfiguredWarning("codex"),
|
|
1772
|
+
"invalid endpoint": INVALID_ENDPOINT_WARNING,
|
|
1773
|
+
"no ingest key": keyMissingWarning("codex"),
|
|
1774
|
+
"keychain unreadable": keyUnreadableWarning("codex"),
|
|
1775
|
+
"destination not verified": DESTINATION_MISMATCH_WARNING.codex
|
|
1776
|
+
};
|
|
1777
|
+
function resolveWithinLimit(deps) {
|
|
1778
|
+
let timer;
|
|
1779
|
+
const slow = new Promise((resolve) => {
|
|
1780
|
+
timer = setTimeout(() => resolve({
|
|
1781
|
+
kind: "skipped",
|
|
1782
|
+
reason: "keychain unreadable"
|
|
1783
|
+
}), KEYCHAIN_READ_TIMEOUT_MS);
|
|
1784
|
+
});
|
|
1785
|
+
return Promise.race([resolveUserDestination({
|
|
1786
|
+
...deps,
|
|
1787
|
+
keychain: deps.keychain.withTimeout(KEYCHAIN_READ_TIMEOUT_MS)
|
|
1788
|
+
}).catch(() => ({
|
|
1789
|
+
kind: "skipped",
|
|
1790
|
+
reason: "keychain unreadable"
|
|
1791
|
+
})), slow]).finally(() => clearTimeout(timer));
|
|
1792
|
+
}
|
|
895
1793
|
async function resolveDestination(deps) {
|
|
896
|
-
const resolved = await
|
|
1794
|
+
const resolved = await resolveWithinLimit(deps);
|
|
897
1795
|
if (resolved.kind === "ok") return { destination: resolved.destination };
|
|
898
1796
|
if (resolved.kind === "none") {
|
|
899
1797
|
report$1(deps, "skipped (not configured)");
|
|
900
1798
|
return { warning: notConfiguredWarning("codex") };
|
|
901
1799
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
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] };
|
|
905
1803
|
}
|
|
906
1804
|
function registrationSource(env) {
|
|
907
1805
|
const issue = env.HARNESSFORCE_ISSUE;
|
|
@@ -926,20 +1824,32 @@ async function register(input, deps) {
|
|
|
926
1824
|
if (outcome.kind === "failed") report$1(deps, `failed (${outcome.reason})`);
|
|
927
1825
|
return { outcome };
|
|
928
1826
|
}
|
|
1827
|
+
async function selectWarning(registered, deps) {
|
|
1828
|
+
if (registered.outcome?.kind === "unauthorized") return REVOKED_MESSAGE;
|
|
1829
|
+
const state = await readCliUpdateState(deps.homeDir, deps.client.version);
|
|
1830
|
+
if (state?.status === "unsupported") return updateLine("cli", state);
|
|
1831
|
+
const sending = registered.warning ?? (registered.outcome?.kind === "failed" ? "Harnessforce session registration failed. Check your network connection" : void 0);
|
|
1832
|
+
if (sending !== void 0 || !state) return sending;
|
|
1833
|
+
return noticeLine("cli", updateStatePath(deps.homeDir), state, {
|
|
1834
|
+
now: deps.now(),
|
|
1835
|
+
env: deps.env
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
929
1838
|
async function sessionStart(deps) {
|
|
930
|
-
const input = parseInput(await deps.readStdin(MAX_INPUT_BYTES
|
|
1839
|
+
const input = parseInput(await deps.readStdin(MAX_INPUT_BYTES).then((buffer) => buffer.toString("utf8")).catch(() => ""));
|
|
931
1840
|
if (!input) return 0;
|
|
932
|
-
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) => {
|
|
933
1844
|
report$1(deps, `failed (${error instanceof Error ? error.name : "unknown"})`);
|
|
934
1845
|
return {};
|
|
935
1846
|
}) : {};
|
|
936
|
-
const warning =
|
|
937
|
-
if (warning !== void 0) {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
}
|
|
942
|
-
deps.stdout(`${JSON.stringify(sessionContext(input.sessionId))}\n`);
|
|
1847
|
+
const warning = registers ? await selectWarning(registered, deps) : void 0;
|
|
1848
|
+
if (warning !== void 0) deps.stderr(`${warning}\n`);
|
|
1849
|
+
deps.stdout(`${JSON.stringify({
|
|
1850
|
+
...sessionContext(input.sessionId),
|
|
1851
|
+
...warning === void 0 ? {} : { systemMessage: warning }
|
|
1852
|
+
})}\n`);
|
|
943
1853
|
return 0;
|
|
944
1854
|
}
|
|
945
1855
|
//#endregion
|
|
@@ -998,7 +1908,7 @@ function parseStoredApiToken(secret, workspaceId) {
|
|
|
998
1908
|
async function verifyCliAccess(deps, messages) {
|
|
999
1909
|
const { keychain } = deps;
|
|
1000
1910
|
const stop = (message) => stopWith(messages[message]);
|
|
1001
|
-
if (!await keychain.isAvailable().catch(() => false))
|
|
1911
|
+
if (!await keychain.isAvailable().catch(() => false)) stopWith(keychainUnavailableMessage(keychain, messages.keychainUnavailable));
|
|
1002
1912
|
const read = (account) => keychain.get(account).catch(() => stop("keychainUnavailable"));
|
|
1003
1913
|
const destinations = await resolveCliDestinations(deps.env, userSettingsPath(deps.env, deps.homeDir), deps.defaultUrl);
|
|
1004
1914
|
const workspaceId = destinations.workspaceId ?? stop("initRequired");
|
|
@@ -1072,7 +1982,7 @@ const LOCK_WAIT_MS$1 = 3e4;
|
|
|
1072
1982
|
const STALE_LOCK_MS$1 = 6e4;
|
|
1073
1983
|
const RETRY_INTERVAL_MS$1 = 100;
|
|
1074
1984
|
const DIR_MODE$2 = 448;
|
|
1075
|
-
const FILE_MODE$
|
|
1985
|
+
const FILE_MODE$4 = 384;
|
|
1076
1986
|
const isAlreadyExists$1 = (error) => error?.code === "EEXIST";
|
|
1077
1987
|
async function removeIfStale$1(path, now) {
|
|
1078
1988
|
const created = await stat(path).catch(() => void 0);
|
|
@@ -1087,7 +1997,7 @@ async function acquireTokenLock(path, deps) {
|
|
|
1087
1997
|
const deadline = deps.now() + LOCK_WAIT_MS$1;
|
|
1088
1998
|
while (deps.now() < deadline) {
|
|
1089
1999
|
try {
|
|
1090
|
-
await (await open(path, "wx", FILE_MODE$
|
|
2000
|
+
await (await open(path, "wx", FILE_MODE$4)).close();
|
|
1091
2001
|
return () => rm(path, { force: true });
|
|
1092
2002
|
} catch (error) {
|
|
1093
2003
|
if (!isAlreadyExists$1(error)) throw error;
|
|
@@ -1099,6 +2009,7 @@ async function acquireTokenLock(path, deps) {
|
|
|
1099
2009
|
//#endregion
|
|
1100
2010
|
//#region src/credentials/api-token-session.ts
|
|
1101
2011
|
const CLIENT_ID$1 = "harnessforce-cli";
|
|
2012
|
+
const LOCKED_KEYCHAIN_TIMEOUT_MS = 1e4;
|
|
1102
2013
|
const isPast = (instant, now) => now >= Date.parse(instant);
|
|
1103
2014
|
async function requestRefresh(endpoint, refreshToken, workspaceId, fetchImpl) {
|
|
1104
2015
|
try {
|
|
@@ -1127,11 +2038,11 @@ async function requestRefresh(endpoint, refreshToken, workspaceId, fetchImpl) {
|
|
|
1127
2038
|
return { kind: "failed" };
|
|
1128
2039
|
}
|
|
1129
2040
|
}
|
|
1130
|
-
function tokenStore(deps) {
|
|
2041
|
+
function tokenStore(deps, keychain) {
|
|
1131
2042
|
const account = apiTokenAccount(deps.workspaceId);
|
|
1132
2043
|
return {
|
|
1133
|
-
read: async () => parseStoredApiToken(await
|
|
1134
|
-
save: (token) =>
|
|
2044
|
+
read: async () => parseStoredApiToken(await keychain.get(account).catch(() => void 0), deps.workspaceId),
|
|
2045
|
+
save: (token) => keychain.set(account, serializeApiToken(token)).then(() => true).catch(() => false)
|
|
1135
2046
|
};
|
|
1136
2047
|
}
|
|
1137
2048
|
async function exchange(stored, deps, store) {
|
|
@@ -1144,7 +2055,8 @@ async function exchange(stored, deps, store) {
|
|
|
1144
2055
|
const reread = await store.read();
|
|
1145
2056
|
return reread && reread.refreshToken !== stored.refreshToken ? reread : void 0;
|
|
1146
2057
|
}
|
|
1147
|
-
async function refreshUnderLock(used, deps
|
|
2058
|
+
async function refreshUnderLock(used, deps) {
|
|
2059
|
+
const store = tokenStore(deps, deps.keychain.withTimeout(LOCKED_KEYCHAIN_TIMEOUT_MS));
|
|
1148
2060
|
const release = await acquireTokenLock(tokenLockPath(deps.homeDir), {
|
|
1149
2061
|
now: deps.now,
|
|
1150
2062
|
sleep: deps.sleep
|
|
@@ -1159,8 +2071,8 @@ async function refreshUnderLock(used, deps, store) {
|
|
|
1159
2071
|
}
|
|
1160
2072
|
}
|
|
1161
2073
|
function createApiTokenSession(deps) {
|
|
1162
|
-
const store = tokenStore(deps);
|
|
1163
|
-
const refresh = (used) => refreshUnderLock(used, deps
|
|
2074
|
+
const store = tokenStore(deps, deps.keychain);
|
|
2075
|
+
const refresh = (used) => refreshUnderLock(used, deps);
|
|
1164
2076
|
let current;
|
|
1165
2077
|
async function withToken(request, isUnauthorized) {
|
|
1166
2078
|
let token = current ?? await store.read();
|
|
@@ -1206,37 +2118,6 @@ function createApiTokenSession(deps) {
|
|
|
1206
2118
|
};
|
|
1207
2119
|
}
|
|
1208
2120
|
//#endregion
|
|
1209
|
-
//#region src/shared/messages.ts
|
|
1210
|
-
const INIT_MESSAGES = {
|
|
1211
|
-
success: "Harnessforceに接続しました。Claude Codeを再起動すると設定が反映されます",
|
|
1212
|
-
invalidUrl: "接続先のURLが不正です",
|
|
1213
|
-
keychainUnavailable: "OSのキーチェーンを利用できないため、送信キーを保存できません",
|
|
1214
|
-
tooManyKeys: "keychainにある送信キーが多すぎるため、`harnessforce init`を実行できません",
|
|
1215
|
-
tooManyCredentials: "keychainにある送信キーまたはログインの情報が多すぎるため、`harnessforce init`を実行できません",
|
|
1216
|
-
settingsUnreadable: "Claude Codeのuser settingsを読めません",
|
|
1217
|
-
network: "Harnessforceとの通信に失敗しました。もう一度`harnessforce init`を実行してください",
|
|
1218
|
-
listenFailed: "ログインの待ち受けを開始できませんでした",
|
|
1219
|
-
timeout: "ログインが時間内に完了しませんでした。もう一度`harnessforce init`を実行してください",
|
|
1220
|
-
accessDenied: "ログインが拒否されました",
|
|
1221
|
-
noWorkspace: "参加しているWorkspaceがありません。Workspaceを作成するか招待を受諾してから、もう一度`harnessforce init`を実行してください",
|
|
1222
|
-
loginFailed: "ログインに失敗しました。もう一度`harnessforce init`を実行してください",
|
|
1223
|
-
viewer: "閲覧のみのロールでは送信キーを発行できません。Workspaceの管理者に連絡してください",
|
|
1224
|
-
limitOwnerAdmin: "有効な送信キーの数が上限に達しました。送信キーの画面で使っていないキーを失効してください",
|
|
1225
|
-
limitMember: "送信キーの上限に達しています。Workspaceの管理者に連絡してください",
|
|
1226
|
-
gateUnavailable: "受信側へ反映できませんでした。変更はしていません。もう一度お試しください",
|
|
1227
|
-
ingestUnavailable: "この環境はテレメトリを受信しないため、送信キーを発行できません",
|
|
1228
|
-
saveFailed: "送信キーを保存できませんでした。もう一度`harnessforce init`を実行してください",
|
|
1229
|
-
codexSaveFailed: "Codexの設定を書き込めませんでした。もう一度`harnessforce init`を実行してください",
|
|
1230
|
-
contentEnabled: "入力した指示の本文を送る設定を有効にしました。パターンに一致した秘密値と個人情報を伏せ字にしますが、すべてを検出できるとは限りません。保存した本文は、Workspaceのメンバーの中ではあなただけが読め、本文データの保持期間(最大30日)が過ぎると削除されます",
|
|
1231
|
-
contentNotOptedIn: "このWorkspaceは本文データをopt-inしていないため、本文を送る設定は有効にしませんでした。Workspaceの設定のデータの保持でopt-inしてから、もう一度`harnessforce init --send-content`を実行してください"
|
|
1232
|
-
};
|
|
1233
|
-
const codexNotes = (configPath) => [
|
|
1234
|
-
`Codexの設定を書き込みました(${configPath})。Codexを再起動すると反映されます`,
|
|
1235
|
-
"Codexのhookは、Codexの/hooksで内容をtrustするまで実行されません。/hooksでharnessforceのSessionStartのhookをtrustしてください",
|
|
1236
|
-
"HarnessforceのMCPを使う場合は、`codex mcp login harnessforce`でログインしてください"
|
|
1237
|
-
].join("\n");
|
|
1238
|
-
const listenFailedMessage = (port) => `${INIT_MESSAGES.listenFailed}。ポート ${port}を使っているプロセスを終えるか、\`harnessforce init --port\`で別のポートを指定してください`;
|
|
1239
|
-
//#endregion
|
|
1240
2121
|
//#region src/import/read-api.ts
|
|
1241
2122
|
const FAILED$1 = { kind: "failed" };
|
|
1242
2123
|
async function getJson(url, apiToken, fetchImpl) {
|
|
@@ -1333,7 +2214,7 @@ async function recordBase(deps) {
|
|
|
1333
2214
|
//#region src/import/state.ts
|
|
1334
2215
|
const STATE_VERSION = 1;
|
|
1335
2216
|
const DIR_MODE$1 = 448;
|
|
1336
|
-
const FILE_MODE$
|
|
2217
|
+
const FILE_MODE$3 = 384;
|
|
1337
2218
|
const sessionStateKey = (session) => session.agent === "claude_code" ? session.session_id : `${session.agent}:${session.session_id}`;
|
|
1338
2219
|
const importStatePath = (homeDir) => join(homeDir, ".harnessforce", "import-state.json");
|
|
1339
2220
|
const keyOf = ({ workspaceId, endpoint }) => `${workspaceId} ${endpoint}`;
|
|
@@ -1378,10 +2259,10 @@ async function recordSentSessions(path, destination, sessionIds) {
|
|
|
1378
2259
|
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
1379
2260
|
try {
|
|
1380
2261
|
await writeFile(temporary, `${JSON.stringify(next)}\n`, {
|
|
1381
|
-
mode: FILE_MODE$
|
|
2262
|
+
mode: FILE_MODE$3,
|
|
1382
2263
|
flag: "wx"
|
|
1383
2264
|
});
|
|
1384
|
-
await chmod(temporary, FILE_MODE$
|
|
2265
|
+
await chmod(temporary, FILE_MODE$3);
|
|
1385
2266
|
await rename(temporary, path);
|
|
1386
2267
|
} finally {
|
|
1387
2268
|
await rm(temporary, { force: true });
|
|
@@ -1719,7 +2600,7 @@ async function parseRollout(path) {
|
|
|
1719
2600
|
const accumulator = new Accumulator$1();
|
|
1720
2601
|
let skippedLines = 0;
|
|
1721
2602
|
try {
|
|
1722
|
-
const lines = createInterface
|
|
2603
|
+
const lines = createInterface({
|
|
1723
2604
|
input: createReadStream(path, { encoding: "utf8" }),
|
|
1724
2605
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
1725
2606
|
});
|
|
@@ -1948,7 +2829,7 @@ async function readTranscript(path, accumulator) {
|
|
|
1948
2829
|
let lines = 0;
|
|
1949
2830
|
let skippedLines = 0;
|
|
1950
2831
|
try {
|
|
1951
|
-
const reader = createInterface
|
|
2832
|
+
const reader = createInterface({
|
|
1952
2833
|
input: createReadStream(path, { encoding: "utf8" }),
|
|
1953
2834
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
1954
2835
|
});
|
|
@@ -2023,6 +2904,11 @@ async function parseTranscriptUsage(path) {
|
|
|
2023
2904
|
}
|
|
2024
2905
|
//#endregion
|
|
2025
2906
|
//#region src/import/sessions.ts
|
|
2907
|
+
const noneNotImported = () => ({
|
|
2908
|
+
outOfRange: 0,
|
|
2909
|
+
outsideGit: 0,
|
|
2910
|
+
notConnected: 0
|
|
2911
|
+
});
|
|
2026
2912
|
const readDir$1 = (dir) => readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2027
2913
|
async function listTranscripts(projectsDir) {
|
|
2028
2914
|
const projects = (await readDir$1(projectsDir)).filter((entry) => entry.isDirectory());
|
|
@@ -2049,33 +2935,49 @@ function toSessionImport(session, repository) {
|
|
|
2049
2935
|
};
|
|
2050
2936
|
}
|
|
2051
2937
|
const sortedSessions = (sessions) => [...sessions.values()].sort((a, b) => Date.parse(a.started_at) - Date.parse(b.started_at) || (a.session_id < b.session_id ? -1 : a.session_id > b.session_id ? 1 : 0));
|
|
2052
|
-
|
|
2938
|
+
const cachedRepositoryOf = (git) => {
|
|
2053
2939
|
const repositories = /* @__PURE__ */ new Map();
|
|
2054
|
-
|
|
2055
|
-
const cached = repositories.get(cwd) ?? resolveRepository(cwd,
|
|
2940
|
+
return (cwd) => {
|
|
2941
|
+
const cached = repositories.get(cwd) ?? resolveRepository(cwd, git);
|
|
2056
2942
|
repositories.set(cwd, cached);
|
|
2057
2943
|
return cached;
|
|
2058
2944
|
};
|
|
2945
|
+
};
|
|
2946
|
+
async function judge(session, options, repositoryOf) {
|
|
2947
|
+
if (Date.parse(session.endedAt) < options.sinceMs) return { kind: "outOfRange" };
|
|
2948
|
+
const repository = session.cwd ? await repositoryOf(session.cwd) : void 0;
|
|
2949
|
+
if (repository === void 0) return { kind: "outsideGit" };
|
|
2950
|
+
return options.connected.has(repository) ? {
|
|
2951
|
+
kind: "import",
|
|
2952
|
+
repository
|
|
2953
|
+
} : { kind: "notConnected" };
|
|
2954
|
+
}
|
|
2955
|
+
async function scanSessions(options) {
|
|
2956
|
+
const repositoryOf = cachedRepositoryOf(options.git);
|
|
2059
2957
|
const sessions = /* @__PURE__ */ new Map();
|
|
2958
|
+
const notImported = noneNotImported();
|
|
2060
2959
|
let skippedLines = 0;
|
|
2061
2960
|
let skippedFiles = 0;
|
|
2062
2961
|
for (const file of await listTranscripts(options.projectsDir)) {
|
|
2063
2962
|
const modifiedMs = (await stat(file).catch(() => void 0))?.mtimeMs;
|
|
2064
|
-
if (modifiedMs !== void 0 && modifiedMs < options.sinceMs)
|
|
2963
|
+
if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) {
|
|
2964
|
+
notImported.outOfRange += 1;
|
|
2965
|
+
continue;
|
|
2966
|
+
}
|
|
2065
2967
|
const result = await parseTranscript(file);
|
|
2066
2968
|
skippedLines += result.skippedLines;
|
|
2067
2969
|
if (result.kind === "unreadable") skippedFiles += 1;
|
|
2068
2970
|
if (result.kind !== "session") continue;
|
|
2069
2971
|
const { session } = result;
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
if (
|
|
2073
|
-
if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, toSessionImport(session, repository));
|
|
2972
|
+
const verdict = await judge(session, options, repositoryOf);
|
|
2973
|
+
if (verdict.kind !== "import") notImported[verdict.kind] += 1;
|
|
2974
|
+
else if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, toSessionImport(session, verdict.repository));
|
|
2074
2975
|
}
|
|
2075
2976
|
return {
|
|
2076
2977
|
sessions: sortedSessions(sessions),
|
|
2077
2978
|
skippedLines,
|
|
2078
|
-
skippedFiles
|
|
2979
|
+
skippedFiles,
|
|
2980
|
+
notImported
|
|
2079
2981
|
};
|
|
2080
2982
|
}
|
|
2081
2983
|
const numericName = (name, length) => name.length === length && /^\d+$/.test(name) ? Number(name) : void 0;
|
|
@@ -2125,43 +3027,39 @@ function toCodexSessionImport(session, repository) {
|
|
|
2125
3027
|
};
|
|
2126
3028
|
}
|
|
2127
3029
|
async function scanCodexSessions(options) {
|
|
2128
|
-
const
|
|
2129
|
-
const repositoryOf = (cwd) => {
|
|
2130
|
-
const cached = repositories.get(cwd) ?? resolveRepository(cwd, options.git);
|
|
2131
|
-
repositories.set(cwd, cached);
|
|
2132
|
-
return cached;
|
|
2133
|
-
};
|
|
2134
|
-
const isSent = async (cwd) => {
|
|
2135
|
-
if (!cwd) return void 0;
|
|
2136
|
-
const repository = await repositoryOf(cwd);
|
|
2137
|
-
return repository !== void 0 && options.connected.has(repository) ? repository : void 0;
|
|
2138
|
-
};
|
|
3030
|
+
const repositoryOf = cachedRepositoryOf(options.git);
|
|
2139
3031
|
const { files, compressed } = await listRollouts$1(join(options.codexDir, "sessions"), localDayKey(options.sinceMs));
|
|
2140
3032
|
const sessions = /* @__PURE__ */ new Map();
|
|
3033
|
+
const notImported = noneNotImported();
|
|
2141
3034
|
let skippedLines = 0;
|
|
2142
3035
|
let skippedFiles = 0;
|
|
2143
3036
|
let unextractable = 0;
|
|
2144
3037
|
for (const file of files) {
|
|
2145
3038
|
const modifiedMs = (await stat(file).catch(() => void 0))?.mtimeMs;
|
|
2146
|
-
if (modifiedMs !== void 0 && modifiedMs < options.sinceMs)
|
|
3039
|
+
if (modifiedMs !== void 0 && modifiedMs < options.sinceMs) {
|
|
3040
|
+
notImported.outOfRange += 1;
|
|
3041
|
+
continue;
|
|
3042
|
+
}
|
|
2147
3043
|
const result = await parseRollout(file);
|
|
2148
3044
|
skippedLines += result.skippedLines;
|
|
2149
3045
|
if (result.kind === "unreadable") skippedFiles += 1;
|
|
2150
3046
|
if (result.kind === "unextractable") {
|
|
2151
|
-
|
|
3047
|
+
const verdict = await judge(result, options, repositoryOf);
|
|
3048
|
+
if (verdict.kind === "import") unextractable += 1;
|
|
3049
|
+
else notImported[verdict.kind] += 1;
|
|
2152
3050
|
continue;
|
|
2153
3051
|
}
|
|
2154
3052
|
if (result.kind !== "session") continue;
|
|
2155
3053
|
const { session } = result;
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
if (
|
|
2159
|
-
if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, toCodexSessionImport(session, repository));
|
|
3054
|
+
const verdict = await judge(session, options, repositoryOf);
|
|
3055
|
+
if (verdict.kind !== "import") notImported[verdict.kind] += 1;
|
|
3056
|
+
else if (!sessions.has(session.sessionId)) sessions.set(session.sessionId, toCodexSessionImport(session, verdict.repository));
|
|
2160
3057
|
}
|
|
2161
3058
|
return {
|
|
2162
3059
|
sessions: sortedSessions(sessions),
|
|
2163
3060
|
skippedLines,
|
|
2164
3061
|
skippedFiles,
|
|
3062
|
+
notImported,
|
|
2165
3063
|
unextractable,
|
|
2166
3064
|
compressed
|
|
2167
3065
|
};
|
|
@@ -2169,49 +3067,78 @@ async function scanCodexSessions(options) {
|
|
|
2169
3067
|
//#endregion
|
|
2170
3068
|
//#region src/import/command.ts
|
|
2171
3069
|
const DAY_MS$1 = 864e5;
|
|
2172
|
-
const MESSAGES
|
|
2173
|
-
runInit: "`harnessforce init
|
|
2174
|
-
invalidUrl: "
|
|
3070
|
+
const MESSAGES = {
|
|
3071
|
+
runInit: "Run `harnessforce init`",
|
|
3072
|
+
invalidUrl: "The connection URL is invalid",
|
|
2175
3073
|
keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
|
|
2176
|
-
readFailed: "Harnessforce
|
|
2177
|
-
sendFailed: "Harnessforce
|
|
2178
|
-
revoked: "
|
|
2179
|
-
loginExpired: "
|
|
2180
|
-
stateFailed: "
|
|
3074
|
+
readFailed: "Communication with Harnessforce failed. Run `harnessforce import` again",
|
|
3075
|
+
sendFailed: "Communication with Harnessforce failed. Run `harnessforce import` again to resume importing",
|
|
3076
|
+
revoked: "Your ingest key has been revoked. Run `harnessforce init`",
|
|
3077
|
+
loginExpired: "Your login has expired. Run `harnessforce init`",
|
|
3078
|
+
stateFailed: "Could not save the import state",
|
|
3079
|
+
connectNone: "No GitHub repository is connected to the Workspace. Connect the GitHub App in Harnessforce to import sessions with `harnessforce import`",
|
|
3080
|
+
connectOther: "The session's repository is not connected to the Workspace. Connect that repository to the GitHub App in Harnessforce to import it with `harnessforce import`",
|
|
3081
|
+
nothingFound: "No session records to import were found"
|
|
2181
3082
|
};
|
|
2182
3083
|
const limitMessage = {
|
|
2183
|
-
monthly_event_limit: (count) =>
|
|
2184
|
-
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`
|
|
2185
3086
|
};
|
|
2186
|
-
const ACCESS_MESSAGES$
|
|
2187
|
-
keychainUnavailable: MESSAGES
|
|
2188
|
-
initRequired: MESSAGES
|
|
2189
|
-
invalidUrl: MESSAGES
|
|
2190
|
-
apiTokenMissing: MESSAGES
|
|
3087
|
+
const ACCESS_MESSAGES$2 = {
|
|
3088
|
+
keychainUnavailable: MESSAGES.keychainUnavailable,
|
|
3089
|
+
initRequired: MESSAGES.runInit,
|
|
3090
|
+
invalidUrl: MESSAGES.invalidUrl,
|
|
3091
|
+
apiTokenMissing: MESSAGES.runInit
|
|
2191
3092
|
};
|
|
2192
3093
|
function unwrap(outcome) {
|
|
2193
3094
|
if (outcome.kind === "ok") return outcome.value;
|
|
2194
|
-
return stopWith(outcome.kind === "unauthorized" ? MESSAGES
|
|
3095
|
+
return stopWith(outcome.kind === "unauthorized" ? MESSAGES.loginExpired : MESSAGES.readFailed);
|
|
2195
3096
|
}
|
|
2196
|
-
|
|
3097
|
+
const NOT_IMPORTED_LABELS = [
|
|
3098
|
+
["alreadySent", "already sent"],
|
|
3099
|
+
["outOfRange", "outside the period"],
|
|
3100
|
+
["outsideGit", "outside a git repository"],
|
|
3101
|
+
["notConnected", "in a repository not connected to the Workspace"]
|
|
3102
|
+
];
|
|
3103
|
+
function breakdownLine(breakdown) {
|
|
3104
|
+
const parts = NOT_IMPORTED_LABELS.filter(([key]) => breakdown[key] > 0).map(([key, label]) => `${label} ${breakdown[key]}`);
|
|
3105
|
+
return parts.length > 0 ? `Sessions not imported: ${parts.join(", ")}` : void 0;
|
|
3106
|
+
}
|
|
3107
|
+
function reportNotImported(imported, notes, deps) {
|
|
3108
|
+
const line = breakdownLine(notes.breakdown);
|
|
3109
|
+
if (line) deps.stdout(`${line}\n`);
|
|
3110
|
+
if (imported > 0) return;
|
|
3111
|
+
if (notes.breakdown.notConnected > 0) deps.stdout(`${notes.connectedCount === 0 ? MESSAGES.connectNone : MESSAGES.connectOther}\n`);
|
|
3112
|
+
else if (!line && !notes.otherNoticeShown) deps.stdout(`${MESSAGES.nothingFound}\n`);
|
|
3113
|
+
}
|
|
3114
|
+
function report(result, notes, deps) {
|
|
2197
3115
|
switch (result.kind) {
|
|
2198
3116
|
case "done":
|
|
2199
|
-
deps.stdout(
|
|
2200
|
-
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);
|
|
2201
3123
|
return 0;
|
|
2202
3124
|
case "limited":
|
|
2203
3125
|
deps.stderr(`${limitMessage[result.reason](result.notImported)}\n`);
|
|
2204
3126
|
return 1;
|
|
2205
3127
|
case "unauthorized":
|
|
2206
|
-
deps.stderr(`${MESSAGES
|
|
3128
|
+
deps.stderr(`${MESSAGES.revoked}\n`);
|
|
2207
3129
|
return 1;
|
|
2208
3130
|
case "failed":
|
|
2209
|
-
deps.stderr(`${MESSAGES
|
|
3131
|
+
deps.stderr(`${MESSAGES.sendFailed}\n`);
|
|
2210
3132
|
return 1;
|
|
2211
3133
|
}
|
|
2212
3134
|
}
|
|
3135
|
+
const sumNotImported = (a, b) => ({
|
|
3136
|
+
outOfRange: a.outOfRange + b.outOfRange,
|
|
3137
|
+
outsideGit: a.outsideGit + b.outsideGit,
|
|
3138
|
+
notConnected: a.notConnected + b.notConnected
|
|
3139
|
+
});
|
|
2213
3140
|
async function runImport(deps) {
|
|
2214
|
-
const access = await verifyCliAccess(deps, ACCESS_MESSAGES$
|
|
3141
|
+
const access = await verifyCliAccess(deps, ACCESS_MESSAGES$2);
|
|
2215
3142
|
const destination = {
|
|
2216
3143
|
workspaceId: access.workspaceId,
|
|
2217
3144
|
endpoint: withoutExtras(access.ingest)
|
|
@@ -2242,20 +3169,28 @@ async function runImport(deps) {
|
|
|
2242
3169
|
});
|
|
2243
3170
|
const skippedLines = claude.skippedLines + codex.skippedLines;
|
|
2244
3171
|
const skippedFiles = claude.skippedFiles + codex.skippedFiles;
|
|
2245
|
-
if (skippedLines > 0 || skippedFiles > 0) deps.stdout(
|
|
2246
|
-
if (codex.unextractable > 0) deps.stdout(`
|
|
2247
|
-
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`);
|
|
2248
3175
|
const scanned = [...claude.sessions, ...codex.sessions];
|
|
2249
3176
|
const statePath = importStatePath(deps.homeDir);
|
|
2250
3177
|
const sent = await readSentSessions(statePath, destination);
|
|
3178
|
+
const unsent = scanned.filter((s) => !sent.has(sessionStateKey(s)));
|
|
2251
3179
|
return report(await sendSessions({
|
|
2252
3180
|
endpoint: access.ingest,
|
|
2253
3181
|
ingestKey: access.ingestKey,
|
|
2254
|
-
sessions:
|
|
3182
|
+
sessions: unsent,
|
|
2255
3183
|
fetch: deps.fetch,
|
|
2256
3184
|
sleep: deps.sleep,
|
|
2257
|
-
record: (ids) => recordSentSessions(statePath, destination, ids).catch(() => stopWith(MESSAGES
|
|
2258
|
-
}),
|
|
3185
|
+
record: (ids) => recordSentSessions(statePath, destination, ids).catch(() => stopWith(MESSAGES.stateFailed))
|
|
3186
|
+
}), {
|
|
3187
|
+
breakdown: {
|
|
3188
|
+
alreadySent: scanned.length - unsent.length,
|
|
3189
|
+
...sumNotImported(claude.notImported, codex.notImported)
|
|
3190
|
+
},
|
|
3191
|
+
otherNoticeShown: skippedLines > 0 || skippedFiles > 0 || codex.unextractable > 0 || codex.compressed > 0,
|
|
3192
|
+
connectedCount: connected.size
|
|
3193
|
+
}, deps);
|
|
2259
3194
|
}
|
|
2260
3195
|
function importCommand(deps) {
|
|
2261
3196
|
return runUntilStop(() => runImport(deps), deps.stderr);
|
|
@@ -8735,7 +9670,7 @@ function Ko(e, t) {
|
|
|
8735
9670
|
const HOOK_COMMAND = "harnessforce hook session-start";
|
|
8736
9671
|
const HOOK_COMMAND_WINDOWS = "harnessforce.cmd hook session-start";
|
|
8737
9672
|
const HOOK_TIMEOUT_SECONDS = 10;
|
|
8738
|
-
const FILE_MODE$
|
|
9673
|
+
const FILE_MODE$2 = 384;
|
|
8739
9674
|
function codexConfigPath(env, homeDir) {
|
|
8740
9675
|
return join(absoluteEnv(env.CODEX_HOME) ?? join(homeDir, ".codex"), "config.toml");
|
|
8741
9676
|
}
|
|
@@ -8810,10 +9745,10 @@ async function writeCodexConfig(path, text) {
|
|
|
8810
9745
|
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
8811
9746
|
try {
|
|
8812
9747
|
await writeFile(temporary, text, {
|
|
8813
|
-
mode: FILE_MODE$
|
|
9748
|
+
mode: FILE_MODE$2,
|
|
8814
9749
|
flag: "wx"
|
|
8815
9750
|
});
|
|
8816
|
-
await chmod(temporary, FILE_MODE$
|
|
9751
|
+
await chmod(temporary, FILE_MODE$2);
|
|
8817
9752
|
await rename(temporary, path);
|
|
8818
9753
|
} finally {
|
|
8819
9754
|
await rm(temporary, { force: true });
|
|
@@ -8898,8 +9833,15 @@ const stop$1 = (message) => stopWith(INIT_MESSAGES[message]);
|
|
|
8898
9833
|
const sha256Hex = (value) => createHash("sha256").update(value, "utf8").digest("hex");
|
|
8899
9834
|
function init(args, deps) {
|
|
8900
9835
|
return runUntilStop(async () => {
|
|
8901
|
-
const codexPath = await runInit(args, deps);
|
|
8902
|
-
deps.stdout(`${
|
|
9836
|
+
const { codexPath, connection, isKernelKeyring, isPluginAutoUpdateOn } = await runInit(args, deps);
|
|
9837
|
+
if (connection !== withoutExtras(new URL(deps.defaultUrl))) deps.stdout(`${nonDefaultConnectionMessage(connection)}\n`);
|
|
9838
|
+
const lines = [
|
|
9839
|
+
INIT_MESSAGES.success,
|
|
9840
|
+
...isKernelKeyring ? [INIT_MESSAGES.savedToKernelKeyring] : [],
|
|
9841
|
+
...isPluginAutoUpdateOn ? [INIT_MESSAGES.pluginAutoUpdateOn] : [],
|
|
9842
|
+
codexNotes(codexPath)
|
|
9843
|
+
];
|
|
9844
|
+
deps.stdout(`${lines.join("\n")}\n`);
|
|
8903
9845
|
return 0;
|
|
8904
9846
|
}, deps.stderr);
|
|
8905
9847
|
}
|
|
@@ -8907,9 +9849,11 @@ async function runInit(args, deps) {
|
|
|
8907
9849
|
const port = args.port ?? DEFAULT_PORT;
|
|
8908
9850
|
const base = parseAllowedUrl(args.url ?? deps.defaultUrl) ?? stop$1("invalidUrl");
|
|
8909
9851
|
const recordedUrl = withoutExtras(base);
|
|
8910
|
-
const revoke = await readRevokeHashes(deps.keychain);
|
|
8911
9852
|
const settingsPath = userSettingsPath(deps.env, deps.homeDir);
|
|
8912
|
-
|
|
9853
|
+
const store = await openKeychain(deps, (await readSettingsEnv$1(settingsPath)).HARNESSFORCE_WORKSPACE_ID);
|
|
9854
|
+
const revoke = revokeHashes(store);
|
|
9855
|
+
const settings = await readUserSettings(settingsPath);
|
|
9856
|
+
if (settings.kind === "invalid" || !hasWritableMarketplaces(settings.settings)) stop$1("settingsUnreadable");
|
|
8913
9857
|
const authorizationEndpoint = await discoverAuthorizationEndpoint(base, deps.fetch) ?? stop$1("network");
|
|
8914
9858
|
const secrets = createLoginSecrets();
|
|
8915
9859
|
const { code, redirectUri } = await logIn(authorizationEndpoint, secrets, port, deps);
|
|
@@ -8921,18 +9865,27 @@ async function runInit(args, deps) {
|
|
|
8921
9865
|
revoke_key_hashes: revoke.keyHashes,
|
|
8922
9866
|
revoke_api_token_hashes: revoke.apiTokenHashes
|
|
8923
9867
|
}, deps.fetch));
|
|
8924
|
-
await save(issued, recordedUrl, settingsPath, args
|
|
9868
|
+
const isPluginAutoUpdateOn = await save(issued, recordedUrl, settingsPath, args, store).catch((error) => {
|
|
8925
9869
|
if (isStop(error)) throw error;
|
|
8926
9870
|
return stop$1("saveFailed");
|
|
8927
9871
|
});
|
|
8928
9872
|
const codexPath = codexConfigPath(deps.env, deps.homeDir);
|
|
8929
9873
|
await saveCodexConfig(issued, recordedUrl, codexPath, args.sendContent).catch(() => stop$1("codexSaveFailed"));
|
|
8930
9874
|
if (args.sendContent) deps.stdout(`${issued.contentOptIn ? INIT_MESSAGES.contentEnabled : INIT_MESSAGES.contentNotOptedIn}\n`);
|
|
8931
|
-
return
|
|
9875
|
+
return {
|
|
9876
|
+
codexPath,
|
|
9877
|
+
connection: recordedUrl,
|
|
9878
|
+
isKernelKeyring: store.isKernelKeyring,
|
|
9879
|
+
isPluginAutoUpdateOn
|
|
9880
|
+
};
|
|
9881
|
+
}
|
|
9882
|
+
async function openKeychain(deps, previousWorkspaceId) {
|
|
9883
|
+
return await deps.keychain.openForInit({
|
|
9884
|
+
previousWorkspaceId,
|
|
9885
|
+
onWaiting: () => deps.stderr(`${INIT_MESSAGES.waitingForSecretService}\n`)
|
|
9886
|
+
}).catch(() => stop$1("keychainUnavailable")) ?? stopWith(keychainUnavailableMessage(deps.keychain));
|
|
8932
9887
|
}
|
|
8933
|
-
|
|
8934
|
-
if (!await keychain.isAvailable()) stop$1("keychainUnavailable");
|
|
8935
|
-
const items = await keychain.list().catch(() => void 0) ?? stop$1("keychainUnavailable");
|
|
9888
|
+
function revokeHashes({ items }) {
|
|
8936
9889
|
const keyHashes = items.filter((item) => isIngestKeyAccount(item.account)).map((item) => sha256Hex(item.secret));
|
|
8937
9890
|
if (keyHashes.length > MAX_REVOKE_HASHES) stop$1("tooManyKeys");
|
|
8938
9891
|
const apiTokenHashes = items.flatMap((item) => {
|
|
@@ -8985,15 +9938,16 @@ function issuedOrStop(outcome) {
|
|
|
8985
9938
|
case "unexpected": return stop$1("network");
|
|
8986
9939
|
}
|
|
8987
9940
|
}
|
|
8988
|
-
async function save(issued, connection, settingsPath,
|
|
8989
|
-
await
|
|
8990
|
-
await
|
|
8991
|
-
await
|
|
8992
|
-
await
|
|
8993
|
-
await
|
|
8994
|
-
await
|
|
9941
|
+
async function save(issued, connection, settingsPath, args, store) {
|
|
9942
|
+
await store.delete(ingestOriginAccount(issued.workspaceId));
|
|
9943
|
+
await store.delete(urlOriginAccount(issued.workspaceId));
|
|
9944
|
+
await store.set(ingestKeyAccount(issued.workspaceId), issued.ingestKey);
|
|
9945
|
+
await store.set(apiTokenAccount(issued.workspaceId), serializeApiToken(issued.apiToken));
|
|
9946
|
+
await store.set(ingestOriginAccount(issued.workspaceId), new URL(issued.ingestEndpoint).origin);
|
|
9947
|
+
await store.set(urlOriginAccount(issued.workspaceId), new URL(connection).origin);
|
|
9948
|
+
await store.deleteElsewhere(issued.workspaceId);
|
|
8995
9949
|
const current = await readUserSettings(settingsPath);
|
|
8996
|
-
if (current.kind === "invalid") return stop$1("saveFailed");
|
|
9950
|
+
if (current.kind === "invalid" || !hasWritableMarketplaces(current.settings)) return stop$1("saveFailed");
|
|
8997
9951
|
const env = {
|
|
8998
9952
|
HARNESSFORCE_URL: connection,
|
|
8999
9953
|
HARNESSFORCE_ENDPOINT: issued.ingestEndpoint,
|
|
@@ -9007,8 +9961,10 @@ async function save(issued, connection, settingsPath, sendContent, deps) {
|
|
|
9007
9961
|
OTEL_TRACES_EXPORTER: "otlp",
|
|
9008
9962
|
OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf"
|
|
9009
9963
|
};
|
|
9010
|
-
if (sendContent && issued.contentOptIn) env.OTEL_LOG_USER_PROMPTS = "1";
|
|
9011
|
-
|
|
9964
|
+
if (args.sendContent && issued.contentOptIn) env.OTEL_LOG_USER_PROMPTS = "1";
|
|
9965
|
+
const { settings, isOn } = withPluginAutoUpdate(mergeUserSettings(current.settings, env), args.noPluginAutoUpdate);
|
|
9966
|
+
await writeUserSettings(settingsPath, settings);
|
|
9967
|
+
return isOn;
|
|
9012
9968
|
}
|
|
9013
9969
|
async function saveCodexConfig(issued, connection, path, sendContent) {
|
|
9014
9970
|
await writeCodexConfig(path, mergeCodexConfig(await readCodexConfig(path), {
|
|
@@ -9028,27 +9984,46 @@ const DESTINATION_VARIABLES = [
|
|
|
9028
9984
|
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"
|
|
9029
9985
|
];
|
|
9030
9986
|
const WITHHELD = "harnessforce: user key withheld (destination not verified)\n";
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9987
|
+
const EXIT_KEY_MISSING = 3;
|
|
9988
|
+
const EXIT_WITHHELD = 4;
|
|
9989
|
+
const EXIT_FAILED = 1;
|
|
9990
|
+
async function otelHeaders(env, managedDir, keychain, stdout, stderr, endStdout) {
|
|
9991
|
+
const selection = await selectKey(env, managedDir, keychain).catch(() => ({ kind: "failed" }));
|
|
9992
|
+
if (selection.kind === "withheld") {
|
|
9993
|
+
stderr(WITHHELD);
|
|
9994
|
+
return EXIT_WITHHELD;
|
|
9995
|
+
}
|
|
9996
|
+
if (selection.kind === "missing") return EXIT_KEY_MISSING;
|
|
9997
|
+
if (selection.kind === "failed") return EXIT_FAILED;
|
|
9035
9998
|
stdout(`${JSON.stringify({ Authorization: `Bearer ${selection.key}` })}\n`);
|
|
9999
|
+
const { user } = selection;
|
|
10000
|
+
if (user && !env.HARNESSFORCE_NO_MIGRATE) {
|
|
10001
|
+
await endStdout();
|
|
10002
|
+
await user.keychain.migrateFromKernel?.(user.workspaceId).catch(() => {});
|
|
10003
|
+
}
|
|
9036
10004
|
return 0;
|
|
9037
10005
|
}
|
|
9038
|
-
|
|
10006
|
+
const HOOK_READ_TIMEOUT_MS = 1e3;
|
|
10007
|
+
async function selectKey(env, managedDir, unlimited) {
|
|
9039
10008
|
const managed = await readManagedEnv(managedDir, ["HARNESSFORCE_INGEST_KEY"]);
|
|
9040
10009
|
if (managed.HARNESSFORCE_INGEST_KEY) return {
|
|
9041
10010
|
kind: "key",
|
|
9042
10011
|
key: managed.HARNESSFORCE_INGEST_KEY
|
|
9043
10012
|
};
|
|
9044
10013
|
const workspaceId = env.HARNESSFORCE_WORKSPACE_ID;
|
|
9045
|
-
if (!workspaceId
|
|
10014
|
+
if (!workspaceId) return { kind: "missing" };
|
|
10015
|
+
const keychain = unlimited.withTimeout(HOOK_READ_TIMEOUT_MS);
|
|
10016
|
+
if (!await keychain.isAvailable()) return { kind: "failed" };
|
|
9046
10017
|
const key = await keychain.get(ingestKeyAccount(workspaceId));
|
|
9047
|
-
if (!key) return { kind: "
|
|
10018
|
+
if (!key) return { kind: "missing" };
|
|
9048
10019
|
const pinnedOrigin = await keychain.get(ingestOriginAccount(workspaceId));
|
|
9049
10020
|
return pinnedOrigin && isPinnedDestination(env, pinnedOrigin) ? {
|
|
9050
10021
|
kind: "key",
|
|
9051
|
-
key
|
|
10022
|
+
key,
|
|
10023
|
+
user: {
|
|
10024
|
+
keychain,
|
|
10025
|
+
workspaceId
|
|
10026
|
+
}
|
|
9052
10027
|
} : { kind: "withheld" };
|
|
9053
10028
|
}
|
|
9054
10029
|
function isPinnedDestination(env, pinnedOrigin) {
|
|
@@ -9379,16 +10354,16 @@ function buildLaunch(request) {
|
|
|
9379
10354
|
//#region src/run/messages.ts
|
|
9380
10355
|
const RUN_MESSAGES = {
|
|
9381
10356
|
keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
|
|
9382
|
-
initRequired: "`harnessforce init
|
|
10357
|
+
initRequired: "Run `harnessforce init`",
|
|
9383
10358
|
invalidUrl: INIT_MESSAGES.invalidUrl,
|
|
9384
|
-
invalidIssue: "Issue
|
|
9385
|
-
issueInitRequired: "Issue
|
|
9386
|
-
loginExpired: "
|
|
9387
|
-
issueFailed: "Issue
|
|
9388
|
-
noMatch: "
|
|
9389
|
-
candidatesHeader: "Issue
|
|
10359
|
+
invalidIssue: "The Issue identifier is invalid",
|
|
10360
|
+
issueInitRequired: "Could not resolve the Issue. Run `harnessforce init`",
|
|
10361
|
+
loginExpired: "Your login has expired. Run `harnessforce init`",
|
|
10362
|
+
issueFailed: "Could not resolve the Issue or fetch candidates",
|
|
10363
|
+
noMatch: "No matching Issue",
|
|
10364
|
+
candidatesHeader: "Could not resolve the Issue. Candidates:"
|
|
9390
10365
|
};
|
|
9391
|
-
const launchFailedMessage = (agent) =>
|
|
10366
|
+
const launchFailedMessage = (agent) => `Could not start ${agent}`;
|
|
9392
10367
|
//#endregion
|
|
9393
10368
|
//#region src/run/run.ts
|
|
9394
10369
|
const stop = (message) => stopWith(RUN_MESSAGES[message]);
|
|
@@ -9397,14 +10372,14 @@ function candidateList(candidates) {
|
|
|
9397
10372
|
const lines = candidates.map(({ identifier, title }) => `${printable(identifier)} ${printable(title)}`);
|
|
9398
10373
|
return [RUN_MESSAGES.candidatesHeader, ...lines].join("\n");
|
|
9399
10374
|
}
|
|
9400
|
-
const ACCESS_MESSAGES$
|
|
10375
|
+
const ACCESS_MESSAGES$1 = {
|
|
9401
10376
|
keychainUnavailable: RUN_MESSAGES.keychainUnavailable,
|
|
9402
10377
|
initRequired: RUN_MESSAGES.initRequired,
|
|
9403
10378
|
invalidUrl: RUN_MESSAGES.invalidUrl,
|
|
9404
10379
|
apiTokenMissing: RUN_MESSAGES.issueInitRequired
|
|
9405
10380
|
};
|
|
9406
10381
|
async function prepareLaunch(args, deps) {
|
|
9407
|
-
const { workspaceId, ingestEndpoint, readApiBase, accessToken } = await verifyCliAccess(deps, ACCESS_MESSAGES$
|
|
10382
|
+
const { workspaceId, ingestEndpoint, readApiBase, accessToken } = await verifyCliAccess(deps, ACCESS_MESSAGES$1);
|
|
9408
10383
|
if (!isIssueIdentifier(args.issue)) stop("invalidIssue");
|
|
9409
10384
|
const readFetch = createApiTokenSession({
|
|
9410
10385
|
keychain: deps.keychain,
|
|
@@ -9455,6 +10430,20 @@ function runIssue(args, deps) {
|
|
|
9455
10430
|
return 1;
|
|
9456
10431
|
}, deps.stderr);
|
|
9457
10432
|
}
|
|
10433
|
+
async function runWithoutHarnessforce(args, deps) {
|
|
10434
|
+
const env = Object.fromEntries(Object.entries({
|
|
10435
|
+
...deps.env,
|
|
10436
|
+
...deps.restoredEnv
|
|
10437
|
+
}).filter((entry) => entry[1] !== void 0));
|
|
10438
|
+
const outcome = await deps.launch({
|
|
10439
|
+
command: args.agent,
|
|
10440
|
+
args: [...args.agentArgs],
|
|
10441
|
+
env
|
|
10442
|
+
});
|
|
10443
|
+
if (outcome.kind === "exited") return outcome.code;
|
|
10444
|
+
deps.stderr(`${launchFailedMessage(args.agent)}\n`);
|
|
10445
|
+
return 1;
|
|
10446
|
+
}
|
|
9458
10447
|
//#endregion
|
|
9459
10448
|
//#region src/top/args.ts
|
|
9460
10449
|
const TOP_USAGE = "Usage: harnessforce top [--once] [--json] [--ascii] [--theme <auto|light|dark|ansi>]\n";
|
|
@@ -9690,7 +10679,7 @@ function renderList(sessions, options) {
|
|
|
9690
10679
|
const active = sessions.filter((s) => sessionState(s.lastEventAtMs, options.nowMs) === "active").length;
|
|
9691
10680
|
return [clip(`${sessions.length} sessions, ${active} active`, options)];
|
|
9692
10681
|
}
|
|
9693
|
-
const body = sessions.length === 0 ? ["
|
|
10682
|
+
const body = sessions.length === 0 ? ["No sessions recorded in the last 24 hours"].map((t) => clip(t, options)) : options.width >= COMPACT_WIDTH ? table(sessions, options, options.width >= FULL_WIDTH) : shortRows(sessions, options);
|
|
9694
10683
|
return [
|
|
9695
10684
|
header(sessions, options),
|
|
9696
10685
|
...body,
|
|
@@ -9751,7 +10740,7 @@ function renderDetail(row, options) {
|
|
|
9751
10740
|
clip(options.painter.dim(DETAIL_HINT), options)
|
|
9752
10741
|
];
|
|
9753
10742
|
}
|
|
9754
|
-
const skippedNotice = (snapshot) => snapshot.skippedLines > 0 || snapshot.skippedFiles > 0 ? [
|
|
10743
|
+
const skippedNotice = (snapshot) => snapshot.skippedLines > 0 || snapshot.skippedFiles > 0 ? [`Skipped ${snapshot.skippedLines} unreadable line(s) and ${snapshot.skippedFiles} unreadable file(s)`] : [];
|
|
9755
10744
|
//#endregion
|
|
9756
10745
|
//#region src/top/codex-session.ts
|
|
9757
10746
|
const DETACHED = "HEAD";
|
|
@@ -10366,7 +11355,7 @@ const DEFAULT_SETTINGS = {
|
|
|
10366
11355
|
};
|
|
10367
11356
|
const unreadable = {
|
|
10368
11357
|
settings: DEFAULT_SETTINGS,
|
|
10369
|
-
notice: "
|
|
11358
|
+
notice: "Cannot read the device settings (`~/.harnessforce/config.json`), so the default display is used"
|
|
10370
11359
|
};
|
|
10371
11360
|
async function readTopSettings(homeDir) {
|
|
10372
11361
|
let text;
|
|
@@ -10856,7 +11845,9 @@ const median = (values) => {
|
|
|
10856
11845
|
const middle = Math.floor(sorted.length / 2);
|
|
10857
11846
|
return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
10858
11847
|
};
|
|
10859
|
-
const
|
|
11848
|
+
const escapeForTerminal = (text) => text.replace(/[^\x20-\x7e]/gu, (c) => `\\u{${c.codePointAt(0).toString(16)}}`);
|
|
11849
|
+
const asciiJson = (value, space) => JSON.stringify(value, null, space).replace(/[^\n\x20-\x7e]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
11850
|
+
const secondsText = (value) => value === null ? "—" : `${value.toFixed(1)}s`;
|
|
10860
11851
|
function summarize(reports, group, kind, countField, medianField) {
|
|
10861
11852
|
const values = reports.map((r) => r[group][kind]).filter((v) => v?.measurement === "measured");
|
|
10862
11853
|
if (values.length === 0) return void 0;
|
|
@@ -10869,16 +11860,16 @@ function summarize(reports, group, kind, countField, medianField) {
|
|
|
10869
11860
|
};
|
|
10870
11861
|
}
|
|
10871
11862
|
function renderAnalysis(reports, usages, header) {
|
|
10872
|
-
const lines = [
|
|
11863
|
+
const lines = [`Sessions analyzed: ${header.sessionCount} (analyzer ${header.analyzerVersion}, parser ${header.parserVersion})`, "Human interventions (count, median of per-session median wait time):"];
|
|
10873
11864
|
for (const kind of INTERVENTION_KINDS) {
|
|
10874
11865
|
const s = summarize(reports, "interventions", kind, "count", "wait_seconds_median");
|
|
10875
|
-
lines.push(s === void 0 ? ` ${kind}:
|
|
11866
|
+
lines.push(s === void 0 ? ` ${kind}: not measured` : ` ${kind}: ${s.total} time(s), ${secondsText(s.median)}`);
|
|
10876
11867
|
}
|
|
10877
|
-
lines.push("
|
|
11868
|
+
lines.push("Repeatable loops (count, interventions during them, median of median duration):");
|
|
10878
11869
|
for (const kind of LOOP_KINDS) {
|
|
10879
11870
|
const s = summarize(reports, "loops", kind, "occurrences", "duration_seconds_median");
|
|
10880
11871
|
const interventions = s?.values.reduce((sum, v) => sum + (v.interventions ?? 0), 0) ?? 0;
|
|
10881
|
-
lines.push(s === void 0 ? ` ${kind}:
|
|
11872
|
+
lines.push(s === void 0 ? ` ${kind}: not measured` : ` ${kind}: ${s.total} time(s), ${interventions} intervention(s), ${secondsText(s.median)}`);
|
|
10882
11873
|
}
|
|
10883
11874
|
const servers = /* @__PURE__ */ new Map();
|
|
10884
11875
|
for (const report of reports) for (const s of report.mcp_servers) {
|
|
@@ -10893,19 +11884,19 @@ function renderAnalysis(reports, usages, header) {
|
|
|
10893
11884
|
configured: current.configured + (s.configured ? 1 : 0)
|
|
10894
11885
|
});
|
|
10895
11886
|
}
|
|
10896
|
-
lines.push("MCP
|
|
10897
|
-
if (servers.size === 0) lines.push("
|
|
10898
|
-
for (const [server, s] of [...servers].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) lines.push(` ${server}: ${s.calls === null ? "
|
|
11887
|
+
lines.push("MCP servers (calls, failures, sessions where configured):");
|
|
11888
|
+
if (servers.size === 0) lines.push(" none");
|
|
11889
|
+
for (const [server, s] of [...servers].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) lines.push(` ${escapeForTerminal(server)}: ${s.calls === null ? "not measured" : `${s.calls} call(s)`}, ${s.failures === null ? "not measured" : `${s.failures} failure(s)`}, ${s.configured} session(s)`);
|
|
10899
11890
|
lines.push(...renderUsage(usages));
|
|
10900
11891
|
return `${lines.join("\n")}\n`;
|
|
10901
11892
|
}
|
|
10902
11893
|
const percent = (share) => `${(share * 100).toFixed(1)}%`;
|
|
10903
11894
|
function renderUsage(usages) {
|
|
10904
|
-
const lines = ["
|
|
11895
|
+
const lines = ["Usage (this device only; not sent):"];
|
|
10905
11896
|
for (const kind of ["frequent_compaction", "low_cache_reuse"]) lines.push(` ${kind}: ${usages.filter((u) => u.kinds.includes(kind)).length} session`);
|
|
10906
11897
|
const shares = modelShares(usages);
|
|
10907
|
-
const parts = shares && [...shares.main.map((m) =>
|
|
10908
|
-
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"}`);
|
|
10909
11900
|
return lines;
|
|
10910
11901
|
}
|
|
10911
11902
|
function localIso(ms) {
|
|
@@ -11026,7 +12017,7 @@ const LOCK_WAIT_MS = 3e4;
|
|
|
11026
12017
|
const STALE_LOCK_MS = 6e4;
|
|
11027
12018
|
const HEARTBEAT_MS = 1e4;
|
|
11028
12019
|
const RETRY_INTERVAL_MS = 100;
|
|
11029
|
-
const FILE_MODE$
|
|
12020
|
+
const FILE_MODE$1 = 384;
|
|
11030
12021
|
const isAlreadyExists = (error) => error?.code === "EEXIST";
|
|
11031
12022
|
async function removeIfStale(path, now) {
|
|
11032
12023
|
const current = await stat(path).catch(() => void 0);
|
|
@@ -11036,7 +12027,7 @@ async function acquireTuneLock(path, deps) {
|
|
|
11036
12027
|
const deadline = deps.now() + LOCK_WAIT_MS;
|
|
11037
12028
|
while (deps.now() < deadline) {
|
|
11038
12029
|
try {
|
|
11039
|
-
const handle = await open(path, "wx", FILE_MODE$
|
|
12030
|
+
const handle = await open(path, "wx", FILE_MODE$1);
|
|
11040
12031
|
const token = randomUUID();
|
|
11041
12032
|
await handle.writeFile(token);
|
|
11042
12033
|
await handle.close();
|
|
@@ -11081,33 +12072,33 @@ const NOTICE_ORDER = [
|
|
|
11081
12072
|
];
|
|
11082
12073
|
const TUNE_MESSAGES = {
|
|
11083
12074
|
keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
|
|
11084
|
-
initRequired: "`harnessforce init
|
|
11085
|
-
invalidUrl: "
|
|
11086
|
-
locked: "
|
|
11087
|
-
insufficient: (count) =>
|
|
11088
|
-
outOfRange: (days, count) =>
|
|
11089
|
-
notConnected: (count) =>
|
|
11090
|
-
partlyUnreadable: (lines, skipped) =>
|
|
11091
|
-
savedList: (fetchedAt) =>
|
|
11092
|
-
noList: "
|
|
11093
|
-
noSend: "
|
|
11094
|
-
settingUnreadable: "
|
|
11095
|
-
importLimited: (count) =>
|
|
11096
|
-
importFailed: "
|
|
11097
|
-
sent: (count) =>
|
|
11098
|
-
unreachable: (count) => `Harnessforce
|
|
11099
|
-
expiredUnsent: (count) =>
|
|
11100
|
-
readOnly: (count) => `
|
|
11101
|
-
invalidReports: (count, reasons) =>
|
|
11102
|
-
rejected: (status, count) => `Harnessforce
|
|
11103
|
-
keyUnusable: (count) =>
|
|
11104
|
-
loginExpired: "
|
|
11105
|
-
unwritable: "`~/.harnessforce/tune
|
|
11106
|
-
recordFailed: "
|
|
11107
|
-
recordInvalid: (field) =>
|
|
11108
|
-
usage: "
|
|
11109
|
-
purged: "`~/.harnessforce/tune
|
|
11110
|
-
purgeFailed: "`~/.harnessforce/tune
|
|
12075
|
+
initRequired: "Run `harnessforce init`. The Viewer role cannot use `harnessforce tune`",
|
|
12076
|
+
invalidUrl: "The connection URL is invalid",
|
|
12077
|
+
locked: "Another `harnessforce tune` is running, so this run cannot start",
|
|
12078
|
+
insufficient: (count) => `Not enough data: ${count} session(s) analyzed. Suggestions need at least 10 (${10 - count} more needed)`,
|
|
12079
|
+
outOfRange: (days, count) => `Outside the sending range: ${count} session(s) that started more than ${days} days ago will not be sent`,
|
|
12080
|
+
notConnected: (count) => `${count} session(s) in repositories that are not connected will not be sent`,
|
|
12081
|
+
partlyUnreadable: (lines, skipped) => `Some records could not be read: skipped ${skipped} of ${lines} lines read`,
|
|
12082
|
+
savedList: (fetchedAt) => `Could not fetch the list of connected repositories, so the list saved at ${fetchedAt} is used`,
|
|
12083
|
+
noList: "Could not verify the connected repositories, so nothing is sent",
|
|
12084
|
+
noSend: "Sending is turned off, so the analysis results are not sent",
|
|
12085
|
+
settingUnreadable: "Cannot read the device settings (`~/.harnessforce/config.json`), so the analysis results are not sent",
|
|
12086
|
+
importLimited: (count) => `Could not import ${count} session(s) because of the monthly event limit or view-only mode. The analysis results are sent; importing is retried in a run after the restriction is lifted`,
|
|
12087
|
+
importFailed: "Importing sessions before sending failed. The analysis results are sent; importing is retried in the next run",
|
|
12088
|
+
sent: (count) => `Sent ${count} analysis result(s)`,
|
|
12089
|
+
unreachable: (count) => `Could not send to Harnessforce. ${count} analysis result(s) will be sent in the next run`,
|
|
12090
|
+
expiredUnsent: (count) => `Deleted ${count} unsent analysis result(s) that fell outside the sending range`,
|
|
12091
|
+
readOnly: (count) => `Could not send ${count} analysis result(s) because the Workspace is view-only. They will be sent in the next run`,
|
|
12092
|
+
invalidReports: (count, reasons) => `Deleted ${count} analysis result(s) without sending because their format is invalid (${reasons.join(", ")})`,
|
|
12093
|
+
rejected: (status, count) => `Harnessforce rejected the submission (HTTP ${status}). Deleted ${count} analysis result(s) without sending`,
|
|
12094
|
+
keyUnusable: (count) => `The ingest key cannot be used. Deleted ${count} unsent analysis result(s). Run \`harnessforce init\``,
|
|
12095
|
+
loginExpired: "Your login has expired. Run `harnessforce init`",
|
|
12096
|
+
unwritable: "Cannot write to `~/.harnessforce/tune/`, so the analysis results are not sent",
|
|
12097
|
+
recordFailed: "Could not record the suggestion",
|
|
12098
|
+
recordInvalid: (field) => `The suggestion record input is invalid: ${field}`,
|
|
12099
|
+
usage: "Usage: harnessforce tune [--all] [--no-send] [--show-report] [--json], harnessforce tune record, or harnessforce tune --purge",
|
|
12100
|
+
purged: "Deleted the analysis results, suggestions and unsent analysis results in `~/.harnessforce/tune/`",
|
|
12101
|
+
purgeFailed: "Could not delete some files in `~/.harnessforce/tune/`"
|
|
11111
12102
|
};
|
|
11112
12103
|
var Notices = class {
|
|
11113
12104
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -11148,7 +12139,7 @@ const tunePaths = (homeDir) => {
|
|
|
11148
12139
|
};
|
|
11149
12140
|
};
|
|
11150
12141
|
const DIR_MODE = 448;
|
|
11151
|
-
const FILE_MODE
|
|
12142
|
+
const FILE_MODE = 384;
|
|
11152
12143
|
const destinationKey = ({ workspaceId, endpoint }) => `${workspaceId} ${endpoint}`;
|
|
11153
12144
|
async function readJsonFile(path) {
|
|
11154
12145
|
try {
|
|
@@ -11168,10 +12159,10 @@ async function writeFileAtomic(path, content) {
|
|
|
11168
12159
|
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
11169
12160
|
try {
|
|
11170
12161
|
await writeFile(temporary, content, {
|
|
11171
|
-
mode: FILE_MODE
|
|
12162
|
+
mode: FILE_MODE,
|
|
11172
12163
|
flag: "wx"
|
|
11173
12164
|
});
|
|
11174
|
-
await chmod(temporary, FILE_MODE
|
|
12165
|
+
await chmod(temporary, FILE_MODE);
|
|
11175
12166
|
await rename(temporary, path);
|
|
11176
12167
|
} finally {
|
|
11177
12168
|
await rm(temporary, { force: true });
|
|
@@ -11461,7 +12452,7 @@ async function readSendSetting(homeDir) {
|
|
|
11461
12452
|
const DAY_MS = 864e5;
|
|
11462
12453
|
const DEFAULT_RANGE_DAYS = 30;
|
|
11463
12454
|
const SKIPPED_RATIO = .05;
|
|
11464
|
-
const ACCESS_MESSAGES
|
|
12455
|
+
const ACCESS_MESSAGES = {
|
|
11465
12456
|
keychainUnavailable: TUNE_MESSAGES.keychainUnavailable,
|
|
11466
12457
|
initRequired: TUNE_MESSAGES.initRequired,
|
|
11467
12458
|
invalidUrl: TUNE_MESSAGES.invalidUrl,
|
|
@@ -11697,14 +12688,14 @@ async function analyzeUnderLock(options, access, paths, deps) {
|
|
|
11697
12688
|
writeSessionConfigs(paths.sessionConfigs, configs)
|
|
11698
12689
|
]).then(() => true, () => false);
|
|
11699
12690
|
if (!isWritten) notices.add("unwritable", TUNE_MESSAGES.unwritable, 1);
|
|
11700
|
-
if (options.json) deps.stdout(`${
|
|
12691
|
+
if (options.json) deps.stdout(`${asciiJson(output)}\n`);
|
|
11701
12692
|
else {
|
|
11702
12693
|
deps.stdout(renderAnalysis(analyzed.map((a) => a.report), analyzed.map((a) => a.tune.usage), {
|
|
11703
12694
|
sessionCount: analyzed.length,
|
|
11704
12695
|
analyzerVersion: ANALYZER_VERSION,
|
|
11705
12696
|
parserVersion: PARSER_VERSION
|
|
11706
12697
|
}));
|
|
11707
|
-
if (options.showReport) deps.stdout(`${
|
|
12698
|
+
if (options.showReport) deps.stdout(`${asciiJson(analyzed.filter((a) => a.sendable === true).map((a) => a.report), 2)}\n`);
|
|
11708
12699
|
}
|
|
11709
12700
|
if (analyzed.length < 10) notices.add("insufficient", TUNE_MESSAGES.insufficient(analyzed.length));
|
|
11710
12701
|
const lines = analyzed.reduce((sum, a) => sum + a.tune.lines, 0);
|
|
@@ -11727,7 +12718,7 @@ async function analyzeUnderLock(options, access, paths, deps) {
|
|
|
11727
12718
|
}
|
|
11728
12719
|
function analyzeCommand(options, deps) {
|
|
11729
12720
|
return runUntilStop(async () => {
|
|
11730
|
-
const access = await verifyCliAccess(deps, ACCESS_MESSAGES
|
|
12721
|
+
const access = await verifyCliAccess(deps, ACCESS_MESSAGES);
|
|
11731
12722
|
const paths = tunePaths(deps.homeDir);
|
|
11732
12723
|
await ensureDir(paths.dir).catch(() => stopWith(TUNE_MESSAGES.unwritable));
|
|
11733
12724
|
const release = await acquireTuneLock(paths.lock, deps).catch(() => stopWith(TUNE_MESSAGES.unwritable));
|
|
@@ -11856,7 +12847,7 @@ const sha256 = (text) => createHash("sha256").update(Buffer.from(text, "utf8")).
|
|
|
11856
12847
|
function expectedHash(input, component) {
|
|
11857
12848
|
if (component !== null && VALUE_KINDS.has(component.kind)) {
|
|
11858
12849
|
if (!("value" in input)) return invalid("value");
|
|
11859
|
-
return hashValue(input.value);
|
|
12850
|
+
return hashValue(hashInput(component.kind, input.value));
|
|
11860
12851
|
}
|
|
11861
12852
|
if (typeof input.content !== "string") return invalid("content");
|
|
11862
12853
|
return hashFileContent(Buffer.from(input.content, "utf8"));
|
|
@@ -11963,6 +12954,10 @@ function parseArgs(args) {
|
|
|
11963
12954
|
options
|
|
11964
12955
|
};
|
|
11965
12956
|
}
|
|
12957
|
+
function canTuneSend(args) {
|
|
12958
|
+
const parsed = parseArgs(args);
|
|
12959
|
+
return parsed?.kind === "analyze" && !parsed.options.noSend;
|
|
12960
|
+
}
|
|
11966
12961
|
async function underLock(deps, failure, body) {
|
|
11967
12962
|
return runUntilStop(async () => {
|
|
11968
12963
|
const paths = tunePaths(deps.homeDir);
|
|
@@ -12016,464 +13011,14 @@ async function tuneCommand(args, deps) {
|
|
|
12016
13011
|
return analyzeCommand(parsed.options, deps);
|
|
12017
13012
|
}
|
|
12018
13013
|
//#endregion
|
|
12019
|
-
//#region src/usage-limits/consent.ts
|
|
12020
|
-
const consentUrl = (base) => underBase(base, "api/v1/cli/usage-limits/consent");
|
|
12021
|
-
async function call(base, fetchImpl, method, body) {
|
|
12022
|
-
try {
|
|
12023
|
-
return await fetchImpl(consentUrl(base), {
|
|
12024
|
-
method,
|
|
12025
|
-
headers: {
|
|
12026
|
-
accept: "application/json",
|
|
12027
|
-
...body ? { "content-type": "application/json" } : {}
|
|
12028
|
-
},
|
|
12029
|
-
...body ? { body: JSON.stringify(body) } : {},
|
|
12030
|
-
redirect: "manual",
|
|
12031
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
12032
|
-
});
|
|
12033
|
-
} catch {
|
|
12034
|
-
return;
|
|
12035
|
-
}
|
|
12036
|
-
}
|
|
12037
|
-
async function readConsent(base, fetchImpl) {
|
|
12038
|
-
const response = await call(base, fetchImpl, "GET");
|
|
12039
|
-
if (response?.status === 401) return { kind: "unauthorized" };
|
|
12040
|
-
if (response?.status !== 200) return { kind: "failed" };
|
|
12041
|
-
const body = await readJsonObjectBody(response);
|
|
12042
|
-
if (typeof body?.workspace_opted_in !== "boolean" || typeof body.shared !== "boolean" || typeof body.current_text_version !== "number") return { kind: "failed" };
|
|
12043
|
-
return {
|
|
12044
|
-
kind: "ok",
|
|
12045
|
-
state: {
|
|
12046
|
-
workspaceOptedIn: body.workspace_opted_in,
|
|
12047
|
-
shared: body.shared,
|
|
12048
|
-
currentTextVersion: body.current_text_version
|
|
12049
|
-
}
|
|
12050
|
-
};
|
|
12051
|
-
}
|
|
12052
|
-
async function recordConsent(base, fetchImpl, textVersion) {
|
|
12053
|
-
const response = await call(base, fetchImpl, "PUT", { text_version: textVersion });
|
|
12054
|
-
if (!response) return { kind: "failed" };
|
|
12055
|
-
if (response.status === 204) return { kind: "ok" };
|
|
12056
|
-
if (response.status === 401) return { kind: "unauthorized" };
|
|
12057
|
-
const body = await readJsonObjectBody(response);
|
|
12058
|
-
if (response.status === 400 && body?.code === "invalid_text_version") return { kind: "invalid_text_version" };
|
|
12059
|
-
if (response.status === 403) return body?.code === "workspace_read_only" ? { kind: "read_only" } : { kind: "viewer" };
|
|
12060
|
-
return { kind: "failed" };
|
|
12061
|
-
}
|
|
12062
|
-
async function revokeConsent(base, fetchImpl) {
|
|
12063
|
-
const response = await call(base, fetchImpl, "DELETE");
|
|
12064
|
-
if (response?.status === 204) return { kind: "ok" };
|
|
12065
|
-
return response?.status === 401 ? { kind: "unauthorized" } : { kind: "failed" };
|
|
12066
|
-
}
|
|
12067
|
-
//#endregion
|
|
12068
|
-
//#region src/usage-limits/files.ts
|
|
12069
|
-
const DIRECTORY_MODE = 448;
|
|
12070
|
-
const FILE_MODE = 384;
|
|
12071
|
-
const markPath = (homeDir) => join(homeDir, ".harnessforce", "usage-limits.json");
|
|
12072
|
-
const statePath = (homeDir) => join(homeDir, ".harnessforce", "usage-limits-state.json");
|
|
12073
|
-
async function readJson(path) {
|
|
12074
|
-
try {
|
|
12075
|
-
return JSON.parse(await readFile(path, "utf8"));
|
|
12076
|
-
} catch {
|
|
12077
|
-
return;
|
|
12078
|
-
}
|
|
12079
|
-
}
|
|
12080
|
-
async function readMark(homeDir) {
|
|
12081
|
-
const value = await readJson(markPath(homeDir));
|
|
12082
|
-
if (!isObject(value)) return void 0;
|
|
12083
|
-
const { consented, text_version: textVersion, original } = value;
|
|
12084
|
-
if (typeof consented !== "boolean" || typeof textVersion !== "number" || !(original === null || isObject(original))) return void 0;
|
|
12085
|
-
return {
|
|
12086
|
-
consented,
|
|
12087
|
-
text_version: textVersion,
|
|
12088
|
-
original
|
|
12089
|
-
};
|
|
12090
|
-
}
|
|
12091
|
-
async function readSendState(homeDir) {
|
|
12092
|
-
const value = await readJson(statePath(homeDir));
|
|
12093
|
-
if (!isObject(value) || typeof value.sent_at !== "number") return void 0;
|
|
12094
|
-
const resets = value.resets_at;
|
|
12095
|
-
if (!isObject(resets)) return void 0;
|
|
12096
|
-
const entries = Object.entries(resets);
|
|
12097
|
-
if (entries.some(([, at]) => typeof at !== "number")) return void 0;
|
|
12098
|
-
return {
|
|
12099
|
-
sent_at: value.sent_at,
|
|
12100
|
-
resets_at: Object.fromEntries(entries)
|
|
12101
|
-
};
|
|
12102
|
-
}
|
|
12103
|
-
async function writeOwnerOnly(path, value) {
|
|
12104
|
-
await mkdir(dirname(path), {
|
|
12105
|
-
recursive: true,
|
|
12106
|
-
mode: DIRECTORY_MODE
|
|
12107
|
-
});
|
|
12108
|
-
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
12109
|
-
try {
|
|
12110
|
-
await writeFile(temporary, `${JSON.stringify(value)}\n`, {
|
|
12111
|
-
mode: FILE_MODE,
|
|
12112
|
-
flag: "wx"
|
|
12113
|
-
});
|
|
12114
|
-
await chmod(temporary, FILE_MODE);
|
|
12115
|
-
await rename(temporary, path);
|
|
12116
|
-
} finally {
|
|
12117
|
-
await rm(temporary, { force: true });
|
|
12118
|
-
}
|
|
12119
|
-
}
|
|
12120
|
-
const writeSendState = (homeDir, state) => writeOwnerOnly(statePath(homeDir), state);
|
|
12121
|
-
const writeMark = (homeDir, mark) => writeOwnerOnly(markPath(homeDir), mark);
|
|
12122
|
-
async function removeMarkAndState(homeDir) {
|
|
12123
|
-
await rm(markPath(homeDir), { force: true });
|
|
12124
|
-
await rm(statePath(homeDir), { force: true });
|
|
12125
|
-
}
|
|
12126
|
-
//#endregion
|
|
12127
|
-
//#region src/usage-limits/messages.ts
|
|
12128
|
-
const CONSENT_TEXT = {
|
|
12129
|
-
ja: "あなたの名前とともに、あなたの利用枠(5時間・7日などの窓の使用率とリセット時刻だけ)を、このWorkspaceのメンバー全員が見られます。プロンプトなどの本文は送りません。いつでもやめられ、やめると保存済みの値も削除します。メンバーの順位づけには使いません。",
|
|
12130
|
-
en: "Everyone in this workspace can see your usage limits together with your name (only the usage percentage and reset time of each window, such as 5 hours and 7 days). No prompt or other content is sent. You can stop at any time, and stopping deletes the stored values. They are not used to rank members."
|
|
12131
|
-
};
|
|
12132
|
-
const CONSENT_PROMPT = {
|
|
12133
|
-
ja: "同意しますか? [y/N] ",
|
|
12134
|
-
en: "Do you agree? [y/N] "
|
|
12135
|
-
};
|
|
12136
|
-
const MESSAGES = {
|
|
12137
|
-
windows: "この版のWindowsでは、利用枠の共有を設定できません",
|
|
12138
|
-
settingsUnreadable: "Claude Codeのuser settingsを読めません",
|
|
12139
|
-
statusLineUnsupported: "このstatusLineは組み込めません",
|
|
12140
|
-
noUserKey: "この端末には利用者用の送信キーがありません。`harnessforce init`を実行してください",
|
|
12141
|
-
keychainUnavailable: "OSのキーチェーンを利用できないため、利用枠の共有を設定できません",
|
|
12142
|
-
invalidUrl: "接続先のURLが不正です",
|
|
12143
|
-
loginExpired: "ログインの有効期限が切れました。`harnessforce init`をもう一度実行してください",
|
|
12144
|
-
notAgreed: "同意されなかったため、何も変えませんでした",
|
|
12145
|
-
invalidTextVersion: "このCLIの利用枠の文面が古いか新しすぎます。`harnessforce`を更新してください",
|
|
12146
|
-
viewer: "閲覧のみのロールでは、利用枠を共有できません。Workspaceの管理者に連絡してください",
|
|
12147
|
-
readOnly: "このWorkspaceは閲覧のみのため、利用枠の共有を設定できません",
|
|
12148
|
-
failed: "Harnessforceとの通信に失敗しました。もう一度お試しください",
|
|
12149
|
-
saveFailed: "端末の設定を書き込めなかったため、同意を取り消しました。もう一度お試しください",
|
|
12150
|
-
revokeFailedAfterSave: "端末の設定を書き込めず、同意の取り消しにも失敗しました。`harnessforce usage-limits status`で状態を確かめ、`harnessforce usage-limits off`を実行してください",
|
|
12151
|
-
notOptedIn: "同意を記録しました。Workspaceが利用枠の記録をopt-inするまで、値は保存されません",
|
|
12152
|
-
enabled: "利用枠の共有を有効にしました。Claude Codeを再起動すると反映されます",
|
|
12153
|
-
disabled: "利用枠の共有をやめ、保存済みの値の削除を依頼しました",
|
|
12154
|
-
revokedButNotRestored: "同意は撤回しましたが、statusLineを元に戻せませんでした。`harnessforce usage-limits off`をもう一度実行してください",
|
|
12155
|
-
usage: "Usage: harnessforce usage-limits on [--yes] | harnessforce usage-limits off | harnessforce usage-limits status | harnessforce usage-limits statusline\n"
|
|
12156
|
-
};
|
|
12157
|
-
const STATUS_LABELS = {
|
|
12158
|
-
consent: {
|
|
12159
|
-
shared: "同意している",
|
|
12160
|
-
notShared: "同意していない",
|
|
12161
|
-
outdated: "文面が古い",
|
|
12162
|
-
unknown: "確認できません"
|
|
12163
|
-
},
|
|
12164
|
-
optIn: {
|
|
12165
|
-
on: "有効",
|
|
12166
|
-
off: "無効",
|
|
12167
|
-
unknown: "確認できません"
|
|
12168
|
-
},
|
|
12169
|
-
statusLine: {
|
|
12170
|
-
embeddedWithOriginal: "組み込まれている(元のcommandあり)",
|
|
12171
|
-
embeddedWithoutOriginal: "組み込まれている(元のcommandなし)",
|
|
12172
|
-
notEmbedded: "組み込まれていない"
|
|
12173
|
-
}
|
|
12174
|
-
};
|
|
12175
|
-
//#endregion
|
|
12176
|
-
//#region src/usage-limits/statusline-settings.ts
|
|
12177
|
-
const STATUSLINE_COMMAND = "harnessforce usage-limits statusline";
|
|
12178
|
-
const isEmbedded = (statusLine) => isObject(statusLine) && statusLine.command === STATUSLINE_COMMAND;
|
|
12179
|
-
function canEmbed(settings) {
|
|
12180
|
-
const { statusLine } = settings;
|
|
12181
|
-
if (statusLine === void 0) return true;
|
|
12182
|
-
return isObject(statusLine) && statusLine.type === "command" && typeof statusLine.command === "string";
|
|
12183
|
-
}
|
|
12184
|
-
function originalFor(settings, mark) {
|
|
12185
|
-
const { statusLine } = settings;
|
|
12186
|
-
if (isEmbedded(statusLine)) return mark ? mark.original : null;
|
|
12187
|
-
return isObject(statusLine) ? statusLine : null;
|
|
12188
|
-
}
|
|
12189
|
-
function embedStatusLine(settings) {
|
|
12190
|
-
const { statusLine } = settings;
|
|
12191
|
-
if (isEmbedded(statusLine)) return settings;
|
|
12192
|
-
return {
|
|
12193
|
-
...settings,
|
|
12194
|
-
statusLine: isObject(statusLine) ? {
|
|
12195
|
-
...statusLine,
|
|
12196
|
-
command: STATUSLINE_COMMAND
|
|
12197
|
-
} : {
|
|
12198
|
-
type: "command",
|
|
12199
|
-
command: STATUSLINE_COMMAND
|
|
12200
|
-
}
|
|
12201
|
-
};
|
|
12202
|
-
}
|
|
12203
|
-
function restoreStatusLine(settings, original) {
|
|
12204
|
-
if (!isEmbedded(settings.statusLine)) return settings;
|
|
12205
|
-
const { statusLine: _embedded, ...rest } = settings;
|
|
12206
|
-
return original === null ? rest : {
|
|
12207
|
-
...settings,
|
|
12208
|
-
statusLine: original
|
|
12209
|
-
};
|
|
12210
|
-
}
|
|
12211
|
-
const isStatusLineEmbedded = (settings) => isEmbedded(settings.statusLine);
|
|
12212
|
-
//#endregion
|
|
12213
|
-
//#region src/usage-limits/manage.ts
|
|
12214
|
-
const ACCESS_MESSAGES = {
|
|
12215
|
-
keychainUnavailable: MESSAGES.keychainUnavailable,
|
|
12216
|
-
initRequired: MESSAGES.noUserKey,
|
|
12217
|
-
invalidUrl: MESSAGES.invalidUrl,
|
|
12218
|
-
apiTokenMissing: MESSAGES.noUserKey
|
|
12219
|
-
};
|
|
12220
|
-
function language(env) {
|
|
12221
|
-
return (env.LC_ALL || env.LC_MESSAGES || env.LANG || "").toLowerCase().startsWith("ja") ? "ja" : "en";
|
|
12222
|
-
}
|
|
12223
|
-
function consentClient(deps, access) {
|
|
12224
|
-
const authorized = createApiTokenSession({
|
|
12225
|
-
keychain: deps.keychain,
|
|
12226
|
-
workspaceId: access.workspaceId,
|
|
12227
|
-
readBase: access.readApiBase,
|
|
12228
|
-
fetch: deps.fetch,
|
|
12229
|
-
now: () => deps.now().getTime(),
|
|
12230
|
-
sleep: deps.sleep,
|
|
12231
|
-
homeDir: deps.homeDir
|
|
12232
|
-
}).authorizedFetch(deps.fetch);
|
|
12233
|
-
const base = access.readApiBase;
|
|
12234
|
-
return {
|
|
12235
|
-
read: () => readConsent(base, authorized),
|
|
12236
|
-
record: () => recordConsent(base, authorized, 1),
|
|
12237
|
-
revoke: () => revokeConsent(base, authorized)
|
|
12238
|
-
};
|
|
12239
|
-
}
|
|
12240
|
-
const settingsPathOf = (deps) => userSettingsPath(deps.env, deps.homeDir);
|
|
12241
|
-
async function runOn(yes, deps) {
|
|
12242
|
-
if (deps.platform === "win32") return stopWith(MESSAGES.windows);
|
|
12243
|
-
const client = consentClient(deps, await verifyCliAccess(deps, ACCESS_MESSAGES));
|
|
12244
|
-
const read = await readUserSettings(settingsPathOf(deps));
|
|
12245
|
-
if (read.kind === "invalid") return stopWith(MESSAGES.settingsUnreadable);
|
|
12246
|
-
if (!canEmbed(read.settings)) return stopWith(MESSAGES.statusLineUnsupported);
|
|
12247
|
-
const settings = read.settings;
|
|
12248
|
-
const before = await client.read();
|
|
12249
|
-
const lang = language(deps.env);
|
|
12250
|
-
deps.stdout(`${CONSENT_TEXT[lang]}\n`);
|
|
12251
|
-
if (!yes && (await deps.ask(CONSENT_PROMPT[lang])).trim() !== "y") return stopWith(MESSAGES.notAgreed);
|
|
12252
|
-
const recorded = await client.record();
|
|
12253
|
-
if (recorded.kind === "unauthorized") return stopWith(MESSAGES.loginExpired);
|
|
12254
|
-
if (recorded.kind === "invalid_text_version") return stopWith(MESSAGES.invalidTextVersion);
|
|
12255
|
-
if (recorded.kind === "viewer") return stopWith(MESSAGES.viewer);
|
|
12256
|
-
if (recorded.kind === "read_only") return stopWith(MESSAGES.readOnly);
|
|
12257
|
-
if (recorded.kind === "failed") return stopWith(MESSAGES.failed);
|
|
12258
|
-
const mark = await readMark(deps.homeDir);
|
|
12259
|
-
const undo = async () => {
|
|
12260
|
-
await (mark ? writeMark(deps.homeDir, {
|
|
12261
|
-
...mark,
|
|
12262
|
-
consented: false
|
|
12263
|
-
}) : removeMarkAndState(deps.homeDir)).catch(() => {});
|
|
12264
|
-
const revoked = await client.revoke();
|
|
12265
|
-
return stopWith(revoked.kind === "ok" ? MESSAGES.saveFailed : MESSAGES.revokeFailedAfterSave);
|
|
12266
|
-
};
|
|
12267
|
-
try {
|
|
12268
|
-
await writeMark(deps.homeDir, {
|
|
12269
|
-
consented: true,
|
|
12270
|
-
text_version: 1,
|
|
12271
|
-
original: originalFor(settings, mark)
|
|
12272
|
-
});
|
|
12273
|
-
} catch {
|
|
12274
|
-
return undo();
|
|
12275
|
-
}
|
|
12276
|
-
try {
|
|
12277
|
-
await writeUserSettings(settingsPathOf(deps), embedStatusLine(settings));
|
|
12278
|
-
} catch {
|
|
12279
|
-
return undo();
|
|
12280
|
-
}
|
|
12281
|
-
const optedOut = before.kind === "ok" && !before.state.workspaceOptedIn;
|
|
12282
|
-
deps.stdout(`${optedOut ? MESSAGES.notOptedIn : MESSAGES.enabled}\n`);
|
|
12283
|
-
return 0;
|
|
12284
|
-
}
|
|
12285
|
-
async function runOff(deps) {
|
|
12286
|
-
const revoked = await consentClient(deps, await verifyCliAccess(deps, ACCESS_MESSAGES)).revoke();
|
|
12287
|
-
if (revoked.kind === "unauthorized") return stopWith(MESSAGES.loginExpired);
|
|
12288
|
-
if (revoked.kind === "failed") return stopWith(MESSAGES.failed);
|
|
12289
|
-
const mark = await readMark(deps.homeDir);
|
|
12290
|
-
const read = await readUserSettings(settingsPathOf(deps));
|
|
12291
|
-
const keepWithoutConsent = async () => {
|
|
12292
|
-
if (mark) await writeMark(deps.homeDir, {
|
|
12293
|
-
...mark,
|
|
12294
|
-
consented: false
|
|
12295
|
-
}).catch(() => {});
|
|
12296
|
-
return stopWith(MESSAGES.revokedButNotRestored);
|
|
12297
|
-
};
|
|
12298
|
-
if (read.kind === "invalid") return keepWithoutConsent();
|
|
12299
|
-
if (isStatusLineEmbedded(read.settings)) try {
|
|
12300
|
-
await writeUserSettings(settingsPathOf(deps), restoreStatusLine(read.settings, mark?.original ?? null));
|
|
12301
|
-
} catch {
|
|
12302
|
-
return keepWithoutConsent();
|
|
12303
|
-
}
|
|
12304
|
-
await removeMarkAndState(deps.homeDir).catch(() => {});
|
|
12305
|
-
deps.stdout(`${MESSAGES.disabled}\n`);
|
|
12306
|
-
return 0;
|
|
12307
|
-
}
|
|
12308
|
-
function consentLabel(result, mark) {
|
|
12309
|
-
if (result.kind !== "ok") return STATUS_LABELS.consent.unknown;
|
|
12310
|
-
if (result.state.shared) return STATUS_LABELS.consent.shared;
|
|
12311
|
-
return mark?.consented === true && mark.text_version !== result.state.currentTextVersion ? STATUS_LABELS.consent.outdated : STATUS_LABELS.consent.notShared;
|
|
12312
|
-
}
|
|
12313
|
-
async function runStatus(deps) {
|
|
12314
|
-
const mark = await readMark(deps.homeDir);
|
|
12315
|
-
const read = await readUserSettings(settingsPathOf(deps));
|
|
12316
|
-
const statusLine = !(read.kind === "ok" && isStatusLineEmbedded(read.settings)) ? STATUS_LABELS.statusLine.notEmbedded : mark?.original ? STATUS_LABELS.statusLine.embeddedWithOriginal : STATUS_LABELS.statusLine.embeddedWithoutOriginal;
|
|
12317
|
-
const result = await readRemote(deps);
|
|
12318
|
-
deps.stdout([
|
|
12319
|
-
`本人の同意: ${consentLabel(result, mark)}`,
|
|
12320
|
-
`Workspaceのopt-in: ${result.kind !== "ok" ? STATUS_LABELS.optIn.unknown : result.state.workspaceOptedIn ? STATUS_LABELS.optIn.on : STATUS_LABELS.optIn.off}`,
|
|
12321
|
-
`statusLineの組み込み: ${statusLine}`,
|
|
12322
|
-
""
|
|
12323
|
-
].join("\n"));
|
|
12324
|
-
return 0;
|
|
12325
|
-
}
|
|
12326
|
-
async function readRemote(deps) {
|
|
12327
|
-
try {
|
|
12328
|
-
return await consentClient(deps, await verifyCliAccess(deps, ACCESS_MESSAGES)).read();
|
|
12329
|
-
} catch (error) {
|
|
12330
|
-
if (isStop(error)) return { kind: "failed" };
|
|
12331
|
-
throw error;
|
|
12332
|
-
}
|
|
12333
|
-
}
|
|
12334
|
-
function onCommand(yes, deps) {
|
|
12335
|
-
return runUntilStop(() => runOn(yes, deps), deps.stderr);
|
|
12336
|
-
}
|
|
12337
|
-
function offCommand(deps) {
|
|
12338
|
-
return runUntilStop(() => runOff(deps), deps.stderr);
|
|
12339
|
-
}
|
|
12340
|
-
const statusCommand = runStatus;
|
|
12341
|
-
//#endregion
|
|
12342
|
-
//#region src/usage-limits/summary.ts
|
|
12343
|
-
const WINDOWS = [{
|
|
12344
|
-
name: "five_hour",
|
|
12345
|
-
minutes: 300
|
|
12346
|
-
}, {
|
|
12347
|
-
name: "seven_day",
|
|
12348
|
-
minutes: 10080
|
|
12349
|
-
}];
|
|
12350
|
-
const MIN_SEND_INTERVAL_MS = 3e5;
|
|
12351
|
-
const instant = (ms) => new Date(ms).toISOString().replace(".000Z", "Z");
|
|
12352
|
-
function windowFrom(value, minutes, nowMs) {
|
|
12353
|
-
if (!isObject(value)) return void 0;
|
|
12354
|
-
const { used_percentage: used, resets_at: resetsAt } = value;
|
|
12355
|
-
if (typeof used !== "number" || !(used >= 0 && used <= 100)) return void 0;
|
|
12356
|
-
if (typeof resetsAt !== "number" || !Number.isFinite(resetsAt)) return void 0;
|
|
12357
|
-
const resetsAtMs = Math.floor(resetsAt) * 1e3;
|
|
12358
|
-
if (resetsAtMs <= nowMs) return void 0;
|
|
12359
|
-
return {
|
|
12360
|
-
window_minutes: minutes,
|
|
12361
|
-
used_percent: used,
|
|
12362
|
-
resets_at: instant(resetsAtMs)
|
|
12363
|
-
};
|
|
12364
|
-
}
|
|
12365
|
-
function summaryFrom(stdin, nowMs) {
|
|
12366
|
-
let json;
|
|
12367
|
-
try {
|
|
12368
|
-
json = JSON.parse(stdin);
|
|
12369
|
-
} catch {
|
|
12370
|
-
return;
|
|
12371
|
-
}
|
|
12372
|
-
const limits = isObject(json) ? json.rate_limits : void 0;
|
|
12373
|
-
if (!isObject(limits)) return void 0;
|
|
12374
|
-
const windows = WINDOWS.flatMap(({ name, minutes }) => {
|
|
12375
|
-
const window = windowFrom(limits[name], minutes, nowMs);
|
|
12376
|
-
return window ? [window] : [];
|
|
12377
|
-
});
|
|
12378
|
-
return windows.length === 0 ? void 0 : {
|
|
12379
|
-
agent: "claude_code",
|
|
12380
|
-
observed_at: instant(Math.floor(nowMs / 1e3) * 1e3),
|
|
12381
|
-
windows
|
|
12382
|
-
};
|
|
12383
|
-
}
|
|
12384
|
-
const resetSeconds = (window) => Date.parse(window.resets_at) / 1e3;
|
|
12385
|
-
function isDue(summary, state, nowMs) {
|
|
12386
|
-
if (!state) return true;
|
|
12387
|
-
return summary.windows.some((window) => state.resets_at[String(window.window_minutes)] !== resetSeconds(window)) || nowMs - state.sent_at >= MIN_SEND_INTERVAL_MS;
|
|
12388
|
-
}
|
|
12389
|
-
const stateFor = (summary, nowMs) => ({
|
|
12390
|
-
sent_at: nowMs,
|
|
12391
|
-
resets_at: Object.fromEntries(summary.windows.map((window) => [String(window.window_minutes), resetSeconds(window)]))
|
|
12392
|
-
});
|
|
12393
|
-
function parseSummary(raw) {
|
|
12394
|
-
let value;
|
|
12395
|
-
try {
|
|
12396
|
-
value = JSON.parse(raw);
|
|
12397
|
-
} catch {
|
|
12398
|
-
return;
|
|
12399
|
-
}
|
|
12400
|
-
if (!isObject(value) || value.agent !== "claude_code") return void 0;
|
|
12401
|
-
const { observed_at: observedAt, windows } = value;
|
|
12402
|
-
if (typeof observedAt !== "string" || !Array.isArray(windows)) return void 0;
|
|
12403
|
-
const rebuilt = windows.flatMap((window) => {
|
|
12404
|
-
if (!isObject(window)) return [];
|
|
12405
|
-
const { window_minutes: minutes, used_percent: used } = window;
|
|
12406
|
-
const resetsAt = window.resets_at;
|
|
12407
|
-
return WINDOWS.some((w) => w.minutes === minutes) && typeof minutes === "number" && typeof used === "number" && used >= 0 && used <= 100 && typeof resetsAt === "string" && !Number.isNaN(Date.parse(resetsAt)) ? [{
|
|
12408
|
-
window_minutes: minutes,
|
|
12409
|
-
used_percent: used,
|
|
12410
|
-
resets_at: resetsAt
|
|
12411
|
-
}] : [];
|
|
12412
|
-
});
|
|
12413
|
-
return rebuilt.length > 0 && rebuilt.length === windows.length ? {
|
|
12414
|
-
agent: "claude_code",
|
|
12415
|
-
observed_at: observedAt,
|
|
12416
|
-
windows: rebuilt
|
|
12417
|
-
} : void 0;
|
|
12418
|
-
}
|
|
12419
|
-
//#endregion
|
|
12420
|
-
//#region src/usage-limits/send.ts
|
|
12421
|
-
const SEND_TIMEOUT_MS = 5e3;
|
|
12422
|
-
async function sendUsageLimits(raw, deps) {
|
|
12423
|
-
const summary = parseSummary(raw);
|
|
12424
|
-
if (!summary) return 0;
|
|
12425
|
-
const mark = await readMark(deps.homeDir);
|
|
12426
|
-
if (!mark?.consented || mark.text_version !== 1) return 0;
|
|
12427
|
-
const resolved = await resolveUserDestination(deps).catch(() => void 0);
|
|
12428
|
-
if (resolved?.kind !== "ok") return 0;
|
|
12429
|
-
await postItem(resolved.destination, "v1/usage-limits", summary, deps.fetch, SEND_TIMEOUT_MS);
|
|
12430
|
-
return 0;
|
|
12431
|
-
}
|
|
12432
|
-
//#endregion
|
|
12433
|
-
//#region src/usage-limits/statusline.ts
|
|
12434
|
-
const MAX_INPUT_BYTES = Number.MAX_SAFE_INTEGER;
|
|
12435
|
-
function originalCommand(original) {
|
|
12436
|
-
const command = original?.command;
|
|
12437
|
-
return isObject(original) && typeof command === "string" && command !== "" ? command : void 0;
|
|
12438
|
-
}
|
|
12439
|
-
async function statusline(deps) {
|
|
12440
|
-
const input = await deps.readStdin(MAX_INPUT_BYTES).catch(() => Buffer.alloc(0));
|
|
12441
|
-
const mark = await readMark(deps.homeDir);
|
|
12442
|
-
const command = originalCommand(mark?.original ?? null);
|
|
12443
|
-
const code = command ? await deps.runOriginal(command, input) : void 0;
|
|
12444
|
-
await sendIfDue(deps, input, mark).catch(() => {});
|
|
12445
|
-
return code ?? 0;
|
|
12446
|
-
}
|
|
12447
|
-
async function sendIfDue(deps, input, mark) {
|
|
12448
|
-
if (!mark?.consented || mark.text_version !== 1) return;
|
|
12449
|
-
const nowMs = deps.now().getTime();
|
|
12450
|
-
const summary = summaryFrom(input.toString("utf8"), nowMs);
|
|
12451
|
-
if (!summary) return;
|
|
12452
|
-
if (!isDue(summary, await readSendState(deps.homeDir), nowMs)) return;
|
|
12453
|
-
await writeSendState(deps.homeDir, stateFor(summary, nowMs));
|
|
12454
|
-
deps.spawnSender(JSON.stringify(summary));
|
|
12455
|
-
}
|
|
12456
|
-
//#endregion
|
|
12457
|
-
//#region src/usage-limits/command.ts
|
|
12458
|
-
async function usageLimitsCommand(args, deps) {
|
|
12459
|
-
const [subcommand, payload] = args;
|
|
12460
|
-
if (subcommand === "statusline" && args.length === 1) return statusline(deps);
|
|
12461
|
-
if (subcommand === "send" && payload !== void 0 && args.length === 2) return sendUsageLimits(payload, deps);
|
|
12462
|
-
const isYes = args[1] === "--yes";
|
|
12463
|
-
if (subcommand === "on" && (args.length === 1 || args.length === 2 && isYes)) return onCommand(isYes, deps);
|
|
12464
|
-
if (subcommand === "off" && args.length === 1) return offCommand(deps);
|
|
12465
|
-
if (subcommand === "status" && args.length === 1) return statusCommand(deps);
|
|
12466
|
-
deps.stderr(MESSAGES.usage);
|
|
12467
|
-
return 1;
|
|
12468
|
-
}
|
|
12469
|
-
//#endregion
|
|
12470
13014
|
//#region src/main.ts
|
|
12471
13015
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
12472
|
-
const
|
|
13016
|
+
const EXIT_CLIENT_UNSUPPORTED = 5;
|
|
12473
13017
|
function parseInitArgs(args) {
|
|
12474
13018
|
let url;
|
|
12475
13019
|
let port;
|
|
12476
13020
|
let sendContent = false;
|
|
13021
|
+
let noPluginAutoUpdate = false;
|
|
12477
13022
|
for (let i = 0; i < args.length;) {
|
|
12478
13023
|
if (args[i] === "--url") {
|
|
12479
13024
|
if (url !== void 0 || args[i + 1] === void 0) return void 0;
|
|
@@ -12496,12 +13041,19 @@ function parseInitArgs(args) {
|
|
|
12496
13041
|
i += 1;
|
|
12497
13042
|
continue;
|
|
12498
13043
|
}
|
|
13044
|
+
if (args[i] === "--no-plugin-auto-update") {
|
|
13045
|
+
if (noPluginAutoUpdate) return void 0;
|
|
13046
|
+
noPluginAutoUpdate = true;
|
|
13047
|
+
i += 1;
|
|
13048
|
+
continue;
|
|
13049
|
+
}
|
|
12499
13050
|
return;
|
|
12500
13051
|
}
|
|
12501
13052
|
return {
|
|
12502
13053
|
url,
|
|
12503
13054
|
port,
|
|
12504
|
-
sendContent
|
|
13055
|
+
sendContent,
|
|
13056
|
+
noPluginAutoUpdate
|
|
12505
13057
|
};
|
|
12506
13058
|
}
|
|
12507
13059
|
function parseRunArgs(args) {
|
|
@@ -12514,55 +13066,94 @@ function parseRunArgs(args) {
|
|
|
12514
13066
|
} : void 0;
|
|
12515
13067
|
}
|
|
12516
13068
|
async function run(argv, deps) {
|
|
13069
|
+
const help = resolveHelp(argv);
|
|
13070
|
+
if (help !== void 0) {
|
|
13071
|
+
deps.stdout(help);
|
|
13072
|
+
return 0;
|
|
13073
|
+
}
|
|
12517
13074
|
const [command, ...rest] = argv;
|
|
12518
13075
|
if (command === "--version" && rest.length === 0) {
|
|
12519
13076
|
deps.stdout(`${version}\n`);
|
|
12520
13077
|
return 0;
|
|
12521
13078
|
}
|
|
12522
|
-
|
|
12523
|
-
|
|
12524
|
-
|
|
12525
|
-
|
|
12526
|
-
|
|
12527
|
-
|
|
12528
|
-
if (command === "tune") return tuneCommand(rest, {
|
|
13079
|
+
const client = {
|
|
13080
|
+
component: "cli",
|
|
13081
|
+
version,
|
|
13082
|
+
dev: deps.devBuild
|
|
13083
|
+
};
|
|
13084
|
+
const cli = {
|
|
12529
13085
|
...deps,
|
|
12530
|
-
|
|
12531
|
-
|
|
12532
|
-
|
|
13086
|
+
fetch: recordingFetch(deps.fetch, client, updateStatePath(deps.homeDir), deps.now)
|
|
13087
|
+
};
|
|
13088
|
+
const updateDeps = {
|
|
13089
|
+
...cli,
|
|
13090
|
+
client
|
|
13091
|
+
};
|
|
13092
|
+
const unsupported = async () => (await readCliUpdateState(deps.homeDir, version))?.status === "unsupported";
|
|
13093
|
+
const guarded = async (command) => {
|
|
13094
|
+
if (await isSendingBlocked(updateDeps)) {
|
|
13095
|
+
await noticeUpdate(updateDeps);
|
|
13096
|
+
return 1;
|
|
13097
|
+
}
|
|
13098
|
+
const code = await command();
|
|
13099
|
+
await noticeUpdate(updateDeps);
|
|
13100
|
+
return code;
|
|
13101
|
+
};
|
|
13102
|
+
if (command === "otel-headers" && rest.length === 0) {
|
|
13103
|
+
if (await unsupported()) return EXIT_CLIENT_UNSUPPORTED;
|
|
13104
|
+
return otelHeaders(cli.env, cli.managedDir, cli.keychain, cli.stdout, cli.stderr, cli.endStdout);
|
|
13105
|
+
}
|
|
13106
|
+
if (command === "import" && rest.length === 0) return guarded(() => importCommand({
|
|
13107
|
+
...cli,
|
|
13108
|
+
git: cli.importGit,
|
|
13109
|
+
now: () => cli.now().getTime()
|
|
13110
|
+
}));
|
|
13111
|
+
if (command === "tune") return guarded(() => tuneCommand(rest, {
|
|
13112
|
+
...cli,
|
|
13113
|
+
git: cli.importGit,
|
|
13114
|
+
now: () => cli.now().getTime()
|
|
13115
|
+
}));
|
|
12533
13116
|
if (command === "hook") {
|
|
12534
13117
|
if (rest.length !== 1 || rest[0] !== "session-start") {
|
|
12535
|
-
|
|
13118
|
+
cli.stderr(HOOK_USAGE);
|
|
12536
13119
|
return 1;
|
|
12537
13120
|
}
|
|
12538
13121
|
return sessionStart({
|
|
12539
|
-
env:
|
|
12540
|
-
homeDir:
|
|
12541
|
-
now:
|
|
12542
|
-
git:
|
|
12543
|
-
fetch:
|
|
12544
|
-
keychain:
|
|
12545
|
-
readStdin:
|
|
12546
|
-
stdout:
|
|
12547
|
-
stderr:
|
|
13122
|
+
env: cli.env,
|
|
13123
|
+
homeDir: cli.homeDir,
|
|
13124
|
+
now: cli.now,
|
|
13125
|
+
git: cli.hookGit,
|
|
13126
|
+
fetch: cli.fetch,
|
|
13127
|
+
keychain: cli.keychain,
|
|
13128
|
+
readStdin: cli.readStdin,
|
|
13129
|
+
stdout: cli.stdout,
|
|
13130
|
+
stderr: cli.stderr,
|
|
13131
|
+
client,
|
|
13132
|
+
defaultUrl: cli.defaultUrl
|
|
12548
13133
|
});
|
|
12549
13134
|
}
|
|
12550
13135
|
if (command === "top") return topCommand(rest, {
|
|
12551
|
-
homeDir:
|
|
12552
|
-
managedDir:
|
|
12553
|
-
env:
|
|
12554
|
-
stdout:
|
|
12555
|
-
stderr:
|
|
12556
|
-
now: () =>
|
|
12557
|
-
git:
|
|
12558
|
-
top:
|
|
13136
|
+
homeDir: cli.homeDir,
|
|
13137
|
+
managedDir: cli.managedDir,
|
|
13138
|
+
env: cli.env,
|
|
13139
|
+
stdout: cli.stdout,
|
|
13140
|
+
stderr: cli.stderr,
|
|
13141
|
+
now: () => cli.now().getTime(),
|
|
13142
|
+
git: cli.importGit,
|
|
13143
|
+
top: cli.top
|
|
12559
13144
|
});
|
|
12560
|
-
if (command === "usage-limits") return usageLimitsCommand(rest, deps);
|
|
12561
13145
|
const initArgs = command === "init" ? parseInitArgs(rest) : void 0;
|
|
12562
|
-
if (initArgs) return init(initArgs,
|
|
13146
|
+
if (initArgs) return guarded(() => init(initArgs, cli));
|
|
12563
13147
|
const runArgs = command === "run" ? parseRunArgs(rest) : void 0;
|
|
12564
|
-
if (runArgs)
|
|
12565
|
-
|
|
13148
|
+
if (runArgs) {
|
|
13149
|
+
if (await isSendingBlocked(updateDeps)) {
|
|
13150
|
+
await noticeUpdate(updateDeps);
|
|
13151
|
+
return runWithoutHarnessforce(runArgs, cli);
|
|
13152
|
+
}
|
|
13153
|
+
await noticeUpdate(updateDeps);
|
|
13154
|
+
return runIssue(runArgs, cli);
|
|
13155
|
+
}
|
|
13156
|
+
cli.stderr(UNKNOWN_COMMAND_HINT);
|
|
12566
13157
|
return 1;
|
|
12567
13158
|
}
|
|
12568
13159
|
//#endregion
|
|
@@ -12573,9 +13164,9 @@ const SENDING_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
12573
13164
|
"import",
|
|
12574
13165
|
"hook"
|
|
12575
13166
|
]);
|
|
12576
|
-
const isSending = (argv) => SENDING_COMMANDS.has(argv[0] ?? "") || argv[0] === "
|
|
13167
|
+
const isSending = (argv) => SENDING_COMMANDS.has(argv[0] ?? "") || argv[0] === "tune" && canTuneSend(argv.slice(1));
|
|
12577
13168
|
const HOOK_RESTART_FAILED = "harnessforce: session registration skipped (restart failed)";
|
|
12578
|
-
const RESTART_FAILED = "harnessforce
|
|
13169
|
+
const RESTART_FAILED = "Could not restart harnessforce";
|
|
12579
13170
|
async function relaunchHf(argv, deps) {
|
|
12580
13171
|
if (!isSending(argv)) return void 0;
|
|
12581
13172
|
const outcome = await relaunchWithoutRuntimeVariables({
|
|
@@ -12737,42 +13328,9 @@ function createProcessIo(proc = process) {
|
|
|
12737
13328
|
};
|
|
12738
13329
|
}
|
|
12739
13330
|
//#endregion
|
|
12740
|
-
//#region src/usage-limits/original.ts
|
|
12741
|
-
function runOriginalCommand(command, input) {
|
|
12742
|
-
return new Promise((resolve) => {
|
|
12743
|
-
let child;
|
|
12744
|
-
try {
|
|
12745
|
-
child = spawn("sh", ["-c", command], { stdio: [
|
|
12746
|
-
"pipe",
|
|
12747
|
-
"inherit",
|
|
12748
|
-
"inherit"
|
|
12749
|
-
] });
|
|
12750
|
-
} catch {
|
|
12751
|
-
return resolve(void 0);
|
|
12752
|
-
}
|
|
12753
|
-
child.once("error", () => resolve(void 0));
|
|
12754
|
-
child.once("exit", (code, signal) => resolve(code ?? 128 + (signal ? constants.signals[signal] : 0)));
|
|
12755
|
-
child.stdin?.on("error", () => {});
|
|
12756
|
-
child.stdin?.end(input);
|
|
12757
|
-
});
|
|
12758
|
-
}
|
|
12759
|
-
//#endregion
|
|
12760
|
-
//#region src/usage-limits/spawn-sender.ts
|
|
12761
|
-
function spawnUsageSender(payload) {
|
|
12762
|
-
spawn(process.execPath, [
|
|
12763
|
-
...process.execArgv,
|
|
12764
|
-
...process.argv.slice(1, 2),
|
|
12765
|
-
"usage-limits",
|
|
12766
|
-
"send",
|
|
12767
|
-
payload
|
|
12768
|
-
], {
|
|
12769
|
-
detached: true,
|
|
12770
|
-
stdio: "ignore"
|
|
12771
|
-
}).unref();
|
|
12772
|
-
}
|
|
12773
|
-
//#endregion
|
|
12774
13331
|
//#region src/bin.ts
|
|
12775
|
-
const
|
|
13332
|
+
const require = createRequire(import.meta.url);
|
|
13333
|
+
const { url: defaultUrl, isDefault } = require("./build-config.json");
|
|
12776
13334
|
const CALLBACK_TIMEOUT_MS = 3e5;
|
|
12777
13335
|
const argv = process.argv.slice(2);
|
|
12778
13336
|
const restoredEnv = takeStashedRuntimeEnv(process.env, process.platform);
|
|
@@ -12787,10 +13345,15 @@ process.exitCode = relaunched ?? await run(argv, {
|
|
|
12787
13345
|
env: process.env,
|
|
12788
13346
|
keychain: createOsKeychain({
|
|
12789
13347
|
platform: process.platform,
|
|
12790
|
-
load: () => import("@napi-rs/keyring")
|
|
13348
|
+
load: () => import("@napi-rs/keyring"),
|
|
13349
|
+
secretService: createSecretServiceProcess({
|
|
13350
|
+
execPath: process.execPath,
|
|
13351
|
+
resolveKeyring: () => require.resolve("@napi-rs/keyring")
|
|
13352
|
+
})
|
|
12791
13353
|
}),
|
|
12792
13354
|
stdout: (text) => process.stdout.write(text),
|
|
12793
13355
|
stderr: (text) => process.stderr.write(text),
|
|
13356
|
+
endStdout: () => new Promise((resolve) => process.stdout.end(() => resolve())),
|
|
12794
13357
|
fetch: (url, init) => fetch(url, init),
|
|
12795
13358
|
openBrowser: (url) => openBrowser(url, {
|
|
12796
13359
|
platform: process.platform,
|
|
@@ -12802,6 +13365,7 @@ process.exitCode = relaunched ?? await run(argv, {
|
|
|
12802
13365
|
homeDir: homedir(),
|
|
12803
13366
|
managedDir: managedDirFor(process.platform),
|
|
12804
13367
|
defaultUrl,
|
|
13368
|
+
devBuild: !isDefault,
|
|
12805
13369
|
callbackTimeoutMs: CALLBACK_TIMEOUT_MS,
|
|
12806
13370
|
cwd: process.cwd(),
|
|
12807
13371
|
git: createGitRunner({
|
|
@@ -12828,19 +13392,6 @@ process.exitCode = relaunched ?? await run(argv, {
|
|
|
12828
13392
|
}),
|
|
12829
13393
|
sleep: (ms) => setTimeout$1(ms),
|
|
12830
13394
|
top: createProcessIo(),
|
|
12831
|
-
runOriginal: runOriginalCommand,
|
|
12832
|
-
spawnSender: spawnUsageSender,
|
|
12833
|
-
ask: async (question) => {
|
|
12834
|
-
const lines = createInterface({
|
|
12835
|
-
input: process.stdin,
|
|
12836
|
-
output: process.stdout
|
|
12837
|
-
});
|
|
12838
|
-
try {
|
|
12839
|
-
return await lines.question(question);
|
|
12840
|
-
} finally {
|
|
12841
|
-
lines.close();
|
|
12842
|
-
}
|
|
12843
|
-
},
|
|
12844
13395
|
readStdin: async (maxBytes) => {
|
|
12845
13396
|
const chunks = [];
|
|
12846
13397
|
let size = 0;
|