@f-o-h/cli 0.1.14 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +17 -10
  2. package/dist/foh.js +103 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@ AI-operator provisioning CLI for Front Of House.
4
4
 
5
5
  Public mirror: https://github.com/iiko38/front-of-house-cli
6
6
 
7
- Current published baseline: `@f-o-h/cli@0.1.14`
7
+ Current published baseline: `@f-o-h/cli@0.1.15`
8
8
 
9
9
  This mirror is a generated release artifact. The private product monorepo is not
10
10
  published here, and no open-source license is granted unless stated separately.
@@ -62,15 +62,22 @@ foh auth login --email "$FOH_EMAIL" --password "$FOH_PASSWORD" --json
62
62
  foh org list --json
63
63
  foh org use --org <org-id> --json
64
64
  foh setup --org <org-id> --agent-template <template-id> --agent-name "Demo Agent" --json
65
- foh prove --agent <agent-id> --json --out foh-proof.json
66
- foh test run --suite ./suite.yml --agent <agent-id> --json --out foh-test-report.json
67
- foh agent replay --file ./transcript-export.json --json
68
- foh bug improve --from-file foh-proof.json --out foh-improvement.json --json
69
- ```
70
-
71
- `auth signup --web` opens the console signup page when possible and always
72
- prints the fallback URL. `auth login --web` starts browser device
73
- authorization, opens `/cli-auth`, waits for console approval, and stores the
65
+ foh prove --agent <agent-id> --json --out foh-proof.json
66
+ foh test run --suite ./suite.yml --agent <agent-id> --json --out foh-test-report.json
67
+ foh agent replay --file ./transcript-export.json --json
68
+ foh bug improve --from-file foh-proof.json --out foh-improvement.json --json
69
+ ```
70
+
71
+ Trusted server-side automation can use a scoped service token without browser
72
+ approval:
73
+
74
+ ```bash
75
+ FOH_SERVICE_TOKEN="$FOH_SERVICE_TOKEN" FOH_ORG_ID="$FOH_ORG_ID" foh auth whoami --json
76
+ ```
77
+
78
+ `auth signup --web` opens the console signup page when possible and always
79
+ prints the fallback URL. `auth login --web` starts browser device
80
+ authorization, opens `/cli-auth`, waits for console approval, and stores the
74
81
  returned short-lived token. Credential auth remains available as fallback.
75
82
 
76
83
  `foh prove` produces a compact signed proof report across auth, org context,
package/dist/foh.js CHANGED
@@ -10029,9 +10029,56 @@ function resolveApiBaseUrl(cliOverride) {
10029
10029
  return resolveApiUrlOverride(cliOverride) ?? DEFAULT_FOH_API_URL;
10030
10030
  }
10031
10031
 
10032
+ // src/lib/org-id.ts
10033
+ function isUsableOrgId(value) {
10034
+ const orgId = String(value ?? "").trim();
10035
+ return Boolean(orgId);
10036
+ }
10037
+
10032
10038
  // src/lib/credentials.ts
10033
10039
  var CONFIG_PATH = (0, import_path.join)((0, import_os.homedir)(), ".config", "foh", "credentials.json");
10040
+ var SERVICE_TOKEN_ENV = "FOH_SERVICE_TOKEN";
10041
+ var ORG_ID_ENV = "FOH_ORG_ID";
10042
+ var API_URL_ENV = "FOH_API_URL";
10043
+ var TOKEN_EXPIRES_AT_ENV = "FOH_TOKEN_EXPIRES_AT";
10044
+ function normalizeEnv(value) {
10045
+ const normalized = String(value ?? "").trim();
10046
+ return normalized ? normalized : void 0;
10047
+ }
10048
+ function decodeBase64UrlJson(value) {
10049
+ try {
10050
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
10051
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
10052
+ } catch {
10053
+ return void 0;
10054
+ }
10055
+ }
10056
+ function expiryFromJwt(token) {
10057
+ const [, payload] = token.split(".");
10058
+ if (!payload) return void 0;
10059
+ const decoded = decodeBase64UrlJson(payload);
10060
+ const exp = Number(decoded?.exp);
10061
+ if (!Number.isFinite(exp) || exp <= 0) return void 0;
10062
+ return new Date(exp * 1e3).toISOString();
10063
+ }
10064
+ function fallbackExpiry() {
10065
+ return new Date(Date.now() + 60 * 60 * 1e3).toISOString();
10066
+ }
10067
+ function credentialsFromEnv(apiUrlOverride, env = process.env) {
10068
+ const token = normalizeEnv(env[SERVICE_TOKEN_ENV]);
10069
+ if (!token) return void 0;
10070
+ const apiUrl = resolveApiUrlOverride(apiUrlOverride) ?? normalizeEnv(env[API_URL_ENV]) ?? DEFAULT_FOH_API_URL;
10071
+ const orgId = normalizeEnv(env[ORG_ID_ENV]);
10072
+ return {
10073
+ apiUrl,
10074
+ token,
10075
+ expiresAt: normalizeEnv(env[TOKEN_EXPIRES_AT_ENV]) ?? expiryFromJwt(token) ?? fallbackExpiry(),
10076
+ ...isUsableOrgId(orgId) ? { orgId } : {}
10077
+ };
10078
+ }
10034
10079
  function loadCredentials(apiUrlOverride) {
10080
+ const envCreds = credentialsFromEnv(apiUrlOverride);
10081
+ if (envCreds) return envCreds;
10035
10082
  let creds;
10036
10083
  try {
10037
10084
  const raw = (0, import_fs.readFileSync)(CONFIG_PATH, "utf-8");
@@ -10059,12 +10106,6 @@ function clearCredentials() {
10059
10106
  }
10060
10107
  }
10061
10108
 
10062
- // src/lib/org-id.ts
10063
- function isUsableOrgId(value) {
10064
- const orgId = String(value ?? "").trim();
10065
- return Boolean(orgId);
10066
- }
10067
-
10068
10109
  // src/lib/command-runtime.ts
10069
10110
  function markCommandFailed(code = 1) {
10070
10111
  const normalized = Number.isFinite(code) ? Math.max(1, Math.trunc(code)) : 1;
@@ -16034,9 +16075,9 @@ function registerVoice(program3) {
16034
16075
  const outputPath = String(opts.out || `foh-voice-preview-${provider}-${voiceId}.mp3`).trim();
16035
16076
  const audio = Buffer.from(await res.arrayBuffer());
16036
16077
  const { mkdirSync: mkdirSync7, writeFileSync: writeFileSync9 } = await import("fs");
16037
- const { dirname: dirname5, resolve: resolve12 } = await import("path");
16078
+ const { dirname: dirname6, resolve: resolve12 } = await import("path");
16038
16079
  const absolutePath = resolve12(outputPath);
16039
- mkdirSync7(dirname5(absolutePath), { recursive: true });
16080
+ mkdirSync7(dirname6(absolutePath), { recursive: true });
16040
16081
  writeFileSync9(absolutePath, audio);
16041
16082
  format({
16042
16083
  status: "ok",
@@ -32640,7 +32681,7 @@ var StdioServerTransport = class {
32640
32681
  };
32641
32682
 
32642
32683
  // src/lib/cli-version.ts
32643
- var CLI_VERSION = "0.1.14";
32684
+ var CLI_VERSION = "0.1.15";
32644
32685
 
32645
32686
  // src/commands/mcp-serve.ts
32646
32687
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -38281,7 +38322,8 @@ var TEXT_ARTIFACT_NAMES = /* @__PURE__ */ new Set([
38281
38322
  "run.json",
38282
38323
  "terminal-transcript.txt"
38283
38324
  ]);
38284
- var SECRET_RE2 = /\b(?:Bearer\s+)?(?:sk|pk|xai|whsec|EAAN|ghp|gho|github_pat|npm_)[A-Za-z0-9_\-.]{12,}\b/gi;
38325
+ var SECRET_RE2 = /\b(?:Bearer\s+[A-Za-z0-9_\-.]{12,}|(?:sk|pk|xai|whsec|EAAN|ghp|gho|github_pat)[A-Za-z0-9_\-.]{12,}|npm_[A-Za-z0-9]{20,})\b/g;
38326
+ var SECRET_QUERY_PARAM_RE = /\b((?:device_code|access_token|refresh_token|api_key|token)=)[A-Za-z0-9_.~-]{12,}/gi;
38285
38327
  var EMAIL_RE2 = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
38286
38328
  var PHONE_RE2 = /(?<!\w)(?:\+?\d[\d\s().-]{7,}\d)(?!\w)/g;
38287
38329
  function escapeRegExp(value) {
@@ -38308,6 +38350,9 @@ function matchesFor(pattern, text) {
38308
38350
  const matches = text.match(pattern);
38309
38351
  return matches ? matches.length : 0;
38310
38352
  }
38353
+ function secretMatches(text) {
38354
+ return matchesFor(SECRET_RE2, text) + matchesFor(SECRET_QUERY_PARAM_RE, text);
38355
+ }
38311
38356
  function redactionPatterns(input) {
38312
38357
  return {
38313
38358
  secret: SECRET_RE2,
@@ -38319,11 +38364,14 @@ function redactionPatterns(input) {
38319
38364
  }
38320
38365
  function redactExternalAgentArtifactText(text, input = {}) {
38321
38366
  const patterns = redactionPatterns(input);
38322
- let redacted = text.replace(patterns.secret, "[redacted_secret]").replace(patterns.email, "[redacted_email]").replace(patterns.phone, "[redacted_phone]");
38367
+ let redacted = redactExternalAgentSecretText(text).replace(patterns.email, "[redacted_email]").replace(patterns.phone, "[redacted_phone]");
38323
38368
  if (patterns.privateRepo) redacted = redacted.replace(patterns.privateRepo, "[redacted_private_repo_path]");
38324
38369
  if (patterns.home) redacted = redacted.replace(patterns.home, "[redacted_home_path]");
38325
38370
  return redacted;
38326
38371
  }
38372
+ function redactExternalAgentSecretText(text) {
38373
+ return text.replace(SECRET_QUERY_PARAM_RE, "$1[redacted_secret]").replace(SECRET_RE2, "[redacted_secret]");
38374
+ }
38327
38375
  function artifactFiles(runDir) {
38328
38376
  if (!(0, import_fs12.existsSync)(runDir)) return [];
38329
38377
  return (0, import_fs12.readdirSync)(runDir).map((name) => (0, import_path10.join)(runDir, name)).filter((path2) => {
@@ -38343,7 +38391,7 @@ function finding(kind, file2, count) {
38343
38391
  function scanText(input) {
38344
38392
  const patterns = redactionPatterns(input);
38345
38393
  return [
38346
- finding("secret_like_token", input.file, matchesFor(patterns.secret, input.text)),
38394
+ finding("secret_like_token", input.file, secretMatches(input.text)),
38347
38395
  finding("private_repo_path", input.file, matchesFor(patterns.privateRepo, input.text)),
38348
38396
  finding("local_home_path", input.file, matchesFor(patterns.home, input.text)),
38349
38397
  finding("email_address", input.file, matchesFor(patterns.email, input.text)),
@@ -38483,6 +38531,12 @@ var CHILD_ENV_ALLOWLIST = [
38483
38531
  "USERPROFILE",
38484
38532
  "WINDIR"
38485
38533
  ];
38534
+ var EXTERNAL_AGENT_EVAL_AUTH_ENV_MAP = {
38535
+ FOH_EXTERNAL_AGENT_EVAL_TOKEN: "FOH_SERVICE_TOKEN",
38536
+ FOH_EXTERNAL_AGENT_EVAL_ORG_ID: "FOH_ORG_ID",
38537
+ FOH_EXTERNAL_AGENT_EVAL_API_URL: "FOH_API_URL",
38538
+ FOH_EXTERNAL_AGENT_EVAL_TOKEN_EXPIRES_AT: "FOH_TOKEN_EXPIRES_AT"
38539
+ };
38486
38540
  var ExternalAgentExecutorError = class extends Error {
38487
38541
  reasonCode;
38488
38542
  constructor(reasonCode, message) {
@@ -38505,6 +38559,12 @@ function buildCodexExecutorEnv(input) {
38505
38559
  env[key] = value;
38506
38560
  }
38507
38561
  }
38562
+ for (const [sourceKey, childKey] of Object.entries(EXTERNAL_AGENT_EVAL_AUTH_ENV_MAP)) {
38563
+ const value = source[sourceKey];
38564
+ if (typeof value === "string" && value.trim() && !isDeniedEnvKey(childKey)) {
38565
+ env[childKey] = value;
38566
+ }
38567
+ }
38508
38568
  env[EXTERNAL_AGENT_RUN_DIR_ENV] = input.runDir;
38509
38569
  env[EXTERNAL_AGENT_PROMPT_VERSION_ENV] = input.promptVersion;
38510
38570
  env.FOH_CLI_SUPPRESS_BANNER = "1";
@@ -38671,7 +38731,6 @@ function createExternalAgentExecutorPlan(options) {
38671
38731
  "--skip-git-repo-check",
38672
38732
  "--ephemeral",
38673
38733
  "--ignore-rules",
38674
- "--ignore-user-config",
38675
38734
  "--sandbox",
38676
38735
  "workspace-write",
38677
38736
  "--full-auto",
@@ -38744,6 +38803,22 @@ function proofArtifactPasses(runDir) {
38744
38803
  function readIfExists(path2) {
38745
38804
  return (0, import_fs14.existsSync)(path2) ? (0, import_fs14.readFileSync)(path2, "utf8") : "";
38746
38805
  }
38806
+ function redactSecretLikeFile(path2) {
38807
+ if (!(0, import_fs14.existsSync)(path2)) return;
38808
+ const original = (0, import_fs14.readFileSync)(path2, "utf8");
38809
+ const redacted = redactExternalAgentSecretText(original);
38810
+ if (redacted !== original) (0, import_fs14.writeFileSync)(path2, redacted, "utf8");
38811
+ }
38812
+ function redactSecretLikeOutputArtifacts(run) {
38813
+ redactSecretLikeFile(run.outputs.jsonl);
38814
+ redactSecretLikeFile(run.outputs.last_message);
38815
+ redactSecretLikeFile(run.outputs.stderr);
38816
+ }
38817
+ function copyCommandCaptureArtifacts(input) {
38818
+ const commandLog = (0, import_path12.join)(input.captureDir, "commands.ndjson");
38819
+ if (!(0, import_fs14.existsSync)(commandLog)) return;
38820
+ (0, import_fs14.writeFileSync)((0, import_path12.join)(input.runDir, "commands.ndjson"), (0, import_fs14.readFileSync)(commandLog, "utf8"), "utf8");
38821
+ }
38747
38822
  function relativeArtifactName(path2) {
38748
38823
  return (0, import_path12.basename)(path2);
38749
38824
  }
@@ -38824,11 +38899,11 @@ function buildExecutedRunArtifact(input) {
38824
38899
  function spawnCodex(input) {
38825
38900
  return new Promise((resolveRun) => {
38826
38901
  const started = Date.now();
38827
- const useShell = process.platform === "win32" && input.command.toLowerCase().endsWith(".cmd");
38828
- const child = (0, import_child_process4.spawn)(input.command, input.args, {
38902
+ const commandInvocation = buildCommandInvocation(input.command, input.args);
38903
+ const child = (0, import_child_process4.spawn)(commandInvocation.command, commandInvocation.args, {
38829
38904
  cwd: input.cwd,
38830
38905
  env: input.env,
38831
- shell: useShell,
38906
+ shell: false,
38832
38907
  stdio: ["pipe", "pipe", "pipe"],
38833
38908
  windowsHide: true
38834
38909
  });
@@ -38868,15 +38943,24 @@ function spawnCodex(input) {
38868
38943
  });
38869
38944
  });
38870
38945
  }
38946
+ function buildCommandInvocation(command, args) {
38947
+ if (process.platform === "win32" && command.toLowerCase().endsWith(".cmd")) {
38948
+ const codexEntrypoint = (0, import_path12.join)((0, import_path12.dirname)(command), "node_modules", "@openai", "codex", "bin", "codex.js");
38949
+ if ((0, import_fs14.existsSync)(codexEntrypoint)) return { command: process.execPath, args: [codexEntrypoint, ...args] };
38950
+ }
38951
+ return { command, args };
38952
+ }
38871
38953
  async function executeExternalAgentExecutorPlan(plan, options = {}) {
38872
38954
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
38873
38955
  const results = [];
38874
38956
  const runnerCommand = options.runnerCommand || resolveCodexExecutionCommand();
38875
38957
  for (const run of plan.runs) {
38876
38958
  const runStartedAt = (/* @__PURE__ */ new Date()).toISOString();
38959
+ const commandCaptureDir = (0, import_path12.join)(run.workspace_dir, ".foh-capture");
38960
+ (0, import_fs14.mkdirSync)(commandCaptureDir, { recursive: true });
38877
38961
  const env = buildCodexExecutorEnv({
38878
38962
  sourceEnv: options.env,
38879
- runDir: run.run_dir,
38963
+ runDir: commandCaptureDir,
38880
38964
  promptVersion: run.prompt_version
38881
38965
  });
38882
38966
  const spawned = await spawnCodex({
@@ -38889,6 +38973,8 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
38889
38973
  stderrPath: run.outputs.stderr,
38890
38974
  timeoutMs: plan.timeout_minutes * 60 * 1e3
38891
38975
  });
38976
+ copyCommandCaptureArtifacts({ captureDir: commandCaptureDir, runDir: run.run_dir });
38977
+ redactSecretLikeOutputArtifacts(run);
38892
38978
  const artifactSafety = scanExternalAgentArtifacts({
38893
38979
  runDir: run.run_dir,
38894
38980
  privateRepoRoot: options.privateRepoRoot || plan.private_repo_root,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@f-o-h/cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "FOH CLI - AI-operator provisioning tool for Front Of House",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {