@f-o-h/cli 0.1.87 → 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,12 +15180,407 @@ 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
  ];
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
+ });
15583
+ }
15189
15584
  async function resolveChannelPublicKey(agentId, orgId, apiUrlOverride) {
15190
15585
  const data = await apiFetch(
15191
15586
  "/v1/console/channels/widget/ensure",
@@ -15200,7 +15595,209 @@ async function resolveChannelPublicKey(agentId, orgId, apiUrlOverride) {
15200
15595
  }
15201
15596
  return data.channel.public_key;
15202
15597
  }
15203
- 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) {
15204
15801
  let conversationId;
15205
15802
  const traceIds = [];
15206
15803
  const correlationIds = [];
@@ -15209,30 +15806,32 @@ async function runWidgetSmoke(publicKey, apiUrlOverride) {
15209
15806
  const message = SMOKE_TURNS[i];
15210
15807
  const start = Date.now();
15211
15808
  try {
15212
- const data = await apiFetch(
15213
- "/v1/widget/inbound",
15214
- {
15215
- method: "POST",
15216
- body: JSON.stringify({
15217
- channel_public_key: publicKey,
15218
- message_body: message,
15219
- preview: true,
15220
- ...conversationId ? { conversation_id: conversationId } : {}
15221
- }),
15222
- apiUrlOverride
15223
- }
15224
- );
15809
+ const data = await postPublicWidgetInbound({
15810
+ channel_public_key: publicKey,
15811
+ message_body: message,
15812
+ ...conversationId ? { conversation_id: conversationId } : {}
15813
+ }, apiUrlOverride, originOverride);
15225
15814
  const latencyMs = Date.now() - start;
15226
15815
  if (!data.reply) throw new Error("Empty reply");
15227
15816
  conversationId = data.conversationId;
15228
15817
  if (data.trace_id) traceIds.push(data.trace_id);
15229
15818
  if (data.correlation_id) correlationIds.push(data.correlation_id);
15819
+ const genericTroubleReply = isGenericTroubleReply(data.reply);
15820
+ const placeholderReply = isProgressPlaceholderReply(data.reply);
15821
+ const unhealthyReply = genericTroubleReply || placeholderReply;
15230
15822
  turns.push({
15231
15823
  turn: i + 1,
15232
15824
  message,
15233
- ok: true,
15825
+ ok: !unhealthyReply,
15234
15826
  latency_ms: latencyMs,
15235
15827
  reply: data.reply,
15828
+ ...genericTroubleReply ? {
15829
+ reason_code: "widget_generic_trouble_reply",
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."
15834
+ } : {},
15236
15835
  conversation_id: data.conversationId,
15237
15836
  trace_id: data.trace_id ?? null,
15238
15837
  correlation_id: data.correlation_id ?? null,
@@ -15252,6 +15851,119 @@ async function runWidgetSmoke(publicKey, apiUrlOverride) {
15252
15851
  turns
15253
15852
  };
15254
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
15255
15967
  function registerWidget(program3) {
15256
15968
  const widget = program3.command("widget").description("Manage the web widget channel");
15257
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 () => {
@@ -15265,9 +15977,9 @@ function registerWidget(program3) {
15265
15977
  }));
15266
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 () => {
15267
15979
  const domains = opts.domains.split(",").map((domain2) => domain2.trim());
15268
- const data = await apiFetch(`/v1/console/agents/${opts.agent}/draft`, {
15269
- method: "PATCH",
15270
- 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 }),
15271
15983
  orgId: opts.org,
15272
15984
  apiUrlOverride: opts.apiUrl
15273
15985
  });
@@ -15285,8 +15997,47 @@ function registerWidget(program3) {
15285
15997
  process.stdout.write(data.snippet + "\n");
15286
15998
  }
15287
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
+ }));
15288
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 () => {
15289
- const publicKey = await resolveChannelPublicKey(opts.agent, opts.org, opts.apiUrl);
16040
+ const publicKey = await ensureWidgetChannelPublicKey(opts.agent, opts.org, opts.apiUrl);
15290
16041
  const start = Date.now();
15291
16042
  const data = await apiFetch("/v1/widget/inbound", {
15292
16043
  method: "POST",
@@ -15307,12 +16058,52 @@ function registerWidget(program3) {
15307
16058
  };
15308
16059
  format(result, { json: opts.json ?? false });
15309
16060
  }));
15310
- 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 () => {
15311
- const publicKey = await resolveChannelPublicKey(opts.agent, opts.org, opts.apiUrl);
15312
- 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);
15313
16072
  format(summary, { json: opts.json ?? false });
15314
16073
  if (summary.failed > 0) markCommandFailed(1);
15315
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
+ }));
15316
16107
  }
15317
16108
 
15318
16109
  // src/commands/channel-instagram.ts
@@ -15400,7 +16191,7 @@ function buildReasonedNextSteps({
15400
16191
  const verifyTokenValue = verifyToken || "<verify_token>";
15401
16192
  if (has("whatsapp_channel_not_ready") || has("whatsapp_access_token_missing") || has("whatsapp_verify_token_missing") || has("whatsapp_app_secret_missing")) {
15402
16193
  steps.push(
15403
- "Connect/update channel credentials: foh channel whatsapp connect --phone-number-id <meta_phone_number_id> --access-token <meta_access_token> --verify-token <verify_token> --app-secret <meta_app_secret>"
16194
+ "Connect/update channel credentials: foh channel whatsapp onboard --phone-number-id <meta_phone_number_id> --access-token <meta_access_token> --verify-token <verify_token> --app-secret <meta_app_secret>"
15404
16195
  );
15405
16196
  }
15406
16197
  if (has("whatsapp_verify_check_failed") || has("whatsapp_webhook_challenge_failed")) {
@@ -15419,8 +16210,8 @@ function buildReasonedNextSteps({
15419
16210
  }
15420
16211
 
15421
16212
  // src/commands/channel-whatsapp-live-proof.ts
15422
- var import_node_fs = require("node:fs");
15423
- var path = __toESM(require("node:path"));
16213
+ var import_node_fs3 = require("node:fs");
16214
+ var path3 = __toESM(require("node:path"));
15424
16215
 
15425
16216
  // src/lib/channel-live-proof-evaluator.mjs
15426
16217
  function normalizeStatusValue(value) {
@@ -15505,8 +16296,8 @@ function resolveLiveProof({
15505
16296
  freshness: { timestamp_field: null, timestamp: null, age_hours: null, max_age_hours: maxAgeHours }
15506
16297
  };
15507
16298
  }
15508
- const artifactPath = path.resolve(process.cwd(), artifactPathRaw);
15509
- if (!(0, import_node_fs.existsSync)(artifactPath)) {
16299
+ const artifactPath = path3.resolve(process.cwd(), artifactPathRaw);
16300
+ if (!(0, import_node_fs3.existsSync)(artifactPath)) {
15510
16301
  return {
15511
16302
  requested: true,
15512
16303
  artifact_path: artifactPath,
@@ -15518,7 +16309,7 @@ function resolveLiveProof({
15518
16309
  }
15519
16310
  let payload;
15520
16311
  try {
15521
- payload = JSON.parse((0, import_node_fs.readFileSync)(artifactPath, "utf8"));
16312
+ payload = JSON.parse((0, import_node_fs3.readFileSync)(artifactPath, "utf8"));
15522
16313
  } catch {
15523
16314
  return {
15524
16315
  requested: true,
@@ -15547,7 +16338,7 @@ function resolveLiveProof({
15547
16338
  }
15548
16339
 
15549
16340
  // src/commands/channel-whatsapp-onboarding.ts
15550
- var import_node_fs2 = require("node:fs");
16341
+ var import_node_fs4 = require("node:fs");
15551
16342
 
15552
16343
  // src/commands/channel-whatsapp-setup.ts
15553
16344
  var import_node_crypto = require("node:crypto");
@@ -15676,24 +16467,6 @@ async function runWhatsAppOnboardingSession(params) {
15676
16467
  apiUrlOverride: params.apiUrl
15677
16468
  });
15678
16469
  }
15679
- function emitLegacyCommandNotice({
15680
- command,
15681
- canonical,
15682
- jsonMode
15683
- }) {
15684
- if (!jsonMode) {
15685
- process.stderr.write(
15686
- `[deprecated] foh channel whatsapp ${command} is a compatibility wrapper.
15687
- Use: ${canonical}
15688
- `
15689
- );
15690
- }
15691
- return {
15692
- command,
15693
- canonical,
15694
- status: "deprecated_compat_wrapper"
15695
- };
15696
- }
15697
16470
  function parseBatchManifest(manifestPathRaw) {
15698
16471
  const manifestPath = String(manifestPathRaw || "").trim();
15699
16472
  if (!manifestPath) {
@@ -15704,7 +16477,7 @@ function parseBatchManifest(manifestPathRaw) {
15704
16477
  });
15705
16478
  }
15706
16479
  try {
15707
- const raw = (0, import_node_fs2.readFileSync)(manifestPath, "utf8");
16480
+ const raw = (0, import_node_fs4.readFileSync)(manifestPath, "utf8");
15708
16481
  const parsed = JSON.parse(raw);
15709
16482
  if (!Array.isArray(parsed.businesses) || parsed.businesses.length === 0) {
15710
16483
  throw new Error("manifest_missing_businesses");
@@ -15843,51 +16616,10 @@ async function runWhatsAppOnboardingWizard(opts) {
15843
16616
  throw new FohError({
15844
16617
  step: "channel.whatsapp.onboard",
15845
16618
  error: "Unable to complete WhatsApp onboarding after guided retries.",
15846
- remediation: "Run again with --phone-number-id and --waba-id, or use `foh channel whatsapp start` for deterministic next steps."
16619
+ remediation: "Run again with --phone-number-id and --waba-id, or use `foh channel whatsapp guide` for deterministic next steps."
15847
16620
  });
15848
16621
  }
15849
16622
  function registerWhatsAppOnboardingCommands(whatsapp, addCommonOptions) {
15850
- addCommonOptions(
15851
- whatsapp.command("start").description("[Deprecated wrapper] Start onboarding using the canonical session flow")
15852
- ).option("--access-token <token>", "Meta access token with WhatsApp Business access").option("--waba-id <id>", "Optional explicit WhatsApp Business Account id when /me discovery is restricted").option("--phone-number-id <id>", "Optional explicit phone number id when discovery returns multiple candidates").option("--agent-id <id>", "Agent to bind to the WhatsApp channel").option("--verify-token <token>", "Optional webhook verify token (auto-generated when omitted)").option("--app-secret <secret>", "Optional Meta app secret (required for full signature-ready closure)").option("--business-slug <slug>", "Business identifier for operator-level targeting").option("--audio-enabled <bool>", "Enable inbound WhatsApp audio fallback auto-reply (true/false)", "true").option("--wizard", "Run guided onboarding with automatic recovery prompts").action(async (opts) => withCommandErrorHandling(async () => {
15853
- const accessToken = String(opts.accessToken || "").trim();
15854
- const useWizard = Boolean(opts.wizard) || !accessToken;
15855
- const legacy = emitLegacyCommandNotice({
15856
- command: "start",
15857
- canonical: "foh channel whatsapp onboard --wizard",
15858
- jsonMode: Boolean(opts.json)
15859
- });
15860
- const data = useWizard ? await runWhatsAppOnboardingWizard({
15861
- ...opts,
15862
- dryRun: true
15863
- }) : await runWhatsAppOnboardingSession({
15864
- orgId: opts.org,
15865
- apiUrl: opts.apiUrl,
15866
- accessToken,
15867
- wabaId: String(opts.wabaId || "").trim() || void 0,
15868
- phoneNumberId: String(opts.phoneNumberId || "").trim() || void 0,
15869
- verifyToken: String(opts.verifyToken || "").trim() || void 0,
15870
- appSecret: String(opts.appSecret || "").trim() || void 0,
15871
- agentId: opts.agentId,
15872
- businessSlug: opts.businessSlug,
15873
- audioEnabled: parseBooleanOption({
15874
- value: opts.audioEnabled,
15875
- fallback: true,
15876
- optionName: "--audio-enabled",
15877
- step: "channel.whatsapp.start"
15878
- }),
15879
- dryRun: true
15880
- });
15881
- format({
15882
- ...data,
15883
- legacy_wrapper: legacy,
15884
- next_steps: dedupeSteps([
15885
- "Run canonical onboarding apply flow: foh channel whatsapp onboard --wizard",
15886
- "Run deterministic closure: foh channel whatsapp proof --strict",
15887
- "Capture live provider evidence: corepack pnpm ops:whatsapp:proof:live"
15888
- ])
15889
- }, { json: opts.json ?? false });
15890
- }));
15891
16623
  addCommonOptions(
15892
16624
  whatsapp.command("onboard").description("Run one-session WhatsApp onboarding (discover -> bind -> verify -> prove)")
15893
16625
  ).option("--access-token <token>", "Meta access token with WhatsApp Business access").option("--waba-id <id>", "Optional explicit WhatsApp Business Account id when /me discovery is restricted").option("--phone-number-id <id>", "Optional explicit phone number id when discovery returns multiple candidates").option("--verify-token <token>", "Optional webhook verify token (auto-generated when omitted)").option("--app-secret <secret>", "Optional Meta app secret (required for full signature-ready closure)").option("--agent-id <id>", "Agent to bind to the WhatsApp channel").option("--business-slug <slug>", "Business identifier for operator-level targeting").option("--audio-enabled <bool>", "Enable inbound WhatsApp audio fallback auto-reply (true/false)", "true").option("--dry-run", "Run discovery/binding preflight without writing channel config").option("--wizard", "Run guided onboarding with automatic recovery prompts").action(async (opts) => withCommandErrorHandling(async () => {
@@ -15993,75 +16725,6 @@ function registerWhatsAppOnboardingCommands(whatsapp, addCommonOptions) {
15993
16725
  results
15994
16726
  }, { json: opts.json ?? false });
15995
16727
  }));
15996
- addCommonOptions(
15997
- whatsapp.command("setup").description("[Deprecated wrapper] Use canonical onboarding session flow")
15998
- ).option("--phone-number-id <id>", "Meta WhatsApp phone number id").option("--access-token <token>", "Meta access token").option("--verify-token <token>", "Webhook verify token").option("--app-secret <secret>", "Meta app secret for signature verification").option("--agent-id <id>", "Agent to bind to the WhatsApp channel").option("--audio-enabled <bool>", "Enable inbound WhatsApp audio fallback auto-reply (true/false)").option("--generate-verify-token", "Generate webhook verify token automatically when missing").option("--business-slug <slug>", "Business identifier for operator-level targeting").option("--wizard", "Run guided setup wizard prompts").action(async (opts) => withCommandErrorHandling(async () => {
15999
- const legacy = emitLegacyCommandNotice({
16000
- command: "setup",
16001
- canonical: "foh channel whatsapp onboard --wizard",
16002
- jsonMode: Boolean(opts.json)
16003
- });
16004
- const useWizard = Boolean(opts.wizard) || !String(opts.accessToken || "").trim();
16005
- const generatedVerifyToken = String(opts.verifyToken || "").trim() || (Boolean(opts.generateVerifyToken) ? generateVerifyToken() : "");
16006
- const data = useWizard ? await runWhatsAppOnboardingWizard({
16007
- ...opts,
16008
- verifyToken: generatedVerifyToken || void 0,
16009
- dryRun: false
16010
- }) : await runWhatsAppOnboardingSession({
16011
- orgId: opts.org,
16012
- apiUrl: opts.apiUrl,
16013
- accessToken: String(opts.accessToken || "").trim(),
16014
- wabaId: void 0,
16015
- phoneNumberId: String(opts.phoneNumberId || "").trim() || void 0,
16016
- verifyToken: generatedVerifyToken || void 0,
16017
- appSecret: String(opts.appSecret || "").trim() || void 0,
16018
- agentId: opts.agentId,
16019
- businessSlug: String(opts.businessSlug || "").trim() || void 0,
16020
- audioEnabled: parseBooleanOption({
16021
- value: opts.audioEnabled,
16022
- fallback: true,
16023
- optionName: "--audio-enabled",
16024
- step: "channel.whatsapp.setup"
16025
- }),
16026
- dryRun: false
16027
- });
16028
- format({
16029
- ...data,
16030
- legacy_wrapper: legacy,
16031
- generated_verify_token: generatedVerifyToken && !opts.verifyToken ? generatedVerifyToken : null
16032
- }, { json: opts.json ?? false });
16033
- }));
16034
- addCommonOptions(
16035
- whatsapp.command("connect").description("[Deprecated wrapper] Use canonical onboarding session flow")
16036
- ).requiredOption("--phone-number-id <id>", "Meta WhatsApp phone number id").requiredOption("--access-token <token>", "Meta access token").option("--agent-id <id>", "Agent to bind to the WhatsApp channel").option("--verify-token <token>", "Webhook verify token").option("--app-secret <secret>", "Meta app secret for signature verification").option("--business-slug <slug>", "Business identifier for operator-level targeting").option("--audio-enabled <bool>", "Enable inbound WhatsApp audio fallback auto-reply (true/false)", "true").action(async (opts) => withCommandErrorHandling(async () => {
16037
- const legacy = emitLegacyCommandNotice({
16038
- command: "connect",
16039
- canonical: "foh channel whatsapp onboard --access-token <token> --phone-number-id <id>",
16040
- jsonMode: Boolean(opts.json)
16041
- });
16042
- const data = await runWhatsAppOnboardingSession({
16043
- orgId: opts.org,
16044
- apiUrl: opts.apiUrl,
16045
- accessToken: String(opts.accessToken || "").trim(),
16046
- wabaId: void 0,
16047
- phoneNumberId: String(opts.phoneNumberId || "").trim() || void 0,
16048
- verifyToken: String(opts.verifyToken || "").trim() || void 0,
16049
- appSecret: String(opts.appSecret || "").trim() || void 0,
16050
- agentId: opts.agentId,
16051
- businessSlug: String(opts.businessSlug || "").trim() || void 0,
16052
- audioEnabled: parseBooleanOption({
16053
- value: opts.audioEnabled,
16054
- fallback: true,
16055
- optionName: "--audio-enabled",
16056
- step: "channel.whatsapp.connect"
16057
- }),
16058
- dryRun: false
16059
- });
16060
- format({
16061
- ...data,
16062
- legacy_wrapper: legacy
16063
- }, { json: opts.json ?? false });
16064
- }));
16065
16728
  }
16066
16729
 
16067
16730
  // src/commands/channel-whatsapp.ts
@@ -16632,7 +17295,7 @@ function registerTools(program3) {
16632
17295
  }
16633
17296
 
16634
17297
  // src/commands/mcp-serve.ts
16635
- var import_node_child_process = require("node:child_process");
17298
+ var import_node_child_process2 = require("node:child_process");
16636
17299
 
16637
17300
  // ../../node_modules/.pnpm/zod@4.3.6/node_modules/zod/v3/helpers/util.js
16638
17301
  var util;
@@ -16993,8 +17656,8 @@ function getErrorMap() {
16993
17656
 
16994
17657
  // ../../node_modules/.pnpm/zod@4.3.6/node_modules/zod/v3/helpers/parseUtil.js
16995
17658
  var makeIssue = (params) => {
16996
- const { data, path: path2, errorMaps, issueData } = params;
16997
- const fullPath = [...path2, ...issueData.path || []];
17659
+ const { data, path: path5, errorMaps, issueData } = params;
17660
+ const fullPath = [...path5, ...issueData.path || []];
16998
17661
  const fullIssue = {
16999
17662
  ...issueData,
17000
17663
  path: fullPath
@@ -17109,11 +17772,11 @@ var errorUtil;
17109
17772
 
17110
17773
  // ../../node_modules/.pnpm/zod@4.3.6/node_modules/zod/v3/types.js
17111
17774
  var ParseInputLazyPath = class {
17112
- constructor(parent, value, path2, key) {
17775
+ constructor(parent, value, path5, key) {
17113
17776
  this._cachedPath = [];
17114
17777
  this.parent = parent;
17115
17778
  this.data = value;
17116
- this._path = path2;
17779
+ this._path = path5;
17117
17780
  this._key = key;
17118
17781
  }
17119
17782
  get path() {
@@ -20611,7 +21274,7 @@ __export(util_exports, {
20611
21274
  createTransparentProxy: () => createTransparentProxy,
20612
21275
  defineLazy: () => defineLazy,
20613
21276
  esc: () => esc,
20614
- escapeRegex: () => escapeRegex,
21277
+ escapeRegex: () => escapeRegex2,
20615
21278
  extend: () => extend3,
20616
21279
  finalizeIssue: () => finalizeIssue,
20617
21280
  floatSafeRemainder: () => floatSafeRemainder2,
@@ -20759,10 +21422,10 @@ function mergeDefs(...defs) {
20759
21422
  function cloneDef(schema2) {
20760
21423
  return mergeDefs(schema2._zod.def);
20761
21424
  }
20762
- function getElementAtPath(obj, path2) {
20763
- if (!path2)
21425
+ function getElementAtPath(obj, path5) {
21426
+ if (!path5)
20764
21427
  return obj;
20765
- return path2.reduce((acc, key) => acc?.[key], obj);
21428
+ return path5.reduce((acc, key) => acc?.[key], obj);
20766
21429
  }
20767
21430
  function promiseAllObject(promisesObj) {
20768
21431
  const keys = Object.keys(promisesObj);
@@ -20884,7 +21547,7 @@ var getParsedType2 = (data) => {
20884
21547
  };
20885
21548
  var propertyKeyTypes = /* @__PURE__ */ new Set(["string", "number", "symbol"]);
20886
21549
  var primitiveTypes = /* @__PURE__ */ new Set(["string", "number", "bigint", "boolean", "symbol", "undefined"]);
20887
- function escapeRegex(str2) {
21550
+ function escapeRegex2(str2) {
20888
21551
  return str2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20889
21552
  }
20890
21553
  function clone(inst, def, params) {
@@ -21145,11 +21808,11 @@ function aborted(x, startIndex = 0) {
21145
21808
  }
21146
21809
  return false;
21147
21810
  }
21148
- function prefixIssues(path2, issues) {
21811
+ function prefixIssues(path5, issues) {
21149
21812
  return issues.map((iss) => {
21150
21813
  var _a2;
21151
21814
  (_a2 = iss).path ?? (_a2.path = []);
21152
- iss.path.unshift(path2);
21815
+ iss.path.unshift(path5);
21153
21816
  return iss;
21154
21817
  });
21155
21818
  }
@@ -21503,7 +22166,7 @@ function emoji() {
21503
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])$/;
21504
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}|:))$/;
21505
22168
  var mac = (delimiter) => {
21506
- const escapedDelim = escapeRegex(delimiter ?? ":");
22169
+ const escapedDelim = escapeRegex2(delimiter ?? ":");
21507
22170
  return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
21508
22171
  };
21509
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])$/;
@@ -22004,7 +22667,7 @@ var $ZodCheckUpperCase = /* @__PURE__ */ $constructor("$ZodCheckUpperCase", (ins
22004
22667
  });
22005
22668
  var $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (inst, def) => {
22006
22669
  $ZodCheck.init(inst, def);
22007
- const escapedRegex = escapeRegex(def.includes);
22670
+ const escapedRegex = escapeRegex2(def.includes);
22008
22671
  const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);
22009
22672
  def.pattern = pattern;
22010
22673
  inst._zod.onattach.push((inst2) => {
@@ -22028,7 +22691,7 @@ var $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (inst,
22028
22691
  });
22029
22692
  var $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith", (inst, def) => {
22030
22693
  $ZodCheck.init(inst, def);
22031
- const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`);
22694
+ const pattern = new RegExp(`^${escapeRegex2(def.prefix)}.*`);
22032
22695
  def.pattern ?? (def.pattern = pattern);
22033
22696
  inst._zod.onattach.push((inst2) => {
22034
22697
  const bag = inst2._zod.bag;
@@ -22051,7 +22714,7 @@ var $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith", (i
22051
22714
  });
22052
22715
  var $ZodCheckEndsWith = /* @__PURE__ */ $constructor("$ZodCheckEndsWith", (inst, def) => {
22053
22716
  $ZodCheck.init(inst, def);
22054
- const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`);
22717
+ const pattern = new RegExp(`.*${escapeRegex2(def.suffix)}$`);
22055
22718
  def.pattern ?? (def.pattern = pattern);
22056
22719
  inst._zod.onattach.push((inst2) => {
22057
22720
  const bag = inst2._zod.bag;
@@ -23593,7 +24256,7 @@ var $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
23593
24256
  const values = getEnumValues(def.entries);
23594
24257
  const valuesSet = new Set(values);
23595
24258
  inst._zod.values = valuesSet;
23596
- 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("|")})$`);
23597
24260
  inst._zod.parse = (payload, _ctx) => {
23598
24261
  const input = payload.value;
23599
24262
  if (valuesSet.has(input)) {
@@ -23615,7 +24278,7 @@ var $ZodLiteral = /* @__PURE__ */ $constructor("$ZodLiteral", (inst, def) => {
23615
24278
  }
23616
24279
  const values = new Set(def.values);
23617
24280
  inst._zod.values = values;
23618
- 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("|")})$`);
23619
24282
  inst._zod.parse = (payload, _ctx) => {
23620
24283
  const input = payload.value;
23621
24284
  if (values.has(input)) {
@@ -23971,7 +24634,7 @@ var $ZodTemplateLiteral = /* @__PURE__ */ $constructor("$ZodTemplateLiteral", (i
23971
24634
  const end = source.endsWith("$") ? source.length - 1 : source.length;
23972
24635
  regexParts.push(source.slice(start, end));
23973
24636
  } else if (part === null || primitiveTypes.has(typeof part)) {
23974
- regexParts.push(escapeRegex(`${part}`));
24637
+ regexParts.push(escapeRegex2(`${part}`));
23975
24638
  } else {
23976
24639
  throw new Error(`Invalid template literal part: ${part}`);
23977
24640
  }
@@ -33060,7 +33723,7 @@ var StdioServerTransport = class {
33060
33723
  };
33061
33724
 
33062
33725
  // src/lib/cli-version.ts
33063
- var injectedVersion = true ? String("0.1.87").trim() : "";
33726
+ var injectedVersion = true ? String("0.1.89").trim() : "";
33064
33727
  var envVersion = String(process.env.FOH_CLI_VERSION || process.env.npm_package_version || "").trim();
33065
33728
  var CLI_VERSION = injectedVersion || envVersion || "0.0.0-dev";
33066
33729
 
@@ -33248,7 +33911,7 @@ async function runFohCli(params) {
33248
33911
  }
33249
33912
  const command = `foh ${effectiveArgv.join(" ")}`;
33250
33913
  return await new Promise((resolve16) => {
33251
- 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], {
33252
33915
  stdio: ["ignore", "pipe", "pipe"],
33253
33916
  env: {
33254
33917
  ...process.env,
@@ -33326,6 +33989,19 @@ var TOOL_EXECUTION_SCHEMA = {
33326
33989
  var APPROVAL_TOKEN_SCHEMA = {
33327
33990
  approval_token: string2().min(1).max(256).optional()
33328
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
+ }
33329
34005
  var TYPED_TOOL_SPECS = [
33330
34006
  {
33331
34007
  name: "foh_auth_whoami",
@@ -33382,6 +34058,193 @@ var TYPED_TOOL_SPECS = [
33382
34058
  return argv;
33383
34059
  }
33384
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
+ },
33385
34248
  {
33386
34249
  name: "foh_agent_list",
33387
34250
  title: "FOH Agent List",
@@ -34070,9 +34933,9 @@ function parsePositiveInt(value, fallback, min, max) {
34070
34933
  if (!Number.isFinite(parsed)) return fallback;
34071
34934
  return Math.max(min, Math.min(max, Math.trunc(parsed)));
34072
34935
  }
34073
- function withQuery(path2, params) {
34936
+ function withQuery(path5, params) {
34074
34937
  const query = params.toString();
34075
- return query ? `${path2}?${query}` : path2;
34938
+ return query ? `${path5}?${query}` : path5;
34076
34939
  }
34077
34940
 
34078
34941
  // src/commands/knowledge.ts
@@ -34864,8 +35727,8 @@ function signReport(reportPayload) {
34864
35727
  }
34865
35728
  };
34866
35729
  }
34867
- function writeSignedJsonArtifact(path2, value) {
34868
- const absolutePath = (0, import_path3.resolve)(path2);
35730
+ function writeSignedJsonArtifact(path5, value) {
35731
+ const absolutePath = (0, import_path3.resolve)(path5);
34869
35732
  (0, import_fs5.mkdirSync)((0, import_path3.dirname)(absolutePath), { recursive: true });
34870
35733
  (0, import_fs5.writeFileSync)(absolutePath, stableStringify(value), "utf-8");
34871
35734
  return absolutePath;
@@ -34882,8 +35745,190 @@ function writeSetupRunReport(reportPayload, reportOut) {
34882
35745
  }
34883
35746
 
34884
35747
  // src/commands/setup.ts
35748
+ var OBJECTIVE_SETUP_INDUSTRIES = ["real_estate", "restaurant", "general"];
35749
+ function normalizeString(value) {
35750
+ return typeof value === "string" ? value.trim() : "";
35751
+ }
35752
+ function csv(value, fallback) {
35753
+ return String(value || fallback).split(",").map((entry) => entry.trim()).filter(Boolean);
35754
+ }
35755
+ function objectiveChannels(tools) {
35756
+ const allowed = /* @__PURE__ */ new Set(["widget", "voice", "whatsapp", "instagram", "sms"]);
35757
+ const channels = tools.filter((tool) => allowed.has(tool));
35758
+ return channels.length > 0 ? channels : ["widget"];
35759
+ }
35760
+ function objectiveOptimizationTarget(industry, objective) {
35761
+ const text = objective.toLowerCase();
35762
+ if (industry === "restaurant" || /book|booking|reservation|table/.test(text)) return "booking_rate";
35763
+ if (/support|resolve|resolution/.test(text)) return "support_resolution";
35764
+ if (/speed|lead|callback|call back/.test(text)) return "speed_to_lead";
35765
+ return "lead_quality";
35766
+ }
35767
+ function objectiveSetupCommand(opts) {
35768
+ const parts = [
35769
+ "npx --yes @f-o-h/cli@latest setup",
35770
+ `--objective ${JSON.stringify(normalizeString(opts.objective) || "<front-of-house objective>")}`,
35771
+ `--business-name ${JSON.stringify(normalizeString(opts.businessName) || "<business name>")}`,
35772
+ `--industry ${normalizeString(opts.industry) || "<real_estate|restaurant>"}`,
35773
+ `--source-url ${JSON.stringify(normalizeString(opts.sourceUrl) || "<official url>")}`,
35774
+ "--json"
35775
+ ];
35776
+ return parts.join(" ");
35777
+ }
35778
+ function selectedTemplateSummary(selection) {
35779
+ const candidate = Array.isArray(selection?.candidates) ? selection.candidates[0] : null;
35780
+ const template = candidate?.template;
35781
+ const contract = template?.template_contract ?? {};
35782
+ if (!template) return null;
35783
+ return {
35784
+ template_id: String(template.id || contract.template_id || ""),
35785
+ template_name: String(template.name || contract.name || ""),
35786
+ template_slug: String(contract.slug || ""),
35787
+ industry: String(contract.industry || ""),
35788
+ use_case: String(contract.use_case || ""),
35789
+ match_score: Number.isFinite(Number(candidate.match_score)) ? Number(candidate.match_score) : null,
35790
+ matched_reasons: Array.isArray(candidate.matched_reasons) ? candidate.matched_reasons.map(String) : []
35791
+ };
35792
+ }
35793
+ async function emitObjectiveSetupBootstrap(opts) {
35794
+ const businessName = normalizeString(opts.businessName);
35795
+ const objective = normalizeString(opts.objective);
35796
+ const industry = normalizeString(opts.industry);
35797
+ const sourceUrl = normalizeString(opts.sourceUrl);
35798
+ const tools = csv(opts.tools, "widget,voice,whatsapp");
35799
+ let credentials = null;
35800
+ try {
35801
+ credentials = loadCredentials(opts.apiUrl);
35802
+ if (!opts.org) opts.org = credentials.orgId;
35803
+ } catch {
35804
+ credentials = null;
35805
+ }
35806
+ if (!credentials && !opts.org) {
35807
+ format({
35808
+ schema_version: "foh_cli_objective_setup_bootstrap.v1",
35809
+ ok: false,
35810
+ status: "blocked",
35811
+ reason_code: "auth_required",
35812
+ summary: "Authenticate before objective setup so template selection and setup planning can use the correct org.",
35813
+ spend_policy: resolveCliSpendPolicy(),
35814
+ next_commands: [
35815
+ "npx --yes @f-o-h/cli@latest auth signup --web --json",
35816
+ "npx --yes @f-o-h/cli@latest auth login --web --json",
35817
+ "npx --yes @f-o-h/cli@latest org list --json",
35818
+ objectiveSetupCommand(opts)
35819
+ ]
35820
+ }, { json: opts.json ?? false });
35821
+ markCommandFailed(1);
35822
+ return;
35823
+ }
35824
+ const missing = [];
35825
+ if (!businessName) missing.push("--business-name");
35826
+ if (!industry) missing.push("--industry");
35827
+ if (!objective) missing.push("--objective");
35828
+ if (!sourceUrl) missing.push("--source-url");
35829
+ if (industry && !OBJECTIVE_SETUP_INDUSTRIES.includes(industry)) missing.push("--industry:supported_value");
35830
+ if (missing.length > 0) {
35831
+ format({
35832
+ schema_version: "foh_cli_objective_setup_bootstrap.v1",
35833
+ ok: false,
35834
+ status: "blocked",
35835
+ reason_code: "objective_setup_required_options_missing",
35836
+ summary: "Objective setup needs an explicit business name, industry, objective, and official source URL.",
35837
+ missing_options: missing,
35838
+ org_id: opts.org ?? null,
35839
+ next_commands: [
35840
+ objectiveSetupCommand(opts),
35841
+ "npx --yes @f-o-h/cli@latest templates list --category general --json",
35842
+ "npx --yes @f-o-h/cli@latest templates list --category buyer --json"
35843
+ ],
35844
+ claim_boundaries: {
35845
+ customer_live_claim_allowed: false,
35846
+ production_claim_allowed: false
35847
+ }
35848
+ }, { json: opts.json ?? false });
35849
+ markCommandFailed(1);
35850
+ return;
35851
+ }
35852
+ const brief = {
35853
+ schema_version: "business_requirement_brief.v1",
35854
+ business_name: businessName,
35855
+ industry,
35856
+ desired_use_cases: [objective],
35857
+ channels: objectiveChannels(tools),
35858
+ knowledge_sources: [{ type: "website", label: `${businessName} official website`, uri: sourceUrl }],
35859
+ required_outcomes: [objective],
35860
+ handoff_rules: ["handoff when approved facts, credentials, or action availability are missing"],
35861
+ constraints: ["do not invent business facts or claim live readiness without accepted evidence"],
35862
+ optimization_target: objectiveOptimizationTarget(industry, objective)
35863
+ };
35864
+ const templateSelection = await apiFetch("/v1/console/templates/select", {
35865
+ method: "POST",
35866
+ body: JSON.stringify(brief),
35867
+ apiUrlOverride: opts.apiUrl
35868
+ });
35869
+ const selectedTemplate = selectedTemplateSummary(templateSelection);
35870
+ if (!selectedTemplate) {
35871
+ format({
35872
+ schema_version: "foh_cli_objective_setup_bootstrap.v1",
35873
+ ok: false,
35874
+ status: "blocked",
35875
+ reason_code: "objective_template_selection_empty",
35876
+ summary: "No supported template matched this objective. Do not continue with a guessed template.",
35877
+ requirement_brief: brief,
35878
+ template_selection: templateSelection,
35879
+ next_commands: [objectiveSetupCommand(opts)]
35880
+ }, { json: opts.json ?? false });
35881
+ markCommandFailed(1);
35882
+ return;
35883
+ }
35884
+ const setupWorkflow = await apiFetch("/v1/console/agency-setup/workflow", {
35885
+ method: "POST",
35886
+ body: JSON.stringify({
35887
+ agency_name: businessName,
35888
+ business_objective: objective,
35889
+ requested_tool_surface: tools,
35890
+ target_exposure_mode: normalizeString(opts.targetMode) || "customer_owned_voice_trial",
35891
+ source_url: sourceUrl,
35892
+ ...normalizeString(opts.location) ? { branch_location: normalizeString(opts.location) } : {}
35893
+ }),
35894
+ orgId: opts.org,
35895
+ apiUrlOverride: opts.apiUrl
35896
+ });
35897
+ format({
35898
+ schema_version: "foh_cli_objective_setup_bootstrap.v1",
35899
+ ok: true,
35900
+ status: "hold",
35901
+ reason_code: "objective_setup_plan_ready",
35902
+ summary: "Objective setup plan is ready. Treat this as dry-run planning until evidence is applied and status explicitly allows live claims.",
35903
+ dry_run: opts.apply === true ? false : true,
35904
+ apply_requested: opts.apply === true,
35905
+ org_id: opts.org ?? null,
35906
+ requirement_brief: brief,
35907
+ selected_template: selectedTemplate,
35908
+ setup_workflow: {
35909
+ decision: setupWorkflow?.decision ?? setupWorkflow?.status ?? null,
35910
+ reason_codes: Array.isArray(setupWorkflow?.reason_codes) ? setupWorkflow.reason_codes : [],
35911
+ evidence_packet: setupWorkflow?.evidence_packet ?? null,
35912
+ operator_status: setupWorkflow?.operator_status ?? null
35913
+ },
35914
+ claim_boundaries: {
35915
+ customer_live_claim_allowed: false,
35916
+ production_claim_allowed: false,
35917
+ fully_autonomous_claim_allowed: false
35918
+ },
35919
+ next_commands: [
35920
+ `npx --yes @f-o-h/cli@latest objective status --business-name ${JSON.stringify(businessName)} --industry ${industry} --business-objective ${JSON.stringify(objective)} --source-url ${JSON.stringify(sourceUrl)} --out test-results/objective-status.latest.json --json`,
35921
+ "npx --yes @f-o-h/cli@latest objective debug --from test-results/objective-status.latest.json --json",
35922
+ `npx --yes @f-o-h/cli@latest templates show --template ${selectedTemplate.template_id} --json`
35923
+ ]
35924
+ }, { json: opts.json ?? false });
35925
+ }
34885
35926
  function registerSetup(program3) {
34886
- program3.command("setup").description("Fully provision a new agency customer in one command").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--agent-template <id>", "Agent template ID (e.g. viewing-request)").option("--agent-name <name>", "Name for the new agent").option("--phone-country <cc>", "Phone number country code", "GB").option("--phone-area-code <code>", "Phone area code preference").option("--phone-mode <mode>", "Phone path: observe, skip, or purchase", "purchase").option("--widget-domains <domains>", "Comma-separated widget domain allowlist").option("--voice-provider <p>", "TTS provider: openai, azure, twilio").option("--voice-id <id>", "Voice ID").option("--skip-compliance", "Skip compliance submission and wait").option("--skip-voice", "Skip voice configuration").option("--skip-tests", "Skip smoke tests").option("--cert-mode <m>", "Simulation cert mode: quick, full, stress", "quick").option("--cert-adaptive-runs <n>", "Adaptive run count for certification loop", "30").option("--cert-max-improvement-rounds <n>", "Max instruction improvement rounds in cert loop (0-5)", "1").option("--resume-from <step>", `Resume from a setup step (${SETUP_STEP_ORDER.join(", ")})`).option("--report-out <path>", "Optional path to write signed setup run report JSON").option("--dry-run", "Print all steps that would run without making any API calls").option("--api-url <url>", "API base URL override").option("--console-url <url>", "Console sign-in URL override").option("--json", "Output as JSON").action(async (opts) => {
35927
+ program3.command("setup").description("Fully provision a customer or plan objective-first setup").option("--objective <text>", "Objective-first setup mode: plain-English front-of-house objective").option("--business-name <name>", "Business trading name for objective-first setup").option("--industry <industry>", "Business industry for objective-first setup: real_estate, restaurant, general").option("--source-url <url>", "Official business source URL for objective-first setup").option("--location <value>", "Location or branch represented by this setup").option("--tools <csv>", "Requested surfaces for objective-first setup", "widget,voice,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode for objective-first setup", "customer_owned_voice_trial").option("--apply", "Explicitly request apply mode for objective setup; default is dry-run planning").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--agent-template <id>", "Agent template ID (e.g. viewing-request)").option("--agent-name <name>", "Name for the new agent").option("--phone-country <cc>", "Phone number country code", "GB").option("--phone-area-code <code>", "Phone area code preference").option("--phone-mode <mode>", "Phone path: observe, skip, or purchase", "purchase").option("--widget-domains <domains>", "Comma-separated widget domain allowlist").option("--voice-provider <p>", "TTS provider: openai, azure, twilio").option("--voice-id <id>", "Voice ID").option("--skip-compliance", "Skip compliance submission and wait").option("--skip-voice", "Skip voice configuration").option("--skip-tests", "Skip smoke tests").option("--cert-mode <m>", "Simulation cert mode: quick, full, stress", "quick").option("--cert-adaptive-runs <n>", "Adaptive run count for certification loop", "30").option("--cert-max-improvement-rounds <n>", "Max instruction improvement rounds in cert loop (0-5)", "1").option("--resume-from <step>", `Resume from a setup step (${SETUP_STEP_ORDER.join(", ")})`).option("--report-out <path>", "Optional path to write signed setup run report JSON").option("--dry-run", "Print all steps that would run without making any API calls").option("--api-url <url>", "API base URL override").option("--console-url <url>", "Console sign-in URL override").option("--json", "Output as JSON").action(async (opts) => {
35928
+ if (opts.objective) {
35929
+ await emitObjectiveSetupBootstrap(opts);
35930
+ return;
35931
+ }
34887
35932
  if (!opts.org) {
34888
35933
  try {
34889
35934
  opts.org = loadCredentials(opts.apiUrl).orgId;
@@ -35717,7 +36762,7 @@ ${passIcon} Certification loop summary
35717
36762
  }
35718
36763
 
35719
36764
  // src/commands/certify.ts
35720
- var import_node_fs3 = require("node:fs");
36765
+ var import_node_fs5 = require("node:fs");
35721
36766
  function normalizeProfile(raw) {
35722
36767
  const value = String(raw || "release").trim().toLowerCase();
35723
36768
  if (value === "smoke" || value === "release" || value === "stress") return value;
@@ -35738,7 +36783,7 @@ function defaultAdaptiveRuns(profile) {
35738
36783
  if (profile === "stress") return 30;
35739
36784
  return 5;
35740
36785
  }
35741
- function csv(raw) {
36786
+ function csv2(raw) {
35742
36787
  if (!raw) return void 0;
35743
36788
  const values = String(raw).split(",").map((value) => value.trim()).filter(Boolean);
35744
36789
  return values.length > 0 ? values : void 0;
@@ -35764,8 +36809,8 @@ function registerCertify(program3) {
35764
36809
  body: JSON.stringify({
35765
36810
  mode,
35766
36811
  adaptive_runs: adaptiveRuns,
35767
- journeys: csv(opts.journeys),
35768
- scenario_ids: csv(opts.scenarioIds),
36812
+ journeys: csv2(opts.journeys),
36813
+ scenario_ids: csv2(opts.scenarioIds),
35769
36814
  channel: channel(opts.channel)
35770
36815
  }),
35771
36816
  apiUrlOverride: opts.apiUrl
@@ -35795,7 +36840,7 @@ function registerCertify(program3) {
35795
36840
  next_commands: passed ? [`foh agent publish --agent ${opts.agent} --json`] : [`foh certify run --agent ${opts.agent} --profile ${profile} --json`]
35796
36841
  };
35797
36842
  if (opts.out) {
35798
- (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");
35799
36844
  }
35800
36845
  if (opts.json ?? false) {
35801
36846
  format(result, { json: true });
@@ -35835,8 +36880,8 @@ function registerConversations(program3) {
35835
36880
  if (opts.provider) params.set("provider", String(opts.provider));
35836
36881
  params.set("page", String(parsePositiveInt(opts.page, 1, 1, 1e4)));
35837
36882
  params.set("limit", String(parsePositiveInt(opts.limit, 20, 1, 100)));
35838
- const path2 = withQuery(`/v1/console/agents/${opts.agent}/conversations`, params);
35839
- 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 });
35840
36885
  format(data, { json: opts.json ?? false });
35841
36886
  }));
35842
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 () => {
@@ -35853,13 +36898,13 @@ function registerConversations(program3) {
35853
36898
  q: query,
35854
36899
  limit: String(parsePositiveInt(opts.limit, 20, 1, 100))
35855
36900
  });
35856
- const path2 = withQuery(`/v1/console/agents/${opts.agent}/conversations/semantic-search`, params);
35857
- 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 });
35858
36903
  format(data, { json: opts.json ?? false });
35859
36904
  }));
35860
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 () => {
35861
- const path2 = `/v1/console/agents/${opts.agent}/conversations/${opts.conversation}/lead-data`;
35862
- 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 });
35863
36908
  format(data, { json: opts.json ?? false });
35864
36909
  }));
35865
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 () => {
@@ -35881,8 +36926,8 @@ function registerConversations(program3) {
35881
36926
  statusCode: 400
35882
36927
  });
35883
36928
  }
35884
- const path2 = `/v1/console/agents/${opts.agent}/conversations/${opts.conversation}/inject-variables`;
35885
- const data = await apiFetch(path2, {
36929
+ const path5 = `/v1/console/agents/${opts.agent}/conversations/${opts.conversation}/inject-variables`;
36930
+ const data = await apiFetch(path5, {
35886
36931
  method: "POST",
35887
36932
  body: JSON.stringify({
35888
36933
  variables: parsed,
@@ -35904,8 +36949,8 @@ function registerConversations(program3) {
35904
36949
  statusCode: 400
35905
36950
  });
35906
36951
  }
35907
- const path2 = `/v1/console/agents/${opts.agent}/conversations/${opts.conversation}/inject-event`;
35908
- const data = await apiFetch(path2, {
36952
+ const path5 = `/v1/console/agents/${opts.agent}/conversations/${opts.conversation}/inject-event`;
36953
+ const data = await apiFetch(path5, {
35909
36954
  method: "POST",
35910
36955
  body: JSON.stringify({
35911
36956
  event_type: String(opts.eventType),
@@ -36025,8 +37070,8 @@ function registerTranscripts(program3) {
36025
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 () => {
36026
37071
  const params = new URLSearchParams();
36027
37072
  if (opts.includeTraces) params.set("include_traces", "true");
36028
- const path2 = withQuery(`/v1/console/agents/${opts.agent}/conversations/${opts.conversation}`, params);
36029
- 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 });
36030
37075
  format(data, { json: opts.json ?? false });
36031
37076
  }));
36032
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 () => {
@@ -36209,8 +37254,8 @@ function registerTests(program3) {
36209
37254
  if (opts.status) params.set("status", String(opts.status));
36210
37255
  if (opts.type) params.set("type", String(opts.type));
36211
37256
  params.set("limit", String(parsePositiveInt(opts.limit, 100, 1, 500)));
36212
- const path2 = withQuery(`/v1/console/agents/${opts.agent}/tests`, params);
36213
- 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 });
36214
37259
  format(data, { json: opts.json ?? false });
36215
37260
  }));
36216
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 () => {
@@ -36336,8 +37381,8 @@ function registerTests(program3) {
36336
37381
  if (opts.test) params.set("testId", String(opts.test));
36337
37382
  if (opts.batch) params.set("batchId", String(opts.batch));
36338
37383
  if (opts.includeResults) params.set("includeResults", "true");
36339
- const path2 = withQuery(`/v1/console/agents/${opts.agent}/tests/runs`, params);
36340
- const data = await apiFetch(path2, {
37384
+ const path5 = withQuery(`/v1/console/agents/${opts.agent}/tests/runs`, params);
37385
+ const data = await apiFetch(path5, {
36341
37386
  orgId: opts.org,
36342
37387
  apiUrlOverride: opts.apiUrl
36343
37388
  });
@@ -36397,9 +37442,9 @@ function asStringList(value) {
36397
37442
  if (Array.isArray(value)) return value.map((entry) => String(entry || "").trim()).filter(Boolean);
36398
37443
  return [];
36399
37444
  }
36400
- function getPath(source, path2) {
37445
+ function getPath(source, path5) {
36401
37446
  if (!source || typeof source !== "object") return void 0;
36402
- return path2.split(".").reduce((current, part) => {
37447
+ return path5.split(".").reduce((current, part) => {
36403
37448
  if (!current || typeof current !== "object") return void 0;
36404
37449
  return current[part];
36405
37450
  }, source);
@@ -36413,12 +37458,12 @@ function valuesEqual(actual, expected) {
36413
37458
  }
36414
37459
  return String(actual ?? "") === String(expected ?? "");
36415
37460
  }
36416
- function parseStructuredFile(path2) {
36417
- const raw = (0, import_fs7.readFileSync)(path2, "utf-8");
36418
- 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);
36419
37464
  }
36420
- function parseSuiteFile(path2) {
36421
- const parsed = parseStructuredFile(path2);
37465
+ function parseSuiteFile(path5) {
37466
+ const parsed = parseStructuredFile(path5);
36422
37467
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
36423
37468
  throw new FohError({
36424
37469
  step: "test.run",
@@ -36442,8 +37487,8 @@ function turnFromFixtureEntry(entry) {
36442
37487
  expect: row.expect && typeof row.expect === "object" && !Array.isArray(row.expect) ? row.expect : void 0
36443
37488
  };
36444
37489
  }
36445
- function loadFixtureTurns(path2) {
36446
- const parsed = parseStructuredFile(path2);
37490
+ function loadFixtureTurns(path5) {
37491
+ const parsed = parseStructuredFile(path5);
36447
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 : [];
36448
37493
  return entries.map(turnFromFixtureEntry).filter((entry) => !!entry);
36449
37494
  }
@@ -36503,8 +37548,8 @@ function pickVariables(response) {
36503
37548
  return null;
36504
37549
  }
36505
37550
  function pickToolCalls(response) {
36506
- for (const path2 of ["tool_calls", "toolCalls", "telemetry.tool_calls", "trace.tool_calls"]) {
36507
- 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);
36508
37553
  if (Array.isArray(value)) return value;
36509
37554
  }
36510
37555
  return [];
@@ -36560,9 +37605,9 @@ function evaluateStructuredExpectations(response, expect, latencyMs) {
36560
37605
  const variables = pickVariables(response);
36561
37606
  if (!variables) failures.push("variables expected but response had none");
36562
37607
  else {
36563
- for (const [path2, expected] of Object.entries(expect.variables)) {
36564
- const actual = getPath(variables, path2);
36565
- 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)}`);
36566
37611
  }
36567
37612
  }
36568
37613
  }
@@ -36604,9 +37649,9 @@ function evaluateStructuredExpectations(response, expect, latencyMs) {
36604
37649
  }
36605
37650
  }
36606
37651
  if (expect.fields && typeof expect.fields === "object") {
36607
- for (const [path2, expected] of Object.entries(expect.fields)) {
36608
- const actual = getPath(response, path2);
36609
- 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)}`);
36610
37655
  }
36611
37656
  }
36612
37657
  return failures;
@@ -37301,9 +38346,9 @@ function nonEmpty2(value) {
37301
38346
  const text = String(value ?? "").trim();
37302
38347
  return text.length > 0 ? text : void 0;
37303
38348
  }
37304
- function getPath2(value, path2) {
38349
+ function getPath2(value, path5) {
37305
38350
  let current = value;
37306
- for (const segment of path2.split(".")) {
38351
+ for (const segment of path5.split(".")) {
37307
38352
  const record2 = asRecord2(current);
37308
38353
  if (!record2) return void 0;
37309
38354
  current = record2[segment];
@@ -37460,10 +38505,10 @@ function assertRedacted(value) {
37460
38505
  });
37461
38506
  }
37462
38507
  }
37463
- function readSourceArtifact(path2) {
37464
- if (!path2) return null;
38508
+ function readSourceArtifact(path5) {
38509
+ if (!path5) return null;
37465
38510
  try {
37466
- return JSON.parse((0, import_fs9.readFileSync)(path2, "utf-8"));
38511
+ return JSON.parse((0, import_fs9.readFileSync)(path5, "utf-8"));
37467
38512
  } catch (error2) {
37468
38513
  throw new FohError({
37469
38514
  step: "bug.improve",
@@ -37640,8 +38685,8 @@ function parseRequestBody(raw) {
37640
38685
  return text;
37641
38686
  }
37642
38687
  }
37643
- function writeJsonArtifact(path2, value) {
37644
- const absolutePath = (0, import_path8.resolve)(path2);
38688
+ function writeJsonArtifact(path5, value) {
38689
+ const absolutePath = (0, import_path8.resolve)(path5);
37645
38690
  (0, import_fs10.mkdirSync)((0, import_path8.dirname)(absolutePath), { recursive: true });
37646
38691
  (0, import_fs10.writeFileSync)(absolutePath, stableStringify(value), "utf-8");
37647
38692
  return absolutePath;
@@ -37925,8 +38970,8 @@ function registerBug(program3) {
37925
38970
 
37926
38971
  // src/lib/proof-cache.ts
37927
38972
  var import_node_crypto2 = require("node:crypto");
37928
- var import_node_fs4 = require("node:fs");
37929
- var import_node_path = require("node:path");
38973
+ var import_node_fs6 = require("node:fs");
38974
+ var import_node_path3 = require("node:path");
37930
38975
  var DEFAULT_MAX_AGE_MS = 15 * 60 * 1e3;
37931
38976
  var DEFAULT_WAIT_MS = 180 * 1e3;
37932
38977
  var DEFAULT_POLL_MS = 500;
@@ -37944,12 +38989,12 @@ function cacheKey2(kind, keyParts) {
37944
38989
  return (0, import_node_crypto2.createHash)("sha256").update(stableJson({ kind, keyParts, schema_version: "foh_proof_cache_key.v1" })).digest("hex").slice(0, 32);
37945
38990
  }
37946
38991
  function publicPath(filePath) {
37947
- const rel = (0, import_node_path.relative)(process.cwd(), filePath).replaceAll("\\", "/");
38992
+ const rel = (0, import_node_path3.relative)(process.cwd(), filePath).replaceAll("\\", "/");
37948
38993
  return rel.startsWith("..") ? filePath.replaceAll("\\", "/") : rel;
37949
38994
  }
37950
38995
  function readFreshCache(filePath, maxAgeMs) {
37951
38996
  try {
37952
- const payload = JSON.parse((0, import_node_fs4.readFileSync)(filePath, "utf8"));
38997
+ const payload = JSON.parse((0, import_node_fs6.readFileSync)(filePath, "utf8"));
37953
38998
  const createdAt = Date.parse(String(payload.created_at || ""));
37954
38999
  if (!Number.isFinite(createdAt)) return null;
37955
39000
  const ageMs = Date.now() - createdAt;
@@ -37961,8 +39006,8 @@ function readFreshCache(filePath, maxAgeMs) {
37961
39006
  }
37962
39007
  }
37963
39008
  function writeCache(filePath, value) {
37964
- (0, import_node_fs4.mkdirSync)((0, import_node_path.dirname)(filePath), { recursive: true });
37965
- (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({
37966
39011
  schema_version: "foh_proof_cache_entry.v1",
37967
39012
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
37968
39013
  value
@@ -37972,7 +39017,7 @@ function writeCache(filePath, value) {
37972
39017
  function resolveProofCacheDir(input) {
37973
39018
  const value = String(input || process.env.FOH_PROOF_CACHE_DIR || "").trim();
37974
39019
  if (!value) return null;
37975
- 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);
37976
39021
  }
37977
39022
  async function withProofCache(options, run) {
37978
39023
  const resolvedDir = resolveProofCacheDir(options.cacheDir);
@@ -37992,12 +39037,12 @@ async function withProofCache(options, run) {
37992
39037
  };
37993
39038
  }
37994
39039
  const key = cacheKey2(options.kind, options.keyParts);
37995
- const cachePath = (0, import_node_path.join)(resolvedDir, `${key}.json`);
37996
- 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`);
37997
39042
  const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
37998
39043
  const waitMs = options.waitMs ?? Number(process.env.FOH_PROOF_CACHE_WAIT_MS || DEFAULT_WAIT_MS);
37999
39044
  const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
38000
- (0, import_node_fs4.mkdirSync)(resolvedDir, { recursive: true });
39045
+ (0, import_node_fs6.mkdirSync)(resolvedDir, { recursive: true });
38001
39046
  const existing = readFreshCache(cachePath, maxAgeMs);
38002
39047
  if (existing) {
38003
39048
  return {
@@ -38016,7 +39061,7 @@ async function withProofCache(options, run) {
38016
39061
  }
38017
39062
  let lockOwner = false;
38018
39063
  try {
38019
- (0, import_node_fs4.mkdirSync)(lockPath);
39064
+ (0, import_node_fs6.mkdirSync)(lockPath);
38020
39065
  lockOwner = true;
38021
39066
  } catch {
38022
39067
  const started = Date.now();
@@ -38057,7 +39102,7 @@ async function withProofCache(options, run) {
38057
39102
  }
38058
39103
  };
38059
39104
  } finally {
38060
- 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 });
38061
39106
  }
38062
39107
  }
38063
39108
 
@@ -38114,6 +39159,9 @@ function fail(name, reasonCode, error2, nextCommand) {
38114
39159
  function skipped(name, reasonCode, summary, nextCommand) {
38115
39160
  return { name, category: categoryForCheck(name), status: "skipped", reason_code: reasonCode, summary, next_command: nextCommand };
38116
39161
  }
39162
+ function defaultCertificationProfileForMission(mission) {
39163
+ return mission === "publish" ? "release" : "smoke";
39164
+ }
38117
39165
  function hasBlockingChecks(checks) {
38118
39166
  return checks.some((check2) => check2.status === "hold" || check2.status === "fail");
38119
39167
  }
@@ -38187,7 +39235,7 @@ function isProviderCapacityBlocked(onboarding) {
38187
39235
  return /maximum number of subaccounts|subaccount limit|reserve[- ]number pool|reserve pool exhausted|global safety limit/.test(message);
38188
39236
  }
38189
39237
  function registerProve(program3) {
38190
- 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 () => {
38191
39239
  const commandStartedMs = Date.now();
38192
39240
  const checks = [];
38193
39241
  const checkTimings = /* @__PURE__ */ new Map();
@@ -38384,16 +39432,25 @@ function registerProve(program3) {
38384
39432
  headers: { "x-agent-id": String(ctx.agentId) }
38385
39433
  }));
38386
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]);
38387
39437
  if (publicKey) {
38388
39438
  checkTimings.set("widget_channel", Math.max(checkTimings.get("widget_channel") ?? 0, checkTimings.get("widget_embed") ?? 0));
38389
39439
  ctx.widgetPublicKey = publicKey;
39440
+ ctx.widgetOrigin = origin;
38390
39441
  checks.push(pass("widget_channel", "Widget channel is available in read-only proof mode.", {
38391
39442
  public_key_present: true,
38392
- mutation_mode: mutationMode
39443
+ mutation_mode: mutationMode,
39444
+ origin,
39445
+ allowed_domains: allowedDomains
38393
39446
  }));
38394
39447
  }
38395
39448
  if (typeof embed.snippet === "string" && embed.snippet.trim()) {
38396
- 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
+ }));
38397
39454
  } else {
38398
39455
  checks.push(hold("widget_embed", "widget_embed_missing", "Widget embed snippet is missing.", `foh widget embed-snippet --agent ${ctx.agentId}`));
38399
39456
  }
@@ -38433,29 +39490,57 @@ function registerProve(program3) {
38433
39490
  checks.push(skipped("widget_smoke", "operator_skipped", "Skipped by --skip-smoke.", `foh widget smoke --agent ${ctx.agentId} --json`));
38434
39491
  } else if (!ctx.widgetPublicKey) {
38435
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`));
38436
39494
  } else {
38437
39495
  try {
38438
- 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));
38439
39502
  ctx.conversationId = smoke.conversation_id;
38440
39503
  ctx.traceIds = smoke.trace_ids;
38441
39504
  ctx.correlationIds = smoke.correlation_ids;
38442
39505
  if (smoke.failed > 0) {
38443
- 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`));
38444
39511
  } else {
38445
- 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
+ }
38446
39529
  }
38447
39530
  } catch (error2) {
38448
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`));
38449
39533
  }
38450
39534
  }
38451
39535
  if (opts.skipCert) {
38452
39536
  checks.push(skipped("simulation_certification", "operator_skipped", "Skipped by --skip-cert.", `foh certify run --agent ${ctx.agentId} --profile release --json`));
38453
39537
  } else if (!opts.includeCertification) {
39538
+ const certificationProfile = defaultCertificationProfileForMission(mission);
38454
39539
  checks.push(skipped(
38455
39540
  "simulation_certification",
38456
39541
  "certification_explicitly_required",
38457
- "Runtime proof does not run release certification by default.",
38458
- `foh certify run --agent ${ctx.agentId} --profile release --json`
39542
+ certificationProfile === "release" ? "Runtime proof does not run release certification by default." : "Runtime proof does not run certification by default; use smoke certification for bounded external-agent verification.",
39543
+ `foh certify run --agent ${ctx.agentId} --profile ${certificationProfile} --json`
38459
39544
  ));
38460
39545
  } else {
38461
39546
  try {
@@ -38530,6 +39615,7 @@ function registerProve(program3) {
38530
39615
  contact_path: contactPath,
38531
39616
  mutation_mode: mutationMode,
38532
39617
  widget_public_key_present: Boolean(ctx.widgetPublicKey),
39618
+ widget_origin: ctx.widgetOrigin ?? null,
38533
39619
  conversation_id: ctx.conversationId ?? null,
38534
39620
  trace_ids: ctx.traceIds,
38535
39621
  correlation_ids: ctx.correlationIds
@@ -38548,17 +39634,25 @@ function registerProve(program3) {
38548
39634
  }
38549
39635
 
38550
39636
  // src/commands/objective.ts
38551
- var import_node_fs5 = require("node:fs");
38552
- var import_node_path2 = require("node:path");
39637
+ var import_node_fs7 = require("node:fs");
39638
+ var import_node_path4 = require("node:path");
38553
39639
  var DEFAULT_OBJECTIVE_REPORT_PATH = "test-results/objective-status.latest.json";
39640
+ var VALID_OBJECTIVE_INDUSTRIES = ["real_estate", "restaurant", "general"];
39641
+ var BUSINESS_REQUIREMENT_BRIEF_SCHEMA_VERSION = "business_requirement_brief.v1";
38554
39642
  function asRecord3(value) {
38555
39643
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
38556
39644
  }
38557
- function normalizeString(value) {
39645
+ function normalizeString2(value) {
38558
39646
  return typeof value === "string" ? value.trim() : "";
38559
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
+ }
38560
39654
  function uniqueStrings(values) {
38561
- return Array.from(new Set(values.map(normalizeString).filter(Boolean)));
39655
+ return Array.from(new Set(values.map(normalizeString2).filter(Boolean)));
38562
39656
  }
38563
39657
  function asArray(value) {
38564
39658
  return Array.isArray(value) ? value : [];
@@ -38615,22 +39709,26 @@ function finiteNumber(value) {
38615
39709
  return Number.isFinite(number3) ? number3 : null;
38616
39710
  }
38617
39711
  function normalizeCustomerEvidenceActions(value) {
38618
- return asArray(value).map(asRecord3).map((action) => ({
38619
- id: firstString(action, ["id", "action_id"]),
38620
- title: firstString(action, ["title"]) || null,
38621
- owner: firstString(action, ["owner"]) || null,
38622
- blocker_count: finiteNumber(action.blocker_count) ?? (uniqueStrings(asArray(action.reason_codes)).length || null),
38623
- target_evidence_paths: uniqueStrings([
38624
- ...asArray(action.target_evidence_paths),
38625
- firstString(action, ["target_evidence_path"])
38626
- ]),
38627
- validator_commands: uniqueStrings([
38628
- ...asArray(action.validator_commands),
38629
- ...asArray(action.next_commands)
38630
- ]),
38631
- required_evidence: firstString(action, ["required_evidence"]) || null,
38632
- unlocks: firstString(action, ["unlocks"]) || null
38633
- })).filter((action) => normalizeString(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));
38634
39732
  }
38635
39733
  function normalizeCustomerEvidenceActionPacket(value) {
38636
39734
  const packet = asRecord3(value);
@@ -38647,7 +39745,63 @@ function normalizeCustomerEvidenceActionPacket(value) {
38647
39745
  instructions: uniqueStrings(asArray(packet.instructions))
38648
39746
  };
38649
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
+ }
38650
39803
  function customerEvidenceActionPacketFromSources(...sources) {
39804
+ const skeletonHints = skeletonHintsById(...sources);
38651
39805
  for (const source of sources) {
38652
39806
  const report = asRecord3(source);
38653
39807
  const requestPackets = asRecord3(report.request_packets);
@@ -38667,14 +39821,14 @@ function customerEvidenceActionPacketFromSources(...sources) {
38667
39821
  report.customer_actions ? { actions: report.customer_actions } : null
38668
39822
  ];
38669
39823
  for (const candidate of candidates) {
38670
- const normalized = normalizeCustomerEvidenceActionPacket(candidate);
39824
+ const normalized = mergeActionPacketWithSkeletonHints(normalizeCustomerEvidenceActionPacket(candidate), skeletonHints);
38671
39825
  if (normalized) return normalized;
38672
39826
  }
38673
39827
  }
38674
39828
  return void 0;
38675
39829
  }
38676
39830
  function normalizeObjectiveArtifactPath(value) {
38677
- return value.trim() ? (0, import_node_path2.resolve)(value.trim()) : "";
39831
+ return value.trim() ? (0, import_node_path4.resolve)(value.trim()) : "";
38678
39832
  }
38679
39833
  function resolveObjectiveArtifactPath(value) {
38680
39834
  return normalizeObjectiveArtifactPath(value);
@@ -38689,10 +39843,26 @@ function pickEvidencePacket(value) {
38689
39843
  const payload = firstNonEmptyObject(record2.payload);
38690
39844
  return payload ?? record2;
38691
39845
  }
38692
- function readEvidencePacketFromPlan(path2) {
38693
- 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));
38694
39863
  const sourceReports = asRecord3(plan.source_reports);
38695
- 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;
38696
39866
  }
38697
39867
  function inferEvidenceFromProofPaths(evidence) {
38698
39868
  const record2 = asRecord3(evidence);
@@ -38716,7 +39886,7 @@ async function resolveEvidenceInput(opts) {
38716
39886
  }
38717
39887
  function firstString(record2, keys) {
38718
39888
  for (const key of keys) {
38719
- const value = normalizeString(record2[key]);
39889
+ const value = normalizeString2(record2[key]);
38720
39890
  if (value) return value;
38721
39891
  }
38722
39892
  return "";
@@ -38734,13 +39904,13 @@ function statusFromDecision(value) {
38734
39904
  return "hold";
38735
39905
  }
38736
39906
  function firstCustomerEvidenceAction(packet) {
38737
- const actions = asArray(packet?.actions).map(asRecord3).filter((action) => normalizeString(action.id));
39907
+ const actions = asArray(packet?.actions).map(asRecord3).filter((action) => normalizeString2(action.id));
38738
39908
  return actions[0] ?? null;
38739
39909
  }
38740
39910
  function buildDeveloperReadinessPacket(input) {
38741
39911
  const businessName = resolveBusinessName(input.opts);
38742
- const sourceUrl = normalizeString(input.opts.sourceUrl);
38743
- const businessObjective = normalizeString(input.opts.businessObjective);
39912
+ const sourceUrl = normalizeString2(input.opts.sourceUrl);
39913
+ const businessObjective = normalizeString2(input.opts.businessObjective);
38744
39914
  const location = resolveLocation(input.opts);
38745
39915
  const tools = parseCsvOption(input.opts.tools ?? "widget,voice,email,callback,calendar,crm,webhook,whatsapp") ?? [];
38746
39916
  const setupDecision = firstString(input.setupWorkflow, ["decision", "current_decision", "status"]);
@@ -38757,9 +39927,10 @@ function buildDeveloperReadinessPacket(input) {
38757
39927
  business_context: {
38758
39928
  business_name: businessName || null,
38759
39929
  business_objective: businessObjective || null,
39930
+ industry: objectiveIndustryOrNull(input.opts),
38760
39931
  source_url: sourceUrl || null,
38761
39932
  location: location || null,
38762
- target_mode: normalizeString(input.opts.targetMode) || "customer_owned_voice_trial",
39933
+ target_mode: normalizeString2(input.opts.targetMode) || "customer_owned_voice_trial",
38763
39934
  tools
38764
39935
  },
38765
39936
  input_completeness: {
@@ -38767,7 +39938,7 @@ function buildDeveloperReadinessPacket(input) {
38767
39938
  source_url: Boolean(sourceUrl),
38768
39939
  business_objective: Boolean(businessObjective),
38769
39940
  requested_tools: tools.length > 0,
38770
- target_mode: Boolean(normalizeString(input.opts.targetMode) || "customer_owned_voice_trial")
39941
+ target_mode: Boolean(normalizeString2(input.opts.targetMode) || "customer_owned_voice_trial")
38771
39942
  },
38772
39943
  readiness_dimensions: {
38773
39944
  setup_workflow: {
@@ -38781,7 +39952,7 @@ function buildDeveloperReadinessPacket(input) {
38781
39952
  customer_evidence_actions: {
38782
39953
  status: firstAction ? "hold" : "pass",
38783
39954
  action_count: finiteNumber(input.customerEvidenceActionPacket?.action_count) ?? asArray(input.customerEvidenceActionPacket?.actions).length,
38784
- first_action_id: firstAction ? normalizeString(firstAction.id) : null,
39955
+ first_action_id: firstAction ? normalizeString2(firstAction.id) : null,
38785
39956
  first_validator_command: firstActionValidator
38786
39957
  },
38787
39958
  agent_operability: {
@@ -38805,17 +39976,17 @@ function buildDeveloperReadinessPacket(input) {
38805
39976
  };
38806
39977
  }
38807
39978
  function resolveObjectiveReportPath(value) {
38808
- const raw = normalizeString(value);
38809
- if (!raw || raw === "latest") return (0, import_node_path2.resolve)(DEFAULT_OBJECTIVE_REPORT_PATH);
38810
- return (0, import_node_path2.resolve)(raw);
39979
+ const raw = normalizeString2(value);
39980
+ if (!raw || raw === "latest") return (0, import_node_path4.resolve)(DEFAULT_OBJECTIVE_REPORT_PATH);
39981
+ return (0, import_node_path4.resolve)(raw);
38811
39982
  }
38812
- function writeJsonArtifact2(path2, value) {
38813
- (0, import_node_fs5.mkdirSync)((0, import_node_path2.dirname)(path2), { recursive: true });
38814
- (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)}
38815
39986
  `, "utf8");
38816
39987
  }
38817
- function readJsonArtifact(path2) {
38818
- 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"));
38819
39990
  }
38820
39991
  function buildSetupBody(opts) {
38821
39992
  const tools = parseCsvOption(opts.tools ?? "widget,voice,email,callback,calendar,crm,webhook,whatsapp") ?? [];
@@ -38823,11 +39994,11 @@ function buildSetupBody(opts) {
38823
39994
  const location = resolveLocation(opts);
38824
39995
  const body = {
38825
39996
  agency_name: businessName,
38826
- business_objective: normalizeString(opts.businessObjective) || null,
39997
+ business_objective: normalizeString2(opts.businessObjective) || null,
38827
39998
  requested_tool_surface: tools,
38828
- target_exposure_mode: normalizeString(opts.targetMode) || "customer_owned_voice_trial"
39999
+ target_exposure_mode: normalizeString2(opts.targetMode) || "customer_owned_voice_trial"
38829
40000
  };
38830
- if (opts.sourceUrl) body.source_url = normalizeString(opts.sourceUrl);
40001
+ if (opts.sourceUrl) body.source_url = normalizeString2(opts.sourceUrl);
38831
40002
  if (location) body.branch_location = location;
38832
40003
  return body;
38833
40004
  }
@@ -38835,19 +40006,176 @@ function buildStatusParams(opts) {
38835
40006
  const params = new URLSearchParams();
38836
40007
  const businessName = resolveBusinessName(opts);
38837
40008
  const location = resolveLocation(opts);
38838
- if (opts.environment) params.set("environment", normalizeString(opts.environment));
40009
+ if (opts.environment) params.set("environment", normalizeString2(opts.environment));
38839
40010
  if (businessName) params.set("agency_name", businessName);
38840
- if (opts.sourceUrl) params.set("source_url", normalizeString(opts.sourceUrl));
40011
+ if (opts.sourceUrl) params.set("source_url", normalizeString2(opts.sourceUrl));
38841
40012
  if (location) params.set("branch_location", location);
38842
- if (opts.tools) params.set("tools", normalizeString(opts.tools));
38843
- if (opts.targetMode) params.set("target_mode", normalizeString(opts.targetMode));
40013
+ if (opts.tools) params.set("tools", normalizeString2(opts.tools));
40014
+ if (opts.targetMode) params.set("target_mode", normalizeString2(opts.targetMode));
38844
40015
  return params;
38845
40016
  }
38846
40017
  function resolveBusinessName(opts) {
38847
- return normalizeString(opts.businessName) || normalizeString(opts.agencyName);
40018
+ return normalizeString2(opts.businessName) || normalizeString2(opts.agencyName);
38848
40019
  }
38849
40020
  function resolveLocation(opts) {
38850
- return normalizeString(opts.location) || normalizeString(opts.branchLocation);
40021
+ return normalizeString2(opts.location) || normalizeString2(opts.branchLocation);
40022
+ }
40023
+ function hasAnyToken(value, tokens) {
40024
+ const normalized = ` ${value.toLowerCase().replace(/[^a-z0-9]+/g, " ")} `;
40025
+ return tokens.some((token) => normalized.includes(` ${token} `));
40026
+ }
40027
+ function inferObjectiveIndustry(opts) {
40028
+ const explicit = normalizeString2(opts.industry);
40029
+ if (explicit) {
40030
+ if (VALID_OBJECTIVE_INDUSTRIES.includes(explicit)) return explicit;
40031
+ throw new FohError({
40032
+ step: "objective.brief",
40033
+ error: `Unsupported industry: ${explicit}.`,
40034
+ remediation: `Use one of: ${VALID_OBJECTIVE_INDUSTRIES.join(", ")}.`,
40035
+ statusCode: 400,
40036
+ reasonCode: "objective_industry_unsupported"
40037
+ });
40038
+ }
40039
+ const objectiveText = [
40040
+ opts.businessObjective,
40041
+ opts.businessName,
40042
+ opts.agencyName,
40043
+ opts.sourceUrl
40044
+ ].map(normalizeString2).join(" ");
40045
+ const realEstate = hasAnyToken(objectiveText, [
40046
+ "estate",
40047
+ "property",
40048
+ "properties",
40049
+ "viewing",
40050
+ "valuation",
40051
+ "buyer",
40052
+ "seller",
40053
+ "landlord",
40054
+ "tenant",
40055
+ "lettings"
40056
+ ]);
40057
+ const restaurant = hasAnyToken(objectiveText, [
40058
+ "restaurant",
40059
+ "table",
40060
+ "booking",
40061
+ "reservation",
40062
+ "reservations",
40063
+ "diner",
40064
+ "diners",
40065
+ "menu",
40066
+ "allergy",
40067
+ "hospitality"
40068
+ ]);
40069
+ if (realEstate && !restaurant) return "real_estate";
40070
+ if (restaurant && !realEstate) return "restaurant";
40071
+ throw new FohError({
40072
+ step: "objective.brief",
40073
+ error: realEstate && restaurant ? "Objective matches multiple industries." : "Objective is too ambiguous to select a business template.",
40074
+ remediation: "Pass --industry real_estate or --industry restaurant, and keep --business-objective specific to one front-of-house outcome.",
40075
+ statusCode: 400,
40076
+ reasonCode: realEstate && restaurant ? "objective_industry_ambiguous" : "objective_industry_missing",
40077
+ nextCommands: [
40078
+ 'foh objective plan --business-name <name> --industry restaurant --business-objective "Book tables from website and voice enquiries" --source-url <official_url> --json',
40079
+ 'foh objective plan --business-name <name> --industry real_estate --business-objective "Qualify buyers and book viewings" --source-url <official_url> --json'
40080
+ ]
40081
+ });
40082
+ }
40083
+ function objectiveIndustryOrNull(opts) {
40084
+ try {
40085
+ return inferObjectiveIndustry(opts);
40086
+ } catch {
40087
+ return null;
40088
+ }
40089
+ }
40090
+ function channelsFromTools(tools) {
40091
+ const supported = /* @__PURE__ */ new Set(["widget", "voice", "whatsapp", "instagram", "sms"]);
40092
+ const channels = tools.filter((tool) => supported.has(tool));
40093
+ return channels.length > 0 ? channels : ["widget"];
40094
+ }
40095
+ function inferOptimizationTarget(industry, objective) {
40096
+ const normalized = objective.toLowerCase();
40097
+ if (industry === "restaurant" || /book|booking|reservation|table/.test(normalized)) return "booking_rate";
40098
+ if (/speed|fast|callback|call back|lead/.test(normalized)) return "speed_to_lead";
40099
+ if (/support|help|resolve|resolution/.test(normalized)) return "support_resolution";
40100
+ return "lead_quality";
40101
+ }
40102
+ function buildRequirementBrief(opts) {
40103
+ const businessName = resolveBusinessName(opts);
40104
+ const businessObjective = normalizeString2(opts.businessObjective);
40105
+ if (!businessObjective) {
40106
+ throw new FohError({
40107
+ step: "objective.brief",
40108
+ error: "Missing business objective.",
40109
+ remediation: "Pass --business-objective with the concrete front-of-house outcome to configure.",
40110
+ statusCode: 400,
40111
+ reasonCode: "objective_business_objective_missing"
40112
+ });
40113
+ }
40114
+ const industry = inferObjectiveIndustry(opts);
40115
+ const tools = parseCsvOption(opts.tools ?? "widget,voice,email,callback,calendar,crm,webhook,whatsapp") ?? [];
40116
+ const knowledgeSources = [];
40117
+ const sourceUrl = normalizeString2(opts.sourceUrl);
40118
+ if (sourceUrl) {
40119
+ knowledgeSources.push({
40120
+ type: "website",
40121
+ label: `${businessName} official website`,
40122
+ uri: sourceUrl
40123
+ });
40124
+ }
40125
+ return {
40126
+ schema_version: BUSINESS_REQUIREMENT_BRIEF_SCHEMA_VERSION,
40127
+ business_name: businessName,
40128
+ industry,
40129
+ desired_use_cases: [businessObjective],
40130
+ channels: channelsFromTools(tools),
40131
+ knowledge_sources: knowledgeSources,
40132
+ required_outcomes: [businessObjective],
40133
+ handoff_rules: ["handoff when customer facts, credentials, or booking/action availability are missing"],
40134
+ constraints: ["do not invent business facts or confirm actions without configured evidence/tool results"],
40135
+ optimization_target: inferOptimizationTarget(industry, businessObjective)
40136
+ };
40137
+ }
40138
+ function selectedTemplateSummary2(templateSelection) {
40139
+ const candidates = asArray(asRecord3(templateSelection).candidates).map(asRecord3);
40140
+ const top = candidates[0];
40141
+ const template = asRecord3(top?.template);
40142
+ if (!template || Object.keys(template).length === 0) return null;
40143
+ const contract = asRecord3(template.template_contract);
40144
+ return {
40145
+ template_id: normalizeString2(template.id) || normalizeString2(contract.template_id) || null,
40146
+ template_name: normalizeString2(template.name) || normalizeString2(contract.name) || null,
40147
+ industry: normalizeString2(contract.industry) || null,
40148
+ use_case: normalizeString2(contract.use_case) || null,
40149
+ match_score: finiteNumber(top.match_score),
40150
+ matched_reasons: uniqueStrings(asArray(top.matched_reasons))
40151
+ };
40152
+ }
40153
+ function assertTemplateSelected(templateSelection) {
40154
+ if (selectedTemplateSummary2(templateSelection)) return;
40155
+ throw new FohError({
40156
+ step: "objective.template_selection",
40157
+ error: "No supported template matched this business objective.",
40158
+ remediation: "Use a more specific --business-objective or choose a supported --industry before setup. Do not continue with a guessed template.",
40159
+ statusCode: 422,
40160
+ reasonCode: "objective_template_selection_empty",
40161
+ nextCommands: [
40162
+ "foh templates select --brief @business-brief.json --json"
40163
+ ]
40164
+ });
40165
+ }
40166
+ async function selectTemplateForObjective(opts) {
40167
+ const brief = buildRequirementBrief(opts);
40168
+ const selection = await apiFetch("/v1/console/templates/select", {
40169
+ method: "POST",
40170
+ body: JSON.stringify(brief),
40171
+ apiUrlOverride: opts.apiUrl
40172
+ });
40173
+ assertTemplateSelected(selection);
40174
+ return {
40175
+ brief,
40176
+ selection,
40177
+ selected: selectedTemplateSummary2(selection)
40178
+ };
38851
40179
  }
38852
40180
  function assertBusinessName(opts, step) {
38853
40181
  if (resolveBusinessName(opts)) return;
@@ -38894,8 +40222,14 @@ function buildObjectiveReport(input) {
38894
40222
  ...collectStringArrays(setup, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"])),
38895
40223
  ...collectStringArrays(live, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"]))
38896
40224
  ]);
38897
- const debugSource = normalizeString(input.opts.out) || "latest";
40225
+ const objectiveArtifactPath = normalizeString2(input.opts.out) || DEFAULT_OBJECTIVE_REPORT_PATH;
40226
+ const debugSource = normalizeString2(input.opts.out) || "latest";
40227
+ let customerEvidenceActionPacket = customerEvidenceActionPacketFromSources(live, setup);
40228
+ if (customerEvidenceActionPacket) {
40229
+ customerEvidenceActionPacket = injectObjectiveApplyGuidance(customerEvidenceActionPacket, objectiveArtifactPath);
40230
+ }
38898
40231
  const nextCommands = dedupeCommands([
40232
+ ...customerEvidenceActionPacket ? [buildObjectiveApplyPlanCommand(objectiveArtifactPath)] : [],
38899
40233
  ...collectStringArrays(setup, /* @__PURE__ */ new Set(["next_commands"])),
38900
40234
  ...collectStringArrays(live, /* @__PURE__ */ new Set(["next_commands"])),
38901
40235
  `foh objective debug --from ${debugSource} --json`
@@ -38909,7 +40243,6 @@ function buildObjectiveReport(input) {
38909
40243
  ...collectArtifactRefs(setup),
38910
40244
  ...collectArtifactRefs(live)
38911
40245
  ]);
38912
- const customerEvidenceActionPacket = customerEvidenceActionPacketFromSources(live, setup);
38913
40246
  const developerReadinessPacket = buildDeveloperReadinessPacket({
38914
40247
  opts: input.opts,
38915
40248
  setupWorkflow: setup,
@@ -38934,15 +40267,19 @@ function buildObjectiveReport(input) {
38934
40267
  safeToRetry: true,
38935
40268
  extra: {
38936
40269
  objective: {
38937
- business_objective: normalizeString(input.opts.businessObjective) || null,
40270
+ business_objective: normalizeString2(input.opts.businessObjective) || null,
38938
40271
  business_name: resolveBusinessName(input.opts) || null,
38939
- agency_name: normalizeString(input.opts.agencyName) || resolveBusinessName(input.opts) || null,
38940
- source_url: normalizeString(input.opts.sourceUrl) || null,
40272
+ agency_name: normalizeString2(input.opts.agencyName) || resolveBusinessName(input.opts) || null,
40273
+ industry: objectiveIndustryOrNull(input.opts),
40274
+ source_url: normalizeString2(input.opts.sourceUrl) || null,
38941
40275
  location: resolveLocation(input.opts) || null,
38942
- branch_location: normalizeString(input.opts.branchLocation) || resolveLocation(input.opts) || null,
38943
- target_mode: normalizeString(input.opts.targetMode) || "customer_owned_voice_trial",
40276
+ branch_location: normalizeString2(input.opts.branchLocation) || resolveLocation(input.opts) || null,
40277
+ target_mode: normalizeString2(input.opts.targetMode) || "customer_owned_voice_trial",
38944
40278
  tools: parseCsvOption(input.opts.tools ?? "widget,voice,email,callback,calendar,crm,webhook,whatsapp") ?? []
38945
40279
  },
40280
+ requirement_brief: firstNonEmptyObject(input.requirementBrief) ?? null,
40281
+ selected_template: firstNonEmptyObject(input.selectedTemplate) ?? null,
40282
+ template_selection: firstNonEmptyObject(input.templateSelection) ?? null,
38946
40283
  allowed_mode: allowedMode,
38947
40284
  blocked_modes: blockedModes,
38948
40285
  reason_codes: reasonCodes,
@@ -38996,7 +40333,7 @@ function stripDiagnosticField(target, fieldPath) {
38996
40333
  function buildObjectiveNormalPathOutput(report) {
38997
40334
  const output = JSON.parse(JSON.stringify(report));
38998
40335
  const artifactPolicy = asRecord3(output.artifact_policy);
38999
- const diagnosticFields = uniqueStrings(asArray(artifactPolicy.diagnostic_fields).map((value) => normalizeString(value)));
40336
+ const diagnosticFields = uniqueStrings(asArray(artifactPolicy.diagnostic_fields).map((value) => normalizeString2(value)));
39000
40337
  for (const fieldPath of diagnosticFields) stripDiagnosticField(output, fieldPath);
39001
40338
  return output;
39002
40339
  }
@@ -39072,8 +40409,9 @@ function buildDebugReport(sourcePath, objectiveReport) {
39072
40409
  }
39073
40410
  function registerObjective(program3) {
39074
40411
  const objective = program3.command("objective").description("Agent-native objective workflow: plan, apply, prove, status, debug");
39075
- objective.command("plan").description("Generate an objective setup/workflow plan").option("--business-name <name>", "Business trading name").option("--agency-name <name>", "Legacy alias for --business-name").option("--business-objective <text>", "Natural-language business objective").option("--source-url <url>", "Official source URL").option("--location <value>", "Location or branch this setup represents").option("--branch-location <value>", "Legacy alias for --location").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode", "customer_owned_voice_trial").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--out <path>", "Write objective plan JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
40412
+ objective.command("plan").description("Generate an objective setup/workflow plan").option("--business-name <name>", "Business trading name").option("--agency-name <name>", "Legacy alias for --business-name").option("--industry <industry>", "Business industry: real_estate, restaurant, general").option("--business-objective <text>", "Natural-language business objective").option("--source-url <url>", "Official source URL").option("--location <value>", "Location or branch this setup represents").option("--branch-location <value>", "Legacy alias for --location").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode", "customer_owned_voice_trial").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--out <path>", "Write objective plan JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39076
40413
  assertBusinessName(opts, "objective.plan");
40414
+ const templateSelection = await selectTemplateForObjective(opts);
39077
40415
  const report = await apiFetch("/v1/console/agency-setup/workflow", {
39078
40416
  method: "POST",
39079
40417
  body: JSON.stringify(buildSetupBody(opts)),
@@ -39081,7 +40419,13 @@ function registerObjective(program3) {
39081
40419
  apiUrlOverride: opts.apiUrl
39082
40420
  });
39083
40421
  const outPath = opts.out ? resolveObjectiveReportPath(opts.out) : null;
39084
- const output = outPath ? { ...asRecord3(report), artifact_path: outPath } : report;
40422
+ const objectiveSetup = {
40423
+ ...asRecord3(report),
40424
+ requirement_brief: templateSelection.brief,
40425
+ template_selection: templateSelection.selection,
40426
+ selected_template: templateSelection.selected
40427
+ };
40428
+ const output = outPath ? { ...objectiveSetup, artifact_path: outPath } : objectiveSetup;
39085
40429
  if (outPath) writeJsonArtifact2(outPath, output);
39086
40430
  format(output, { json: opts.json ?? false });
39087
40431
  }));
@@ -39102,7 +40446,7 @@ function registerObjective(program3) {
39102
40446
  if (outPath) writeJsonArtifact2(outPath, output);
39103
40447
  format(output, { json: opts.json ?? false });
39104
40448
  }));
39105
- objective.command("prove").description("Run objective proof status against customer-live gate").option("--business-name <name>", "Business trading name").option("--agency-name <name>", "Legacy alias for --business-name").option("--business-objective <text>", "Natural-language business objective").option("--source-url <url>", "Official source URL").option("--location <value>", "Location or branch this setup represents").option("--branch-location <value>", "Legacy alias for --location").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode", "customer_owned_voice_trial").option("--environment <value>", "Environment filter").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--out <path>", "Write objective proof JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
40449
+ objective.command("prove").description("Run objective proof status against customer-live gate").option("--business-name <name>", "Business trading name").option("--agency-name <name>", "Legacy alias for --business-name").option("--industry <industry>", "Business industry: real_estate, restaurant, general").option("--business-objective <text>", "Natural-language business objective").option("--source-url <url>", "Official source URL").option("--location <value>", "Location or branch this setup represents").option("--branch-location <value>", "Legacy alias for --location").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode", "customer_owned_voice_trial").option("--environment <value>", "Environment filter").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--out <path>", "Write objective proof JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39106
40450
  assertBusinessName(opts, "objective.prove");
39107
40451
  const status = await apiFetch(withQuery("/v1/console/customer-live-status", buildStatusParams(opts)), {
39108
40452
  orgId: opts.org,
@@ -39113,8 +40457,9 @@ function registerObjective(program3) {
39113
40457
  if (outPath) writeJsonArtifact2(outPath, output);
39114
40458
  format(output, { json: opts.json ?? false });
39115
40459
  }));
39116
- objective.command("status").description("Compose setup and launch evidence into one agent workbench status envelope").option("--business-name <name>", "Business trading name").option("--agency-name <name>", "Legacy alias for --business-name").option("--business-objective <text>", "Natural-language business objective").option("--source-url <url>", "Official source URL").option("--location <value>", "Location or branch this setup represents").option("--branch-location <value>", "Legacy alias for --location").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode", "customer_owned_voice_trial").option("--environment <value>", "Environment filter").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--out <path>", "Write objective report JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
40460
+ objective.command("status").description("Compose setup and launch evidence into one agent workbench status envelope").option("--business-name <name>", "Business trading name").option("--agency-name <name>", "Legacy alias for --business-name").option("--industry <industry>", "Business industry: real_estate, restaurant, general").option("--business-objective <text>", "Natural-language business objective").option("--source-url <url>", "Official source URL").option("--location <value>", "Location or branch this setup represents").option("--branch-location <value>", "Legacy alias for --location").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode", "customer_owned_voice_trial").option("--environment <value>", "Environment filter").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--out <path>", "Write objective report JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39117
40461
  assertBusinessName(opts, "objective.status");
40462
+ const templateSelection = await selectTemplateForObjective(opts);
39118
40463
  const setupWorkflow = await apiFetch("/v1/console/agency-setup/workflow", {
39119
40464
  method: "POST",
39120
40465
  body: JSON.stringify(buildSetupBody(opts)),
@@ -39125,7 +40470,14 @@ function registerObjective(program3) {
39125
40470
  orgId: opts.org,
39126
40471
  apiUrlOverride: opts.apiUrl
39127
40472
  });
39128
- const report = buildObjectiveReport({ opts, setupWorkflow, customerLiveStatus });
40473
+ const report = buildObjectiveReport({
40474
+ opts,
40475
+ setupWorkflow,
40476
+ customerLiveStatus,
40477
+ requirementBrief: templateSelection.brief,
40478
+ templateSelection: templateSelection.selection,
40479
+ selectedTemplate: templateSelection.selected
40480
+ });
39129
40481
  const artifactPath = resolveObjectiveReportPath(opts.out);
39130
40482
  const fullOutput = withObjectiveArtifactPath(report, artifactPath);
39131
40483
  writeJsonArtifact2(artifactPath, fullOutput);
@@ -39133,7 +40485,7 @@ function registerObjective(program3) {
39133
40485
  }));
39134
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 () => {
39135
40487
  const sourcePath = resolveObjectiveReportPath(opts.from);
39136
- if (!(0, import_node_fs5.existsSync)(sourcePath)) {
40488
+ if (!(0, import_node_fs7.existsSync)(sourcePath)) {
39137
40489
  format(cliEnvelope({
39138
40490
  schemaVersion: "agent_workbench_debug.v1",
39139
40491
  status: "fail",
@@ -39423,9 +40775,9 @@ var COMMAND_SURFACE_DEFINITIONS = [
39423
40775
  includeInSuggestions: false
39424
40776
  },
39425
40777
  {
39426
- id: "whatsapp_start",
39427
- commandPath: ["channel", "whatsapp", "start"],
39428
- label: "whatsapp start",
40778
+ id: "whatsapp_onboard",
40779
+ commandPath: ["channel", "whatsapp", "onboard"],
40780
+ label: "whatsapp onboard",
39429
40781
  descriptionFallback: "WhatsApp readiness path",
39430
40782
  mutatesState: "write",
39431
40783
  shellSlash: "/whatsapp",
@@ -40204,6 +41556,277 @@ function registerInteractive(program3) {
40204
41556
  });
40205
41557
  }
40206
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
+
40207
41830
  // src/commands/home-actions.ts
40208
41831
  function resolveHomeState(apiUrlOverride) {
40209
41832
  try {
@@ -40821,9 +42444,9 @@ function compareSemver(a, b) {
40821
42444
  }
40822
42445
  return 0;
40823
42446
  }
40824
- function readPackageJsonVersion(path2) {
42447
+ function readPackageJsonVersion(path5) {
40825
42448
  try {
40826
- const raw = (0, import_fs12.readFileSync)(path2, "utf-8");
42449
+ const raw = (0, import_fs12.readFileSync)(path5, "utf-8");
40827
42450
  const parsed = JSON.parse(raw);
40828
42451
  const version2 = String(parsed.version ?? "").trim();
40829
42452
  return version2 || void 0;
@@ -41074,8 +42697,8 @@ var PHONE_RE2 = /(?<!\w)(?:\+?\d[\d\s().-]{7,}\d)(?!\w)/g;
41074
42697
  function escapeRegExp(value) {
41075
42698
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
41076
42699
  }
41077
- function pathVariants(path2) {
41078
- const resolved = (0, import_path11.resolve)(path2);
42700
+ function pathVariants(path5) {
42701
+ const resolved = (0, import_path11.resolve)(path5);
41079
42702
  const slash = resolved.replace(/\\/g, "/");
41080
42703
  const backslash = resolved.replace(/\//g, "\\");
41081
42704
  return Array.from(new Set([
@@ -41119,9 +42742,9 @@ function redactExternalAgentSecretText(text) {
41119
42742
  }
41120
42743
  function artifactFiles(runDir) {
41121
42744
  if (!(0, import_fs13.existsSync)(runDir)) return [];
41122
- return (0, import_fs13.readdirSync)(runDir).map((name) => (0, import_path11.join)(runDir, name)).filter((path2) => {
41123
- const stat = (0, import_fs13.statSync)(path2);
41124
- 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() || "";
41125
42748
  if (name.endsWith(".redacted")) return false;
41126
42749
  return stat.isFile() && (TEXT_ARTIFACT_NAMES.has(name) || name.startsWith("command-output-cmd_"));
41127
42750
  }).sort();
@@ -41490,10 +43113,10 @@ function collectDocsFrom(value, docs) {
41490
43113
  }
41491
43114
  function readExternalAgentMetadata(runDir) {
41492
43115
  for (const filename of EXTERNAL_AGENT_METADATA_FILENAMES) {
41493
- const path2 = (0, import_path14.join)(runDir, filename);
41494
- if (!(0, import_fs15.existsSync)(path2)) continue;
43116
+ const path5 = (0, import_path14.join)(runDir, filename);
43117
+ if (!(0, import_fs15.existsSync)(path5)) continue;
41495
43118
  try {
41496
- const parsed = JSON.parse((0, import_fs15.readFileSync)(path2, "utf8"));
43119
+ const parsed = JSON.parse((0, import_fs15.readFileSync)(path5, "utf8"));
41497
43120
  const docs = /* @__PURE__ */ new Set();
41498
43121
  collectDocsFrom(parsed.docs_pages_used, docs);
41499
43122
  collectDocsFrom(parsed.docs_pages_observed, docs);
@@ -41521,11 +43144,11 @@ function readExternalAgentMetadata(runDir) {
41521
43144
  }
41522
43145
 
41523
43146
  // src/lib/external-agent-executor-artifacts.ts
41524
- function redactArtifactFile(path2, input = {}) {
41525
- if (!(0, import_fs16.existsSync)(path2)) return;
41526
- 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");
41527
43150
  const redacted = redactExternalAgentArtifactText(original, input);
41528
- if (redacted !== original) (0, import_fs16.writeFileSync)(path2, redacted, "utf8");
43151
+ if (redacted !== original) (0, import_fs16.writeFileSync)(path5, redacted, "utf8");
41529
43152
  }
41530
43153
  function redactExternalAgentOutputArtifacts(run, input = {}) {
41531
43154
  redactArtifactFile(run.outputs.jsonl, input);
@@ -41566,11 +43189,11 @@ function proofArtifactPasses(runDir) {
41566
43189
  return false;
41567
43190
  }
41568
43191
  }
41569
- function readIfExists(path2) {
41570
- 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") : "";
41571
43194
  }
41572
- function relativeArtifactName(path2) {
41573
- return (0, import_path16.basename)(path2);
43195
+ function relativeArtifactName(path5) {
43196
+ return (0, import_path16.basename)(path5);
41574
43197
  }
41575
43198
  function externalAgentSummaryTemplateCommand() {
41576
43199
  return [
@@ -41903,11 +43526,11 @@ async function runExternalAgentEvalAuthPreflight(env = process.env, options = {}
41903
43526
  matched_org: matchedOrg
41904
43527
  };
41905
43528
  }
41906
- function normalizeForCompare(path2) {
41907
- const resolved = (0, import_path18.resolve)(path2);
43529
+ function normalizeForCompare(path5) {
43530
+ const resolved = (0, import_path18.resolve)(path5);
41908
43531
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
41909
43532
  }
41910
- function isPathInside(childPath, parentPath) {
43533
+ function isPathInside2(childPath, parentPath) {
41911
43534
  const child = normalizeForCompare(childPath);
41912
43535
  const parent = normalizeForCompare(parentPath);
41913
43536
  const rel = (0, import_path18.relative)(parent, child);
@@ -42294,7 +43917,7 @@ function createExternalAgentExecutorPlan(options) {
42294
43917
  });
42295
43918
  const privateRepoRoot = privateRepo.root;
42296
43919
  const workspaceRoot = resolveWorkspaceRoot({ batchPath, workspaceRoot: options.workspaceRoot, privateRepoRoot });
42297
- if (isPathInside(workspaceRoot, privateRepoRoot)) {
43920
+ if (isPathInside2(workspaceRoot, privateRepoRoot)) {
42298
43921
  throw new ExternalAgentExecutorError(
42299
43922
  "external_agent_workspace_inside_private_repo",
42300
43923
  `Workspace root must be outside the private repository. workspace=${workspaceRoot} repo=${privateRepoRoot}`
@@ -42321,10 +43944,11 @@ function createExternalAgentExecutorPlan(options) {
42321
43944
  ].join("\n"),
42322
43945
  "utf8"
42323
43946
  );
43947
+ const explicitPromptVersion = typeof run.prompt_version === "string" && run.prompt_version.trim() ? run.prompt_version.trim() : typeof batch.prompt_version === "string" && batch.prompt_version.trim() ? batch.prompt_version.trim() : promptVersionFromPath(promptPath);
42324
43948
  const env = buildCodexExecutorEnv({
42325
43949
  sourceEnv: options.env,
42326
43950
  runDir,
42327
- promptVersion: promptVersionFromPath(promptPath)
43951
+ promptVersion: explicitPromptVersion
42328
43952
  });
42329
43953
  const promptVersion = String(env[EXTERNAL_AGENT_PROMPT_VERSION_ENV] || "unknown");
42330
43954
  const outputStem = runner === "gemini" ? "gemini" : "codex";
@@ -42423,11 +44047,11 @@ function createExternalAgentExecutorPlan(options) {
42423
44047
  };
42424
44048
  }
42425
44049
  function writeExternalAgentExecutorPlan(plan) {
42426
- 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");
42427
44051
  (0, import_fs19.mkdirSync)(plan.batch_dir, { recursive: true });
42428
- (0, import_fs19.writeFileSync)(path2, `${JSON.stringify(plan, null, 2)}
44052
+ (0, import_fs19.writeFileSync)(path5, `${JSON.stringify(plan, null, 2)}
42429
44053
  `, "utf8");
42430
- return path2;
44054
+ return path5;
42431
44055
  }
42432
44056
  async function executeExternalAgentExecutorPlan(plan, options = {}) {
42433
44057
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -43091,6 +44715,7 @@ var DEFAULT_PROMPT_VERSION = "blank-setup.v1";
43091
44715
  var DEFAULT_BATCH_MODELS = "openai/codex,anthropic/claude,cursor/agent";
43092
44716
  var PROMPTS = {
43093
44717
  "blank-setup.v1": "Go to https://frontofhouse.okii.uk. Use only public docs, public API docs, and the public npm CLI package. Always invoke the CLI with `npx --yes @f-o-h/cli@latest ...`; do not use unpinned `npx @f-o-h/cli ...`, because cached older packages can produce invalid evidence. Install or verify the FOH CLI, authenticate or reach a deterministic auth blocker, then create or configure a Front Of House voice agent and website widget. Mass evals reuse existing eval state: run `npx --yes @f-o-h/cli@latest org status --json` and `npx --yes @f-o-h/cli@latest agent list --json` before trying to create a fresh agent; if an existing eval agent is present, configure and prove that agent instead of creating a second bronze-tier agent. Prefer the certification-oriented buyer templates: run `npx --yes @f-o-h/cli@latest templates list --category buyer --json` and use `UK Buyer Qualification` or `Viewing Booking` when available; do not use a greeting-only template for proof/certification. Prefer `npx --yes @f-o-h/cli@latest setup --phone-mode observe` for the free scaffold path: agent, widget, voice config, smoke test, certification, and publish readiness together. Treat phone-number purchasing as an explicit paid/scarce contact-path step, not part of high-volume eval setup. If `FOH_CLI_SPEND_POLICY=no_spend` is active and a command returns `paid_resource_blocked_by_spend_policy`, do not try to bypass it; continue widget/setup proof and report that exact reason code for the phone path. If the customer/operator explicitly owns a number and asks for real PSTN proof, use `npx --yes @f-o-h/cli@latest provision byon attach --phone-number <e164> --confirm-owned --json`; do not invent ownership or buy a FOH-owned number. Run proof/smoke/certification where available, including widget proof, voice proof, and one explicit `foh certify run --agent <id> --profile release --json` before publish. `foh prove` does not run release certification by default; only pass `--include-certification --proof-cache-dir .foh/proof-cache` when an explicit combined proof/certification run is required. If voice proof returns `contact_phone_missing` or `voice_contact_expected_no_spend_hold`, report that exact reason code unless a BYON/customer-approved phone path already exists. If `FOH_EXTERNAL_AGENT_RUN_DIR` is set, write `${FOH_EXTERNAL_AGENT_RUN_DIR}/external-agent-metadata.json` with `schema_version`, `docs_pages_used`, key decisions, and blocker reason codes before finishing. Produce a final evidence summary with commands run, docs used, artifacts created, and any blocker reason codes. Do not assume access to the private source repository.",
44718
+ "restaurant-table-booking-blackbox.v1": "Set up a front of house system for a real restaurant that takes table bookings. Find Front Of House online and use only public web pages, public docs, public API docs, and public package installs. Do not use or inspect any private repository, local source checkout, unpublished local commands, or hidden runbooks. Pick a real restaurant name and official website from public information, then try to reach a deterministic setup/proof result for a table-booking front-of-house agent. Use the authenticated environment if the public CLI discovers it, but do not expose credentials or secrets. Do not buy phone numbers or paid resources. If the system blocks because customer-owned credentials, booking-system access, approval, or live validation are missing, treat that as a valid hold only when the command returns machine-readable reason codes and exact next commands. If `FOH_EXTERNAL_AGENT_RUN_DIR` is set, write `${FOH_EXTERNAL_AGENT_RUN_DIR}/external-agent-metadata.json` with `schema_version`, `restaurant_name`, `restaurant_source_url`, `docs_pages_used`, `commands_run`, `artifacts_created`, `final_status`, `blocker_reason_codes`, and `friction_points` before finishing. Produce a final evidence summary with commands run, docs used, artifacts created, final pass/hold/fail status, blocker reason codes, and every friction point encountered.",
43094
44719
  "real-estate-buyer-enquiry.v1": "Go to https://frontofhouse.okii.uk. Use only public docs, public API docs, and the public npm CLI package. Always invoke the CLI with `npx --yes @f-o-h/cli@latest ...`; do not use unpinned `npx @f-o-h/cli ...`. Do not assume access to the private source repository. Mission: configure a buyer-enquiry real-estate agent (UK Buyer Qualification preferred), prove widget behavior, and prove voice behavior in no-spend mode. Required path: 1) verify auth/org scope and reuse existing eval org/agent where possible; 2) select/apply buyer template; 3) configure widget + voice; 4) run widget smoke and `foh certify run --profile release`; 5) run voice proof and treat no-spend contact holds as expected only when all non-contact gates pass. Do not buy numbers; if spend policy blocks purchase, record `paid_resource_blocked_by_spend_policy` and continue no-spend proof path. Final artifact must include: selected template id/slug, commands executed, pass/hold/fail per gate, reason codes, docs_pages_used, and next fix commands.",
43095
44720
  "real-estate-seller-valuation.v1": "Go to https://frontofhouse.okii.uk. Use only public docs, public API docs, and the public npm CLI package. Always invoke the CLI with `npx --yes @f-o-h/cli@latest ...`; do not use unpinned `npx @f-o-h/cli ...`. Do not assume access to the private source repository. Mission: configure a seller-valuation real-estate agent, prove valuation lead capture on widget and voice, and keep strict no-spend behavior. Required path: 1) verify auth/org scope and reuse existing eval state; 2) select/apply seller valuation template; 3) configure widget + voice; 4) run widget smoke and release certification; 5) run voice proof and classify holds. Do not claim precise valuation output without approved tooling; safe fallback or handoff must remain explicit. Do not buy numbers. Final artifact must include: selected template id/slug, lead fields observed, tool/action behavior, reason codes, docs_pages_used, and next fix commands.",
43096
44721
  "real-estate-viewing-and-qa.v1": "Go to https://frontofhouse.okii.uk. Use only public docs, public API docs, and the public npm CLI package. Always invoke the CLI with `npx --yes @f-o-h/cli@latest ...`; do not use unpinned `npx @f-o-h/cli ...`. Do not assume access to the private source repository. Mission: configure a viewing-booking/property-QA real-estate agent and prove booking-safe behavior under no-spend policy. Required path: 1) verify auth/org scope and reuse existing eval state; 2) select/apply viewing template; 3) configure widget + voice; 4) run widget smoke, release certification, and voice proof; 5) explicitly check no-duplicate-side-effect behavior and safe fallback/handoff when booking tooling or contact path is unavailable. Do not buy numbers. Final artifact must include: selected template id/slug, booking/fallback evidence, reason codes, docs_pages_used, and next fix commands.",
@@ -43259,16 +44884,16 @@ function writePrompt(runDir, promptVersion, context = {}) {
43259
44884
  knowledgeMissPromptContext(context.knowledgeQuestion, context.expectedAnswer),
43260
44885
  agencySetupPromptContext(context)
43261
44886
  ].join("");
43262
- const path2 = (0, import_path20.join)(runDir, "prompt.txt");
43263
- (0, import_fs21.writeFileSync)(path2, `${prompt}
44887
+ const path5 = (0, import_path20.join)(runDir, "prompt.txt");
44888
+ (0, import_fs21.writeFileSync)(path5, `${prompt}
43264
44889
  `, "utf8");
43265
- return path2;
44890
+ return path5;
43266
44891
  }
43267
44892
  function writeSession(runDir, session) {
43268
- const path2 = (0, import_path20.join)(runDir, "session.json");
43269
- (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)}
43270
44895
  `, "utf8");
43271
- return path2;
44896
+ return path5;
43272
44897
  }
43273
44898
  function buildDefaultEvalState() {
43274
44899
  return {
@@ -43768,11 +45393,12 @@ function installSoftExitTrap() {
43768
45393
  // src/lib/mission-help.ts
43769
45394
  var CLI_MISSION_EXAMPLES = [
43770
45395
  { mission: "Start", command: "foh start", description: "guided setup and next action selector" },
43771
- { mission: "Objective Plan", command: "foh objective plan --business-name <name> --source-url <url> --out test-results/objective-plan.latest.json --json", description: "generate setup and onboarding context" },
45396
+ { mission: "Setup Objective", command: 'foh setup --objective "<goal>" --business-name <name> --industry <industry> --source-url <url> --json', description: "select template and create/update the agent from a business goal" },
45397
+ { mission: "Objective Plan", command: "foh objective plan --business-name <name> --source-url <url> --out test-results/objective-plan.latest.json --json", description: "preview setup and onboarding context without applying" },
43772
45398
  { mission: "Objective Apply", command: "foh objective apply --evidence <json|@file> --out test-results/objective-apply.latest.json --json", description: "submit verified evidence into customer-live gating path" },
43773
45399
  { mission: "Objective Prove", command: "foh objective prove --business-name <name> --source-url <url> --out test-results/objective-live.latest.json --json", description: "run customer-live proof check directly" },
43774
45400
  { mission: "Objective Status", command: "foh objective status --business-name <name> --source-url <url> --out test-results/objective-status.latest.json --json", description: "compose setup and live status into one report envelope" },
43775
- { mission: "Setup", command: "foh setup --phone-mode observe --json", description: "create or update agent, widget, voice config, and proof scaffold" },
45401
+ { mission: "Setup Legacy", command: "foh setup --phone-mode observe --json", description: "create or update agent from an explicit template" },
43776
45402
  { mission: "Prove", command: "foh prove --agent <agent_id> --mission widget --json", description: "produce a machine-readable proof report" },
43777
45403
  { mission: "Publish", command: "foh publish --agent <agent_id> --json", description: "publish when proof and release evidence pass" },
43778
45404
  { mission: "Debug", command: "foh debug --out test-results/foh-cli-diag.latest.json --json", description: "collect auth/org/API diagnostics" }
@@ -43878,6 +45504,7 @@ registerBug(program2);
43878
45504
  registerProve(program2);
43879
45505
  registerObjective(program2);
43880
45506
  registerInteractive(program2);
45507
+ registerInstall(program2);
43881
45508
  registerAgentPublishCommand(program2, { publicAlias: true });
43882
45509
  registerEval(program2);
43883
45510
  registerUpdate(program2);