@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/README.md +1 -1
- package/dist/foh.js +2018 -391
- package/package.json +1 -1
- package/schemas/business-requirement-brief.schema.json +3 -3
- package/schemas/foh-template.schema.json +52 -1
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
|
|
967
|
-
var
|
|
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 =
|
|
1900
|
-
if (
|
|
1901
|
-
if (sourceExt.includes(
|
|
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) =>
|
|
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 =
|
|
1915
|
+
resolvedScriptPath = fs4.realpathSync(this._scriptPath);
|
|
1916
1916
|
} catch (err) {
|
|
1917
1917
|
resolvedScriptPath = this._scriptPath;
|
|
1918
1918
|
}
|
|
1919
|
-
executableDir =
|
|
1920
|
-
|
|
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 =
|
|
1927
|
+
const legacyName = path5.basename(
|
|
1928
1928
|
this._scriptPath,
|
|
1929
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
2795
|
-
if (
|
|
2796
|
-
this._executableDir =
|
|
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(
|
|
6292
|
-
let input =
|
|
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 [
|
|
6492
|
-
wsComponent.path =
|
|
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,
|
|
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,
|
|
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(
|
|
10791
|
-
return
|
|
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(
|
|
10814
|
-
const res = await apiFetchRaw(
|
|
10813
|
+
async function apiFetch(path5, init = {}) {
|
|
10814
|
+
const res = await apiFetchRaw(path5, init);
|
|
10815
10815
|
return res.json();
|
|
10816
10816
|
}
|
|
10817
|
-
async function apiFetchRaw(
|
|
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(/\/$/, "") +
|
|
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(
|
|
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:
|
|
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
|
|
13960
|
+
const path5 = prefix ? `${prefix}.${key}` : key;
|
|
13961
13961
|
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
13962
|
-
Object.assign(result, flattenObject(value,
|
|
13962
|
+
Object.assign(result, flattenObject(value, path5));
|
|
13963
13963
|
} else {
|
|
13964
|
-
result[
|
|
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 [
|
|
13980
|
-
const currentVal = currentFlat[
|
|
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:
|
|
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/
|
|
15184
|
-
var
|
|
15185
|
-
|
|
15186
|
-
|
|
15187
|
-
|
|
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
|
|
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
|
|
15213
|
-
|
|
15214
|
-
|
|
15215
|
-
|
|
15216
|
-
|
|
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:
|
|
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(
|
|
15269
|
-
method: "
|
|
15270
|
-
body: JSON.stringify({
|
|
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
|
|
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").
|
|
15311
|
-
const publicKey = await
|
|
15312
|
-
|
|
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
|
|
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
|
|
15423
|
-
var
|
|
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 =
|
|
15509
|
-
if (!(0,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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:
|
|
16997
|
-
const fullPath = [...
|
|
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,
|
|
17775
|
+
constructor(parent, value, path5, key) {
|
|
17113
17776
|
this._cachedPath = [];
|
|
17114
17777
|
this.parent = parent;
|
|
17115
17778
|
this.data = value;
|
|
17116
|
-
this._path =
|
|
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: () =>
|
|
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,
|
|
20763
|
-
if (!
|
|
21425
|
+
function getElementAtPath(obj, path5) {
|
|
21426
|
+
if (!path5)
|
|
20764
21427
|
return obj;
|
|
20765
|
-
return
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(`^${
|
|
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(`.*${
|
|
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" ?
|
|
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" ?
|
|
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(
|
|
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.
|
|
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,
|
|
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(
|
|
34936
|
+
function withQuery(path5, params) {
|
|
34074
34937
|
const query = params.toString();
|
|
34075
|
-
return query ? `${
|
|
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(
|
|
34868
|
-
const absolutePath = (0, import_path3.resolve)(
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
35768
|
-
scenario_ids:
|
|
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,
|
|
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
|
|
35839
|
-
const data = await apiFetch(
|
|
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
|
|
35857
|
-
const data = await apiFetch(
|
|
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
|
|
35862
|
-
const data = await apiFetch(
|
|
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
|
|
35885
|
-
const data = await apiFetch(
|
|
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
|
|
35908
|
-
const data = await apiFetch(
|
|
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
|
|
36029
|
-
const data = await apiFetch(
|
|
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
|
|
36213
|
-
const data = await apiFetch(
|
|
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
|
|
36340
|
-
const data = await apiFetch(
|
|
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,
|
|
37445
|
+
function getPath(source, path5) {
|
|
36401
37446
|
if (!source || typeof source !== "object") return void 0;
|
|
36402
|
-
return
|
|
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(
|
|
36417
|
-
const raw = (0, import_fs7.readFileSync)(
|
|
36418
|
-
return
|
|
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(
|
|
36421
|
-
const parsed = parseStructuredFile(
|
|
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(
|
|
36446
|
-
const parsed = parseStructuredFile(
|
|
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
|
|
36507
|
-
const value = getPath(response,
|
|
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 [
|
|
36564
|
-
const actual = getPath(variables,
|
|
36565
|
-
if (!valuesEqual(actual, expected)) failures.push(`variables.${
|
|
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 [
|
|
36608
|
-
const actual = getPath(response,
|
|
36609
|
-
if (!valuesEqual(actual, expected)) failures.push(`fields.${
|
|
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,
|
|
38349
|
+
function getPath2(value, path5) {
|
|
37305
38350
|
let current = value;
|
|
37306
|
-
for (const segment of
|
|
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(
|
|
37464
|
-
if (!
|
|
38508
|
+
function readSourceArtifact(path5) {
|
|
38509
|
+
if (!path5) return null;
|
|
37465
38510
|
try {
|
|
37466
|
-
return JSON.parse((0, import_fs9.readFileSync)(
|
|
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(
|
|
37644
|
-
const absolutePath = (0, import_path8.resolve)(
|
|
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
|
|
37929
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
37965
|
-
(0,
|
|
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,
|
|
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,
|
|
37996
|
-
const lockPath = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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.", {
|
|
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
|
|
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`,
|
|
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
|
|
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
|
|
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
|
|
38552
|
-
var
|
|
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
|
|
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(
|
|
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
|
|
38620
|
-
|
|
38621
|
-
|
|
38622
|
-
|
|
38623
|
-
|
|
38624
|
-
|
|
38625
|
-
|
|
38626
|
-
|
|
38627
|
-
|
|
38628
|
-
|
|
38629
|
-
|
|
38630
|
-
|
|
38631
|
-
|
|
38632
|
-
|
|
38633
|
-
|
|
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,
|
|
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
|
|
38693
|
-
const
|
|
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
|
-
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
38743
|
-
const 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:
|
|
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(
|
|
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 ?
|
|
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 =
|
|
38809
|
-
if (!raw || raw === "latest") return (0,
|
|
38810
|
-
return (0,
|
|
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(
|
|
38813
|
-
(0,
|
|
38814
|
-
(0,
|
|
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(
|
|
38818
|
-
return JSON.parse((0,
|
|
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:
|
|
39997
|
+
business_objective: normalizeString2(opts.businessObjective) || null,
|
|
38827
39998
|
requested_tool_surface: tools,
|
|
38828
|
-
target_exposure_mode:
|
|
39999
|
+
target_exposure_mode: normalizeString2(opts.targetMode) || "customer_owned_voice_trial"
|
|
38829
40000
|
};
|
|
38830
|
-
if (opts.sourceUrl) body.source_url =
|
|
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",
|
|
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",
|
|
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",
|
|
38843
|
-
if (opts.targetMode) params.set("target_mode",
|
|
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
|
|
40018
|
+
return normalizeString2(opts.businessName) || normalizeString2(opts.agencyName);
|
|
38848
40019
|
}
|
|
38849
40020
|
function resolveLocation(opts) {
|
|
38850
|
-
return
|
|
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
|
|
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:
|
|
40270
|
+
business_objective: normalizeString2(input.opts.businessObjective) || null,
|
|
38938
40271
|
business_name: resolveBusinessName(input.opts) || null,
|
|
38939
|
-
agency_name:
|
|
38940
|
-
|
|
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:
|
|
38943
|
-
target_mode:
|
|
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) =>
|
|
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
|
|
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({
|
|
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,
|
|
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: "
|
|
39427
|
-
commandPath: ["channel", "whatsapp", "
|
|
39428
|
-
label: "whatsapp
|
|
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(
|
|
42447
|
+
function readPackageJsonVersion(path5) {
|
|
40825
42448
|
try {
|
|
40826
|
-
const raw = (0, import_fs12.readFileSync)(
|
|
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(
|
|
41078
|
-
const resolved = (0, import_path11.resolve)(
|
|
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((
|
|
41123
|
-
const stat = (0, import_fs13.statSync)(
|
|
41124
|
-
const name =
|
|
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
|
|
41494
|
-
if (!(0, import_fs15.existsSync)(
|
|
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)(
|
|
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(
|
|
41525
|
-
if (!(0, import_fs16.existsSync)(
|
|
41526
|
-
const original = (0, import_fs16.readFileSync)(
|
|
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)(
|
|
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(
|
|
41570
|
-
return (0, import_fs17.existsSync)(
|
|
43192
|
+
function readIfExists(path5) {
|
|
43193
|
+
return (0, import_fs17.existsSync)(path5) ? (0, import_fs17.readFileSync)(path5, "utf8") : "";
|
|
41571
43194
|
}
|
|
41572
|
-
function relativeArtifactName(
|
|
41573
|
-
return (0, import_path16.basename)(
|
|
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(
|
|
41907
|
-
const resolved = (0, import_path18.resolve)(
|
|
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
|
|
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 (
|
|
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:
|
|
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
|
|
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)(
|
|
44052
|
+
(0, import_fs19.writeFileSync)(path5, `${JSON.stringify(plan, null, 2)}
|
|
42429
44053
|
`, "utf8");
|
|
42430
|
-
return
|
|
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
|
|
43263
|
-
(0, import_fs21.writeFileSync)(
|
|
44887
|
+
const path5 = (0, import_path20.join)(runDir, "prompt.txt");
|
|
44888
|
+
(0, import_fs21.writeFileSync)(path5, `${prompt}
|
|
43264
44889
|
`, "utf8");
|
|
43265
|
-
return
|
|
44890
|
+
return path5;
|
|
43266
44891
|
}
|
|
43267
44892
|
function writeSession(runDir, session) {
|
|
43268
|
-
const
|
|
43269
|
-
(0, import_fs21.writeFileSync)(
|
|
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
|
|
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
|
|
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
|
|
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);
|