@f-o-h/cli 0.1.88 → 0.1.89

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/dist/foh.js CHANGED
@@ -963,8 +963,8 @@ var require_command = __commonJS({
963
963
  "../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/command.js"(exports2) {
964
964
  var EventEmitter = require("node:events").EventEmitter;
965
965
  var childProcess = require("node:child_process");
966
- var path2 = require("node:path");
967
- var fs = require("node:fs");
966
+ var path5 = require("node:path");
967
+ var fs4 = require("node:fs");
968
968
  var process4 = require("node:process");
969
969
  var { Argument: Argument2, humanReadableArgName } = require_argument();
970
970
  var { CommanderError: CommanderError2 } = require_error();
@@ -1896,11 +1896,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
1896
1896
  let launchWithNode = false;
1897
1897
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1898
1898
  function findFile(baseDir, baseName) {
1899
- const localBin = path2.resolve(baseDir, baseName);
1900
- if (fs.existsSync(localBin)) return localBin;
1901
- if (sourceExt.includes(path2.extname(baseName))) return void 0;
1899
+ const localBin = path5.resolve(baseDir, baseName);
1900
+ if (fs4.existsSync(localBin)) return localBin;
1901
+ if (sourceExt.includes(path5.extname(baseName))) return void 0;
1902
1902
  const foundExt = sourceExt.find(
1903
- (ext) => fs.existsSync(`${localBin}${ext}`)
1903
+ (ext) => fs4.existsSync(`${localBin}${ext}`)
1904
1904
  );
1905
1905
  if (foundExt) return `${localBin}${foundExt}`;
1906
1906
  return void 0;
@@ -1912,21 +1912,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
1912
1912
  if (this._scriptPath) {
1913
1913
  let resolvedScriptPath;
1914
1914
  try {
1915
- resolvedScriptPath = fs.realpathSync(this._scriptPath);
1915
+ resolvedScriptPath = fs4.realpathSync(this._scriptPath);
1916
1916
  } catch (err) {
1917
1917
  resolvedScriptPath = this._scriptPath;
1918
1918
  }
1919
- executableDir = path2.resolve(
1920
- path2.dirname(resolvedScriptPath),
1919
+ executableDir = path5.resolve(
1920
+ path5.dirname(resolvedScriptPath),
1921
1921
  executableDir
1922
1922
  );
1923
1923
  }
1924
1924
  if (executableDir) {
1925
1925
  let localFile = findFile(executableDir, executableFile);
1926
1926
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1927
- const legacyName = path2.basename(
1927
+ const legacyName = path5.basename(
1928
1928
  this._scriptPath,
1929
- path2.extname(this._scriptPath)
1929
+ path5.extname(this._scriptPath)
1930
1930
  );
1931
1931
  if (legacyName !== this._name) {
1932
1932
  localFile = findFile(
@@ -1937,7 +1937,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1937
1937
  }
1938
1938
  executableFile = localFile || executableFile;
1939
1939
  }
1940
- launchWithNode = sourceExt.includes(path2.extname(executableFile));
1940
+ launchWithNode = sourceExt.includes(path5.extname(executableFile));
1941
1941
  let proc;
1942
1942
  if (process4.platform !== "win32") {
1943
1943
  if (launchWithNode) {
@@ -2777,7 +2777,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2777
2777
  * @return {Command}
2778
2778
  */
2779
2779
  nameFromFilename(filename) {
2780
- this._name = path2.basename(filename, path2.extname(filename));
2780
+ this._name = path5.basename(filename, path5.extname(filename));
2781
2781
  return this;
2782
2782
  }
2783
2783
  /**
@@ -2791,9 +2791,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2791
2791
  * @param {string} [path]
2792
2792
  * @return {(string|null|Command)}
2793
2793
  */
2794
- executableDir(path3) {
2795
- if (path3 === void 0) return this._executableDir;
2796
- this._executableDir = path3;
2794
+ executableDir(path6) {
2795
+ if (path6 === void 0) return this._executableDir;
2796
+ this._executableDir = path6;
2797
2797
  return this;
2798
2798
  }
2799
2799
  /**
@@ -6288,8 +6288,8 @@ var require_utils = __commonJS({
6288
6288
  }
6289
6289
  return ind;
6290
6290
  }
6291
- function removeDotSegments(path2) {
6292
- let input = path2;
6291
+ function removeDotSegments(path5) {
6292
+ let input = path5;
6293
6293
  const output = [];
6294
6294
  let nextSlash = -1;
6295
6295
  let len = 0;
@@ -6488,8 +6488,8 @@ var require_schemes = __commonJS({
6488
6488
  wsComponent.secure = void 0;
6489
6489
  }
6490
6490
  if (wsComponent.resourceName) {
6491
- const [path2, query] = wsComponent.resourceName.split("?");
6492
- wsComponent.path = path2 && path2 !== "/" ? path2 : void 0;
6491
+ const [path5, query] = wsComponent.resourceName.split("?");
6492
+ wsComponent.path = path5 && path5 !== "/" ? path5 : void 0;
6493
6493
  wsComponent.query = query;
6494
6494
  wsComponent.resourceName = void 0;
6495
6495
  }
@@ -9851,12 +9851,12 @@ var require_dist = __commonJS({
9851
9851
  throw new Error(`Unknown format "${name}"`);
9852
9852
  return f;
9853
9853
  };
9854
- function addFormats(ajv, list, fs, exportName) {
9854
+ function addFormats(ajv, list, fs4, exportName) {
9855
9855
  var _a2;
9856
9856
  var _b;
9857
9857
  (_a2 = (_b = ajv.opts.code).formats) !== null && _a2 !== void 0 ? _a2 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
9858
9858
  for (const f of list)
9859
- ajv.addFormat(f, fs[f]);
9859
+ ajv.addFormat(f, fs4[f]);
9860
9860
  }
9861
9861
  module2.exports = exports2 = formatsPlugin;
9862
9862
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -10787,8 +10787,8 @@ var resolvedOrgCache = /* @__PURE__ */ new Map();
10787
10787
  function cacheKey(apiUrl, token) {
10788
10788
  return `${apiUrl}::${token}`;
10789
10789
  }
10790
- function isOrgRequiredPath(path2) {
10791
- return path2.startsWith("/v1/console/") && !path2.startsWith("/v1/console/auth/");
10790
+ function isOrgRequiredPath(path5) {
10791
+ return path5.startsWith("/v1/console/") && !path5.startsWith("/v1/console/auth/");
10792
10792
  }
10793
10793
  async function discoverDefaultOrgId(apiUrl, token) {
10794
10794
  const key = cacheKey(apiUrl, token);
@@ -10810,16 +10810,16 @@ async function discoverDefaultOrgId(apiUrl, token) {
10810
10810
  }
10811
10811
  return usable;
10812
10812
  }
10813
- async function apiFetch(path2, init = {}) {
10814
- const res = await apiFetchRaw(path2, init);
10813
+ async function apiFetch(path5, init = {}) {
10814
+ const res = await apiFetchRaw(path5, init);
10815
10815
  return res.json();
10816
10816
  }
10817
- async function apiFetchRaw(path2, init = {}) {
10817
+ async function apiFetchRaw(path5, init = {}) {
10818
10818
  const { orgId, apiUrlOverride, ...fetchInit } = init;
10819
10819
  const creds = loadCredentials(apiUrlOverride);
10820
- const url2 = creds.apiUrl.replace(/\/$/, "") + path2;
10820
+ const url2 = creds.apiUrl.replace(/\/$/, "") + path5;
10821
10821
  let resolvedOrgId = isUsableOrgId(orgId) ? orgId : isUsableOrgId(creds.orgId) ? creds.orgId : void 0;
10822
- if (!resolvedOrgId && isOrgRequiredPath(path2)) {
10822
+ if (!resolvedOrgId && isOrgRequiredPath(path5)) {
10823
10823
  try {
10824
10824
  const discovered = await discoverDefaultOrgId(creds.apiUrl, creds.token);
10825
10825
  if (isUsableOrgId(discovered)) {
@@ -10853,7 +10853,7 @@ async function apiFetchRaw(path2, init = {}) {
10853
10853
  const errorMsg = body.error || responseText || `HTTP ${res.status}`;
10854
10854
  const remediation = body.remediation || getRemediation(res.status);
10855
10855
  throw new FohError({
10856
- step: path2,
10856
+ step: path5,
10857
10857
  error: errorMsg,
10858
10858
  remediation,
10859
10859
  statusCode: res.status,
@@ -13957,11 +13957,11 @@ function serialiseManifest(manifest) {
13957
13957
  function flattenObject(obj, prefix = "") {
13958
13958
  const result = {};
13959
13959
  for (const [key, value] of Object.entries(obj)) {
13960
- const path2 = prefix ? `${prefix}.${key}` : key;
13960
+ const path5 = prefix ? `${prefix}.${key}` : key;
13961
13961
  if (value !== null && typeof value === "object" && !Array.isArray(value)) {
13962
- Object.assign(result, flattenObject(value, path2));
13962
+ Object.assign(result, flattenObject(value, path5));
13963
13963
  } else {
13964
- result[path2] = value;
13964
+ result[path5] = value;
13965
13965
  }
13966
13966
  }
13967
13967
  return result;
@@ -13976,12 +13976,12 @@ function diffManifest(current, desired) {
13976
13976
  "agent"
13977
13977
  );
13978
13978
  const diffs = [];
13979
- for (const [path2, desiredVal] of Object.entries(desiredFlat)) {
13980
- const currentVal = currentFlat[path2];
13979
+ for (const [path5, desiredVal] of Object.entries(desiredFlat)) {
13980
+ const currentVal = currentFlat[path5];
13981
13981
  const currentSer = JSON.stringify(currentVal);
13982
13982
  const desiredSer = JSON.stringify(desiredVal);
13983
13983
  if (currentSer !== desiredSer) {
13984
- diffs.push({ path: path2, from: currentVal, to: desiredVal });
13984
+ diffs.push({ path: path5, from: currentVal, to: desiredVal });
13985
13985
  }
13986
13986
  }
13987
13987
  return diffs;
@@ -15180,15 +15180,406 @@ function registerTemplates(program3) {
15180
15180
  }));
15181
15181
  }
15182
15182
 
15183
- // src/commands/widget.ts
15184
- var SMOKE_TURNS = [
15185
- "Hello, I need help with a property",
15186
- "I am interested in buying a 3-bedroom house in the area",
15187
- "Can I book a viewing for this week?"
15183
+ // src/lib/widget-platforms.ts
15184
+ var import_node_fs = __toESM(require("node:fs"));
15185
+ var import_node_os = __toESM(require("node:os"));
15186
+ var import_node_path = __toESM(require("node:path"));
15187
+ var import_node_child_process = require("node:child_process");
15188
+
15189
+ // src/lib/widget-install-guidance.ts
15190
+ function normalizeOriginHint(originOrHost) {
15191
+ if (!originOrHost) return null;
15192
+ const value = originOrHost.trim();
15193
+ if (!value) return null;
15194
+ if (/^https?:\/\//i.test(value)) return value;
15195
+ return `https://${value}`;
15196
+ }
15197
+ function uniqueCommands(commands) {
15198
+ return Array.from(new Set(commands.filter((command) => Boolean(command && command.trim()))));
15199
+ }
15200
+ function buildWidgetSmokeCommand(params) {
15201
+ const origin = normalizeOriginHint(params.origin);
15202
+ if (params.publicKey) {
15203
+ return `foh widget smoke --channel ${params.publicKey} --json${origin ? ` --origin ${origin}` : ""}`;
15204
+ }
15205
+ if (params.agentId) {
15206
+ return `foh widget smoke --agent ${params.agentId} --json`;
15207
+ }
15208
+ return null;
15209
+ }
15210
+ function buildWidgetVerifyInstallCommand(params) {
15211
+ if (!params.publicKey) return null;
15212
+ const origin = normalizeOriginHint(params.origin);
15213
+ const resolvedOrigin = origin ?? (params.placeholderOrigin ? "https://www.example.com" : null);
15214
+ return `foh widget verify-install --channel ${params.publicKey} --json${resolvedOrigin ? ` --origin ${resolvedOrigin}` : ""}`;
15215
+ }
15216
+ function buildWidgetInstallGuidance(params) {
15217
+ const domainAllowlist = Array.isArray(params.domains) ? params.domains.map((domain2) => String(domain2 || "").trim()).filter(Boolean) : [];
15218
+ const originHint = normalizeOriginHint(params.origin) ?? normalizeOriginHint(domainAllowlist[0]);
15219
+ const installVerificationCommand = buildWidgetVerifyInstallCommand({
15220
+ publicKey: params.publicKey,
15221
+ origin: originHint,
15222
+ placeholderOrigin: params.placeholderOrigin
15223
+ });
15224
+ const runtimeSmokeCommand = buildWidgetSmokeCommand({
15225
+ publicKey: params.publicKey,
15226
+ agentId: params.agentId,
15227
+ origin: originHint
15228
+ });
15229
+ return {
15230
+ widget_public_key: params.publicKey?.trim() || null,
15231
+ domain_allowlist: domainAllowlist,
15232
+ origin_hint: originHint,
15233
+ install_verification_command: installVerificationCommand,
15234
+ runtime_smoke_command: runtimeSmokeCommand,
15235
+ verification_commands: uniqueCommands([installVerificationCommand ?? runtimeSmokeCommand])
15236
+ };
15237
+ }
15238
+
15239
+ // src/lib/widget-platforms.ts
15240
+ var DEFAULT_WIDGET_API_URL = "https://api.frontofhouse.okii.uk";
15241
+ var DEFAULT_WORDPRESS_PLUGIN_NAME = "Front Of House Widget";
15242
+ var SUPPORTED_INSTALL_PLATFORMS = ["custom", "wordpress", "wix", "squarespace", "webflow", "shopify"];
15243
+ var SHARED_BUILDER_INTRO = [
15244
+ "Use the platform custom-code or embed surface, not a theme redesign path.",
15245
+ "Paste the hosted Front Of House snippet exactly as generated unless a native adapter is provided.",
15246
+ "Keep the widget domain allowlist aligned to the real production website hostnames."
15188
15247
  ];
15189
- function isGenericTroubleReply(reply) {
15190
- const normalized = reply.trim().toLowerCase();
15191
- return /\bi'?m sorry\b/.test(normalized) && /having trouble|try again|something went wrong|unable to help right now/.test(normalized);
15248
+ var BUILDER_PLATFORM_GUIDES = {
15249
+ wordpress: {
15250
+ label: "WordPress",
15251
+ paste_surface: "Custom HTML block or site-wide code injection area",
15252
+ steps: [
15253
+ "Preferred path: generate the preconfigured WordPress plugin bundle and upload/activate it in WordPress.",
15254
+ "Fallback path: open the page, template, or footer area where the widget should load.",
15255
+ "Use a Custom HTML block for page-level install, or the site code-injection/header-footer area for site-wide install.",
15256
+ "If using manual embed, paste the snippet once, publish the page/theme change, then verify on the live site domain."
15257
+ ],
15258
+ nativeAdapterCommand: ({ publicKey }) => publicKey ? `foh widget wordpress-plugin --channel ${publicKey} --out ./front-of-house-widget --json` : null
15259
+ },
15260
+ wix: {
15261
+ label: "Wix",
15262
+ paste_surface: "Custom code or embed area",
15263
+ steps: [
15264
+ "Open the site custom-code or embed settings for the target page or all pages.",
15265
+ "Paste the snippet into the body/site code area that supports custom scripts.",
15266
+ "Publish the Wix site, then verify on the live site domain."
15267
+ ]
15268
+ },
15269
+ squarespace: {
15270
+ label: "Squarespace",
15271
+ paste_surface: "Code Injection or Embed block",
15272
+ steps: [
15273
+ "Open the target page or the global code-injection area.",
15274
+ "Paste the snippet into a code/embed block for page-level install or the global injection area for site-wide install.",
15275
+ "Save, publish, and verify on the live site domain."
15276
+ ]
15277
+ },
15278
+ webflow: {
15279
+ label: "Webflow",
15280
+ paste_surface: "Embed element or page/site custom code",
15281
+ steps: [
15282
+ "Open the target page or the site/page custom-code settings.",
15283
+ "Paste the snippet into an Embed element or the footer custom-code area.",
15284
+ "Publish the site, then verify on the live site domain."
15285
+ ]
15286
+ },
15287
+ shopify: {
15288
+ label: "Shopify",
15289
+ paste_surface: "Theme custom liquid/embed area",
15290
+ steps: [
15291
+ "Open the active theme customization or theme code area.",
15292
+ "Add the snippet once through a custom liquid/embed block or the theme custom-code area.",
15293
+ "Save the theme change, publish if needed, then verify on the live storefront domain."
15294
+ ]
15295
+ }
15296
+ };
15297
+ function resolveWidgetApiUrl(apiUrlOverride) {
15298
+ return (apiUrlOverride || DEFAULT_WIDGET_API_URL).replace(/\/$/, "");
15299
+ }
15300
+ function widgetSnippetFromPublicKey(publicKey, apiUrl) {
15301
+ const resolvedApiUrl = resolveWidgetApiUrl(apiUrl);
15302
+ return `<script src="${resolvedApiUrl}/widget.js" data-channel="${publicKey}" data-api-url="${resolvedApiUrl}" async></script>`;
15303
+ }
15304
+ function normalizeInstallPlatform(value) {
15305
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : "custom";
15306
+ if (!normalized) return "custom";
15307
+ if (SUPPORTED_INSTALL_PLATFORMS.includes(normalized)) return normalized;
15308
+ throw new FohError({
15309
+ step: "install.platform",
15310
+ error: `Unsupported platform: ${String(value || "")}`,
15311
+ remediation: `Use one of: ${SUPPORTED_INSTALL_PLATFORMS.join(", ")}`
15312
+ });
15313
+ }
15314
+ function isBuilderHostedPlatform(platform) {
15315
+ return platform !== "custom";
15316
+ }
15317
+ function buildBuilderPlatformInstructions(params) {
15318
+ const guide = BUILDER_PLATFORM_GUIDES[params.platform];
15319
+ const guidance = buildWidgetInstallGuidance({
15320
+ publicKey: params.publicKey,
15321
+ domains: params.domains,
15322
+ origin: params.origin
15323
+ });
15324
+ return {
15325
+ schema_version: "foh_widget_builder_install.v1",
15326
+ platform: params.platform,
15327
+ platform_label: guide.label,
15328
+ paste_surface: guide.paste_surface,
15329
+ snippet: params.snippet,
15330
+ ...guidance,
15331
+ steps: [...SHARED_BUILDER_INTRO, ...guide.steps],
15332
+ native_adapter_command: guide.nativeAdapterCommand?.({ publicKey: params.publicKey }) ?? null
15333
+ };
15334
+ }
15335
+ function slugifyWordPressPluginName(name) {
15336
+ const normalized = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
15337
+ return normalized || "front-of-house-widget";
15338
+ }
15339
+ function renderWordPressPluginPhp(params) {
15340
+ return `<?php
15341
+ /**
15342
+ * Plugin Name: ${params.pluginName}
15343
+ * Description: Injects the hosted Front Of House widget site-wide.
15344
+ * Version: 0.1.0
15345
+ * Author: Front Of House
15346
+ */
15347
+
15348
+ if (!defined('ABSPATH')) {
15349
+ exit;
15350
+ }
15351
+
15352
+ function foh_widget_render() {
15353
+ ?>
15354
+ <script src="<?php echo esc_url('${params.apiUrl}/widget.js'); ?>" data-channel="<?php echo esc_attr('${params.publicKey}'); ?>" data-api-url="<?php echo esc_url('${params.apiUrl}'); ?>" async></script>
15355
+ <?php
15356
+ }
15357
+
15358
+ add_action('wp_footer', 'foh_widget_render', 100);
15359
+ `;
15360
+ }
15361
+ function renderWordPressPluginReadme(params) {
15362
+ return `${params.pluginName}
15363
+ ======================
15364
+
15365
+ This plugin injects the hosted Front Of House widget site-wide.
15366
+
15367
+ Public widget key: ${params.publicKey}
15368
+ API URL: ${params.apiUrl}
15369
+
15370
+ Install:
15371
+ 1. Upload this plugin to WordPress.
15372
+ 2. Activate it.
15373
+ 3. Verify the live site with:
15374
+ ${params.verificationCommand}
15375
+ `;
15376
+ }
15377
+ function writeUtf8File(targetPath, content, dryRun) {
15378
+ if (dryRun) return;
15379
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(targetPath), { recursive: true });
15380
+ import_node_fs.default.writeFileSync(targetPath, content, "utf8");
15381
+ }
15382
+ function packZipDirectory(sourceDir, outputZipPath) {
15383
+ if (process.platform === "win32") {
15384
+ const script = `Compress-Archive -LiteralPath '${sourceDir.replace(/'/g, "''")}' -DestinationPath '${outputZipPath.replace(/'/g, "''")}' -Force`;
15385
+ const result2 = (0, import_node_child_process.spawnSync)("powershell.exe", ["-NoProfile", "-Command", script], {
15386
+ encoding: "utf8",
15387
+ stdio: ["ignore", "pipe", "pipe"],
15388
+ windowsHide: true
15389
+ });
15390
+ if (result2.status !== 0) {
15391
+ throw new FohError({
15392
+ step: "widget.wordpress_plugin",
15393
+ error: "Failed to package the WordPress plugin zip.",
15394
+ remediation: "Use a directory output path instead of a .zip path, or rerun on a host with zip packaging support.",
15395
+ detail: {
15396
+ stdout: String(result2.stdout || ""),
15397
+ stderr: String(result2.stderr || "")
15398
+ }
15399
+ });
15400
+ }
15401
+ return;
15402
+ }
15403
+ const result = (0, import_node_child_process.spawnSync)("zip", ["-qr", outputZipPath, import_node_path.default.basename(sourceDir)], {
15404
+ cwd: import_node_path.default.dirname(sourceDir),
15405
+ encoding: "utf8",
15406
+ stdio: ["ignore", "pipe", "pipe"]
15407
+ });
15408
+ if (result.error || result.status !== 0) {
15409
+ throw new FohError({
15410
+ step: "widget.wordpress_plugin",
15411
+ error: "Failed to package the WordPress plugin zip.",
15412
+ remediation: "Use a directory output path instead of a .zip path, or install a zip tool and retry.",
15413
+ detail: {
15414
+ stdout: String(result.stdout || ""),
15415
+ stderr: String(result.stderr || "")
15416
+ }
15417
+ });
15418
+ }
15419
+ }
15420
+ function buildWordPressPluginBundle(params) {
15421
+ const pluginName = (params.pluginName || DEFAULT_WORDPRESS_PLUGIN_NAME).trim() || DEFAULT_WORDPRESS_PLUGIN_NAME;
15422
+ const pluginSlug = slugifyWordPressPluginName(pluginName);
15423
+ const apiUrl = resolveWidgetApiUrl(params.apiUrl);
15424
+ const snippet2 = widgetSnippetFromPublicKey(params.publicKey, apiUrl);
15425
+ const guidance = buildWidgetInstallGuidance({
15426
+ publicKey: params.publicKey,
15427
+ placeholderOrigin: true
15428
+ });
15429
+ const requestedOutput = import_node_path.default.resolve(params.outPath);
15430
+ const zipOutput = /\.zip$/i.test(requestedOutput);
15431
+ const artifactType = zipOutput ? "zip" : "directory";
15432
+ const bundleRoot = zipOutput ? import_node_path.default.join(import_node_fs.default.mkdtempSync(import_node_path.default.join(import_node_os.default.tmpdir(), "foh-wp-plugin-")), pluginSlug) : requestedOutput;
15433
+ const entryFile = import_node_path.default.join(bundleRoot, `${pluginSlug}.php`);
15434
+ const readmeFile = import_node_path.default.join(bundleRoot, "README.txt");
15435
+ if (!params.dryRun) {
15436
+ if (artifactType === "directory" && import_node_fs.default.existsSync(requestedOutput)) {
15437
+ throw new FohError({
15438
+ step: "widget.wordpress_plugin",
15439
+ error: `Output path already exists: ${requestedOutput}`,
15440
+ remediation: "Choose a new output path for the plugin bundle."
15441
+ });
15442
+ }
15443
+ if (artifactType === "zip" && import_node_fs.default.existsSync(requestedOutput)) {
15444
+ throw new FohError({
15445
+ step: "widget.wordpress_plugin",
15446
+ error: `Output zip already exists: ${requestedOutput}`,
15447
+ remediation: "Choose a new .zip output path for the plugin bundle."
15448
+ });
15449
+ }
15450
+ }
15451
+ try {
15452
+ writeUtf8File(entryFile, renderWordPressPluginPhp({
15453
+ pluginName,
15454
+ publicKey: params.publicKey,
15455
+ apiUrl
15456
+ }), Boolean(params.dryRun));
15457
+ writeUtf8File(readmeFile, renderWordPressPluginReadme({
15458
+ pluginName,
15459
+ publicKey: params.publicKey,
15460
+ apiUrl,
15461
+ verificationCommand: guidance.install_verification_command || `foh widget verify-install --channel ${params.publicKey} --json --origin https://www.example.com`
15462
+ }), Boolean(params.dryRun));
15463
+ if (!params.dryRun && artifactType === "zip") {
15464
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(requestedOutput), { recursive: true });
15465
+ packZipDirectory(bundleRoot, requestedOutput);
15466
+ }
15467
+ return {
15468
+ schema_version: "foh_wordpress_plugin_bundle.v1",
15469
+ status: params.dryRun ? "dry_run" : "generated",
15470
+ artifact_type: artifactType,
15471
+ output_path: requestedOutput,
15472
+ plugin_name: pluginName,
15473
+ plugin_slug: pluginSlug,
15474
+ plugin_entry_file: artifactType === "zip" ? `${pluginSlug}/${pluginSlug}.php` : import_node_path.default.relative(process.cwd(), entryFile).replaceAll("\\", "/"),
15475
+ ...guidance,
15476
+ api_url: apiUrl,
15477
+ snippet: snippet2
15478
+ };
15479
+ } finally {
15480
+ if (!params.dryRun && artifactType === "zip") {
15481
+ try {
15482
+ import_node_fs.default.rmSync(import_node_path.default.dirname(bundleRoot), { recursive: true, force: true });
15483
+ } catch {
15484
+ }
15485
+ }
15486
+ }
15487
+ }
15488
+
15489
+ // src/lib/widget-site-install.ts
15490
+ var import_node_fs2 = __toESM(require("node:fs"));
15491
+ var import_node_path2 = __toESM(require("node:path"));
15492
+ var WIDGET_MARKER_START = "<!-- Front Of House widget start -->";
15493
+ var WIDGET_MARKER_END = "<!-- Front Of House widget end -->";
15494
+ var WIDGET_JSX_MARKER_START = "{/* Front Of House widget start */}";
15495
+ var WIDGET_JSX_MARKER_END = "{/* Front Of House widget end */}";
15496
+ function isPathInside(basePath, candidatePath) {
15497
+ const relative4 = import_node_path2.default.relative(basePath, candidatePath);
15498
+ return relative4 === "" || !relative4.startsWith("..") && !import_node_path2.default.isAbsolute(relative4);
15499
+ }
15500
+ function normalizeSnippet(snippet2) {
15501
+ return snippet2.trim();
15502
+ }
15503
+ function escapeRegex(value) {
15504
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15505
+ }
15506
+ function isJsxLayoutPath(targetPath) {
15507
+ return /\.(tsx|jsx)$/i.test(targetPath);
15508
+ }
15509
+ function markedSnippet(snippet2, targetPath) {
15510
+ const normalized = normalizeSnippet(snippet2);
15511
+ if (isJsxLayoutPath(targetPath)) {
15512
+ return `${WIDGET_JSX_MARKER_START}
15513
+ ${normalized}
15514
+ ${WIDGET_JSX_MARKER_END}`;
15515
+ }
15516
+ return `${WIDGET_MARKER_START}
15517
+ ${normalized}
15518
+ ${WIDGET_MARKER_END}`;
15519
+ }
15520
+ function extractChannelPublicKeyFromSnippet(snippet2) {
15521
+ const match = snippet2.match(/\bdata-channel=(["'])([^"']+)\1/i);
15522
+ return match?.[2]?.trim() || void 0;
15523
+ }
15524
+ function installWidgetSnippetIntoShell(source, snippet2, targetPath) {
15525
+ const jsxShell = isJsxLayoutPath(targetPath);
15526
+ const markerStart = jsxShell ? WIDGET_JSX_MARKER_START : WIDGET_MARKER_START;
15527
+ const markerEnd = jsxShell ? WIDGET_JSX_MARKER_END : WIDGET_MARKER_END;
15528
+ const block = markedSnippet(snippet2, targetPath);
15529
+ const existingBlockPattern = new RegExp(
15530
+ `${escapeRegex(markerStart)}[\\s\\S]*?${escapeRegex(markerEnd)}`,
15531
+ "m"
15532
+ );
15533
+ if (existingBlockPattern.test(source)) {
15534
+ const html2 = source.replace(existingBlockPattern, block);
15535
+ return {
15536
+ html: html2,
15537
+ changed: html2 !== source,
15538
+ mode: html2 === source ? "unchanged" : "replaced",
15539
+ markerStart,
15540
+ markerEnd
15541
+ };
15542
+ }
15543
+ if (!/<\/body>/i.test(source)) {
15544
+ throw new FohError({
15545
+ step: "widget.install_site",
15546
+ error: "No </body> tag found in target file.",
15547
+ remediation: "Pass --target for a site shell such as index.html or app/layout.tsx, or paste the embed snippet into the site shell manually."
15548
+ });
15549
+ }
15550
+ const html = source.replace(/<\/body>/i, ` ${block}
15551
+ </body>`);
15552
+ return { html, changed: html !== source, mode: "inserted", markerStart, markerEnd };
15553
+ }
15554
+ function resolveWidgetInstallTarget(siteRoot, target) {
15555
+ const root = import_node_path2.default.resolve(siteRoot || ".");
15556
+ const candidates = target ? [target] : [
15557
+ "index.html",
15558
+ import_node_path2.default.join("apps", "web", "index.html"),
15559
+ import_node_path2.default.join("apps", "frontend", "index.html"),
15560
+ import_node_path2.default.join("apps", "site", "index.html"),
15561
+ import_node_path2.default.join("apps", "landing", "index.html"),
15562
+ import_node_path2.default.join("app", "layout.tsx"),
15563
+ import_node_path2.default.join("app", "layout.jsx"),
15564
+ import_node_path2.default.join("src", "app", "layout.tsx"),
15565
+ import_node_path2.default.join("src", "app", "layout.jsx")
15566
+ ];
15567
+ for (const candidate of candidates) {
15568
+ const resolved = import_node_path2.default.resolve(root, candidate);
15569
+ if (!isPathInside(root, resolved)) {
15570
+ throw new FohError({
15571
+ step: "widget.install_site",
15572
+ error: `Refusing to patch a file outside --site-root: ${candidate}`,
15573
+ remediation: "Run the command from the website repository root or pass a target inside --site-root."
15574
+ });
15575
+ }
15576
+ if (import_node_fs2.default.existsSync(resolved) && import_node_fs2.default.statSync(resolved).isFile()) return resolved;
15577
+ }
15578
+ throw new FohError({
15579
+ step: "widget.install_site",
15580
+ error: "Could not find a supported site shell to patch.",
15581
+ remediation: "Pass --target index.html, --target apps/web/index.html, or --target app/layout.tsx from the website repository root."
15582
+ });
15192
15583
  }
15193
15584
  async function resolveChannelPublicKey(agentId, orgId, apiUrlOverride) {
15194
15585
  const data = await apiFetch(
@@ -15204,7 +15595,209 @@ async function resolveChannelPublicKey(agentId, orgId, apiUrlOverride) {
15204
15595
  }
15205
15596
  return data.channel.public_key;
15206
15597
  }
15207
- async function runWidgetSmoke(publicKey, apiUrlOverride) {
15598
+ async function installWidgetSite(opts) {
15599
+ if (!opts.snippet && !opts.agent) {
15600
+ throw new FohError({
15601
+ step: "widget.install_site",
15602
+ error: "Missing --agent or --snippet.",
15603
+ remediation: "Run: foh install <install-link>, or foh install --agent <agent-id> --domains example.com,www.example.com"
15604
+ });
15605
+ }
15606
+ const agentId = opts.agent;
15607
+ const domains = Array.isArray(opts.domains) ? opts.domains.map((domain2) => domain2.trim()).filter(Boolean) : [];
15608
+ let snippet2 = typeof opts.snippet === "string" ? opts.snippet : "";
15609
+ let widgetPublicKey;
15610
+ if (!snippet2) {
15611
+ if (!agentId) {
15612
+ throw new FohError({
15613
+ step: "widget.install_site",
15614
+ error: "Missing --agent.",
15615
+ remediation: "Run: foh install <install-link>, or foh install --agent <agent-id>"
15616
+ });
15617
+ }
15618
+ await apiFetch("/v1/console/channels/widget/ensure", {
15619
+ method: "POST",
15620
+ body: JSON.stringify({ agentId }),
15621
+ orgId: opts.org,
15622
+ apiUrlOverride: opts.apiUrl
15623
+ });
15624
+ if (domains.length > 0) {
15625
+ await apiFetch("/v1/console/channels/widget/domains", {
15626
+ method: "POST",
15627
+ body: JSON.stringify({ agentId, domains }),
15628
+ orgId: opts.org,
15629
+ apiUrlOverride: opts.apiUrl
15630
+ });
15631
+ }
15632
+ const embed = await apiFetch(
15633
+ "/v1/console/channels/widget/embed-snippet",
15634
+ {
15635
+ orgId: opts.org,
15636
+ apiUrlOverride: opts.apiUrl,
15637
+ headers: { "x-agent-id": agentId }
15638
+ }
15639
+ );
15640
+ snippet2 = String(embed.snippet || "").trim();
15641
+ widgetPublicKey = embed.widget_public_key;
15642
+ } else {
15643
+ widgetPublicKey = extractChannelPublicKeyFromSnippet(snippet2);
15644
+ }
15645
+ if (!snippet2) {
15646
+ throw new FohError({
15647
+ step: "widget.install_site",
15648
+ error: "Widget embed snippet is empty.",
15649
+ remediation: "Run: foh widget embed-snippet --agent <agent-id> --json"
15650
+ });
15651
+ }
15652
+ const targetPath = resolveWidgetInstallTarget(opts.siteRoot || ".", opts.target);
15653
+ const before = import_node_fs2.default.readFileSync(targetPath, "utf8");
15654
+ const patch = installWidgetSnippetIntoShell(before, snippet2, targetPath);
15655
+ if (!opts.dryRun && patch.changed) {
15656
+ import_node_fs2.default.writeFileSync(targetPath, patch.html, "utf8");
15657
+ }
15658
+ const relativeTarget = import_node_path2.default.relative(process.cwd(), targetPath).replaceAll("\\", "/");
15659
+ const guidance = buildWidgetInstallGuidance({
15660
+ publicKey: widgetPublicKey ?? null,
15661
+ agentId: opts.agent,
15662
+ domains
15663
+ });
15664
+ return {
15665
+ schema_version: "foh_widget_site_install.v1",
15666
+ status: opts.dryRun ? "dry_run" : "installed",
15667
+ target_path: relativeTarget,
15668
+ absolute_target_path: targetPath,
15669
+ changed: patch.changed,
15670
+ mode: patch.mode,
15671
+ marker_start: patch.markerStart,
15672
+ marker_end: patch.markerEnd,
15673
+ ...guidance,
15674
+ snippet: snippet2,
15675
+ next_commands: ["build the website", ...guidance.verification_commands]
15676
+ };
15677
+ }
15678
+ async function ensureWidgetChannelPublicKey(agentId, orgId, apiUrlOverride) {
15679
+ return resolveChannelPublicKey(agentId, orgId, apiUrlOverride);
15680
+ }
15681
+
15682
+ // src/lib/widget-public.ts
15683
+ var SMOKE_TURNS = [
15684
+ "Hello, I need help with a property",
15685
+ "I am interested in buying a 3-bedroom house in the area",
15686
+ "Can I book a viewing for this week?"
15687
+ ];
15688
+ function isGenericTroubleReply(reply) {
15689
+ const normalized = reply.trim().toLowerCase();
15690
+ return /\bi'?m sorry\b/.test(normalized) && /having trouble|try again|something went wrong|unable to help right now/.test(normalized);
15691
+ }
15692
+ function isProgressPlaceholderReply(reply) {
15693
+ const normalized = reply.trim().toLowerCase();
15694
+ return /still working on that|one moment|just a moment|please hold|bear with me/.test(normalized);
15695
+ }
15696
+ function normalizeOrigin(originOrHost) {
15697
+ if (!originOrHost) return void 0;
15698
+ const value = originOrHost.trim();
15699
+ if (!value) return void 0;
15700
+ if (/^https?:\/\//i.test(value)) return value;
15701
+ return `https://${value}`;
15702
+ }
15703
+ async function postPublicWidgetInbound(payload, apiUrlOverride, originOverride) {
15704
+ const res = await fetch(`${resolveWidgetApiUrl(apiUrlOverride)}/v1/widget/inbound`, {
15705
+ method: "POST",
15706
+ headers: {
15707
+ "Content-Type": "application/json",
15708
+ ...originOverride ? { Origin: originOverride } : {}
15709
+ },
15710
+ body: JSON.stringify(payload)
15711
+ });
15712
+ let body = {};
15713
+ try {
15714
+ body = await res.json();
15715
+ } catch {
15716
+ body = {};
15717
+ }
15718
+ if (!res.ok) {
15719
+ throw new FohError({
15720
+ step: "/v1/widget/inbound",
15721
+ error: String(body?.error || `HTTP ${res.status}`),
15722
+ remediation: String(body?.remediation || "Check the widget public key and API health, then retry."),
15723
+ statusCode: res.status,
15724
+ detail: body
15725
+ });
15726
+ }
15727
+ return body;
15728
+ }
15729
+ async function fetchPublicWidgetInstallVerification(publicKey, apiUrlOverride, originOverride) {
15730
+ const apiUrl = resolveWidgetApiUrl(apiUrlOverride);
15731
+ const url2 = new URL(`${apiUrl}/v1/widget/install-verification`);
15732
+ url2.searchParams.set("key", publicKey);
15733
+ const res = await fetch(url2.toString(), {
15734
+ method: "GET",
15735
+ headers: {
15736
+ ...originOverride ? { Origin: originOverride } : {}
15737
+ }
15738
+ });
15739
+ let body = {};
15740
+ try {
15741
+ body = await res.json();
15742
+ } catch {
15743
+ body = {};
15744
+ }
15745
+ if (!res.ok) {
15746
+ throw new FohError({
15747
+ step: "/v1/widget/install-verification",
15748
+ error: String(body?.error || `HTTP ${res.status}`),
15749
+ remediation: String(body?.code === "domain_not_allowed" ? "Use the live website origin with --origin https://www.example.com and confirm the domain allowlist." : body?.code === "invalid_widget_public_key" ? "Check the widget public key or regenerate the install link." : "Check the widget public key, origin, and API health, then retry."),
15750
+ statusCode: res.status,
15751
+ detail: body
15752
+ });
15753
+ }
15754
+ return body;
15755
+ }
15756
+ function normalizeReplyText(value) {
15757
+ return value.trim().toLowerCase();
15758
+ }
15759
+ function matchAnyTerms(reply, terms) {
15760
+ if (!Array.isArray(terms) || terms.length === 0) return true;
15761
+ const normalized = normalizeReplyText(reply);
15762
+ return terms.some((term) => normalized.includes(term.toLowerCase()));
15763
+ }
15764
+ function matchForbiddenTerms(reply, terms) {
15765
+ if (!Array.isArray(terms) || terms.length === 0) return false;
15766
+ const normalized = normalizeReplyText(reply);
15767
+ return terms.some((term) => normalized.includes(term.toLowerCase()));
15768
+ }
15769
+ function resolveSemanticEvalPack(profile) {
15770
+ const semanticEvalProfile = typeof profile?.semantic_eval_profile === "string" ? profile.semantic_eval_profile.trim().toLowerCase() : "";
15771
+ const businessName = typeof profile?.business_name === "string" ? profile.business_name.trim().toLowerCase() : "";
15772
+ const businessCategory = typeof profile?.business_category === "string" ? profile.business_category.trim().toLowerCase() : "";
15773
+ if (semanticEvalProfile === "okii_consulting" || businessName === "okii" || businessCategory === "real_estate_ai_consulting") {
15774
+ return {
15775
+ id: "okii_consulting",
15776
+ scenarios: [
15777
+ {
15778
+ id: "identity",
15779
+ message: "What does OKII do?",
15780
+ required_any: ["okii", "ai", "front", "automation"],
15781
+ forbidden_any: ["estate agency", "estate agent", "book a viewing"]
15782
+ },
15783
+ {
15784
+ id: "non_estate_identity",
15785
+ message: "Are you an estate agent?",
15786
+ required_any: ["not an estate agency", "not an estate agent", "okii"],
15787
+ forbidden_any: ["we can book a viewing", "valuation request"]
15788
+ },
15789
+ {
15790
+ id: "consulting_lead",
15791
+ message: "I run an estate agency and want AI help with calls and leads.",
15792
+ required_any: ["ai", "calls", "leads", "workflow", "automate"],
15793
+ forbidden_any: ["which property", "book the viewing"]
15794
+ }
15795
+ ]
15796
+ };
15797
+ }
15798
+ return null;
15799
+ }
15800
+ async function runWidgetSmoke(publicKey, apiUrlOverride, originOverride) {
15208
15801
  let conversationId;
15209
15802
  const traceIds = [];
15210
15803
  const correlationIds = [];
@@ -15213,34 +15806,31 @@ async function runWidgetSmoke(publicKey, apiUrlOverride) {
15213
15806
  const message = SMOKE_TURNS[i];
15214
15807
  const start = Date.now();
15215
15808
  try {
15216
- const data = await apiFetch(
15217
- "/v1/widget/inbound",
15218
- {
15219
- method: "POST",
15220
- body: JSON.stringify({
15221
- channel_public_key: publicKey,
15222
- message_body: message,
15223
- preview: true,
15224
- ...conversationId ? { conversation_id: conversationId } : {}
15225
- }),
15226
- apiUrlOverride
15227
- }
15228
- );
15809
+ const data = await postPublicWidgetInbound({
15810
+ channel_public_key: publicKey,
15811
+ message_body: message,
15812
+ ...conversationId ? { conversation_id: conversationId } : {}
15813
+ }, apiUrlOverride, originOverride);
15229
15814
  const latencyMs = Date.now() - start;
15230
15815
  if (!data.reply) throw new Error("Empty reply");
15231
15816
  conversationId = data.conversationId;
15232
15817
  if (data.trace_id) traceIds.push(data.trace_id);
15233
15818
  if (data.correlation_id) correlationIds.push(data.correlation_id);
15234
15819
  const genericTroubleReply = isGenericTroubleReply(data.reply);
15820
+ const placeholderReply = isProgressPlaceholderReply(data.reply);
15821
+ const unhealthyReply = genericTroubleReply || placeholderReply;
15235
15822
  turns.push({
15236
15823
  turn: i + 1,
15237
15824
  message,
15238
- ok: !genericTroubleReply,
15825
+ ok: !unhealthyReply,
15239
15826
  latency_ms: latencyMs,
15240
15827
  reply: data.reply,
15241
15828
  ...genericTroubleReply ? {
15242
15829
  reason_code: "widget_generic_trouble_reply",
15243
15830
  error: "Widget returned a generic trouble reply instead of advancing the customer request."
15831
+ } : placeholderReply ? {
15832
+ reason_code: "widget_progress_placeholder_reply",
15833
+ error: "Widget returned a placeholder holding reply instead of advancing the customer request."
15244
15834
  } : {},
15245
15835
  conversation_id: data.conversationId,
15246
15836
  trace_id: data.trace_id ?? null,
@@ -15261,6 +15851,119 @@ async function runWidgetSmoke(publicKey, apiUrlOverride) {
15261
15851
  turns
15262
15852
  };
15263
15853
  }
15854
+ async function runWidgetSemanticEval(publicKey, apiUrlOverride, originOverride, profile) {
15855
+ const pack = resolveSemanticEvalPack(profile);
15856
+ if (!pack) {
15857
+ return {
15858
+ pack_id: null,
15859
+ passed: 0,
15860
+ failed: 0,
15861
+ turns: []
15862
+ };
15863
+ }
15864
+ const turns = [];
15865
+ for (const scenario of pack.scenarios) {
15866
+ try {
15867
+ const data = await postPublicWidgetInbound({
15868
+ channel_public_key: publicKey,
15869
+ message_body: scenario.message
15870
+ }, apiUrlOverride, originOverride);
15871
+ const reply = String(data.reply || "");
15872
+ const missingRequired = !matchAnyTerms(reply, scenario.required_any);
15873
+ const foundForbidden = matchForbiddenTerms(reply, scenario.forbidden_any);
15874
+ const ok = !missingRequired && !foundForbidden && !isGenericTroubleReply(reply) && !isProgressPlaceholderReply(reply);
15875
+ turns.push({
15876
+ scenario_id: scenario.id,
15877
+ ok,
15878
+ message: scenario.message,
15879
+ reply,
15880
+ ...ok ? {} : {
15881
+ reason_code: missingRequired ? "widget_semantic_required_terms_missing" : foundForbidden ? "widget_semantic_forbidden_terms_found" : isGenericTroubleReply(reply) ? "widget_generic_trouble_reply" : "widget_progress_placeholder_reply"
15882
+ },
15883
+ trace_id: data.trace_id ?? null,
15884
+ correlation_id: data.correlation_id ?? null
15885
+ });
15886
+ } catch (err) {
15887
+ turns.push({
15888
+ scenario_id: scenario.id,
15889
+ ok: false,
15890
+ message: scenario.message,
15891
+ error: err?.message || String(err),
15892
+ reason_code: "widget_semantic_request_failed"
15893
+ });
15894
+ }
15895
+ }
15896
+ return {
15897
+ pack_id: pack.id,
15898
+ passed: turns.filter((turn) => turn.ok).length,
15899
+ failed: turns.filter((turn) => !turn.ok).length,
15900
+ turns
15901
+ };
15902
+ }
15903
+ async function runWidgetInstallVerification(publicKey, apiUrlOverride, originOverride) {
15904
+ const checks = [];
15905
+ try {
15906
+ const config2 = await fetchPublicWidgetInstallVerification(publicKey, apiUrlOverride, originOverride);
15907
+ checks.push({
15908
+ name: "install_config",
15909
+ status: "pass",
15910
+ reason_code: "install_config_ok",
15911
+ summary: "Public install verification config is reachable and the origin passed the domain gate.",
15912
+ detail: config2
15913
+ });
15914
+ } catch (error2) {
15915
+ checks.push({
15916
+ name: "install_config",
15917
+ status: "fail",
15918
+ reason_code: error2?.detail?.code === "domain_not_allowed" ? "domain_not_allowed" : error2?.detail?.code === "invalid_widget_public_key" ? "invalid_widget_public_key" : "install_config_request_failed",
15919
+ summary: error2?.message || String(error2),
15920
+ detail: error2?.detail || null,
15921
+ next_command: "Confirm the live site origin and widget public key, then rerun verification."
15922
+ });
15923
+ checks.push({
15924
+ name: "widget_smoke",
15925
+ status: "skipped",
15926
+ reason_code: "install_config_required",
15927
+ summary: "Skipped because install-config verification failed.",
15928
+ next_command: "Fix install-config verification first, then rerun this command."
15929
+ });
15930
+ return {
15931
+ schema_version: "foh_widget_public_install_verification.v1",
15932
+ status: "fail",
15933
+ widget_public_key: publicKey,
15934
+ origin: originOverride ?? null,
15935
+ checks
15936
+ };
15937
+ }
15938
+ const smoke = await runWidgetSmoke(publicKey, apiUrlOverride, originOverride);
15939
+ if (smoke.failed > 0) {
15940
+ checks.push({
15941
+ name: "widget_smoke",
15942
+ status: "fail",
15943
+ reason_code: "widget_smoke_failed",
15944
+ summary: `${smoke.failed} widget smoke turn(s) failed.`,
15945
+ detail: smoke,
15946
+ next_command: `Run: foh widget smoke --channel ${publicKey} --json${originOverride ? ` --origin ${originOverride}` : ""}`
15947
+ });
15948
+ } else {
15949
+ checks.push({
15950
+ name: "widget_smoke",
15951
+ status: "pass",
15952
+ reason_code: "widget_smoke_ok",
15953
+ summary: "Widget smoke passed from the public builder-install path.",
15954
+ detail: smoke
15955
+ });
15956
+ }
15957
+ return {
15958
+ schema_version: "foh_widget_public_install_verification.v1",
15959
+ status: checks.some((check2) => check2.status === "fail") ? "fail" : "pass",
15960
+ widget_public_key: publicKey,
15961
+ origin: originOverride ?? null,
15962
+ checks
15963
+ };
15964
+ }
15965
+
15966
+ // src/commands/widget.ts
15264
15967
  function registerWidget(program3) {
15265
15968
  const widget = program3.command("widget").description("Manage the web widget channel");
15266
15969
  widget.command("ensure").description("Ensure a widget channel exists for an agent (idempotent)").requiredOption("--agent <id>", "Agent ID").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
@@ -15274,9 +15977,9 @@ function registerWidget(program3) {
15274
15977
  }));
15275
15978
  widget.command("set-domains").description("Update widget domain allowlist (idempotent)").requiredOption("--domains <d1,d2>", "Comma-separated domain list").requiredOption("--agent <id>", "Agent ID").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
15276
15979
  const domains = opts.domains.split(",").map((domain2) => domain2.trim());
15277
- const data = await apiFetch(`/v1/console/agents/${opts.agent}/draft`, {
15278
- method: "PATCH",
15279
- body: JSON.stringify({ allowed_domains: domains }),
15980
+ const data = await apiFetch("/v1/console/channels/widget/domains", {
15981
+ method: "POST",
15982
+ body: JSON.stringify({ agentId: opts.agent, domains }),
15280
15983
  orgId: opts.org,
15281
15984
  apiUrlOverride: opts.apiUrl
15282
15985
  });
@@ -15294,8 +15997,47 @@ function registerWidget(program3) {
15294
15997
  process.stdout.write(data.snippet + "\n");
15295
15998
  }
15296
15999
  }));
16000
+ widget.command("install-link").description("Mint a short-lived one-time website install link for an agent").requiredOption("--agent <id>", "Agent ID").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--domains <d1,d2>", "Optional comma-separated widget domain allowlist to set before minting").option("--expires-in <seconds>", "Install-link TTL in seconds", "3600").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
16001
+ const domains = typeof opts.domains === "string" ? opts.domains.split(",").map((domain2) => domain2.trim()).filter(Boolean) : [];
16002
+ const data = await apiFetch("/v1/console/channels/widget/install-link", {
16003
+ method: "POST",
16004
+ body: JSON.stringify({
16005
+ agentId: opts.agent,
16006
+ expiresInSec: Number(opts.expiresIn),
16007
+ ...domains.length > 0 ? { domains } : {}
16008
+ }),
16009
+ orgId: opts.org,
16010
+ apiUrlOverride: opts.apiUrl
16011
+ });
16012
+ const installUrl = typeof data?.install_url === "string" ? String(data.install_url) : null;
16013
+ if (installUrl && domains.length > 0) {
16014
+ const url2 = new URL(installUrl);
16015
+ url2.searchParams.delete("domains");
16016
+ for (const domain2 of domains) {
16017
+ url2.searchParams.append("domains", domain2);
16018
+ }
16019
+ ;
16020
+ data.install_url = url2.toString();
16021
+ data.domains = domains;
16022
+ }
16023
+ format(data, { json: opts.json ?? false });
16024
+ }));
16025
+ widget.command("install-site").description("Install or update the hosted widget snippet in a static/Vite website shell").option("--agent <id>", "Agent ID; required unless --snippet is supplied").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--domains <d1,d2>", "Optional comma-separated widget domain allowlist").option("--site-root <path>", "Website repository root", ".").option("--target <path>", "HTML shell to patch, relative to --site-root").option("--snippet <html>", "Embed snippet to install without calling the FOH API").option("--api-url <url>", "API base URL override").option("--dry-run", "Resolve and report the patch without writing the file").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
16026
+ const domains = typeof opts.domains === "string" ? opts.domains.split(",").map((domain2) => domain2.trim()).filter(Boolean) : [];
16027
+ const result = await installWidgetSite({
16028
+ agent: opts.agent,
16029
+ org: opts.org,
16030
+ domains,
16031
+ siteRoot: opts.siteRoot,
16032
+ target: opts.target,
16033
+ snippet: opts.snippet,
16034
+ apiUrl: opts.apiUrl,
16035
+ dryRun: opts.dryRun
16036
+ });
16037
+ format(result, { json: opts.json ?? false });
16038
+ }));
15297
16039
  widget.command("chat").description("Send a single message through the widget and return the reply").requiredOption("--agent <id>", "Agent ID").requiredOption("--message <text>", "Message to send").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
15298
- const publicKey = await resolveChannelPublicKey(opts.agent, opts.org, opts.apiUrl);
16040
+ const publicKey = await ensureWidgetChannelPublicKey(opts.agent, opts.org, opts.apiUrl);
15299
16041
  const start = Date.now();
15300
16042
  const data = await apiFetch("/v1/widget/inbound", {
15301
16043
  method: "POST",
@@ -15316,12 +16058,52 @@ function registerWidget(program3) {
15316
16058
  };
15317
16059
  format(result, { json: opts.json ?? false });
15318
16060
  }));
15319
- widget.command("smoke").description("Run a canned 3-turn conversation to verify end-to-end widget health").requiredOption("--agent <id>", "Agent ID").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
15320
- const publicKey = await resolveChannelPublicKey(opts.agent, opts.org, opts.apiUrl);
15321
- const summary = await runWidgetSmoke(publicKey, opts.apiUrl);
16061
+ widget.command("smoke").description("Run a canned 3-turn conversation to verify end-to-end widget health").option("--agent <id>", "Agent ID").option("--channel <publicKey>", "Widget public key; skips operator auth").option("--origin <urlOrHost>", "Website origin to simulate for domain allowlists, e.g. https://www.example.com").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
16062
+ const publicKey = typeof opts.channel === "string" && opts.channel.trim() ? opts.channel.trim() : opts.agent ? await ensureWidgetChannelPublicKey(opts.agent, opts.org, opts.apiUrl) : null;
16063
+ if (!publicKey) {
16064
+ throw new FohError({
16065
+ step: "widget.smoke",
16066
+ error: "Missing --agent or --channel.",
16067
+ remediation: "Run: foh widget smoke --channel <public-key> --json, or foh widget smoke --agent <agent-id> --json"
16068
+ });
16069
+ }
16070
+ const origin = normalizeOrigin(typeof opts.origin === "string" ? opts.origin : void 0);
16071
+ const summary = await runWidgetSmoke(publicKey, opts.apiUrl, origin);
15322
16072
  format(summary, { json: opts.json ?? false });
15323
16073
  if (summary.failed > 0) markCommandFailed(1);
15324
16074
  }));
16075
+ widget.command("verify-install").description("Run the public builder-install verification path: config/domain gate plus runtime smoke").option("--agent <id>", "Agent ID").option("--channel <publicKey>", "Widget public key; skips operator auth").option("--origin <urlOrHost>", "Live website origin to verify against, e.g. https://www.example.com").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
16076
+ const publicKey = typeof opts.channel === "string" && opts.channel.trim() ? opts.channel.trim() : opts.agent ? await ensureWidgetChannelPublicKey(opts.agent, opts.org, opts.apiUrl) : null;
16077
+ if (!publicKey) {
16078
+ throw new FohError({
16079
+ step: "widget.verify_install",
16080
+ error: "Missing --agent or --channel.",
16081
+ remediation: "Run: foh widget verify-install --channel <public-key> --origin https://www.example.com --json"
16082
+ });
16083
+ }
16084
+ const origin = normalizeOrigin(typeof opts.origin === "string" ? opts.origin : void 0);
16085
+ const summary = await runWidgetInstallVerification(publicKey, opts.apiUrl, origin);
16086
+ format(summary, { json: opts.json ?? false });
16087
+ if (summary.status !== "pass") markCommandFailed(1);
16088
+ }));
16089
+ widget.command("wordpress-plugin").description("Generate a preconfigured WordPress plugin bundle for the hosted widget").option("--agent <id>", "Agent ID").option("--channel <publicKey>", "Widget public key; skips operator auth").requiredOption("--out <path>", "Output directory or .zip path for the plugin bundle").option("--plugin-name <name>", "WordPress plugin display name", DEFAULT_WORDPRESS_PLUGIN_NAME).option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--dry-run", "Resolve the bundle without writing files").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
16090
+ const publicKey = typeof opts.channel === "string" && opts.channel.trim() ? opts.channel.trim() : opts.agent ? await ensureWidgetChannelPublicKey(opts.agent, opts.org, opts.apiUrl) : null;
16091
+ if (!publicKey) {
16092
+ throw new FohError({
16093
+ step: "widget.wordpress_plugin",
16094
+ error: "Missing --agent or --channel.",
16095
+ remediation: "Run: foh widget wordpress-plugin --channel <public-key> --out <path> --json"
16096
+ });
16097
+ }
16098
+ const result = buildWordPressPluginBundle({
16099
+ publicKey,
16100
+ apiUrl: opts.apiUrl,
16101
+ outPath: opts.out,
16102
+ pluginName: opts.pluginName,
16103
+ dryRun: Boolean(opts.dryRun)
16104
+ });
16105
+ format(result, { json: opts.json ?? false });
16106
+ }));
15325
16107
  }
15326
16108
 
15327
16109
  // src/commands/channel-instagram.ts
@@ -15428,8 +16210,8 @@ function buildReasonedNextSteps({
15428
16210
  }
15429
16211
 
15430
16212
  // src/commands/channel-whatsapp-live-proof.ts
15431
- var import_node_fs = require("node:fs");
15432
- var path = __toESM(require("node:path"));
16213
+ var import_node_fs3 = require("node:fs");
16214
+ var path3 = __toESM(require("node:path"));
15433
16215
 
15434
16216
  // src/lib/channel-live-proof-evaluator.mjs
15435
16217
  function normalizeStatusValue(value) {
@@ -15514,8 +16296,8 @@ function resolveLiveProof({
15514
16296
  freshness: { timestamp_field: null, timestamp: null, age_hours: null, max_age_hours: maxAgeHours }
15515
16297
  };
15516
16298
  }
15517
- const artifactPath = path.resolve(process.cwd(), artifactPathRaw);
15518
- if (!(0, import_node_fs.existsSync)(artifactPath)) {
16299
+ const artifactPath = path3.resolve(process.cwd(), artifactPathRaw);
16300
+ if (!(0, import_node_fs3.existsSync)(artifactPath)) {
15519
16301
  return {
15520
16302
  requested: true,
15521
16303
  artifact_path: artifactPath,
@@ -15527,7 +16309,7 @@ function resolveLiveProof({
15527
16309
  }
15528
16310
  let payload;
15529
16311
  try {
15530
- payload = JSON.parse((0, import_node_fs.readFileSync)(artifactPath, "utf8"));
16312
+ payload = JSON.parse((0, import_node_fs3.readFileSync)(artifactPath, "utf8"));
15531
16313
  } catch {
15532
16314
  return {
15533
16315
  requested: true,
@@ -15556,7 +16338,7 @@ function resolveLiveProof({
15556
16338
  }
15557
16339
 
15558
16340
  // src/commands/channel-whatsapp-onboarding.ts
15559
- var import_node_fs2 = require("node:fs");
16341
+ var import_node_fs4 = require("node:fs");
15560
16342
 
15561
16343
  // src/commands/channel-whatsapp-setup.ts
15562
16344
  var import_node_crypto = require("node:crypto");
@@ -15695,7 +16477,7 @@ function parseBatchManifest(manifestPathRaw) {
15695
16477
  });
15696
16478
  }
15697
16479
  try {
15698
- const raw = (0, import_node_fs2.readFileSync)(manifestPath, "utf8");
16480
+ const raw = (0, import_node_fs4.readFileSync)(manifestPath, "utf8");
15699
16481
  const parsed = JSON.parse(raw);
15700
16482
  if (!Array.isArray(parsed.businesses) || parsed.businesses.length === 0) {
15701
16483
  throw new Error("manifest_missing_businesses");
@@ -16513,7 +17295,7 @@ function registerTools(program3) {
16513
17295
  }
16514
17296
 
16515
17297
  // src/commands/mcp-serve.ts
16516
- var import_node_child_process = require("node:child_process");
17298
+ var import_node_child_process2 = require("node:child_process");
16517
17299
 
16518
17300
  // ../../node_modules/.pnpm/zod@4.3.6/node_modules/zod/v3/helpers/util.js
16519
17301
  var util;
@@ -16874,8 +17656,8 @@ function getErrorMap() {
16874
17656
 
16875
17657
  // ../../node_modules/.pnpm/zod@4.3.6/node_modules/zod/v3/helpers/parseUtil.js
16876
17658
  var makeIssue = (params) => {
16877
- const { data, path: path2, errorMaps, issueData } = params;
16878
- const fullPath = [...path2, ...issueData.path || []];
17659
+ const { data, path: path5, errorMaps, issueData } = params;
17660
+ const fullPath = [...path5, ...issueData.path || []];
16879
17661
  const fullIssue = {
16880
17662
  ...issueData,
16881
17663
  path: fullPath
@@ -16990,11 +17772,11 @@ var errorUtil;
16990
17772
 
16991
17773
  // ../../node_modules/.pnpm/zod@4.3.6/node_modules/zod/v3/types.js
16992
17774
  var ParseInputLazyPath = class {
16993
- constructor(parent, value, path2, key) {
17775
+ constructor(parent, value, path5, key) {
16994
17776
  this._cachedPath = [];
16995
17777
  this.parent = parent;
16996
17778
  this.data = value;
16997
- this._path = path2;
17779
+ this._path = path5;
16998
17780
  this._key = key;
16999
17781
  }
17000
17782
  get path() {
@@ -20492,7 +21274,7 @@ __export(util_exports, {
20492
21274
  createTransparentProxy: () => createTransparentProxy,
20493
21275
  defineLazy: () => defineLazy,
20494
21276
  esc: () => esc,
20495
- escapeRegex: () => escapeRegex,
21277
+ escapeRegex: () => escapeRegex2,
20496
21278
  extend: () => extend3,
20497
21279
  finalizeIssue: () => finalizeIssue,
20498
21280
  floatSafeRemainder: () => floatSafeRemainder2,
@@ -20640,10 +21422,10 @@ function mergeDefs(...defs) {
20640
21422
  function cloneDef(schema2) {
20641
21423
  return mergeDefs(schema2._zod.def);
20642
21424
  }
20643
- function getElementAtPath(obj, path2) {
20644
- if (!path2)
21425
+ function getElementAtPath(obj, path5) {
21426
+ if (!path5)
20645
21427
  return obj;
20646
- return path2.reduce((acc, key) => acc?.[key], obj);
21428
+ return path5.reduce((acc, key) => acc?.[key], obj);
20647
21429
  }
20648
21430
  function promiseAllObject(promisesObj) {
20649
21431
  const keys = Object.keys(promisesObj);
@@ -20765,7 +21547,7 @@ var getParsedType2 = (data) => {
20765
21547
  };
20766
21548
  var propertyKeyTypes = /* @__PURE__ */ new Set(["string", "number", "symbol"]);
20767
21549
  var primitiveTypes = /* @__PURE__ */ new Set(["string", "number", "bigint", "boolean", "symbol", "undefined"]);
20768
- function escapeRegex(str2) {
21550
+ function escapeRegex2(str2) {
20769
21551
  return str2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20770
21552
  }
20771
21553
  function clone(inst, def, params) {
@@ -21026,11 +21808,11 @@ function aborted(x, startIndex = 0) {
21026
21808
  }
21027
21809
  return false;
21028
21810
  }
21029
- function prefixIssues(path2, issues) {
21811
+ function prefixIssues(path5, issues) {
21030
21812
  return issues.map((iss) => {
21031
21813
  var _a2;
21032
21814
  (_a2 = iss).path ?? (_a2.path = []);
21033
- iss.path.unshift(path2);
21815
+ iss.path.unshift(path5);
21034
21816
  return iss;
21035
21817
  });
21036
21818
  }
@@ -21384,7 +22166,7 @@ function emoji() {
21384
22166
  var ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
21385
22167
  var ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
21386
22168
  var mac = (delimiter) => {
21387
- const escapedDelim = escapeRegex(delimiter ?? ":");
22169
+ const escapedDelim = escapeRegex2(delimiter ?? ":");
21388
22170
  return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
21389
22171
  };
21390
22172
  var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
@@ -21885,7 +22667,7 @@ var $ZodCheckUpperCase = /* @__PURE__ */ $constructor("$ZodCheckUpperCase", (ins
21885
22667
  });
21886
22668
  var $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (inst, def) => {
21887
22669
  $ZodCheck.init(inst, def);
21888
- const escapedRegex = escapeRegex(def.includes);
22670
+ const escapedRegex = escapeRegex2(def.includes);
21889
22671
  const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);
21890
22672
  def.pattern = pattern;
21891
22673
  inst._zod.onattach.push((inst2) => {
@@ -21909,7 +22691,7 @@ var $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (inst,
21909
22691
  });
21910
22692
  var $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith", (inst, def) => {
21911
22693
  $ZodCheck.init(inst, def);
21912
- const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`);
22694
+ const pattern = new RegExp(`^${escapeRegex2(def.prefix)}.*`);
21913
22695
  def.pattern ?? (def.pattern = pattern);
21914
22696
  inst._zod.onattach.push((inst2) => {
21915
22697
  const bag = inst2._zod.bag;
@@ -21932,7 +22714,7 @@ var $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith", (i
21932
22714
  });
21933
22715
  var $ZodCheckEndsWith = /* @__PURE__ */ $constructor("$ZodCheckEndsWith", (inst, def) => {
21934
22716
  $ZodCheck.init(inst, def);
21935
- const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`);
22717
+ const pattern = new RegExp(`.*${escapeRegex2(def.suffix)}$`);
21936
22718
  def.pattern ?? (def.pattern = pattern);
21937
22719
  inst._zod.onattach.push((inst2) => {
21938
22720
  const bag = inst2._zod.bag;
@@ -23474,7 +24256,7 @@ var $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
23474
24256
  const values = getEnumValues(def.entries);
23475
24257
  const valuesSet = new Set(values);
23476
24258
  inst._zod.values = valuesSet;
23477
- inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex(o) : o.toString()).join("|")})$`);
24259
+ inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex2(o) : o.toString()).join("|")})$`);
23478
24260
  inst._zod.parse = (payload, _ctx) => {
23479
24261
  const input = payload.value;
23480
24262
  if (valuesSet.has(input)) {
@@ -23496,7 +24278,7 @@ var $ZodLiteral = /* @__PURE__ */ $constructor("$ZodLiteral", (inst, def) => {
23496
24278
  }
23497
24279
  const values = new Set(def.values);
23498
24280
  inst._zod.values = values;
23499
- inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o)).join("|")})$`);
24281
+ inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex2(o) : o ? escapeRegex2(o.toString()) : String(o)).join("|")})$`);
23500
24282
  inst._zod.parse = (payload, _ctx) => {
23501
24283
  const input = payload.value;
23502
24284
  if (values.has(input)) {
@@ -23852,7 +24634,7 @@ var $ZodTemplateLiteral = /* @__PURE__ */ $constructor("$ZodTemplateLiteral", (i
23852
24634
  const end = source.endsWith("$") ? source.length - 1 : source.length;
23853
24635
  regexParts.push(source.slice(start, end));
23854
24636
  } else if (part === null || primitiveTypes.has(typeof part)) {
23855
- regexParts.push(escapeRegex(`${part}`));
24637
+ regexParts.push(escapeRegex2(`${part}`));
23856
24638
  } else {
23857
24639
  throw new Error(`Invalid template literal part: ${part}`);
23858
24640
  }
@@ -32941,7 +33723,7 @@ var StdioServerTransport = class {
32941
33723
  };
32942
33724
 
32943
33725
  // src/lib/cli-version.ts
32944
- var injectedVersion = true ? String("0.1.88").trim() : "";
33726
+ var injectedVersion = true ? String("0.1.89").trim() : "";
32945
33727
  var envVersion = String(process.env.FOH_CLI_VERSION || process.env.npm_package_version || "").trim();
32946
33728
  var CLI_VERSION = injectedVersion || envVersion || "0.0.0-dev";
32947
33729
 
@@ -33129,7 +33911,7 @@ async function runFohCli(params) {
33129
33911
  }
33130
33912
  const command = `foh ${effectiveArgv.join(" ")}`;
33131
33913
  return await new Promise((resolve16) => {
33132
- const child = (0, import_node_child_process.spawn)(process.execPath, [cliEntry, ...effectiveArgv], {
33914
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [cliEntry, ...effectiveArgv], {
33133
33915
  stdio: ["ignore", "pipe", "pipe"],
33134
33916
  env: {
33135
33917
  ...process.env,
@@ -33207,6 +33989,19 @@ var TOOL_EXECUTION_SCHEMA = {
33207
33989
  var APPROVAL_TOKEN_SCHEMA = {
33208
33990
  approval_token: string2().min(1).max(256).optional()
33209
33991
  };
33992
+ var MCP_OBJECTIVE_INDUSTRIES = ["real_estate", "restaurant", "general"];
33993
+ var MCP_TEMPLATE_CATEGORIES = ["buyer", "seller", "landlord", "commercial"];
33994
+ var MCP_PROOF_MISSIONS = ["setup", "widget", "voice", "publish"];
33995
+ var MCP_CONTACT_PATHS = ["auto", "managed", "byon"];
33996
+ var MCP_MUTATION_MODES = ["read-only", "ensure"];
33997
+ function appendOptionalArg(argv, flag, value) {
33998
+ const text = asOptionalString(value);
33999
+ if (text) argv.push(flag, text);
34000
+ }
34001
+ function appendOptionalCsvArg(argv, flag, value) {
34002
+ const values = asStringArray(value);
34003
+ if (values.length > 0) argv.push(flag, values.join(","));
34004
+ }
33210
34005
  var TYPED_TOOL_SPECS = [
33211
34006
  {
33212
34007
  name: "foh_auth_whoami",
@@ -33263,6 +34058,193 @@ var TYPED_TOOL_SPECS = [
33263
34058
  return argv;
33264
34059
  }
33265
34060
  },
34061
+ {
34062
+ name: "foh_templates_list",
34063
+ title: "FOH Templates List",
34064
+ description: "List available setup templates, optionally filtered by category.",
34065
+ commandKey: "templates list",
34066
+ risk: "read",
34067
+ inputSchema: {
34068
+ category: _enum(MCP_TEMPLATE_CATEGORIES).optional(),
34069
+ api_url: string2().url().optional()
34070
+ },
34071
+ toArgv: (args) => {
34072
+ const argv = ["templates", "list"];
34073
+ appendOptionalArg(argv, "--category", args.category);
34074
+ appendOptionalArg(argv, "--api-url", args.api_url);
34075
+ return argv;
34076
+ }
34077
+ },
34078
+ {
34079
+ name: "foh_templates_show",
34080
+ title: "FOH Templates Show",
34081
+ description: "Show a setup template before applying it.",
34082
+ commandKey: "templates show",
34083
+ risk: "read",
34084
+ inputSchema: {
34085
+ template: string2().min(1).max(128),
34086
+ api_url: string2().url().optional()
34087
+ },
34088
+ toArgv: (args) => {
34089
+ const argv = ["templates", "show", "--template", String(args.template)];
34090
+ appendOptionalArg(argv, "--api-url", args.api_url);
34091
+ return argv;
34092
+ }
34093
+ },
34094
+ {
34095
+ name: "foh_templates_select",
34096
+ title: "FOH Templates Select",
34097
+ description: "Select candidate templates from a business requirement brief JSON string.",
34098
+ commandKey: "templates select",
34099
+ risk: "read",
34100
+ inputSchema: {
34101
+ brief_json: string2().min(2).max(5e4),
34102
+ api_url: string2().url().optional()
34103
+ },
34104
+ toArgv: (args) => {
34105
+ const argv = ["templates", "select", "--brief", String(args.brief_json)];
34106
+ appendOptionalArg(argv, "--api-url", args.api_url);
34107
+ return argv;
34108
+ }
34109
+ },
34110
+ {
34111
+ name: "foh_setup_objective",
34112
+ title: "FOH Setup Objective",
34113
+ description: "Plan objective-first setup from explicit business inputs without applying changes.",
34114
+ commandKey: "setup",
34115
+ risk: "read",
34116
+ inputSchema: {
34117
+ objective: string2().min(1).max(1e3),
34118
+ business_name: string2().min(1).max(256),
34119
+ industry: _enum(MCP_OBJECTIVE_INDUSTRIES),
34120
+ source_url: string2().url(),
34121
+ location: string2().min(1).max(256).optional(),
34122
+ tools: array(string2().min(1)).min(1).max(20).optional(),
34123
+ target_mode: string2().min(1).max(128).optional(),
34124
+ org: string2().uuid().optional(),
34125
+ api_url: string2().url().optional()
34126
+ },
34127
+ toArgv: (args) => {
34128
+ const argv = [
34129
+ "setup",
34130
+ "--objective",
34131
+ String(args.objective),
34132
+ "--business-name",
34133
+ String(args.business_name),
34134
+ "--industry",
34135
+ String(args.industry),
34136
+ "--source-url",
34137
+ String(args.source_url)
34138
+ ];
34139
+ appendOptionalArg(argv, "--location", args.location);
34140
+ appendOptionalCsvArg(argv, "--tools", args.tools);
34141
+ appendOptionalArg(argv, "--target-mode", args.target_mode);
34142
+ appendOptionalArg(argv, "--org", args.org);
34143
+ appendOptionalArg(argv, "--api-url", args.api_url);
34144
+ return argv;
34145
+ }
34146
+ },
34147
+ {
34148
+ name: "foh_objective_status",
34149
+ title: "FOH Objective Status",
34150
+ description: "Compose setup and launch evidence into one objective status envelope.",
34151
+ commandKey: "objective status",
34152
+ risk: "read",
34153
+ inputSchema: {
34154
+ business_name: string2().min(1).max(256),
34155
+ source_url: string2().url(),
34156
+ industry: _enum(MCP_OBJECTIVE_INDUSTRIES).optional(),
34157
+ business_objective: string2().min(1).max(1e3).optional(),
34158
+ location: string2().min(1).max(256).optional(),
34159
+ tools: array(string2().min(1)).min(1).max(20).optional(),
34160
+ target_mode: string2().min(1).max(128).optional(),
34161
+ environment: string2().min(1).max(128).optional(),
34162
+ org: string2().uuid().optional(),
34163
+ out: string2().min(1).max(1024).optional(),
34164
+ api_url: string2().url().optional()
34165
+ },
34166
+ toArgv: (args) => {
34167
+ const argv = ["objective", "status", "--business-name", String(args.business_name), "--source-url", String(args.source_url)];
34168
+ appendOptionalArg(argv, "--industry", args.industry);
34169
+ appendOptionalArg(argv, "--business-objective", args.business_objective);
34170
+ appendOptionalArg(argv, "--location", args.location);
34171
+ appendOptionalCsvArg(argv, "--tools", args.tools);
34172
+ appendOptionalArg(argv, "--target-mode", args.target_mode);
34173
+ appendOptionalArg(argv, "--environment", args.environment);
34174
+ appendOptionalArg(argv, "--org", args.org);
34175
+ appendOptionalArg(argv, "--out", args.out);
34176
+ appendOptionalArg(argv, "--api-url", args.api_url);
34177
+ return argv;
34178
+ }
34179
+ },
34180
+ {
34181
+ name: "foh_objective_prove",
34182
+ title: "FOH Objective Prove",
34183
+ description: "Run objective proof status against the customer-live gate.",
34184
+ commandKey: "objective prove",
34185
+ risk: "read",
34186
+ inputSchema: {
34187
+ business_name: string2().min(1).max(256),
34188
+ source_url: string2().url(),
34189
+ industry: _enum(MCP_OBJECTIVE_INDUSTRIES).optional(),
34190
+ business_objective: string2().min(1).max(1e3).optional(),
34191
+ location: string2().min(1).max(256).optional(),
34192
+ tools: array(string2().min(1)).min(1).max(20).optional(),
34193
+ target_mode: string2().min(1).max(128).optional(),
34194
+ environment: string2().min(1).max(128).optional(),
34195
+ org: string2().uuid().optional(),
34196
+ out: string2().min(1).max(1024).optional(),
34197
+ api_url: string2().url().optional()
34198
+ },
34199
+ toArgv: (args) => {
34200
+ const argv = ["objective", "prove", "--business-name", String(args.business_name), "--source-url", String(args.source_url)];
34201
+ appendOptionalArg(argv, "--industry", args.industry);
34202
+ appendOptionalArg(argv, "--business-objective", args.business_objective);
34203
+ appendOptionalArg(argv, "--location", args.location);
34204
+ appendOptionalCsvArg(argv, "--tools", args.tools);
34205
+ appendOptionalArg(argv, "--target-mode", args.target_mode);
34206
+ appendOptionalArg(argv, "--environment", args.environment);
34207
+ appendOptionalArg(argv, "--org", args.org);
34208
+ appendOptionalArg(argv, "--out", args.out);
34209
+ appendOptionalArg(argv, "--api-url", args.api_url);
34210
+ return argv;
34211
+ }
34212
+ },
34213
+ {
34214
+ name: "foh_prove_agent",
34215
+ title: "FOH Prove Agent",
34216
+ description: "Produce a setup/runtime proof bundle for an agent.",
34217
+ commandKey: "prove",
34218
+ risk: "read",
34219
+ inputSchema: {
34220
+ agent: string2().min(1).max(128).optional(),
34221
+ org: string2().uuid().optional(),
34222
+ mission: _enum(MCP_PROOF_MISSIONS).optional(),
34223
+ contact_path: _enum(MCP_CONTACT_PATHS).optional(),
34224
+ mutation_mode: _enum(MCP_MUTATION_MODES).optional(),
34225
+ require_phone: boolean2().optional(),
34226
+ skip_smoke: boolean2().optional(),
34227
+ skip_voice_health: boolean2().optional(),
34228
+ out: string2().min(1).max(1024).optional(),
34229
+ strict: boolean2().optional(),
34230
+ api_url: string2().url().optional()
34231
+ },
34232
+ toArgv: (args) => {
34233
+ const argv = ["prove"];
34234
+ appendOptionalArg(argv, "--agent", args.agent);
34235
+ appendOptionalArg(argv, "--org", args.org);
34236
+ appendOptionalArg(argv, "--mission", args.mission);
34237
+ appendOptionalArg(argv, "--contact-path", args.contact_path);
34238
+ appendOptionalArg(argv, "--mutation-mode", args.mutation_mode);
34239
+ if (asOptionalBoolean(args.require_phone) === true) argv.push("--require-phone");
34240
+ if (asOptionalBoolean(args.skip_smoke) === true) argv.push("--skip-smoke");
34241
+ if (asOptionalBoolean(args.skip_voice_health) === true) argv.push("--skip-voice-health");
34242
+ appendOptionalArg(argv, "--out", args.out);
34243
+ if (asOptionalBoolean(args.strict) === true) argv.push("--strict");
34244
+ appendOptionalArg(argv, "--api-url", args.api_url);
34245
+ return argv;
34246
+ }
34247
+ },
33266
34248
  {
33267
34249
  name: "foh_agent_list",
33268
34250
  title: "FOH Agent List",
@@ -33951,9 +34933,9 @@ function parsePositiveInt(value, fallback, min, max) {
33951
34933
  if (!Number.isFinite(parsed)) return fallback;
33952
34934
  return Math.max(min, Math.min(max, Math.trunc(parsed)));
33953
34935
  }
33954
- function withQuery(path2, params) {
34936
+ function withQuery(path5, params) {
33955
34937
  const query = params.toString();
33956
- return query ? `${path2}?${query}` : path2;
34938
+ return query ? `${path5}?${query}` : path5;
33957
34939
  }
33958
34940
 
33959
34941
  // src/commands/knowledge.ts
@@ -34745,8 +35727,8 @@ function signReport(reportPayload) {
34745
35727
  }
34746
35728
  };
34747
35729
  }
34748
- function writeSignedJsonArtifact(path2, value) {
34749
- const absolutePath = (0, import_path3.resolve)(path2);
35730
+ function writeSignedJsonArtifact(path5, value) {
35731
+ const absolutePath = (0, import_path3.resolve)(path5);
34750
35732
  (0, import_fs5.mkdirSync)((0, import_path3.dirname)(absolutePath), { recursive: true });
34751
35733
  (0, import_fs5.writeFileSync)(absolutePath, stableStringify(value), "utf-8");
34752
35734
  return absolutePath;
@@ -35780,7 +36762,7 @@ ${passIcon} Certification loop summary
35780
36762
  }
35781
36763
 
35782
36764
  // src/commands/certify.ts
35783
- var import_node_fs3 = require("node:fs");
36765
+ var import_node_fs5 = require("node:fs");
35784
36766
  function normalizeProfile(raw) {
35785
36767
  const value = String(raw || "release").trim().toLowerCase();
35786
36768
  if (value === "smoke" || value === "release" || value === "stress") return value;
@@ -35858,7 +36840,7 @@ function registerCertify(program3) {
35858
36840
  next_commands: passed ? [`foh agent publish --agent ${opts.agent} --json`] : [`foh certify run --agent ${opts.agent} --profile ${profile} --json`]
35859
36841
  };
35860
36842
  if (opts.out) {
35861
- (0, import_node_fs3.writeFileSync)(opts.out, JSON.stringify(result, null, 2) + "\n", "utf-8");
36843
+ (0, import_node_fs5.writeFileSync)(opts.out, JSON.stringify(result, null, 2) + "\n", "utf-8");
35862
36844
  }
35863
36845
  if (opts.json ?? false) {
35864
36846
  format(result, { json: true });
@@ -35898,8 +36880,8 @@ function registerConversations(program3) {
35898
36880
  if (opts.provider) params.set("provider", String(opts.provider));
35899
36881
  params.set("page", String(parsePositiveInt(opts.page, 1, 1, 1e4)));
35900
36882
  params.set("limit", String(parsePositiveInt(opts.limit, 20, 1, 100)));
35901
- const path2 = withQuery(`/v1/console/agents/${opts.agent}/conversations`, params);
35902
- const data = await apiFetch(path2, { orgId: opts.org, apiUrlOverride: opts.apiUrl });
36883
+ const path5 = withQuery(`/v1/console/agents/${opts.agent}/conversations`, params);
36884
+ const data = await apiFetch(path5, { orgId: opts.org, apiUrlOverride: opts.apiUrl });
35903
36885
  format(data, { json: opts.json ?? false });
35904
36886
  }));
35905
36887
  conversations.command("semantic-search").description("Run semantic ranking search against an agent conversation corpus").requiredOption("--agent <id>", "Agent ID").requiredOption("--q <query>", "Query text (min 3 chars)").option("--limit <n>", "Result limit (1-100)", "20").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
@@ -35916,13 +36898,13 @@ function registerConversations(program3) {
35916
36898
  q: query,
35917
36899
  limit: String(parsePositiveInt(opts.limit, 20, 1, 100))
35918
36900
  });
35919
- const path2 = withQuery(`/v1/console/agents/${opts.agent}/conversations/semantic-search`, params);
35920
- const data = await apiFetch(path2, { orgId: opts.org, apiUrlOverride: opts.apiUrl });
36901
+ const path5 = withQuery(`/v1/console/agents/${opts.agent}/conversations/semantic-search`, params);
36902
+ const data = await apiFetch(path5, { orgId: opts.org, apiUrlOverride: opts.apiUrl });
35921
36903
  format(data, { json: opts.json ?? false });
35922
36904
  }));
35923
36905
  conversations.command("lead-data").description("Get extracted lead-data fields for a conversation").requiredOption("--agent <id>", "Agent ID").requiredOption("--conversation <id>", "Conversation ID").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
35924
- const path2 = `/v1/console/agents/${opts.agent}/conversations/${opts.conversation}/lead-data`;
35925
- const data = await apiFetch(path2, { orgId: opts.org, apiUrlOverride: opts.apiUrl });
36906
+ const path5 = `/v1/console/agents/${opts.agent}/conversations/${opts.conversation}/lead-data`;
36907
+ const data = await apiFetch(path5, { orgId: opts.org, apiUrlOverride: opts.apiUrl });
35926
36908
  format(data, { json: opts.json ?? false });
35927
36909
  }));
35928
36910
  conversations.command("inject-variables").description("Inject or merge conversation vars into an active flow-state").requiredOption("--agent <id>", "Agent ID").requiredOption("--conversation <id>", "Conversation ID").requiredOption("--variables <json|@file>", "Variables object JSON or @path").option("--mode <value>", "Mode: merge or replace", "merge").option("--source <value>", "Audit source tag", "cli").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
@@ -35944,8 +36926,8 @@ function registerConversations(program3) {
35944
36926
  statusCode: 400
35945
36927
  });
35946
36928
  }
35947
- const path2 = `/v1/console/agents/${opts.agent}/conversations/${opts.conversation}/inject-variables`;
35948
- const data = await apiFetch(path2, {
36929
+ const path5 = `/v1/console/agents/${opts.agent}/conversations/${opts.conversation}/inject-variables`;
36930
+ const data = await apiFetch(path5, {
35949
36931
  method: "POST",
35950
36932
  body: JSON.stringify({
35951
36933
  variables: parsed,
@@ -35967,8 +36949,8 @@ function registerConversations(program3) {
35967
36949
  statusCode: 400
35968
36950
  });
35969
36951
  }
35970
- const path2 = `/v1/console/agents/${opts.agent}/conversations/${opts.conversation}/inject-event`;
35971
- const data = await apiFetch(path2, {
36952
+ const path5 = `/v1/console/agents/${opts.agent}/conversations/${opts.conversation}/inject-event`;
36953
+ const data = await apiFetch(path5, {
35972
36954
  method: "POST",
35973
36955
  body: JSON.stringify({
35974
36956
  event_type: String(opts.eventType),
@@ -36088,8 +37070,8 @@ function registerTranscripts(program3) {
36088
37070
  transcripts.command("get").description("Fetch one conversation transcript and optional trace events").requiredOption("--agent <id>", "Agent ID").requiredOption("--conversation <id>", "Conversation ID").option("--include-traces", "Include ordered trace events").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
36089
37071
  const params = new URLSearchParams();
36090
37072
  if (opts.includeTraces) params.set("include_traces", "true");
36091
- const path2 = withQuery(`/v1/console/agents/${opts.agent}/conversations/${opts.conversation}`, params);
36092
- const data = await apiFetch(path2, { orgId: opts.org, apiUrlOverride: opts.apiUrl });
37073
+ const path5 = withQuery(`/v1/console/agents/${opts.agent}/conversations/${opts.conversation}`, params);
37074
+ const data = await apiFetch(path5, { orgId: opts.org, apiUrlOverride: opts.apiUrl });
36093
37075
  format(data, { json: opts.json ?? false });
36094
37076
  }));
36095
37077
  transcripts.command("export").description("Export recent transcripts as JSON or JSONL").requiredOption("--agent <id>", "Agent ID").option("--q <query>", "Full-text transcript query").option("--from <iso-date>", "Start datetime (ISO8601)").option("--to <iso-date>", "End datetime (ISO8601)").option("--limit <n>", "Rows to export (1-100)", "100").option("--format <value>", "Export format: jsonl or json", "jsonl").option("--hydrate", "Fetch full conversation detail for every exported row").option("--include-traces", "Hydrate each conversation with ordered trace events").option("--no-redact", "Disable default redaction of emails, phone numbers, and obvious secrets").option("--out <path>", "Output file path").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
@@ -36272,8 +37254,8 @@ function registerTests(program3) {
36272
37254
  if (opts.status) params.set("status", String(opts.status));
36273
37255
  if (opts.type) params.set("type", String(opts.type));
36274
37256
  params.set("limit", String(parsePositiveInt(opts.limit, 100, 1, 500)));
36275
- const path2 = withQuery(`/v1/console/agents/${opts.agent}/tests`, params);
36276
- const data = await apiFetch(path2, { orgId: opts.org, apiUrlOverride: opts.apiUrl });
37257
+ const path5 = withQuery(`/v1/console/agents/${opts.agent}/tests`, params);
37258
+ const data = await apiFetch(path5, { orgId: opts.org, apiUrlOverride: opts.apiUrl });
36277
37259
  format(data, { json: opts.json ?? false });
36278
37260
  }));
36279
37261
  tests.command("create").description("Create a new test in an agent test catalog").requiredOption("--agent <id>", "Agent ID").requiredOption("--name <text>", "Test name").requiredOption("--config <json|@file>", "Test config JSON or @path").option("--description <text>", "Description").option("--status <value>", "Status override").option("--enabled <value>", "Enabled flag: true/false").option("--tags <csv>", "Comma-separated tags").option("--metadata <json|@file>", "Metadata JSON object").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
@@ -36399,8 +37381,8 @@ function registerTests(program3) {
36399
37381
  if (opts.test) params.set("testId", String(opts.test));
36400
37382
  if (opts.batch) params.set("batchId", String(opts.batch));
36401
37383
  if (opts.includeResults) params.set("includeResults", "true");
36402
- const path2 = withQuery(`/v1/console/agents/${opts.agent}/tests/runs`, params);
36403
- const data = await apiFetch(path2, {
37384
+ const path5 = withQuery(`/v1/console/agents/${opts.agent}/tests/runs`, params);
37385
+ const data = await apiFetch(path5, {
36404
37386
  orgId: opts.org,
36405
37387
  apiUrlOverride: opts.apiUrl
36406
37388
  });
@@ -36460,9 +37442,9 @@ function asStringList(value) {
36460
37442
  if (Array.isArray(value)) return value.map((entry) => String(entry || "").trim()).filter(Boolean);
36461
37443
  return [];
36462
37444
  }
36463
- function getPath(source, path2) {
37445
+ function getPath(source, path5) {
36464
37446
  if (!source || typeof source !== "object") return void 0;
36465
- return path2.split(".").reduce((current, part) => {
37447
+ return path5.split(".").reduce((current, part) => {
36466
37448
  if (!current || typeof current !== "object") return void 0;
36467
37449
  return current[part];
36468
37450
  }, source);
@@ -36476,12 +37458,12 @@ function valuesEqual(actual, expected) {
36476
37458
  }
36477
37459
  return String(actual ?? "") === String(expected ?? "");
36478
37460
  }
36479
- function parseStructuredFile(path2) {
36480
- const raw = (0, import_fs7.readFileSync)(path2, "utf-8");
36481
- return path2.toLowerCase().endsWith(".json") ? JSON.parse(raw) : load(raw);
37461
+ function parseStructuredFile(path5) {
37462
+ const raw = (0, import_fs7.readFileSync)(path5, "utf-8");
37463
+ return path5.toLowerCase().endsWith(".json") ? JSON.parse(raw) : load(raw);
36482
37464
  }
36483
- function parseSuiteFile(path2) {
36484
- const parsed = parseStructuredFile(path2);
37465
+ function parseSuiteFile(path5) {
37466
+ const parsed = parseStructuredFile(path5);
36485
37467
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
36486
37468
  throw new FohError({
36487
37469
  step: "test.run",
@@ -36505,8 +37487,8 @@ function turnFromFixtureEntry(entry) {
36505
37487
  expect: row.expect && typeof row.expect === "object" && !Array.isArray(row.expect) ? row.expect : void 0
36506
37488
  };
36507
37489
  }
36508
- function loadFixtureTurns(path2) {
36509
- const parsed = parseStructuredFile(path2);
37490
+ function loadFixtureTurns(path5) {
37491
+ const parsed = parseStructuredFile(path5);
36510
37492
  const entries = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.turns) ? parsed.turns : parsed && typeof parsed === "object" && Array.isArray(parsed.messages) ? parsed.messages : [];
36511
37493
  return entries.map(turnFromFixtureEntry).filter((entry) => !!entry);
36512
37494
  }
@@ -36566,8 +37548,8 @@ function pickVariables(response) {
36566
37548
  return null;
36567
37549
  }
36568
37550
  function pickToolCalls(response) {
36569
- for (const path2 of ["tool_calls", "toolCalls", "telemetry.tool_calls", "trace.tool_calls"]) {
36570
- const value = getPath(response, path2);
37551
+ for (const path5 of ["tool_calls", "toolCalls", "telemetry.tool_calls", "trace.tool_calls"]) {
37552
+ const value = getPath(response, path5);
36571
37553
  if (Array.isArray(value)) return value;
36572
37554
  }
36573
37555
  return [];
@@ -36623,9 +37605,9 @@ function evaluateStructuredExpectations(response, expect, latencyMs) {
36623
37605
  const variables = pickVariables(response);
36624
37606
  if (!variables) failures.push("variables expected but response had none");
36625
37607
  else {
36626
- for (const [path2, expected] of Object.entries(expect.variables)) {
36627
- const actual = getPath(variables, path2);
36628
- if (!valuesEqual(actual, expected)) failures.push(`variables.${path2} expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`);
37608
+ for (const [path5, expected] of Object.entries(expect.variables)) {
37609
+ const actual = getPath(variables, path5);
37610
+ if (!valuesEqual(actual, expected)) failures.push(`variables.${path5} expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`);
36629
37611
  }
36630
37612
  }
36631
37613
  }
@@ -36667,9 +37649,9 @@ function evaluateStructuredExpectations(response, expect, latencyMs) {
36667
37649
  }
36668
37650
  }
36669
37651
  if (expect.fields && typeof expect.fields === "object") {
36670
- for (const [path2, expected] of Object.entries(expect.fields)) {
36671
- const actual = getPath(response, path2);
36672
- if (!valuesEqual(actual, expected)) failures.push(`fields.${path2} expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`);
37652
+ for (const [path5, expected] of Object.entries(expect.fields)) {
37653
+ const actual = getPath(response, path5);
37654
+ if (!valuesEqual(actual, expected)) failures.push(`fields.${path5} expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`);
36673
37655
  }
36674
37656
  }
36675
37657
  return failures;
@@ -37364,9 +38346,9 @@ function nonEmpty2(value) {
37364
38346
  const text = String(value ?? "").trim();
37365
38347
  return text.length > 0 ? text : void 0;
37366
38348
  }
37367
- function getPath2(value, path2) {
38349
+ function getPath2(value, path5) {
37368
38350
  let current = value;
37369
- for (const segment of path2.split(".")) {
38351
+ for (const segment of path5.split(".")) {
37370
38352
  const record2 = asRecord2(current);
37371
38353
  if (!record2) return void 0;
37372
38354
  current = record2[segment];
@@ -37523,10 +38505,10 @@ function assertRedacted(value) {
37523
38505
  });
37524
38506
  }
37525
38507
  }
37526
- function readSourceArtifact(path2) {
37527
- if (!path2) return null;
38508
+ function readSourceArtifact(path5) {
38509
+ if (!path5) return null;
37528
38510
  try {
37529
- return JSON.parse((0, import_fs9.readFileSync)(path2, "utf-8"));
38511
+ return JSON.parse((0, import_fs9.readFileSync)(path5, "utf-8"));
37530
38512
  } catch (error2) {
37531
38513
  throw new FohError({
37532
38514
  step: "bug.improve",
@@ -37703,8 +38685,8 @@ function parseRequestBody(raw) {
37703
38685
  return text;
37704
38686
  }
37705
38687
  }
37706
- function writeJsonArtifact(path2, value) {
37707
- const absolutePath = (0, import_path8.resolve)(path2);
38688
+ function writeJsonArtifact(path5, value) {
38689
+ const absolutePath = (0, import_path8.resolve)(path5);
37708
38690
  (0, import_fs10.mkdirSync)((0, import_path8.dirname)(absolutePath), { recursive: true });
37709
38691
  (0, import_fs10.writeFileSync)(absolutePath, stableStringify(value), "utf-8");
37710
38692
  return absolutePath;
@@ -37988,8 +38970,8 @@ function registerBug(program3) {
37988
38970
 
37989
38971
  // src/lib/proof-cache.ts
37990
38972
  var import_node_crypto2 = require("node:crypto");
37991
- var import_node_fs4 = require("node:fs");
37992
- var import_node_path = require("node:path");
38973
+ var import_node_fs6 = require("node:fs");
38974
+ var import_node_path3 = require("node:path");
37993
38975
  var DEFAULT_MAX_AGE_MS = 15 * 60 * 1e3;
37994
38976
  var DEFAULT_WAIT_MS = 180 * 1e3;
37995
38977
  var DEFAULT_POLL_MS = 500;
@@ -38007,12 +38989,12 @@ function cacheKey2(kind, keyParts) {
38007
38989
  return (0, import_node_crypto2.createHash)("sha256").update(stableJson({ kind, keyParts, schema_version: "foh_proof_cache_key.v1" })).digest("hex").slice(0, 32);
38008
38990
  }
38009
38991
  function publicPath(filePath) {
38010
- const rel = (0, import_node_path.relative)(process.cwd(), filePath).replaceAll("\\", "/");
38992
+ const rel = (0, import_node_path3.relative)(process.cwd(), filePath).replaceAll("\\", "/");
38011
38993
  return rel.startsWith("..") ? filePath.replaceAll("\\", "/") : rel;
38012
38994
  }
38013
38995
  function readFreshCache(filePath, maxAgeMs) {
38014
38996
  try {
38015
- const payload = JSON.parse((0, import_node_fs4.readFileSync)(filePath, "utf8"));
38997
+ const payload = JSON.parse((0, import_node_fs6.readFileSync)(filePath, "utf8"));
38016
38998
  const createdAt = Date.parse(String(payload.created_at || ""));
38017
38999
  if (!Number.isFinite(createdAt)) return null;
38018
39000
  const ageMs = Date.now() - createdAt;
@@ -38024,8 +39006,8 @@ function readFreshCache(filePath, maxAgeMs) {
38024
39006
  }
38025
39007
  }
38026
39008
  function writeCache(filePath, value) {
38027
- (0, import_node_fs4.mkdirSync)((0, import_node_path.dirname)(filePath), { recursive: true });
38028
- (0, import_node_fs4.writeFileSync)(filePath, `${JSON.stringify({
39009
+ (0, import_node_fs6.mkdirSync)((0, import_node_path3.dirname)(filePath), { recursive: true });
39010
+ (0, import_node_fs6.writeFileSync)(filePath, `${JSON.stringify({
38029
39011
  schema_version: "foh_proof_cache_entry.v1",
38030
39012
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
38031
39013
  value
@@ -38035,7 +39017,7 @@ function writeCache(filePath, value) {
38035
39017
  function resolveProofCacheDir(input) {
38036
39018
  const value = String(input || process.env.FOH_PROOF_CACHE_DIR || "").trim();
38037
39019
  if (!value) return null;
38038
- return (0, import_node_path.isAbsolute)(value) ? value : (0, import_node_path.resolve)(process.cwd(), value);
39020
+ return (0, import_node_path3.isAbsolute)(value) ? value : (0, import_node_path3.resolve)(process.cwd(), value);
38039
39021
  }
38040
39022
  async function withProofCache(options, run) {
38041
39023
  const resolvedDir = resolveProofCacheDir(options.cacheDir);
@@ -38055,12 +39037,12 @@ async function withProofCache(options, run) {
38055
39037
  };
38056
39038
  }
38057
39039
  const key = cacheKey2(options.kind, options.keyParts);
38058
- const cachePath = (0, import_node_path.join)(resolvedDir, `${key}.json`);
38059
- const lockPath = (0, import_node_path.join)(resolvedDir, `${key}.lock`);
39040
+ const cachePath = (0, import_node_path3.join)(resolvedDir, `${key}.json`);
39041
+ const lockPath = (0, import_node_path3.join)(resolvedDir, `${key}.lock`);
38060
39042
  const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
38061
39043
  const waitMs = options.waitMs ?? Number(process.env.FOH_PROOF_CACHE_WAIT_MS || DEFAULT_WAIT_MS);
38062
39044
  const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
38063
- (0, import_node_fs4.mkdirSync)(resolvedDir, { recursive: true });
39045
+ (0, import_node_fs6.mkdirSync)(resolvedDir, { recursive: true });
38064
39046
  const existing = readFreshCache(cachePath, maxAgeMs);
38065
39047
  if (existing) {
38066
39048
  return {
@@ -38079,7 +39061,7 @@ async function withProofCache(options, run) {
38079
39061
  }
38080
39062
  let lockOwner = false;
38081
39063
  try {
38082
- (0, import_node_fs4.mkdirSync)(lockPath);
39064
+ (0, import_node_fs6.mkdirSync)(lockPath);
38083
39065
  lockOwner = true;
38084
39066
  } catch {
38085
39067
  const started = Date.now();
@@ -38120,7 +39102,7 @@ async function withProofCache(options, run) {
38120
39102
  }
38121
39103
  };
38122
39104
  } finally {
38123
- if (lockOwner) (0, import_node_fs4.rmSync)(lockPath, { recursive: true, force: true });
39105
+ if (lockOwner) (0, import_node_fs6.rmSync)(lockPath, { recursive: true, force: true });
38124
39106
  }
38125
39107
  }
38126
39108
 
@@ -38253,7 +39235,7 @@ function isProviderCapacityBlocked(onboarding) {
38253
39235
  return /maximum number of subaccounts|subaccount limit|reserve[- ]number pool|reserve pool exhausted|global safety limit/.test(message);
38254
39236
  }
38255
39237
  function registerProve(program3) {
38256
- program3.command("prove").description("Produce one setup/runtime proof bundle for an agent").option("--agent <id>", "Agent ID to prove").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--include-certification", "Run explicit simulation certification check (slow)").option("--cert-mode <m>", "Simulation cert mode when --include-certification is set: quick, full, stress", "quick").option("--cert-adaptive-runs <n>", "Adaptive runs for full/stress certification when included", "30").option("--cert-max-improvement-rounds <n>", "Max prompt improvement rounds in included cert loop (0-5)", "1").option("--mission <mission>", "Proof mission: setup, widget, voice, publish", "setup").option("--contact-path <mode>", "Voice contact path: auto, managed, or byon", "auto").option("--mutation-mode <mode>", "Proof mutation mode: read-only or ensure", "read-only").option("--repair", "Alias for --mutation-mode ensure").option("--require-phone", "Hold proof if no phone/contact number is provisioned").option("--skip-cert", "Deprecated compatibility flag; certification is skipped unless --include-certification is set").option("--skip-smoke", "Skip widget runtime smoke check").option("--skip-voice-health", "Skip realtime voice provider health check").option("--proof-cache-dir <path>", "Optional local proof cache directory for shared certification results").option("--out <path>", "Write signed proof report JSON to this path").option("--strict", "Exit non-zero unless all non-skipped checks pass").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39238
+ program3.command("prove").description("Produce one setup/runtime proof bundle for an agent").option("--agent <id>", "Agent ID to prove").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--include-certification", "Run explicit simulation certification check (slow)").option("--cert-mode <m>", "Simulation cert mode when --include-certification is set: quick, full, stress", "quick").option("--cert-adaptive-runs <n>", "Adaptive runs for full/stress certification when included", "30").option("--cert-max-improvement-rounds <n>", "Max prompt improvement rounds in included cert loop (0-5)", "1").option("--mission <mission>", "Proof mission: setup, widget, voice, publish", "setup").option("--contact-path <mode>", "Voice contact path: auto, managed, or byon", "auto").option("--mutation-mode <mode>", "Proof mutation mode: read-only or ensure", "read-only").option("--repair", "Alias for --mutation-mode ensure").option("--require-phone", "Hold proof if no phone/contact number is provisioned").option("--skip-cert", "Deprecated compatibility flag; certification is skipped unless --include-certification is set").option("--skip-smoke", "Skip widget runtime smoke check").option("--skip-voice-health", "Skip realtime voice provider health check").option("--proof-cache-dir <path>", "Optional local proof cache directory for shared certification results").option("--origin <urlOrHost>", "Website origin to simulate for domain allowlists, e.g. https://www.example.com").option("--out <path>", "Write signed proof report JSON to this path").option("--strict", "Exit non-zero unless all non-skipped checks pass").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
38257
39239
  const commandStartedMs = Date.now();
38258
39240
  const checks = [];
38259
39241
  const checkTimings = /* @__PURE__ */ new Map();
@@ -38450,16 +39432,25 @@ function registerProve(program3) {
38450
39432
  headers: { "x-agent-id": String(ctx.agentId) }
38451
39433
  }));
38452
39434
  const publicKey = publicKeyFromEmbedResponse(embed);
39435
+ const allowedDomains = Array.isArray(embed.allowed_domains) ? embed.allowed_domains.map((domain2) => String(domain2 || "").trim()).filter(Boolean) : [];
39436
+ const origin = normalizeOrigin(typeof opts.origin === "string" && opts.origin.trim() ? opts.origin : allowedDomains[0]);
38453
39437
  if (publicKey) {
38454
39438
  checkTimings.set("widget_channel", Math.max(checkTimings.get("widget_channel") ?? 0, checkTimings.get("widget_embed") ?? 0));
38455
39439
  ctx.widgetPublicKey = publicKey;
39440
+ ctx.widgetOrigin = origin;
38456
39441
  checks.push(pass("widget_channel", "Widget channel is available in read-only proof mode.", {
38457
39442
  public_key_present: true,
38458
- mutation_mode: mutationMode
39443
+ mutation_mode: mutationMode,
39444
+ origin,
39445
+ allowed_domains: allowedDomains
38459
39446
  }));
38460
39447
  }
38461
39448
  if (typeof embed.snippet === "string" && embed.snippet.trim()) {
38462
- checks.push(pass("widget_embed", "Widget embed snippet is available.", { snippet_present: true }));
39449
+ checks.push(pass("widget_embed", "Widget embed snippet is available.", {
39450
+ snippet_present: true,
39451
+ allowed_domains: allowedDomains,
39452
+ customer_runtime_profile_present: Boolean(embed.customer_runtime_profile)
39453
+ }));
38463
39454
  } else {
38464
39455
  checks.push(hold("widget_embed", "widget_embed_missing", "Widget embed snippet is missing.", `foh widget embed-snippet --agent ${ctx.agentId}`));
38465
39456
  }
@@ -38499,19 +39490,46 @@ function registerProve(program3) {
38499
39490
  checks.push(skipped("widget_smoke", "operator_skipped", "Skipped by --skip-smoke.", `foh widget smoke --agent ${ctx.agentId} --json`));
38500
39491
  } else if (!ctx.widgetPublicKey) {
38501
39492
  checks.push(skipped("widget_smoke", "widget_public_key_required", "Skipped because widget public key is unavailable.", `foh widget ensure --agent ${ctx.agentId} --json`));
39493
+ checks.push(skipped("widget_semantic", "widget_public_key_required", "Skipped because widget public key is unavailable.", `foh widget ensure --agent ${ctx.agentId} --json`));
38502
39494
  } else {
38503
39495
  try {
38504
- const smoke = await timedCheck(checkTimings, "widget_smoke", () => runWidgetSmoke(ctx.widgetPublicKey, opts.apiUrl));
39496
+ const embed = await apiFetch("/v1/console/channels/widget/embed-snippet", {
39497
+ orgId: ctx.orgId,
39498
+ apiUrlOverride: opts.apiUrl,
39499
+ headers: { "x-agent-id": String(ctx.agentId) }
39500
+ });
39501
+ const smoke = await timedCheck(checkTimings, "widget_smoke", () => runWidgetSmoke(ctx.widgetPublicKey, opts.apiUrl, ctx.widgetOrigin));
38505
39502
  ctx.conversationId = smoke.conversation_id;
38506
39503
  ctx.traceIds = smoke.trace_ids;
38507
39504
  ctx.correlationIds = smoke.correlation_ids;
38508
39505
  if (smoke.failed > 0) {
38509
- checks.push(hold("widget_smoke", "widget_smoke_failed", `${smoke.failed} widget smoke turn(s) failed.`, `foh widget smoke --agent ${ctx.agentId} --json`, smoke));
39506
+ checks.push(hold("widget_smoke", "widget_smoke_failed", `${smoke.failed} widget smoke transport turn(s) failed.`, `foh widget smoke --agent ${ctx.agentId} --json`, {
39507
+ ...smoke,
39508
+ origin: ctx.widgetOrigin ?? null
39509
+ }));
39510
+ checks.push(skipped("widget_semantic", "widget_smoke_required", "Skipped because transport smoke failed.", `foh widget smoke --agent ${ctx.agentId} --json`));
38510
39511
  } else {
38511
- checks.push(pass("widget_smoke", "Widget runtime smoke passed.", smoke));
39512
+ checks.push(pass("widget_smoke", "Widget transport smoke passed.", {
39513
+ ...smoke,
39514
+ origin: ctx.widgetOrigin ?? null
39515
+ }));
39516
+ const semantic = await timedCheck(checkTimings, "widget_semantic", () => runWidgetSemanticEval(
39517
+ ctx.widgetPublicKey,
39518
+ opts.apiUrl,
39519
+ ctx.widgetOrigin,
39520
+ embed.customer_runtime_profile ?? null
39521
+ ));
39522
+ if (!semantic.pack_id) {
39523
+ checks.push(skipped("widget_semantic", "widget_semantic_pack_not_configured", "No widget semantic eval pack is configured for this business.", void 0));
39524
+ } else if (semantic.failed > 0) {
39525
+ checks.push(hold("widget_semantic", "widget_semantic_failed", `${semantic.failed} widget semantic scenario(s) failed.`, `foh widget smoke --agent ${ctx.agentId} --json`, semantic));
39526
+ } else {
39527
+ checks.push(pass("widget_semantic", "Widget semantic eval passed.", semantic));
39528
+ }
38512
39529
  }
38513
39530
  } catch (error2) {
38514
39531
  checks.push(fail("widget_smoke", "widget_smoke_failed", error2, `foh widget smoke --agent ${ctx.agentId} --json`));
39532
+ checks.push(skipped("widget_semantic", "widget_smoke_required", "Skipped because widget smoke threw before semantic evaluation.", `foh widget smoke --agent ${ctx.agentId} --json`));
38515
39533
  }
38516
39534
  }
38517
39535
  if (opts.skipCert) {
@@ -38597,6 +39615,7 @@ function registerProve(program3) {
38597
39615
  contact_path: contactPath,
38598
39616
  mutation_mode: mutationMode,
38599
39617
  widget_public_key_present: Boolean(ctx.widgetPublicKey),
39618
+ widget_origin: ctx.widgetOrigin ?? null,
38600
39619
  conversation_id: ctx.conversationId ?? null,
38601
39620
  trace_ids: ctx.traceIds,
38602
39621
  correlation_ids: ctx.correlationIds
@@ -38615,8 +39634,8 @@ function registerProve(program3) {
38615
39634
  }
38616
39635
 
38617
39636
  // src/commands/objective.ts
38618
- var import_node_fs5 = require("node:fs");
38619
- var import_node_path2 = require("node:path");
39637
+ var import_node_fs7 = require("node:fs");
39638
+ var import_node_path4 = require("node:path");
38620
39639
  var DEFAULT_OBJECTIVE_REPORT_PATH = "test-results/objective-status.latest.json";
38621
39640
  var VALID_OBJECTIVE_INDUSTRIES = ["real_estate", "restaurant", "general"];
38622
39641
  var BUSINESS_REQUIREMENT_BRIEF_SCHEMA_VERSION = "business_requirement_brief.v1";
@@ -38626,6 +39645,12 @@ function asRecord3(value) {
38626
39645
  function normalizeString2(value) {
38627
39646
  return typeof value === "string" ? value.trim() : "";
38628
39647
  }
39648
+ function quoteCliArg(value) {
39649
+ const normalized = value.trim();
39650
+ if (!normalized) return '""';
39651
+ if (!/[\s"]/.test(normalized)) return normalized;
39652
+ return `"${normalized.replace(/"/g, '\\"')}"`;
39653
+ }
38629
39654
  function uniqueStrings(values) {
38630
39655
  return Array.from(new Set(values.map(normalizeString2).filter(Boolean)));
38631
39656
  }
@@ -38684,22 +39709,26 @@ function finiteNumber(value) {
38684
39709
  return Number.isFinite(number3) ? number3 : null;
38685
39710
  }
38686
39711
  function normalizeCustomerEvidenceActions(value) {
38687
- return asArray(value).map(asRecord3).map((action) => ({
38688
- id: firstString(action, ["id", "action_id"]),
38689
- title: firstString(action, ["title"]) || null,
38690
- owner: firstString(action, ["owner"]) || null,
38691
- blocker_count: finiteNumber(action.blocker_count) ?? (uniqueStrings(asArray(action.reason_codes)).length || null),
38692
- target_evidence_paths: uniqueStrings([
38693
- ...asArray(action.target_evidence_paths),
38694
- firstString(action, ["target_evidence_path"])
38695
- ]),
38696
- validator_commands: uniqueStrings([
38697
- ...asArray(action.validator_commands),
38698
- ...asArray(action.next_commands)
38699
- ]),
38700
- required_evidence: firstString(action, ["required_evidence"]) || null,
38701
- unlocks: firstString(action, ["unlocks"]) || null
38702
- })).filter((action) => normalizeString2(action.id));
39712
+ return asArray(value).map(asRecord3).map((action) => {
39713
+ const id = firstString(action, ["id", "action_id"]);
39714
+ return {
39715
+ id,
39716
+ title: firstString(action, ["title"]) || null,
39717
+ owner: firstString(action, ["owner"]) || null,
39718
+ blocker_count: finiteNumber(action.blocker_count) ?? (uniqueStrings(asArray(action.reason_codes)).length || null),
39719
+ target_evidence_paths: uniqueStrings([
39720
+ ...asArray(action.target_evidence_paths),
39721
+ firstString(action, ["target_evidence_path"]),
39722
+ id ? `customer-evidence/${id}.json` : ""
39723
+ ]),
39724
+ validator_commands: uniqueStrings([
39725
+ ...asArray(action.validator_commands),
39726
+ ...asArray(action.next_commands)
39727
+ ]),
39728
+ required_evidence: firstString(action, ["required_evidence"]) || null,
39729
+ unlocks: firstString(action, ["unlocks"]) || null
39730
+ };
39731
+ }).filter((action) => normalizeString2(action.id));
38703
39732
  }
38704
39733
  function normalizeCustomerEvidenceActionPacket(value) {
38705
39734
  const packet = asRecord3(value);
@@ -38716,7 +39745,63 @@ function normalizeCustomerEvidenceActionPacket(value) {
38716
39745
  instructions: uniqueStrings(asArray(packet.instructions))
38717
39746
  };
38718
39747
  }
39748
+ function skeletonHintsById(...sources) {
39749
+ const hints = /* @__PURE__ */ new Map();
39750
+ for (const source of sources) {
39751
+ const record2 = asRecord3(source);
39752
+ const evidencePacket = asRecord3(record2.evidence_packet);
39753
+ const skeletons = asArray(evidencePacket.skeletons).map(asRecord3);
39754
+ for (const skeleton of skeletons) {
39755
+ const id = normalizeString2(skeleton.id);
39756
+ if (!id || hints.has(id)) continue;
39757
+ hints.set(id, skeleton);
39758
+ }
39759
+ }
39760
+ return hints;
39761
+ }
39762
+ function mergeActionPacketWithSkeletonHints(packet, skeletonHints) {
39763
+ if (!packet) return void 0;
39764
+ const actions = asArray(packet.actions).map(asRecord3).filter((action) => normalizeString2(action.id));
39765
+ if (actions.length === 0) return packet;
39766
+ return {
39767
+ ...packet,
39768
+ actions: actions.map((action) => {
39769
+ const hint = skeletonHints.get(normalizeString2(action.id));
39770
+ const hintedTarget = normalizeString2(hint?.target_evidence_path);
39771
+ return {
39772
+ ...action,
39773
+ target_evidence_paths: uniqueStrings([
39774
+ ...asArray(action.target_evidence_paths),
39775
+ hintedTarget
39776
+ ]),
39777
+ required_evidence: action.required_evidence ?? (firstString(asRecord3(hint), ["instructions"]) || null)
39778
+ };
39779
+ })
39780
+ };
39781
+ }
39782
+ function buildObjectiveApplyPlanCommand(reportPath) {
39783
+ return `foh objective apply --plan ${quoteCliArg(reportPath)} --dry-run --json`;
39784
+ }
39785
+ function injectObjectiveApplyGuidance(packet, reportPath) {
39786
+ if (!packet) return void 0;
39787
+ const planCommand = buildObjectiveApplyPlanCommand(reportPath);
39788
+ return {
39789
+ ...packet,
39790
+ validation_commands: dedupeCommands([
39791
+ planCommand,
39792
+ ...uniqueStrings(asArray(packet.validation_commands))
39793
+ ]),
39794
+ actions: asArray(packet.actions).map(asRecord3).filter((action) => normalizeString2(action.id)).map((action) => ({
39795
+ ...action,
39796
+ validator_commands: dedupeCommands([
39797
+ planCommand,
39798
+ ...uniqueStrings(asArray(action.validator_commands))
39799
+ ])
39800
+ }))
39801
+ };
39802
+ }
38719
39803
  function customerEvidenceActionPacketFromSources(...sources) {
39804
+ const skeletonHints = skeletonHintsById(...sources);
38720
39805
  for (const source of sources) {
38721
39806
  const report = asRecord3(source);
38722
39807
  const requestPackets = asRecord3(report.request_packets);
@@ -38736,14 +39821,14 @@ function customerEvidenceActionPacketFromSources(...sources) {
38736
39821
  report.customer_actions ? { actions: report.customer_actions } : null
38737
39822
  ];
38738
39823
  for (const candidate of candidates) {
38739
- const normalized = normalizeCustomerEvidenceActionPacket(candidate);
39824
+ const normalized = mergeActionPacketWithSkeletonHints(normalizeCustomerEvidenceActionPacket(candidate), skeletonHints);
38740
39825
  if (normalized) return normalized;
38741
39826
  }
38742
39827
  }
38743
39828
  return void 0;
38744
39829
  }
38745
39830
  function normalizeObjectiveArtifactPath(value) {
38746
- return value.trim() ? (0, import_node_path2.resolve)(value.trim()) : "";
39831
+ return value.trim() ? (0, import_node_path4.resolve)(value.trim()) : "";
38747
39832
  }
38748
39833
  function resolveObjectiveArtifactPath(value) {
38749
39834
  return normalizeObjectiveArtifactPath(value);
@@ -38758,10 +39843,26 @@ function pickEvidencePacket(value) {
38758
39843
  const payload = firstNonEmptyObject(record2.payload);
38759
39844
  return payload ?? record2;
38760
39845
  }
38761
- function readEvidencePacketFromPlan(path2) {
38762
- const plan = asRecord3(readJsonArtifact(path2));
39846
+ function evidenceRequestFromSkeletonPacket(value) {
39847
+ const packet = asRecord3(value);
39848
+ const skeletons = asArray(packet.skeletons).map(asRecord3).filter((item) => normalizeString2(item.id));
39849
+ if (skeletons.length === 0) return void 0;
39850
+ const launchScopeId = normalizeString2(packet.launch_scope_id) || normalizeString2(skeletons[0]?.launch_scope_id);
39851
+ return {
39852
+ ...launchScopeId ? { launch_scope_id: launchScopeId } : {},
39853
+ evidence: skeletons.map((skeleton) => ({
39854
+ id: normalizeString2(skeleton.id),
39855
+ launch_scope_id: normalizeString2(skeleton.launch_scope_id) || launchScopeId || void 0,
39856
+ target_evidence_path: normalizeString2(skeleton.target_evidence_path) || `customer-evidence/${normalizeString2(skeleton.id)}.json`,
39857
+ payload: asRecord3(skeleton.payload_template)
39858
+ }))
39859
+ };
39860
+ }
39861
+ function readEvidencePacketFromPlan(path5) {
39862
+ const plan = asRecord3(readJsonArtifact(path5));
38763
39863
  const sourceReports = asRecord3(plan.source_reports);
38764
- return pickEvidencePacket(plan.evidence) ?? pickEvidencePacket(plan.evidence_packet) ?? pickEvidencePacket(sourceReports.customer_live_status && asRecord3(sourceReports.customer_live_status).evidence) ?? pickEvidencePacket(asRecord3(sourceReports.setup_workflow).evidence_packet) ?? {};
39864
+ const packet = pickEvidencePacket(plan.evidence) ?? pickEvidencePacket(plan.evidence_packet) ?? pickEvidencePacket(sourceReports.customer_live_status && asRecord3(sourceReports.customer_live_status).evidence) ?? pickEvidencePacket(asRecord3(sourceReports.setup_workflow).evidence_packet) ?? {};
39865
+ return evidenceRequestFromSkeletonPacket(packet) ?? packet;
38765
39866
  }
38766
39867
  function inferEvidenceFromProofPaths(evidence) {
38767
39868
  const record2 = asRecord3(evidence);
@@ -38876,16 +39977,16 @@ function buildDeveloperReadinessPacket(input) {
38876
39977
  }
38877
39978
  function resolveObjectiveReportPath(value) {
38878
39979
  const raw = normalizeString2(value);
38879
- if (!raw || raw === "latest") return (0, import_node_path2.resolve)(DEFAULT_OBJECTIVE_REPORT_PATH);
38880
- return (0, import_node_path2.resolve)(raw);
39980
+ if (!raw || raw === "latest") return (0, import_node_path4.resolve)(DEFAULT_OBJECTIVE_REPORT_PATH);
39981
+ return (0, import_node_path4.resolve)(raw);
38881
39982
  }
38882
- function writeJsonArtifact2(path2, value) {
38883
- (0, import_node_fs5.mkdirSync)((0, import_node_path2.dirname)(path2), { recursive: true });
38884
- (0, import_node_fs5.writeFileSync)(path2, `${JSON.stringify(value, null, 2)}
39983
+ function writeJsonArtifact2(path5, value) {
39984
+ (0, import_node_fs7.mkdirSync)((0, import_node_path4.dirname)(path5), { recursive: true });
39985
+ (0, import_node_fs7.writeFileSync)(path5, `${JSON.stringify(value, null, 2)}
38885
39986
  `, "utf8");
38886
39987
  }
38887
- function readJsonArtifact(path2) {
38888
- return JSON.parse((0, import_node_fs5.readFileSync)(path2, "utf8"));
39988
+ function readJsonArtifact(path5) {
39989
+ return JSON.parse((0, import_node_fs7.readFileSync)(path5, "utf8"));
38889
39990
  }
38890
39991
  function buildSetupBody(opts) {
38891
39992
  const tools = parseCsvOption(opts.tools ?? "widget,voice,email,callback,calendar,crm,webhook,whatsapp") ?? [];
@@ -39121,8 +40222,14 @@ function buildObjectiveReport(input) {
39121
40222
  ...collectStringArrays(setup, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"])),
39122
40223
  ...collectStringArrays(live, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"]))
39123
40224
  ]);
40225
+ const objectiveArtifactPath = normalizeString2(input.opts.out) || DEFAULT_OBJECTIVE_REPORT_PATH;
39124
40226
  const debugSource = normalizeString2(input.opts.out) || "latest";
40227
+ let customerEvidenceActionPacket = customerEvidenceActionPacketFromSources(live, setup);
40228
+ if (customerEvidenceActionPacket) {
40229
+ customerEvidenceActionPacket = injectObjectiveApplyGuidance(customerEvidenceActionPacket, objectiveArtifactPath);
40230
+ }
39125
40231
  const nextCommands = dedupeCommands([
40232
+ ...customerEvidenceActionPacket ? [buildObjectiveApplyPlanCommand(objectiveArtifactPath)] : [],
39126
40233
  ...collectStringArrays(setup, /* @__PURE__ */ new Set(["next_commands"])),
39127
40234
  ...collectStringArrays(live, /* @__PURE__ */ new Set(["next_commands"])),
39128
40235
  `foh objective debug --from ${debugSource} --json`
@@ -39136,7 +40243,6 @@ function buildObjectiveReport(input) {
39136
40243
  ...collectArtifactRefs(setup),
39137
40244
  ...collectArtifactRefs(live)
39138
40245
  ]);
39139
- const customerEvidenceActionPacket = customerEvidenceActionPacketFromSources(live, setup);
39140
40246
  const developerReadinessPacket = buildDeveloperReadinessPacket({
39141
40247
  opts: input.opts,
39142
40248
  setupWorkflow: setup,
@@ -39379,7 +40485,7 @@ function registerObjective(program3) {
39379
40485
  }));
39380
40486
  objective.command("debug").description("Explain the next debugging action for an objective report").option("--from <source>", "Report source alias or path", "latest").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39381
40487
  const sourcePath = resolveObjectiveReportPath(opts.from);
39382
- if (!(0, import_node_fs5.existsSync)(sourcePath)) {
40488
+ if (!(0, import_node_fs7.existsSync)(sourcePath)) {
39383
40489
  format(cliEnvelope({
39384
40490
  schemaVersion: "agent_workbench_debug.v1",
39385
40491
  status: "fail",
@@ -40450,6 +41556,277 @@ function registerInteractive(program3) {
40450
41556
  });
40451
41557
  }
40452
41558
 
41559
+ // src/commands/install.ts
41560
+ var import_node_fs8 = __toESM(require("node:fs"));
41561
+ var import_node_path5 = __toESM(require("node:path"));
41562
+ var import_node_child_process3 = require("node:child_process");
41563
+ function splitCsv(value) {
41564
+ if (typeof value !== "string") return [];
41565
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
41566
+ }
41567
+ function pickFirstParam(url2, names) {
41568
+ for (const name of names) {
41569
+ const value = url2.searchParams.get(name);
41570
+ if (value && value.trim()) return value.trim();
41571
+ }
41572
+ return void 0;
41573
+ }
41574
+ function parseInstallLink(raw) {
41575
+ if (!raw) return {};
41576
+ let url2;
41577
+ try {
41578
+ url2 = new URL(raw);
41579
+ } catch {
41580
+ throw new FohError({
41581
+ step: "install.link",
41582
+ error: "Install link is not a valid URL.",
41583
+ remediation: "Use the one-time Front Of House install link, or run: foh install --agent <agent-id>"
41584
+ });
41585
+ }
41586
+ const domainsFromRepeatedParams = [
41587
+ ...url2.searchParams.getAll("domain"),
41588
+ ...url2.searchParams.getAll("domains")
41589
+ ].flatMap((value) => splitCsv(value));
41590
+ return {
41591
+ agent: pickFirstParam(url2, ["agent", "agent_id"]),
41592
+ org: pickFirstParam(url2, ["org", "org_id"]),
41593
+ domains: Array.from(new Set(domainsFromRepeatedParams)),
41594
+ apiUrl: pickFirstParam(url2, ["api_url", "apiUrl"]),
41595
+ publicKey: pickFirstParam(url2, ["channel", "channel_public_key", "public_key"]),
41596
+ installToken: pickFirstParam(url2, ["token", "install_token"])
41597
+ };
41598
+ }
41599
+ async function resolveInstallLinkToken(token, apiUrl) {
41600
+ const baseUrl = (apiUrl || DEFAULT_WIDGET_API_URL).replace(/\/$/, "");
41601
+ const res = await fetch(`${baseUrl}/v1/widget/install-link/resolve`, {
41602
+ method: "POST",
41603
+ headers: { "Content-Type": "application/json" },
41604
+ body: JSON.stringify({ token })
41605
+ });
41606
+ let body = {};
41607
+ try {
41608
+ body = await res.json();
41609
+ } catch {
41610
+ body = {};
41611
+ }
41612
+ if (!res.ok) {
41613
+ throw new FohError({
41614
+ step: "install.link.resolve",
41615
+ error: String(body?.error || `HTTP ${res.status}`),
41616
+ remediation: String(body?.code === "install_link_token_expired" ? "Ask the operator for a fresh install link." : "Check the install link and retry."),
41617
+ statusCode: res.status,
41618
+ detail: body
41619
+ });
41620
+ }
41621
+ return {
41622
+ agent: typeof body?.agent_id === "string" ? body.agent_id.trim() : void 0,
41623
+ org: typeof body?.org_id === "string" ? body.org_id.trim() : void 0,
41624
+ publicKey: typeof body?.widget_public_key === "string" ? body.widget_public_key.trim() : void 0,
41625
+ apiUrl: typeof body?.api_url === "string" ? body.api_url.trim() : void 0,
41626
+ domains: Array.isArray(body?.allowed_domains) ? body.allowed_domains.map((domain2) => String(domain2 || "").trim()).filter(Boolean) : void 0
41627
+ };
41628
+ }
41629
+ function findPackageJsonForTarget(siteRoot, targetPath) {
41630
+ const root = import_node_path5.default.resolve(siteRoot || ".");
41631
+ let current = import_node_path5.default.dirname(targetPath);
41632
+ while (current.startsWith(root)) {
41633
+ const candidate = import_node_path5.default.join(current, "package.json");
41634
+ if (import_node_fs8.default.existsSync(candidate) && import_node_fs8.default.statSync(candidate).isFile()) return candidate;
41635
+ const next = import_node_path5.default.dirname(current);
41636
+ if (next === current) break;
41637
+ current = next;
41638
+ }
41639
+ return null;
41640
+ }
41641
+ function packageManagerFor(cwd, siteRoot) {
41642
+ const root = import_node_path5.default.resolve(siteRoot || ".");
41643
+ let current = cwd;
41644
+ while (current.startsWith(root)) {
41645
+ if (import_node_fs8.default.existsSync(import_node_path5.default.join(current, "pnpm-lock.yaml"))) return "pnpm";
41646
+ if (import_node_fs8.default.existsSync(import_node_path5.default.join(current, "yarn.lock"))) return "yarn";
41647
+ if (import_node_fs8.default.existsSync(import_node_path5.default.join(current, "bun.lockb")) || import_node_fs8.default.existsSync(import_node_path5.default.join(current, "bun.lock"))) return "bun";
41648
+ const next = import_node_path5.default.dirname(current);
41649
+ if (next === current) break;
41650
+ current = next;
41651
+ }
41652
+ return "npm";
41653
+ }
41654
+ function detectBuildCommand(siteRoot, targetPath) {
41655
+ const packageJsonPath = findPackageJsonForTarget(siteRoot, targetPath);
41656
+ if (!packageJsonPath) return null;
41657
+ const packageJson = JSON.parse(import_node_fs8.default.readFileSync(packageJsonPath, "utf8"));
41658
+ if (!packageJson.scripts?.build) return null;
41659
+ const cwd = import_node_path5.default.dirname(packageJsonPath);
41660
+ const packageManager = packageManagerFor(cwd, siteRoot);
41661
+ if (packageManager === "npm") {
41662
+ return {
41663
+ cwd,
41664
+ command: "npm",
41665
+ args: ["run", "build"],
41666
+ display: "npm run build",
41667
+ package_manager: packageManager
41668
+ };
41669
+ }
41670
+ return {
41671
+ cwd,
41672
+ command: packageManager,
41673
+ args: ["build"],
41674
+ display: `${packageManager} build`,
41675
+ package_manager: packageManager
41676
+ };
41677
+ }
41678
+ function runBuildCommand(build) {
41679
+ const command = process.platform === "win32" ? "cmd.exe" : build.command;
41680
+ const args = process.platform === "win32" ? ["/d", "/s", "/c", build.command, ...build.args] : build.args;
41681
+ const result = (0, import_node_child_process3.spawnSync)(command, args, {
41682
+ cwd: build.cwd,
41683
+ encoding: "utf8",
41684
+ stdio: ["ignore", "pipe", "pipe"],
41685
+ windowsHide: true
41686
+ });
41687
+ const stdout = String(result.stdout || "").split(/\r?\n/).filter(Boolean).slice(-20);
41688
+ const stderr = String(result.stderr || "").split(/\r?\n/).filter(Boolean).slice(-20);
41689
+ return {
41690
+ ok: result.status === 0,
41691
+ exit_code: result.status,
41692
+ command: build.display,
41693
+ cwd: import_node_path5.default.relative(process.cwd(), build.cwd).replaceAll("\\", "/") || ".",
41694
+ stdout_tail: stdout,
41695
+ stderr_tail: stderr,
41696
+ ...result.error ? { error: result.error.message } : {}
41697
+ };
41698
+ }
41699
+ function registerInstall(program3) {
41700
+ program3.command("install [installLink]").description("Install Front Of House on this website").option("--agent <id>", "Agent ID; normally supplied by the install link").option("--org <id>", "Org ID; normally supplied by the install link or stored org context").option("--domains <d1,d2>", "Optional comma-separated widget domain allowlist").option("--platform <platform>", `Install target platform: ${SUPPORTED_INSTALL_PLATFORMS.join("|")}`, "custom").option("--site-root <path>", "Website repository root", ".").option("--target <path>", "HTML shell to patch, relative to --site-root").option("--api-url <url>", "API base URL override").option("--origin <urlOrHost>", "Website origin to simulate for domain allowlists, e.g. https://www.example.com").option("--dry-run", "Resolve and report install actions without writing files or running build/smoke").option("--no-build", "Do not run the website build command after installing").option("--no-smoke", "Do not run the widget smoke check after installing").option("--json", "Output as JSON").action(async (installLink, opts) => withCommandErrorHandling(async () => {
41701
+ const parsedLink = parseInstallLink(installLink);
41702
+ const resolvedLink = parsedLink.installToken ? await resolveInstallLinkToken(parsedLink.installToken, opts.apiUrl || parsedLink.apiUrl) : {};
41703
+ const link = {
41704
+ ...parsedLink,
41705
+ ...resolvedLink,
41706
+ apiUrl: opts.apiUrl || resolvedLink.apiUrl || parsedLink.apiUrl,
41707
+ publicKey: resolvedLink.publicKey || parsedLink.publicKey
41708
+ };
41709
+ const domains = splitCsv(opts.domains);
41710
+ const agent = opts.agent || link.agent;
41711
+ const org = opts.org || link.org;
41712
+ const apiUrl = opts.apiUrl || link.apiUrl;
41713
+ const domainAllowlist = domains.length > 0 ? domains : link.domains || [];
41714
+ const platform = normalizeInstallPlatform(opts.platform);
41715
+ const smokeOrigin = normalizeOrigin(
41716
+ typeof opts.origin === "string" && opts.origin.trim() ? opts.origin : domainAllowlist[0]
41717
+ );
41718
+ const snippet2 = link.publicKey ? widgetSnippetFromPublicKey(link.publicKey, apiUrl) : void 0;
41719
+ if (!agent && !snippet2) {
41720
+ throw new FohError({
41721
+ step: "install",
41722
+ error: "No Front Of House agent was provided.",
41723
+ remediation: "Run: foh install <install-link>, or foh install --agent <agent-id>"
41724
+ });
41725
+ }
41726
+ if (isBuilderHostedPlatform(platform)) {
41727
+ if (!snippet2) {
41728
+ throw new FohError({
41729
+ step: "install",
41730
+ error: "Builder-hosted install requires a public widget snippet or install link.",
41731
+ remediation: "Run: foh install <install-link-with-channel-or-token> --platform <platform>"
41732
+ });
41733
+ }
41734
+ const instructions = buildBuilderPlatformInstructions({
41735
+ platform,
41736
+ snippet: snippet2,
41737
+ publicKey: link.publicKey,
41738
+ domains: domainAllowlist,
41739
+ origin: smokeOrigin
41740
+ });
41741
+ format({
41742
+ schema_version: "foh_site_install.v1",
41743
+ status: opts.dryRun ? "dry_run" : "ready_for_manual_install",
41744
+ install_mode: "builder_platform",
41745
+ platform,
41746
+ site_root: null,
41747
+ target_path: null,
41748
+ changed: false,
41749
+ install: instructions,
41750
+ widget_public_key: instructions.widget_public_key,
41751
+ domain_allowlist: instructions.domain_allowlist,
41752
+ origin_hint: instructions.origin_hint,
41753
+ install_verification_command: instructions.install_verification_command,
41754
+ runtime_smoke_command: instructions.runtime_smoke_command,
41755
+ verification_commands: instructions.verification_commands,
41756
+ build: {
41757
+ ok: null,
41758
+ skipped: true,
41759
+ reason: "builder_platform_manual_install",
41760
+ command: null
41761
+ },
41762
+ smoke: {
41763
+ ok: null,
41764
+ skipped: true,
41765
+ reason: "builder_platform_manual_install"
41766
+ },
41767
+ next_commands: instructions.verification_commands
41768
+ }, { json: opts.json ?? false });
41769
+ return;
41770
+ }
41771
+ const install = await installWidgetSite({
41772
+ agent,
41773
+ org,
41774
+ domains: domainAllowlist,
41775
+ siteRoot: opts.siteRoot,
41776
+ target: opts.target,
41777
+ snippet: snippet2,
41778
+ apiUrl,
41779
+ dryRun: opts.dryRun
41780
+ });
41781
+ const targetPath = install.absolute_target_path || resolveWidgetInstallTarget(opts.siteRoot, opts.target);
41782
+ const buildCommand = detectBuildCommand(opts.siteRoot, targetPath);
41783
+ const installGuidance = buildWidgetInstallGuidance({
41784
+ publicKey: install.widget_public_key || link.publicKey || null,
41785
+ agentId: agent,
41786
+ domains: domainAllowlist,
41787
+ origin: smokeOrigin
41788
+ });
41789
+ const build = !opts.dryRun && opts.build !== false && buildCommand ? runBuildCommand(buildCommand) : null;
41790
+ const smokePublicKey = installGuidance.widget_public_key;
41791
+ const smoke = !opts.dryRun && opts.smoke !== false && smokePublicKey ? await runWidgetSmoke(smokePublicKey, apiUrl, smokeOrigin) : null;
41792
+ const ok = install.status === "installed" && (build ? build.ok : true) && (smoke ? smoke.failed === 0 : true);
41793
+ const result = {
41794
+ schema_version: "foh_site_install.v1",
41795
+ status: opts.dryRun ? "dry_run" : ok ? "installed" : "failed",
41796
+ install_mode: "repo_patch",
41797
+ platform,
41798
+ site_root: import_node_path5.default.resolve(opts.siteRoot || "."),
41799
+ target_path: install.target_path,
41800
+ changed: install.changed,
41801
+ install,
41802
+ widget_public_key: installGuidance.widget_public_key,
41803
+ domain_allowlist: installGuidance.domain_allowlist,
41804
+ origin_hint: installGuidance.origin_hint,
41805
+ install_verification_command: installGuidance.install_verification_command,
41806
+ runtime_smoke_command: installGuidance.runtime_smoke_command,
41807
+ verification_commands: installGuidance.verification_commands,
41808
+ build: build || {
41809
+ ok: null,
41810
+ skipped: opts.dryRun || opts.build === false || !buildCommand,
41811
+ reason: opts.dryRun ? "dry_run" : opts.build === false ? "disabled" : "no package.json build script found near target",
41812
+ command: buildCommand?.display ?? null
41813
+ },
41814
+ smoke: smoke || {
41815
+ ok: null,
41816
+ skipped: opts.dryRun || opts.smoke === false || !install.widget_public_key,
41817
+ reason: opts.dryRun ? "dry_run" : opts.smoke === false ? "disabled" : "widget public key unavailable"
41818
+ },
41819
+ next_commands: ok ? [] : [
41820
+ build && !build.ok ? build.command : null,
41821
+ smoke && smoke.failed > 0 ? installGuidance.runtime_smoke_command : null,
41822
+ !smoke && installGuidance.verification_commands.length > 0 ? installGuidance.verification_commands[0] : null
41823
+ ].filter(Boolean)
41824
+ };
41825
+ format(result, { json: opts.json ?? false });
41826
+ if (!ok && !opts.dryRun) markCommandFailed(1);
41827
+ }));
41828
+ }
41829
+
40453
41830
  // src/commands/home-actions.ts
40454
41831
  function resolveHomeState(apiUrlOverride) {
40455
41832
  try {
@@ -41067,9 +42444,9 @@ function compareSemver(a, b) {
41067
42444
  }
41068
42445
  return 0;
41069
42446
  }
41070
- function readPackageJsonVersion(path2) {
42447
+ function readPackageJsonVersion(path5) {
41071
42448
  try {
41072
- const raw = (0, import_fs12.readFileSync)(path2, "utf-8");
42449
+ const raw = (0, import_fs12.readFileSync)(path5, "utf-8");
41073
42450
  const parsed = JSON.parse(raw);
41074
42451
  const version2 = String(parsed.version ?? "").trim();
41075
42452
  return version2 || void 0;
@@ -41320,8 +42697,8 @@ var PHONE_RE2 = /(?<!\w)(?:\+?\d[\d\s().-]{7,}\d)(?!\w)/g;
41320
42697
  function escapeRegExp(value) {
41321
42698
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
41322
42699
  }
41323
- function pathVariants(path2) {
41324
- const resolved = (0, import_path11.resolve)(path2);
42700
+ function pathVariants(path5) {
42701
+ const resolved = (0, import_path11.resolve)(path5);
41325
42702
  const slash = resolved.replace(/\\/g, "/");
41326
42703
  const backslash = resolved.replace(/\//g, "\\");
41327
42704
  return Array.from(new Set([
@@ -41365,9 +42742,9 @@ function redactExternalAgentSecretText(text) {
41365
42742
  }
41366
42743
  function artifactFiles(runDir) {
41367
42744
  if (!(0, import_fs13.existsSync)(runDir)) return [];
41368
- return (0, import_fs13.readdirSync)(runDir).map((name) => (0, import_path11.join)(runDir, name)).filter((path2) => {
41369
- const stat = (0, import_fs13.statSync)(path2);
41370
- const name = path2.split(/[\\/]/).pop() || "";
42745
+ return (0, import_fs13.readdirSync)(runDir).map((name) => (0, import_path11.join)(runDir, name)).filter((path5) => {
42746
+ const stat = (0, import_fs13.statSync)(path5);
42747
+ const name = path5.split(/[\\/]/).pop() || "";
41371
42748
  if (name.endsWith(".redacted")) return false;
41372
42749
  return stat.isFile() && (TEXT_ARTIFACT_NAMES.has(name) || name.startsWith("command-output-cmd_"));
41373
42750
  }).sort();
@@ -41736,10 +43113,10 @@ function collectDocsFrom(value, docs) {
41736
43113
  }
41737
43114
  function readExternalAgentMetadata(runDir) {
41738
43115
  for (const filename of EXTERNAL_AGENT_METADATA_FILENAMES) {
41739
- const path2 = (0, import_path14.join)(runDir, filename);
41740
- if (!(0, import_fs15.existsSync)(path2)) continue;
43116
+ const path5 = (0, import_path14.join)(runDir, filename);
43117
+ if (!(0, import_fs15.existsSync)(path5)) continue;
41741
43118
  try {
41742
- const parsed = JSON.parse((0, import_fs15.readFileSync)(path2, "utf8"));
43119
+ const parsed = JSON.parse((0, import_fs15.readFileSync)(path5, "utf8"));
41743
43120
  const docs = /* @__PURE__ */ new Set();
41744
43121
  collectDocsFrom(parsed.docs_pages_used, docs);
41745
43122
  collectDocsFrom(parsed.docs_pages_observed, docs);
@@ -41767,11 +43144,11 @@ function readExternalAgentMetadata(runDir) {
41767
43144
  }
41768
43145
 
41769
43146
  // src/lib/external-agent-executor-artifacts.ts
41770
- function redactArtifactFile(path2, input = {}) {
41771
- if (!(0, import_fs16.existsSync)(path2)) return;
41772
- const original = (0, import_fs16.readFileSync)(path2, "utf8");
43147
+ function redactArtifactFile(path5, input = {}) {
43148
+ if (!(0, import_fs16.existsSync)(path5)) return;
43149
+ const original = (0, import_fs16.readFileSync)(path5, "utf8");
41773
43150
  const redacted = redactExternalAgentArtifactText(original, input);
41774
- if (redacted !== original) (0, import_fs16.writeFileSync)(path2, redacted, "utf8");
43151
+ if (redacted !== original) (0, import_fs16.writeFileSync)(path5, redacted, "utf8");
41775
43152
  }
41776
43153
  function redactExternalAgentOutputArtifacts(run, input = {}) {
41777
43154
  redactArtifactFile(run.outputs.jsonl, input);
@@ -41812,11 +43189,11 @@ function proofArtifactPasses(runDir) {
41812
43189
  return false;
41813
43190
  }
41814
43191
  }
41815
- function readIfExists(path2) {
41816
- return (0, import_fs17.existsSync)(path2) ? (0, import_fs17.readFileSync)(path2, "utf8") : "";
43192
+ function readIfExists(path5) {
43193
+ return (0, import_fs17.existsSync)(path5) ? (0, import_fs17.readFileSync)(path5, "utf8") : "";
41817
43194
  }
41818
- function relativeArtifactName(path2) {
41819
- return (0, import_path16.basename)(path2);
43195
+ function relativeArtifactName(path5) {
43196
+ return (0, import_path16.basename)(path5);
41820
43197
  }
41821
43198
  function externalAgentSummaryTemplateCommand() {
41822
43199
  return [
@@ -42149,11 +43526,11 @@ async function runExternalAgentEvalAuthPreflight(env = process.env, options = {}
42149
43526
  matched_org: matchedOrg
42150
43527
  };
42151
43528
  }
42152
- function normalizeForCompare(path2) {
42153
- const resolved = (0, import_path18.resolve)(path2);
43529
+ function normalizeForCompare(path5) {
43530
+ const resolved = (0, import_path18.resolve)(path5);
42154
43531
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
42155
43532
  }
42156
- function isPathInside(childPath, parentPath) {
43533
+ function isPathInside2(childPath, parentPath) {
42157
43534
  const child = normalizeForCompare(childPath);
42158
43535
  const parent = normalizeForCompare(parentPath);
42159
43536
  const rel = (0, import_path18.relative)(parent, child);
@@ -42540,7 +43917,7 @@ function createExternalAgentExecutorPlan(options) {
42540
43917
  });
42541
43918
  const privateRepoRoot = privateRepo.root;
42542
43919
  const workspaceRoot = resolveWorkspaceRoot({ batchPath, workspaceRoot: options.workspaceRoot, privateRepoRoot });
42543
- if (isPathInside(workspaceRoot, privateRepoRoot)) {
43920
+ if (isPathInside2(workspaceRoot, privateRepoRoot)) {
42544
43921
  throw new ExternalAgentExecutorError(
42545
43922
  "external_agent_workspace_inside_private_repo",
42546
43923
  `Workspace root must be outside the private repository. workspace=${workspaceRoot} repo=${privateRepoRoot}`
@@ -42670,11 +44047,11 @@ function createExternalAgentExecutorPlan(options) {
42670
44047
  };
42671
44048
  }
42672
44049
  function writeExternalAgentExecutorPlan(plan) {
42673
- const path2 = (0, import_path18.join)(plan.batch_dir, "executor-plan.json");
44050
+ const path5 = (0, import_path18.join)(plan.batch_dir, "executor-plan.json");
42674
44051
  (0, import_fs19.mkdirSync)(plan.batch_dir, { recursive: true });
42675
- (0, import_fs19.writeFileSync)(path2, `${JSON.stringify(plan, null, 2)}
44052
+ (0, import_fs19.writeFileSync)(path5, `${JSON.stringify(plan, null, 2)}
42676
44053
  `, "utf8");
42677
- return path2;
44054
+ return path5;
42678
44055
  }
42679
44056
  async function executeExternalAgentExecutorPlan(plan, options = {}) {
42680
44057
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -43507,16 +44884,16 @@ function writePrompt(runDir, promptVersion, context = {}) {
43507
44884
  knowledgeMissPromptContext(context.knowledgeQuestion, context.expectedAnswer),
43508
44885
  agencySetupPromptContext(context)
43509
44886
  ].join("");
43510
- const path2 = (0, import_path20.join)(runDir, "prompt.txt");
43511
- (0, import_fs21.writeFileSync)(path2, `${prompt}
44887
+ const path5 = (0, import_path20.join)(runDir, "prompt.txt");
44888
+ (0, import_fs21.writeFileSync)(path5, `${prompt}
43512
44889
  `, "utf8");
43513
- return path2;
44890
+ return path5;
43514
44891
  }
43515
44892
  function writeSession(runDir, session) {
43516
- const path2 = (0, import_path20.join)(runDir, "session.json");
43517
- (0, import_fs21.writeFileSync)(path2, `${JSON.stringify(session, null, 2)}
44893
+ const path5 = (0, import_path20.join)(runDir, "session.json");
44894
+ (0, import_fs21.writeFileSync)(path5, `${JSON.stringify(session, null, 2)}
43518
44895
  `, "utf8");
43519
- return path2;
44896
+ return path5;
43520
44897
  }
43521
44898
  function buildDefaultEvalState() {
43522
44899
  return {
@@ -44127,6 +45504,7 @@ registerBug(program2);
44127
45504
  registerProve(program2);
44128
45505
  registerObjective(program2);
44129
45506
  registerInteractive(program2);
45507
+ registerInstall(program2);
44130
45508
  registerAgentPublishCommand(program2, { publicAlias: true });
44131
45509
  registerEval(program2);
44132
45510
  registerUpdate(program2);