@f-o-h/cli 0.1.13 → 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 +114 -20
  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.13`
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.13";
32684
+ var CLI_VERSION = "0.1.15";
32644
32685
 
32645
32686
  // src/commands/mcp-serve.ts
32646
32687
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -36437,8 +36478,13 @@ function inferReasonCode(artifact) {
36437
36478
  }
36438
36479
  return nonEmpty2(getPath2(artifact, "status"));
36439
36480
  }
36440
- function inferPromotionDecision(sourceType) {
36441
- if (sourceType === "external_agent_run") return "fix_docs";
36481
+ function inferPromotionDecision(sourceType, reasonCode) {
36482
+ const reason = String(reasonCode || "").toLowerCase();
36483
+ if (sourceType === "external_agent_run") {
36484
+ if (reason.includes("exec_policy") || reason.includes("policy_blocked") || reason.includes("auth") || reason.includes("config")) return "fix_config";
36485
+ if (reason.includes("cli") || reason.includes("command") || reason.includes("flag")) return "fix_cli";
36486
+ return "fix_docs";
36487
+ }
36442
36488
  if (sourceType === "knowledge_miss") return "fix_docs";
36443
36489
  if (sourceType === "setup_failure" || sourceType === "proof_failure" || sourceType === "live_proof_failure") return "fix_config";
36444
36490
  if (sourceType === "replay_failure" || sourceType === "runtime_miss") return "add_test";
@@ -36526,7 +36572,6 @@ function readSourceArtifact(path2) {
36526
36572
  function buildImprovementPacket(input) {
36527
36573
  const artifact = input.sourceArtifact ?? null;
36528
36574
  const sourceType = parseEnum(input.sourceType, IMPROVEMENT_SOURCE_TYPES, "--source-type") ?? inferSourceType(artifact);
36529
- const promotionDecision = parseEnum(input.promotionDecision, IMPROVEMENT_DECISIONS, "--recommendation") ?? inferPromotionDecision(sourceType);
36530
36575
  const ids = collectIds(artifact, input.ids);
36531
36576
  assertOrgBoundary(artifact, input.ids?.org_id);
36532
36577
  const reasonCode = nonEmpty2(input.reasonCode) ?? inferReasonCode(artifact);
@@ -36538,6 +36583,7 @@ function buildImprovementPacket(input) {
36538
36583
  statusCode: 400
36539
36584
  });
36540
36585
  }
36586
+ const promotionDecision = parseEnum(input.promotionDecision, IMPROVEMENT_DECISIONS, "--recommendation") ?? inferPromotionDecision(sourceType, reasonCode);
36541
36587
  const evidenceSummary = redactString(
36542
36588
  nonEmpty2(input.evidenceSummary) ?? nonEmpty2(getPath2(artifact, "summary")) ?? `Improvement candidate generated from ${sourceType} with reason ${reasonCode}.`
36543
36589
  );
@@ -38276,7 +38322,8 @@ var TEXT_ARTIFACT_NAMES = /* @__PURE__ */ new Set([
38276
38322
  "run.json",
38277
38323
  "terminal-transcript.txt"
38278
38324
  ]);
38279
- 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;
38280
38327
  var EMAIL_RE2 = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
38281
38328
  var PHONE_RE2 = /(?<!\w)(?:\+?\d[\d\s().-]{7,}\d)(?!\w)/g;
38282
38329
  function escapeRegExp(value) {
@@ -38303,6 +38350,9 @@ function matchesFor(pattern, text) {
38303
38350
  const matches = text.match(pattern);
38304
38351
  return matches ? matches.length : 0;
38305
38352
  }
38353
+ function secretMatches(text) {
38354
+ return matchesFor(SECRET_RE2, text) + matchesFor(SECRET_QUERY_PARAM_RE, text);
38355
+ }
38306
38356
  function redactionPatterns(input) {
38307
38357
  return {
38308
38358
  secret: SECRET_RE2,
@@ -38314,11 +38364,14 @@ function redactionPatterns(input) {
38314
38364
  }
38315
38365
  function redactExternalAgentArtifactText(text, input = {}) {
38316
38366
  const patterns = redactionPatterns(input);
38317
- 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]");
38318
38368
  if (patterns.privateRepo) redacted = redacted.replace(patterns.privateRepo, "[redacted_private_repo_path]");
38319
38369
  if (patterns.home) redacted = redacted.replace(patterns.home, "[redacted_home_path]");
38320
38370
  return redacted;
38321
38371
  }
38372
+ function redactExternalAgentSecretText(text) {
38373
+ return text.replace(SECRET_QUERY_PARAM_RE, "$1[redacted_secret]").replace(SECRET_RE2, "[redacted_secret]");
38374
+ }
38322
38375
  function artifactFiles(runDir) {
38323
38376
  if (!(0, import_fs12.existsSync)(runDir)) return [];
38324
38377
  return (0, import_fs12.readdirSync)(runDir).map((name) => (0, import_path10.join)(runDir, name)).filter((path2) => {
@@ -38338,7 +38391,7 @@ function finding(kind, file2, count) {
38338
38391
  function scanText(input) {
38339
38392
  const patterns = redactionPatterns(input);
38340
38393
  return [
38341
- finding("secret_like_token", input.file, matchesFor(patterns.secret, input.text)),
38394
+ finding("secret_like_token", input.file, secretMatches(input.text)),
38342
38395
  finding("private_repo_path", input.file, matchesFor(patterns.privateRepo, input.text)),
38343
38396
  finding("local_home_path", input.file, matchesFor(patterns.home, input.text)),
38344
38397
  finding("email_address", input.file, matchesFor(patterns.email, input.text)),
@@ -38478,6 +38531,12 @@ var CHILD_ENV_ALLOWLIST = [
38478
38531
  "USERPROFILE",
38479
38532
  "WINDIR"
38480
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
+ };
38481
38540
  var ExternalAgentExecutorError = class extends Error {
38482
38541
  reasonCode;
38483
38542
  constructor(reasonCode, message) {
@@ -38500,6 +38559,12 @@ function buildCodexExecutorEnv(input) {
38500
38559
  env[key] = value;
38501
38560
  }
38502
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
+ }
38503
38568
  env[EXTERNAL_AGENT_RUN_DIR_ENV] = input.runDir;
38504
38569
  env[EXTERNAL_AGENT_PROMPT_VERSION_ENV] = input.promptVersion;
38505
38570
  env.FOH_CLI_SUPPRESS_BANNER = "1";
@@ -38666,7 +38731,6 @@ function createExternalAgentExecutorPlan(options) {
38666
38731
  "--skip-git-repo-check",
38667
38732
  "--ephemeral",
38668
38733
  "--ignore-rules",
38669
- "--ignore-user-config",
38670
38734
  "--sandbox",
38671
38735
  "workspace-write",
38672
38736
  "--full-auto",
@@ -38739,6 +38803,22 @@ function proofArtifactPasses(runDir) {
38739
38803
  function readIfExists(path2) {
38740
38804
  return (0, import_fs14.existsSync)(path2) ? (0, import_fs14.readFileSync)(path2, "utf8") : "";
38741
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
+ }
38742
38822
  function relativeArtifactName(path2) {
38743
38823
  return (0, import_path12.basename)(path2);
38744
38824
  }
@@ -38752,6 +38832,9 @@ ${stderr}`;
38752
38832
  if (/need[^.\n]*(?:private|source)[^.\n]*repo|cannot[^.\n]*without[^.\n]*(?:private|source)[^.\n]*repo|clone[^.\n]*(?:private|source)[^.\n]*repo/i.test(combined)) {
38753
38833
  return { status: "fail", reasonCode: "private_repo_assumption_detected" };
38754
38834
  }
38835
+ if (/(?:blocked|rejected|declined) by policy|EXEC_POLICY_BLOCKED|command execution was rejected|shell commands were rejected/i.test(combined)) {
38836
+ return { status: "hold", reasonCode: "codex_exec_policy_blocked" };
38837
+ }
38755
38838
  if (/browser|approve|approval|login|auth|sign in/i.test(combined) && !proofArtifactPasses(input.run.run_dir)) {
38756
38839
  return { status: "hold", reasonCode: "auth_browser_approval_required" };
38757
38840
  }
@@ -38816,11 +38899,11 @@ function buildExecutedRunArtifact(input) {
38816
38899
  function spawnCodex(input) {
38817
38900
  return new Promise((resolveRun) => {
38818
38901
  const started = Date.now();
38819
- const useShell = process.platform === "win32" && input.command.toLowerCase().endsWith(".cmd");
38820
- 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, {
38821
38904
  cwd: input.cwd,
38822
38905
  env: input.env,
38823
- shell: useShell,
38906
+ shell: false,
38824
38907
  stdio: ["pipe", "pipe", "pipe"],
38825
38908
  windowsHide: true
38826
38909
  });
@@ -38860,15 +38943,24 @@ function spawnCodex(input) {
38860
38943
  });
38861
38944
  });
38862
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
+ }
38863
38953
  async function executeExternalAgentExecutorPlan(plan, options = {}) {
38864
38954
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
38865
38955
  const results = [];
38866
38956
  const runnerCommand = options.runnerCommand || resolveCodexExecutionCommand();
38867
38957
  for (const run of plan.runs) {
38868
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 });
38869
38961
  const env = buildCodexExecutorEnv({
38870
38962
  sourceEnv: options.env,
38871
- runDir: run.run_dir,
38963
+ runDir: commandCaptureDir,
38872
38964
  promptVersion: run.prompt_version
38873
38965
  });
38874
38966
  const spawned = await spawnCodex({
@@ -38881,6 +38973,8 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
38881
38973
  stderrPath: run.outputs.stderr,
38882
38974
  timeoutMs: plan.timeout_minutes * 60 * 1e3
38883
38975
  });
38976
+ copyCommandCaptureArtifacts({ captureDir: commandCaptureDir, runDir: run.run_dir });
38977
+ redactSecretLikeOutputArtifacts(run);
38884
38978
  const artifactSafety = scanExternalAgentArtifacts({
38885
38979
  runDir: run.run_dir,
38886
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.13",
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": {