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