@f-o-h/cli 0.1.3 → 0.1.5
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 +9 -1
- package/dist/foh.js +812 -87
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ AI-operator provisioning CLI for Front Of House.
|
|
|
4
4
|
|
|
5
5
|
Public mirror: https://github.com/iiko38/front-of-house-cli
|
|
6
6
|
|
|
7
|
-
Current published baseline: `@f-o-h/cli@0.1.
|
|
7
|
+
Current published baseline: `@f-o-h/cli@0.1.5`
|
|
8
8
|
|
|
9
9
|
This mirror is a generated release artifact. The private product monorepo is not
|
|
10
10
|
published here, and no open-source license is granted unless stated separately.
|
|
@@ -37,6 +37,7 @@ foh auth login
|
|
|
37
37
|
foh org list
|
|
38
38
|
foh org use --org <org-id>
|
|
39
39
|
foh setup
|
|
40
|
+
foh prove --agent <agent-id> --json
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
For AI agents and text-only terminals:
|
|
@@ -48,6 +49,7 @@ foh auth login --email "$FOH_EMAIL" --password "$FOH_PASSWORD" --json
|
|
|
48
49
|
foh org list --json
|
|
49
50
|
foh org use --org <org-id> --json
|
|
50
51
|
foh setup --org <org-id> --agent-template <template-id> --agent-name "Demo Agent" --json
|
|
52
|
+
foh prove --agent <agent-id> --json --out foh-proof.json
|
|
51
53
|
```
|
|
52
54
|
|
|
53
55
|
`auth signup --web` opens the console signup page when possible and always
|
|
@@ -55,5 +57,11 @@ prints the fallback URL. `auth login --web` starts browser device
|
|
|
55
57
|
authorization, opens `/cli-auth`, waits for console approval, and stores the
|
|
56
58
|
returned short-lived token. Credential auth remains available as fallback.
|
|
57
59
|
|
|
60
|
+
`foh prove` produces a compact signed proof report across auth, org context,
|
|
61
|
+
agent validation, contact phone readiness, voice provider health, widget
|
|
62
|
+
channel/embed readiness, widget smoke, and simulation certification. Use
|
|
63
|
+
`--strict` in automation when holds should fail the command, and
|
|
64
|
+
`--require-phone` when a voice/contact number is mandatory for the demo.
|
|
65
|
+
|
|
58
66
|
The CLI defaults to the production API at `https://api.frontofhouse.okii.uk`.
|
|
59
67
|
|
package/dist/foh.js
CHANGED
|
@@ -6046,7 +6046,7 @@ var require_compile = __commonJS({
|
|
|
6046
6046
|
const schOrFunc = root.refs[ref];
|
|
6047
6047
|
if (schOrFunc)
|
|
6048
6048
|
return schOrFunc;
|
|
6049
|
-
let _sch =
|
|
6049
|
+
let _sch = resolve7.call(this, root, ref);
|
|
6050
6050
|
if (_sch === void 0) {
|
|
6051
6051
|
const schema2 = (_a2 = root.localRefs) === null || _a2 === void 0 ? void 0 : _a2[ref];
|
|
6052
6052
|
const { schemaId } = this.opts;
|
|
@@ -6073,7 +6073,7 @@ var require_compile = __commonJS({
|
|
|
6073
6073
|
function sameSchemaEnv(s1, s2) {
|
|
6074
6074
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
6075
6075
|
}
|
|
6076
|
-
function
|
|
6076
|
+
function resolve7(root, ref) {
|
|
6077
6077
|
let sch;
|
|
6078
6078
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
6079
6079
|
ref = sch;
|
|
@@ -6648,7 +6648,7 @@ var require_fast_uri = __commonJS({
|
|
|
6648
6648
|
}
|
|
6649
6649
|
return uri;
|
|
6650
6650
|
}
|
|
6651
|
-
function
|
|
6651
|
+
function resolve7(baseURI, relativeURI, options) {
|
|
6652
6652
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
6653
6653
|
const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
6654
6654
|
schemelessOptions.skipEscape = true;
|
|
@@ -6875,7 +6875,7 @@ var require_fast_uri = __commonJS({
|
|
|
6875
6875
|
var fastUri = {
|
|
6876
6876
|
SCHEMES,
|
|
6877
6877
|
normalize,
|
|
6878
|
-
resolve:
|
|
6878
|
+
resolve: resolve7,
|
|
6879
6879
|
resolveComponent,
|
|
6880
6880
|
equal,
|
|
6881
6881
|
serialize,
|
|
@@ -10063,21 +10063,21 @@ async function promptLine(label, {
|
|
|
10063
10063
|
allowEmpty = false,
|
|
10064
10064
|
defaultValue
|
|
10065
10065
|
} = {}) {
|
|
10066
|
-
return await new Promise((
|
|
10066
|
+
return await new Promise((resolve7) => {
|
|
10067
10067
|
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
10068
10068
|
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout, terminal: true });
|
|
10069
10069
|
rl.question(`${label}${suffix}: `, (answer) => {
|
|
10070
10070
|
rl.close();
|
|
10071
10071
|
const value = String(answer ?? "").trim();
|
|
10072
10072
|
if (!value && typeof defaultValue === "string") {
|
|
10073
|
-
|
|
10073
|
+
resolve7(defaultValue);
|
|
10074
10074
|
return;
|
|
10075
10075
|
}
|
|
10076
10076
|
if (!value && !allowEmpty) {
|
|
10077
|
-
|
|
10077
|
+
resolve7("");
|
|
10078
10078
|
return;
|
|
10079
10079
|
}
|
|
10080
|
-
|
|
10080
|
+
resolve7(value);
|
|
10081
10081
|
});
|
|
10082
10082
|
});
|
|
10083
10083
|
}
|
|
@@ -10085,7 +10085,7 @@ async function promptSecret(label) {
|
|
|
10085
10085
|
if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== "function") {
|
|
10086
10086
|
return await promptLine(label);
|
|
10087
10087
|
}
|
|
10088
|
-
return await new Promise((
|
|
10088
|
+
return await new Promise((resolve7) => {
|
|
10089
10089
|
const stdin = process.stdin;
|
|
10090
10090
|
const stdout = process.stdout;
|
|
10091
10091
|
const wasRaw = Boolean(stdin.isRaw);
|
|
@@ -10099,7 +10099,7 @@ async function promptSecret(label) {
|
|
|
10099
10099
|
const finish = () => {
|
|
10100
10100
|
cleanup();
|
|
10101
10101
|
stdout.write("\n");
|
|
10102
|
-
|
|
10102
|
+
resolve7(value);
|
|
10103
10103
|
};
|
|
10104
10104
|
const onData = (chunk) => {
|
|
10105
10105
|
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
@@ -10108,7 +10108,7 @@ async function promptSecret(label) {
|
|
|
10108
10108
|
cleanup();
|
|
10109
10109
|
process.exitCode = 130;
|
|
10110
10110
|
stdout.write("\n");
|
|
10111
|
-
return
|
|
10111
|
+
return resolve7("");
|
|
10112
10112
|
}
|
|
10113
10113
|
if (char === "\r" || char === "\n") {
|
|
10114
10114
|
finish();
|
|
@@ -10192,10 +10192,20 @@ function buildCliSignupFallbackInstructions(signUpUrl) {
|
|
|
10192
10192
|
|
|
10193
10193
|
// src/lib/open-url.ts
|
|
10194
10194
|
var import_child_process = require("child_process");
|
|
10195
|
+
function buildOpenUrlCommand(url2, platform = process.platform) {
|
|
10196
|
+
if (platform === "win32") {
|
|
10197
|
+
return {
|
|
10198
|
+
command: "rundll32.exe",
|
|
10199
|
+
args: ["url.dll,FileProtocolHandler", url2]
|
|
10200
|
+
};
|
|
10201
|
+
}
|
|
10202
|
+
if (platform === "darwin") {
|
|
10203
|
+
return { command: "open", args: [url2] };
|
|
10204
|
+
}
|
|
10205
|
+
return { command: "xdg-open", args: [url2] };
|
|
10206
|
+
}
|
|
10195
10207
|
function openUrl(url2) {
|
|
10196
|
-
const
|
|
10197
|
-
const command = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
|
|
10198
|
-
const args = platform === "win32" ? ["/c", "start", "", url2] : [url2];
|
|
10208
|
+
const { command, args } = buildOpenUrlCommand(url2);
|
|
10199
10209
|
try {
|
|
10200
10210
|
const child = (0, import_child_process.spawn)(command, args, {
|
|
10201
10211
|
detached: true,
|
|
@@ -10367,7 +10377,7 @@ async function storeAuthenticatedSession(params) {
|
|
|
10367
10377
|
return output;
|
|
10368
10378
|
}
|
|
10369
10379
|
function sleep(ms) {
|
|
10370
|
-
return new Promise((
|
|
10380
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
10371
10381
|
}
|
|
10372
10382
|
async function runDeviceLogin(opts) {
|
|
10373
10383
|
const jsonMode = Boolean(opts.json);
|
|
@@ -10905,7 +10915,7 @@ async function pollUntil(check2, opts) {
|
|
|
10905
10915
|
}
|
|
10906
10916
|
}
|
|
10907
10917
|
function sleep2(ms) {
|
|
10908
|
-
return new Promise((
|
|
10918
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
10909
10919
|
}
|
|
10910
10920
|
|
|
10911
10921
|
// src/commands/compliance.ts
|
|
@@ -13773,8 +13783,8 @@ function registerAgentGuardrailCommands(agent) {
|
|
|
13773
13783
|
try {
|
|
13774
13784
|
rule = JSON.parse(opts.rule);
|
|
13775
13785
|
} catch {
|
|
13776
|
-
const { readFileSync:
|
|
13777
|
-
rule = JSON.parse(
|
|
13786
|
+
const { readFileSync: readFileSync7 } = await import("fs");
|
|
13787
|
+
rule = JSON.parse(readFileSync7(opts.rule, "utf-8"));
|
|
13778
13788
|
}
|
|
13779
13789
|
const data = await apiFetch(`/v1/console/agents/${opts.agent}/guardrails`, {
|
|
13780
13790
|
method: "POST",
|
|
@@ -14180,6 +14190,58 @@ function registerAgent(program3) {
|
|
|
14180
14190
|
const data = await apiFetch(`/v1/console/agents/${opts.agent}`, { apiUrlOverride: opts.apiUrl });
|
|
14181
14191
|
format(data, { json: opts.json ?? false });
|
|
14182
14192
|
}));
|
|
14193
|
+
agent.command("replay").description("Create a replay/debug packet from a trace or conversation").option("--trace <id>", "Trace event ID to replay through the server trace replay endpoint").option("--conversation <id>", "Conversation ID to package with transcript and traces").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 () => {
|
|
14194
|
+
if (!opts.trace && !opts.conversation) {
|
|
14195
|
+
throw new FohError({
|
|
14196
|
+
step: "agent.replay",
|
|
14197
|
+
error: "Missing replay source",
|
|
14198
|
+
remediation: "Pass --trace <id> or --conversation <id>.",
|
|
14199
|
+
statusCode: 400
|
|
14200
|
+
});
|
|
14201
|
+
}
|
|
14202
|
+
if (opts.trace && opts.conversation) {
|
|
14203
|
+
throw new FohError({
|
|
14204
|
+
step: "agent.replay",
|
|
14205
|
+
error: "Ambiguous replay source",
|
|
14206
|
+
remediation: "Pass only one of --trace or --conversation.",
|
|
14207
|
+
statusCode: 400
|
|
14208
|
+
});
|
|
14209
|
+
}
|
|
14210
|
+
if (opts.trace) {
|
|
14211
|
+
const data2 = await apiFetch(`/v1/console/traces/${opts.trace}/replay`, {
|
|
14212
|
+
method: "POST",
|
|
14213
|
+
orgId: opts.org,
|
|
14214
|
+
apiUrlOverride: opts.apiUrl
|
|
14215
|
+
});
|
|
14216
|
+
format({
|
|
14217
|
+
schema_version: "foh_agent_replay_packet.v1",
|
|
14218
|
+
status: "server_replay_completed",
|
|
14219
|
+
source: { type: "trace", trace_id: opts.trace, agent_id: opts.agent },
|
|
14220
|
+
replay: data2,
|
|
14221
|
+
next_commands: [`foh tests from-trace --agent ${opts.agent} --trace ${opts.trace} --json`]
|
|
14222
|
+
}, { json: opts.json ?? false });
|
|
14223
|
+
return;
|
|
14224
|
+
}
|
|
14225
|
+
const data = await apiFetch(`/v1/console/agents/${opts.agent}/conversations/${opts.conversation}?include_traces=true`, {
|
|
14226
|
+
orgId: opts.org,
|
|
14227
|
+
apiUrlOverride: opts.apiUrl
|
|
14228
|
+
});
|
|
14229
|
+
const traces = Array.isArray(data.traces) ? data.traces : [];
|
|
14230
|
+
const firstTraceId = traces.map((trace) => String(trace.id || "").trim()).find(Boolean);
|
|
14231
|
+
format({
|
|
14232
|
+
schema_version: "foh_agent_replay_packet.v1",
|
|
14233
|
+
status: firstTraceId ? "conversation_replay_packet_created" : "conversation_not_replayable",
|
|
14234
|
+
source: { type: "conversation", conversation_id: opts.conversation, agent_id: opts.agent },
|
|
14235
|
+
conversation: data.conversation ?? null,
|
|
14236
|
+
trace_count: traces.length,
|
|
14237
|
+
traces,
|
|
14238
|
+
not_replayable_reason: firstTraceId ? null : "conversation_has_no_trace_events",
|
|
14239
|
+
next_commands: firstTraceId ? [
|
|
14240
|
+
`foh agent replay --agent ${opts.agent} --trace ${firstTraceId} --json`,
|
|
14241
|
+
`foh tests from-trace --agent ${opts.agent} --trace ${firstTraceId} --json`
|
|
14242
|
+
] : [`foh transcripts get --agent ${opts.agent} --conversation ${opts.conversation} --include-traces --json`]
|
|
14243
|
+
}, { json: opts.json ?? false });
|
|
14244
|
+
}));
|
|
14183
14245
|
const blueprint = agent.command("blueprint").description("Compile or apply Conversation Blueprint v1");
|
|
14184
14246
|
blueprint.command("compile").description("Compile a Conversation Blueprint v1 file to the current policy graph draft without saving it").requiredOption("--agent <id>", "Agent ID").requiredOption("--blueprint <json|@file>", "Conversation Blueprint v1 JSON or @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 () => {
|
|
14185
14247
|
const parsedBlueprint = await parseJsonOption(opts.blueprint, "--blueprint");
|
|
@@ -14306,9 +14368,9 @@ function registerAgent(program3) {
|
|
|
14306
14368
|
process.stdout.write(yaml);
|
|
14307
14369
|
return;
|
|
14308
14370
|
}
|
|
14309
|
-
const { writeFileSync:
|
|
14371
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
14310
14372
|
const outputPath = opts.output ?? "tenant.yaml";
|
|
14311
|
-
|
|
14373
|
+
writeFileSync6(
|
|
14312
14374
|
outputPath,
|
|
14313
14375
|
`# tenant.yaml - Front Of House agent manifest
|
|
14314
14376
|
# Edit this file and run: foh plan tenant.yaml
|
|
@@ -15743,11 +15805,11 @@ function registerVoice(program3) {
|
|
|
15743
15805
|
}
|
|
15744
15806
|
const outputPath = String(opts.out || `foh-voice-preview-${provider}-${voiceId}.mp3`).trim();
|
|
15745
15807
|
const audio = Buffer.from(await res.arrayBuffer());
|
|
15746
|
-
const { mkdirSync: mkdirSync4, writeFileSync:
|
|
15747
|
-
const { dirname: dirname5, resolve:
|
|
15748
|
-
const absolutePath =
|
|
15808
|
+
const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync6 } = await import("fs");
|
|
15809
|
+
const { dirname: dirname5, resolve: resolve7 } = await import("path");
|
|
15810
|
+
const absolutePath = resolve7(outputPath);
|
|
15749
15811
|
mkdirSync4(dirname5(absolutePath), { recursive: true });
|
|
15750
|
-
|
|
15812
|
+
writeFileSync6(absolutePath, audio);
|
|
15751
15813
|
format({
|
|
15752
15814
|
status: "ok",
|
|
15753
15815
|
provider,
|
|
@@ -30228,7 +30290,7 @@ var Protocol = class {
|
|
|
30228
30290
|
return;
|
|
30229
30291
|
}
|
|
30230
30292
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
30231
|
-
await new Promise((
|
|
30293
|
+
await new Promise((resolve7) => setTimeout(resolve7, pollInterval));
|
|
30232
30294
|
options?.signal?.throwIfAborted();
|
|
30233
30295
|
}
|
|
30234
30296
|
} catch (error2) {
|
|
@@ -30245,7 +30307,7 @@ var Protocol = class {
|
|
|
30245
30307
|
*/
|
|
30246
30308
|
request(request, resultSchema, options) {
|
|
30247
30309
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
30248
|
-
return new Promise((
|
|
30310
|
+
return new Promise((resolve7, reject) => {
|
|
30249
30311
|
const earlyReject = (error2) => {
|
|
30250
30312
|
reject(error2);
|
|
30251
30313
|
};
|
|
@@ -30323,7 +30385,7 @@ var Protocol = class {
|
|
|
30323
30385
|
if (!parseResult.success) {
|
|
30324
30386
|
reject(parseResult.error);
|
|
30325
30387
|
} else {
|
|
30326
|
-
|
|
30388
|
+
resolve7(parseResult.data);
|
|
30327
30389
|
}
|
|
30328
30390
|
} catch (error2) {
|
|
30329
30391
|
reject(error2);
|
|
@@ -30584,12 +30646,12 @@ var Protocol = class {
|
|
|
30584
30646
|
}
|
|
30585
30647
|
} catch {
|
|
30586
30648
|
}
|
|
30587
|
-
return new Promise((
|
|
30649
|
+
return new Promise((resolve7, reject) => {
|
|
30588
30650
|
if (signal.aborted) {
|
|
30589
30651
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
30590
30652
|
return;
|
|
30591
30653
|
}
|
|
30592
|
-
const timeoutId = setTimeout(
|
|
30654
|
+
const timeoutId = setTimeout(resolve7, interval);
|
|
30593
30655
|
signal.addEventListener("abort", () => {
|
|
30594
30656
|
clearTimeout(timeoutId);
|
|
30595
30657
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -31689,7 +31751,7 @@ var McpServer = class {
|
|
|
31689
31751
|
let task = createTaskResult.task;
|
|
31690
31752
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
31691
31753
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
31692
|
-
await new Promise((
|
|
31754
|
+
await new Promise((resolve7) => setTimeout(resolve7, pollInterval));
|
|
31693
31755
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
31694
31756
|
if (!updatedTask) {
|
|
31695
31757
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -32338,19 +32400,19 @@ var StdioServerTransport = class {
|
|
|
32338
32400
|
this.onclose?.();
|
|
32339
32401
|
}
|
|
32340
32402
|
send(message) {
|
|
32341
|
-
return new Promise((
|
|
32403
|
+
return new Promise((resolve7) => {
|
|
32342
32404
|
const json3 = serializeMessage(message);
|
|
32343
32405
|
if (this._stdout.write(json3)) {
|
|
32344
|
-
|
|
32406
|
+
resolve7();
|
|
32345
32407
|
} else {
|
|
32346
|
-
this._stdout.once("drain",
|
|
32408
|
+
this._stdout.once("drain", resolve7);
|
|
32347
32409
|
}
|
|
32348
32410
|
});
|
|
32349
32411
|
}
|
|
32350
32412
|
};
|
|
32351
32413
|
|
|
32352
32414
|
// src/lib/cli-version.ts
|
|
32353
|
-
var CLI_VERSION = "0.1.
|
|
32415
|
+
var CLI_VERSION = "0.1.5";
|
|
32354
32416
|
|
|
32355
32417
|
// src/commands/mcp-serve.ts
|
|
32356
32418
|
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
@@ -32535,7 +32597,7 @@ async function runFohCli(params) {
|
|
|
32535
32597
|
effectiveArgv.push("--json");
|
|
32536
32598
|
}
|
|
32537
32599
|
const command = `foh ${effectiveArgv.join(" ")}`;
|
|
32538
|
-
return await new Promise((
|
|
32600
|
+
return await new Promise((resolve7) => {
|
|
32539
32601
|
const child = (0, import_node_child_process.spawn)(process.execPath, [cliEntry, ...effectiveArgv], {
|
|
32540
32602
|
stdio: ["ignore", "pipe", "pipe"],
|
|
32541
32603
|
env: {
|
|
@@ -32560,7 +32622,7 @@ async function runFohCli(params) {
|
|
|
32560
32622
|
});
|
|
32561
32623
|
child.once("error", (error2) => {
|
|
32562
32624
|
clearTimeout(timeoutHandle);
|
|
32563
|
-
|
|
32625
|
+
resolve7({
|
|
32564
32626
|
ok: false,
|
|
32565
32627
|
command,
|
|
32566
32628
|
argv: effectiveArgv,
|
|
@@ -32576,7 +32638,7 @@ async function runFohCli(params) {
|
|
|
32576
32638
|
const stderrText = finalizeBoundedText(stderrBuffer);
|
|
32577
32639
|
const exitCode = Number.isFinite(code ?? NaN) ? Number(code) : 1;
|
|
32578
32640
|
const stdoutJson = tryParseJson(stdoutText);
|
|
32579
|
-
|
|
32641
|
+
resolve7({
|
|
32580
32642
|
ok: !timedOut && exitCode === 0,
|
|
32581
32643
|
command,
|
|
32582
32644
|
argv: effectiveArgv,
|
|
@@ -33351,14 +33413,107 @@ function registerMcp(program3) {
|
|
|
33351
33413
|
// src/commands/knowledge.ts
|
|
33352
33414
|
var import_fs2 = require("fs");
|
|
33353
33415
|
var import_path2 = require("path");
|
|
33416
|
+
|
|
33417
|
+
// src/lib/query-options.ts
|
|
33418
|
+
function parsePositiveInt(value, fallback, min, max) {
|
|
33419
|
+
const parsed = Number(value ?? fallback);
|
|
33420
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
33421
|
+
return Math.max(min, Math.min(max, Math.trunc(parsed)));
|
|
33422
|
+
}
|
|
33423
|
+
function withQuery(path2, params) {
|
|
33424
|
+
const query = params.toString();
|
|
33425
|
+
return query ? `${path2}?${query}` : path2;
|
|
33426
|
+
}
|
|
33427
|
+
|
|
33428
|
+
// src/commands/knowledge.ts
|
|
33354
33429
|
function readDraftKnowledgeText(draft) {
|
|
33355
33430
|
const fromRaw = typeof draft.knowledge_base_raw === "string" ? draft.knowledge_base_raw : "";
|
|
33356
33431
|
if (fromRaw.trim().length > 0) return fromRaw;
|
|
33357
33432
|
const fromLegacy = typeof draft.knowledge_base === "string" ? draft.knowledge_base : "";
|
|
33358
33433
|
return fromLegacy;
|
|
33359
33434
|
}
|
|
33435
|
+
function tokenize(value) {
|
|
33436
|
+
return value.toLowerCase().split(/[^a-z0-9]+/g).map((token) => token.trim()).filter((token) => token.length >= 3);
|
|
33437
|
+
}
|
|
33438
|
+
function chunkKnowledgeText(text) {
|
|
33439
|
+
return text.split(/\n\s*\n|---+/g).map((chunk) => chunk.trim()).filter(Boolean).map((chunk, index) => ({ index, text: chunk }));
|
|
33440
|
+
}
|
|
33441
|
+
function scoreChunk(queryTokens, chunkText) {
|
|
33442
|
+
if (queryTokens.size === 0) return 0;
|
|
33443
|
+
const chunkTokens = new Set(tokenize(chunkText));
|
|
33444
|
+
let overlap = 0;
|
|
33445
|
+
for (const token of queryTokens) {
|
|
33446
|
+
if (chunkTokens.has(token)) overlap += 1;
|
|
33447
|
+
}
|
|
33448
|
+
return Number((overlap / queryTokens.size).toFixed(6));
|
|
33449
|
+
}
|
|
33360
33450
|
function registerKnowledge(program3) {
|
|
33361
33451
|
const knowledge = program3.command("knowledge").description("Manage agent knowledge base");
|
|
33452
|
+
knowledge.command("query").description("Debug agent knowledge retrieval for a question").requiredOption("--agent <id>", "Agent ID").requiredOption("--text <query>", "Question/query text").option("--limit <n>", "Max chunks to return (1-20)", "5").option("--min-score <n>", "Minimum overlap score for pass threshold", "0.2").option("--explain", "Include token/scoring metadata").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 () => {
|
|
33453
|
+
const query = String(opts.text || "").trim();
|
|
33454
|
+
if (query.length < 3) {
|
|
33455
|
+
throw new FohError({
|
|
33456
|
+
step: "knowledge.query",
|
|
33457
|
+
error: "Query text must be at least 3 characters",
|
|
33458
|
+
remediation: 'Pass --text "<question>" with 3+ characters.',
|
|
33459
|
+
statusCode: 400
|
|
33460
|
+
});
|
|
33461
|
+
}
|
|
33462
|
+
const draft = await apiFetch(`/v1/console/agents/${opts.agent}/draft`, {
|
|
33463
|
+
orgId: opts.org,
|
|
33464
|
+
apiUrlOverride: opts.apiUrl
|
|
33465
|
+
});
|
|
33466
|
+
const knowledgeText = readDraftKnowledgeText(draft);
|
|
33467
|
+
const chunks = chunkKnowledgeText(knowledgeText);
|
|
33468
|
+
const queryTokens = new Set(tokenize(query));
|
|
33469
|
+
const minScore = Math.max(0, Math.min(1, Number(opts.minScore ?? 0.2) || 0.2));
|
|
33470
|
+
const limit = parsePositiveInt(opts.limit, 5, 1, 20);
|
|
33471
|
+
const matches = chunks.map((chunk) => ({
|
|
33472
|
+
chunk_id: `agent-draft-${opts.agent}#${chunk.index + 1}`,
|
|
33473
|
+
source: "agent_draft_knowledge",
|
|
33474
|
+
citation: `agent:${opts.agent}:chunk:${chunk.index + 1}`,
|
|
33475
|
+
score: scoreChunk(queryTokens, chunk.text),
|
|
33476
|
+
text: chunk.text.slice(0, 1200)
|
|
33477
|
+
})).filter((chunk) => chunk.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
33478
|
+
const topScore = matches[0]?.score ?? 0;
|
|
33479
|
+
const status = matches.length === 0 ? "no_match" : topScore >= minScore ? "pass" : "low_confidence";
|
|
33480
|
+
const reasonCode = status === "pass" ? "knowledge_query_match" : status === "low_confidence" ? "knowledge_query_low_confidence" : "knowledge_query_no_match";
|
|
33481
|
+
const packet = status === "pass" ? null : {
|
|
33482
|
+
schema_version: "foh_knowledge_query_failure_packet.v1",
|
|
33483
|
+
agent_id: opts.agent,
|
|
33484
|
+
query,
|
|
33485
|
+
reason_code: reasonCode,
|
|
33486
|
+
top_score: topScore,
|
|
33487
|
+
chunk_count: chunks.length,
|
|
33488
|
+
next_commands: [
|
|
33489
|
+
`foh knowledge ingest-file --agent ${opts.agent} --file <path> --json`,
|
|
33490
|
+
`foh knowledge query --agent ${opts.agent} --text "${query.replace(/"/g, '\\"')}" --explain --json`
|
|
33491
|
+
]
|
|
33492
|
+
};
|
|
33493
|
+
format({
|
|
33494
|
+
schema_version: "foh_knowledge_query_debug.v1",
|
|
33495
|
+
ok: status === "pass",
|
|
33496
|
+
status,
|
|
33497
|
+
reason_code: reasonCode,
|
|
33498
|
+
agent_id: opts.agent,
|
|
33499
|
+
query,
|
|
33500
|
+
retrieval: {
|
|
33501
|
+
source: "agent_draft_direct",
|
|
33502
|
+
chunk_count: chunks.length,
|
|
33503
|
+
match_count: matches.length,
|
|
33504
|
+
top_score: topScore,
|
|
33505
|
+
min_score: minScore
|
|
33506
|
+
},
|
|
33507
|
+
matches,
|
|
33508
|
+
failure_packet: packet,
|
|
33509
|
+
...opts.explain ? {
|
|
33510
|
+
explain: {
|
|
33511
|
+
query_tokens: Array.from(queryTokens),
|
|
33512
|
+
scoring: "token_overlap_ratio_v1"
|
|
33513
|
+
}
|
|
33514
|
+
} : {}
|
|
33515
|
+
}, { json: opts.json ?? false });
|
|
33516
|
+
}));
|
|
33362
33517
|
knowledge.command("ingest-file").description("Ingest a local file into the knowledge base").requiredOption("--file <path>", "Path to file to ingest").option("--agent <id>", "Scope to agent").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 () => {
|
|
33363
33518
|
const content = (0, import_fs2.readFileSync)(opts.file, "utf-8");
|
|
33364
33519
|
let data;
|
|
@@ -33987,7 +34142,7 @@ function registerSetup(program3) {
|
|
|
33987
34142
|
const startedAtIso = nowIso();
|
|
33988
34143
|
const startedAtMs = Date.now();
|
|
33989
34144
|
if (shouldResumeSkip(name)) {
|
|
33990
|
-
const
|
|
34145
|
+
const skipped2 = timedStepResult(
|
|
33991
34146
|
{
|
|
33992
34147
|
step: name,
|
|
33993
34148
|
status: "skipped",
|
|
@@ -33996,10 +34151,10 @@ function registerSetup(program3) {
|
|
|
33996
34151
|
startedAtIso,
|
|
33997
34152
|
startedAtMs
|
|
33998
34153
|
);
|
|
33999
|
-
completed.push(
|
|
34154
|
+
completed.push(skipped2);
|
|
34000
34155
|
process.stderr.write(import_picocolors4.default.dim(` [RESUME] ${name}: skipped (resume-from ${resumeState.resumeFrom})
|
|
34001
34156
|
`));
|
|
34002
|
-
return
|
|
34157
|
+
return skipped2;
|
|
34003
34158
|
}
|
|
34004
34159
|
if (opts.dryRun) {
|
|
34005
34160
|
const dryRunResult = timedStepResult(
|
|
@@ -34355,8 +34510,8 @@ function registerSetup(program3) {
|
|
|
34355
34510
|
}
|
|
34356
34511
|
try {
|
|
34357
34512
|
const manifest = await agentExport(resolvedAgentId, { apiUrlOverride: opts.apiUrl });
|
|
34358
|
-
const { writeFileSync:
|
|
34359
|
-
|
|
34513
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
34514
|
+
writeFileSync6(
|
|
34360
34515
|
"tenant.yaml",
|
|
34361
34516
|
`# tenant.yaml - Front Of House agent manifest
|
|
34362
34517
|
# Edit this file and run: foh plan tenant.yaml
|
|
@@ -34496,8 +34651,8 @@ function registerSim(program3) {
|
|
|
34496
34651
|
}
|
|
34497
34652
|
const cert = response.certificate;
|
|
34498
34653
|
if (opts.out) {
|
|
34499
|
-
const { writeFileSync:
|
|
34500
|
-
|
|
34654
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
34655
|
+
writeFileSync6(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
|
|
34501
34656
|
process.stderr.write(` Certificate written to ${opts.out}
|
|
34502
34657
|
`);
|
|
34503
34658
|
}
|
|
@@ -34547,8 +34702,8 @@ function registerSim(program3) {
|
|
|
34547
34702
|
});
|
|
34548
34703
|
}
|
|
34549
34704
|
if (opts.out) {
|
|
34550
|
-
const { writeFileSync:
|
|
34551
|
-
|
|
34705
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
34706
|
+
writeFileSync6(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
|
|
34552
34707
|
process.stderr.write(` Final certificate written to ${opts.out}
|
|
34553
34708
|
`);
|
|
34554
34709
|
}
|
|
@@ -34585,19 +34740,6 @@ ${passIcon} Certification loop summary
|
|
|
34585
34740
|
|
|
34586
34741
|
// src/commands/conversations.ts
|
|
34587
34742
|
var import_crypto4 = require("crypto");
|
|
34588
|
-
|
|
34589
|
-
// src/lib/query-options.ts
|
|
34590
|
-
function parsePositiveInt(value, fallback, min, max) {
|
|
34591
|
-
const parsed = Number(value ?? fallback);
|
|
34592
|
-
if (!Number.isFinite(parsed)) return fallback;
|
|
34593
|
-
return Math.max(min, Math.min(max, Math.trunc(parsed)));
|
|
34594
|
-
}
|
|
34595
|
-
function withQuery(path2, params) {
|
|
34596
|
-
const query = params.toString();
|
|
34597
|
-
return query ? `${path2}?${query}` : path2;
|
|
34598
|
-
}
|
|
34599
|
-
|
|
34600
|
-
// src/commands/conversations.ts
|
|
34601
34743
|
function registerConversations(program3) {
|
|
34602
34744
|
const conversations = program3.command("conversations").description("Search and operate on conversation traces and lead data");
|
|
34603
34745
|
conversations.command("list").description("List/search conversations for an agent").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("--terminal-state <value>", "Terminal state filter").option("--journey <value>", "Journey filter").option("--has-tool-call <value>", "Tool outcome/tool id filter").option("--scope <value>", "Scope: agent or org").option("--scope-agent-id <id>", "Optional scoped agent id when --scope org").option("--guardrail-triggered", "Only conversations with guardrail_triggered trace events").option("--provider <value>", "Provider filter from trace payload").option("--page <n>", "Page number", "1").option("--limit <n>", "Page size (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 () => {
|
|
@@ -34759,6 +34901,140 @@ function registerConversations(program3) {
|
|
|
34759
34901
|
}));
|
|
34760
34902
|
}
|
|
34761
34903
|
|
|
34904
|
+
// src/commands/transcripts.ts
|
|
34905
|
+
var import_fs5 = require("fs");
|
|
34906
|
+
var import_path4 = require("path");
|
|
34907
|
+
function listPath(agentId, opts) {
|
|
34908
|
+
const params = new URLSearchParams();
|
|
34909
|
+
if (opts.q) params.set("q", String(opts.q));
|
|
34910
|
+
if (opts.from) params.set("from", String(opts.from));
|
|
34911
|
+
if (opts.to) params.set("to", String(opts.to));
|
|
34912
|
+
params.set("page", String(parsePositiveInt(opts.page, 1, 1, 1e4)));
|
|
34913
|
+
params.set("limit", String(parsePositiveInt(opts.limit, 20, 1, 100)));
|
|
34914
|
+
return withQuery(`/v1/console/agents/${agentId}/conversations`, params);
|
|
34915
|
+
}
|
|
34916
|
+
function jsonl(rows) {
|
|
34917
|
+
return rows.map((row) => JSON.stringify(row)).join("\n") + (rows.length > 0 ? "\n" : "");
|
|
34918
|
+
}
|
|
34919
|
+
function registerTranscripts(program3) {
|
|
34920
|
+
const transcripts = program3.command("transcripts").description("List, fetch, and export conversation transcripts");
|
|
34921
|
+
transcripts.command("list").description("List transcript-bearing conversations for an agent").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("--page <n>", "Page number", "1").option("--limit <n>", "Page size (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 () => {
|
|
34922
|
+
const data = await apiFetch(listPath(opts.agent, opts), { orgId: opts.org, apiUrlOverride: opts.apiUrl });
|
|
34923
|
+
format(data, { json: opts.json ?? false });
|
|
34924
|
+
}));
|
|
34925
|
+
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 () => {
|
|
34926
|
+
const params = new URLSearchParams();
|
|
34927
|
+
if (opts.includeTraces) params.set("include_traces", "true");
|
|
34928
|
+
const path2 = withQuery(`/v1/console/agents/${opts.agent}/conversations/${opts.conversation}`, params);
|
|
34929
|
+
const data = await apiFetch(path2, { orgId: opts.org, apiUrlOverride: opts.apiUrl });
|
|
34930
|
+
format(data, { json: opts.json ?? false });
|
|
34931
|
+
}));
|
|
34932
|
+
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("--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 () => {
|
|
34933
|
+
const exportFormat = String(opts.format || "jsonl").trim().toLowerCase();
|
|
34934
|
+
if (!["jsonl", "json"].includes(exportFormat)) {
|
|
34935
|
+
throw new FohError({
|
|
34936
|
+
step: "transcripts.export",
|
|
34937
|
+
error: `Invalid format: ${opts.format}`,
|
|
34938
|
+
remediation: "Use --format jsonl or --format json.",
|
|
34939
|
+
statusCode: 400
|
|
34940
|
+
});
|
|
34941
|
+
}
|
|
34942
|
+
const data = await apiFetch(listPath(opts.agent, { ...opts, page: "1" }), {
|
|
34943
|
+
orgId: opts.org,
|
|
34944
|
+
apiUrlOverride: opts.apiUrl
|
|
34945
|
+
});
|
|
34946
|
+
const rows = Array.isArray(data.conversations) ? data.conversations : [];
|
|
34947
|
+
const content = exportFormat === "json" ? stableStringify({ schema_version: "foh_transcript_export.v1", conversations: rows }) : jsonl(rows);
|
|
34948
|
+
if (opts.out) {
|
|
34949
|
+
const outputPath = (0, import_path4.resolve)(String(opts.out));
|
|
34950
|
+
(0, import_fs5.writeFileSync)(outputPath, content, "utf-8");
|
|
34951
|
+
format({ status: "exported", format: exportFormat, count: rows.length, output_path: outputPath }, { json: opts.json ?? false });
|
|
34952
|
+
return;
|
|
34953
|
+
}
|
|
34954
|
+
if (opts.json || exportFormat === "json") {
|
|
34955
|
+
format({ schema_version: "foh_transcript_export.v1", conversations: rows }, { json: opts.json ?? false });
|
|
34956
|
+
return;
|
|
34957
|
+
}
|
|
34958
|
+
process.stdout.write(content);
|
|
34959
|
+
}));
|
|
34960
|
+
}
|
|
34961
|
+
|
|
34962
|
+
// src/commands/analytics.ts
|
|
34963
|
+
function parsePreset(raw) {
|
|
34964
|
+
const value = String(raw || "7d").trim().toLowerCase();
|
|
34965
|
+
if (value === "today" || value === "7d" || value === "failed" || value === "lead-capture") return value;
|
|
34966
|
+
throw new FohError({
|
|
34967
|
+
step: "analytics.fetch",
|
|
34968
|
+
error: `Invalid preset: ${raw}`,
|
|
34969
|
+
remediation: "Use --preset today, 7d, failed, or lead-capture.",
|
|
34970
|
+
statusCode: 400
|
|
34971
|
+
});
|
|
34972
|
+
}
|
|
34973
|
+
function presetWindowDays(preset, rawWindowDays) {
|
|
34974
|
+
if (rawWindowDays !== void 0) return parsePositiveInt(String(rawWindowDays), preset === "today" ? 1 : 7, 1, 90);
|
|
34975
|
+
if (preset === "today") return 1;
|
|
34976
|
+
return 7;
|
|
34977
|
+
}
|
|
34978
|
+
function registerAnalytics(program3) {
|
|
34979
|
+
const analytics = program3.command("analytics").description("Fetch runtime analytics and inspection summaries");
|
|
34980
|
+
analytics.command("fetch").description("Fetch an agent analytics summary from existing reporting lanes").requiredOption("--agent <id>", "Agent ID").option("--preset <value>", "Preset: today, 7d, failed, lead-capture", "7d").option("--window-days <n>", "Lookback window override (1-90)").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 () => {
|
|
34981
|
+
const preset = parsePreset(opts.preset);
|
|
34982
|
+
const windowDays = presetWindowDays(preset, opts.windowDays);
|
|
34983
|
+
const qualityParams = new URLSearchParams({ windowDays: String(windowDays), environment: "production" });
|
|
34984
|
+
const conversationParams = new URLSearchParams({
|
|
34985
|
+
page: "1",
|
|
34986
|
+
limit: "10"
|
|
34987
|
+
});
|
|
34988
|
+
if (preset === "failed") conversationParams.set("terminal_state", "failed_visible");
|
|
34989
|
+
if (preset === "lead-capture") conversationParams.set("journey", "lead_capture");
|
|
34990
|
+
const [qualityScorecard, leadDataTrends, loopKpis, conversations, voiceSlo] = await Promise.all([
|
|
34991
|
+
apiFetch(`/v1/console/agents/${opts.agent}/quality-scorecard?${qualityParams.toString()}`, {
|
|
34992
|
+
orgId: opts.org,
|
|
34993
|
+
apiUrlOverride: opts.apiUrl
|
|
34994
|
+
}),
|
|
34995
|
+
apiFetch(`/v1/console/agents/${opts.agent}/lead-data-trends?windowDays=${windowDays}`, {
|
|
34996
|
+
orgId: opts.org,
|
|
34997
|
+
apiUrlOverride: opts.apiUrl
|
|
34998
|
+
}),
|
|
34999
|
+
apiFetch(`/v1/console/agents/${opts.agent}/loop-kpis?windowDays=${windowDays}`, {
|
|
35000
|
+
orgId: opts.org,
|
|
35001
|
+
apiUrlOverride: opts.apiUrl
|
|
35002
|
+
}),
|
|
35003
|
+
apiFetch(withQuery(`/v1/console/agents/${opts.agent}/conversations`, conversationParams), {
|
|
35004
|
+
orgId: opts.org,
|
|
35005
|
+
apiUrlOverride: opts.apiUrl
|
|
35006
|
+
}),
|
|
35007
|
+
apiFetch(`/v1/console/voice-slo?agentId=${opts.agent}&days=${windowDays}`, {
|
|
35008
|
+
orgId: opts.org,
|
|
35009
|
+
apiUrlOverride: opts.apiUrl
|
|
35010
|
+
}).catch((error2) => ({
|
|
35011
|
+
ok: false,
|
|
35012
|
+
skipped: true,
|
|
35013
|
+
reason_code: "voice_slo_unavailable",
|
|
35014
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
35015
|
+
}))
|
|
35016
|
+
]);
|
|
35017
|
+
format({
|
|
35018
|
+
schema_version: "foh_analytics_fetch.v1",
|
|
35019
|
+
ok: true,
|
|
35020
|
+
agent_id: opts.agent,
|
|
35021
|
+
preset,
|
|
35022
|
+
window_days: windowDays,
|
|
35023
|
+
summaries: {
|
|
35024
|
+
quality_scorecard: qualityScorecard,
|
|
35025
|
+
lead_data_trends: leadDataTrends,
|
|
35026
|
+
loop_kpis: loopKpis,
|
|
35027
|
+
conversations,
|
|
35028
|
+
voice_slo: voiceSlo
|
|
35029
|
+
},
|
|
35030
|
+
next_commands: [
|
|
35031
|
+
`foh transcripts list --agent ${opts.agent} --limit 10 --json`,
|
|
35032
|
+
`foh ops reporting weekly-report --agent ${opts.agent} --window-days ${Math.min(30, windowDays)} --json`
|
|
35033
|
+
]
|
|
35034
|
+
}, { json: opts.json ?? false });
|
|
35035
|
+
}));
|
|
35036
|
+
}
|
|
35037
|
+
|
|
34762
35038
|
// src/commands/tests.ts
|
|
34763
35039
|
function registerTests(program3) {
|
|
34764
35040
|
const tests = program3.command("tests").description("Manage agent test catalog and test-run lifecycle");
|
|
@@ -34943,6 +35219,164 @@ function registerTests(program3) {
|
|
|
34943
35219
|
}));
|
|
34944
35220
|
}
|
|
34945
35221
|
|
|
35222
|
+
// src/commands/test.ts
|
|
35223
|
+
var import_fs6 = require("fs");
|
|
35224
|
+
var import_path5 = require("path");
|
|
35225
|
+
function asStringList(value) {
|
|
35226
|
+
if (typeof value === "string" && value.trim()) return [value.trim()];
|
|
35227
|
+
if (Array.isArray(value)) return value.map((entry) => String(entry || "").trim()).filter(Boolean);
|
|
35228
|
+
return [];
|
|
35229
|
+
}
|
|
35230
|
+
function parseSuiteFile(path2) {
|
|
35231
|
+
const raw = (0, import_fs6.readFileSync)(path2, "utf-8");
|
|
35232
|
+
const parsed = path2.toLowerCase().endsWith(".json") ? JSON.parse(raw) : load(raw);
|
|
35233
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
35234
|
+
throw new FohError({
|
|
35235
|
+
step: "test.run",
|
|
35236
|
+
error: "Suite file must contain an object",
|
|
35237
|
+
remediation: "Use a JSON/YAML object with scenarios[].turns[].",
|
|
35238
|
+
statusCode: 400
|
|
35239
|
+
});
|
|
35240
|
+
}
|
|
35241
|
+
return parsed;
|
|
35242
|
+
}
|
|
35243
|
+
function validateSuite(suite) {
|
|
35244
|
+
const scenarios = Array.isArray(suite.scenarios) ? suite.scenarios : [];
|
|
35245
|
+
if (scenarios.length === 0) {
|
|
35246
|
+
throw new FohError({
|
|
35247
|
+
step: "test.run",
|
|
35248
|
+
error: "Suite contains no scenarios",
|
|
35249
|
+
remediation: "Add at least one scenarios[] entry with turns[].",
|
|
35250
|
+
statusCode: 400
|
|
35251
|
+
});
|
|
35252
|
+
}
|
|
35253
|
+
for (const scenario of scenarios) {
|
|
35254
|
+
if (!Array.isArray(scenario.turns) || scenario.turns.length === 0) {
|
|
35255
|
+
throw new FohError({
|
|
35256
|
+
step: "test.run",
|
|
35257
|
+
error: `Scenario "${scenario.id || scenario.name || "(unnamed)"}" has no turns`,
|
|
35258
|
+
remediation: "Add turns with user/message and expect.contains assertions.",
|
|
35259
|
+
statusCode: 400
|
|
35260
|
+
});
|
|
35261
|
+
}
|
|
35262
|
+
}
|
|
35263
|
+
return scenarios;
|
|
35264
|
+
}
|
|
35265
|
+
function evaluateReply(reply, expect) {
|
|
35266
|
+
const lowerReply = reply.toLowerCase();
|
|
35267
|
+
const contains = asStringList(expect?.contains);
|
|
35268
|
+
const notContains = asStringList(expect?.not_contains);
|
|
35269
|
+
const failures = [];
|
|
35270
|
+
for (const expected of contains) {
|
|
35271
|
+
if (!lowerReply.includes(expected.toLowerCase())) {
|
|
35272
|
+
failures.push(`missing expected text: ${expected}`);
|
|
35273
|
+
}
|
|
35274
|
+
}
|
|
35275
|
+
for (const forbidden of notContains) {
|
|
35276
|
+
if (lowerReply.includes(forbidden.toLowerCase())) {
|
|
35277
|
+
failures.push(`contained forbidden text: ${forbidden}`);
|
|
35278
|
+
}
|
|
35279
|
+
}
|
|
35280
|
+
return failures;
|
|
35281
|
+
}
|
|
35282
|
+
function registerTest(program3) {
|
|
35283
|
+
const test = program3.command("test").description("Run local scenario suites against runtime channels");
|
|
35284
|
+
test.command("run").description("Run a local YAML/JSON scenario suite").requiredOption("--suite <path>", "Suite YAML/JSON path").option("--agent <id>", "Agent ID (defaults to suite.agent)").option("--out <path>", "Write report JSON to 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 () => {
|
|
35285
|
+
const suitePath = (0, import_path5.resolve)(String(opts.suite));
|
|
35286
|
+
const suite = parseSuiteFile(suitePath);
|
|
35287
|
+
const agentId = String(opts.agent || suite.agent || "").trim();
|
|
35288
|
+
if (!agentId) {
|
|
35289
|
+
throw new FohError({
|
|
35290
|
+
step: "test.run",
|
|
35291
|
+
error: "Missing agent ID",
|
|
35292
|
+
remediation: "Pass --agent <id> or include agent in the suite file.",
|
|
35293
|
+
statusCode: 400
|
|
35294
|
+
});
|
|
35295
|
+
}
|
|
35296
|
+
const scenarios = validateSuite(suite);
|
|
35297
|
+
const ensure = await apiFetch("/v1/console/channels/widget/ensure", {
|
|
35298
|
+
method: "POST",
|
|
35299
|
+
body: JSON.stringify({ agentId }),
|
|
35300
|
+
orgId: opts.org,
|
|
35301
|
+
apiUrlOverride: opts.apiUrl
|
|
35302
|
+
});
|
|
35303
|
+
const publicKey = ensure.channel?.public_key;
|
|
35304
|
+
if (!publicKey) {
|
|
35305
|
+
throw new FohError({
|
|
35306
|
+
step: "test.run",
|
|
35307
|
+
error: "Widget channel public key missing",
|
|
35308
|
+
remediation: `Run: foh widget ensure --agent ${agentId} --json`,
|
|
35309
|
+
statusCode: 409
|
|
35310
|
+
});
|
|
35311
|
+
}
|
|
35312
|
+
let passed = 0;
|
|
35313
|
+
let failed = 0;
|
|
35314
|
+
const scenarioResults = [];
|
|
35315
|
+
for (let scenarioIndex = 0; scenarioIndex < scenarios.length; scenarioIndex += 1) {
|
|
35316
|
+
const scenario = scenarios[scenarioIndex];
|
|
35317
|
+
let conversationId;
|
|
35318
|
+
const turnResults = [];
|
|
35319
|
+
for (let turnIndex = 0; turnIndex < (scenario.turns || []).length; turnIndex += 1) {
|
|
35320
|
+
const turn = scenario.turns[turnIndex];
|
|
35321
|
+
const message = String(turn.user || turn.message || "").trim();
|
|
35322
|
+
if (!message) {
|
|
35323
|
+
failed += 1;
|
|
35324
|
+
turnResults.push({ turn: turnIndex + 1, ok: false, failures: ["missing user/message"] });
|
|
35325
|
+
continue;
|
|
35326
|
+
}
|
|
35327
|
+
const response = await apiFetch("/v1/widget/inbound", {
|
|
35328
|
+
method: "POST",
|
|
35329
|
+
body: JSON.stringify({
|
|
35330
|
+
channel_public_key: publicKey,
|
|
35331
|
+
message_body: message,
|
|
35332
|
+
preview: true,
|
|
35333
|
+
...conversationId ? { conversation_id: conversationId } : {}
|
|
35334
|
+
}),
|
|
35335
|
+
apiUrlOverride: opts.apiUrl
|
|
35336
|
+
});
|
|
35337
|
+
conversationId = response.conversationId || conversationId;
|
|
35338
|
+
const reply = String(response.reply || "");
|
|
35339
|
+
const failures = reply ? evaluateReply(reply, turn.expect) : ["empty reply"];
|
|
35340
|
+
if (failures.length === 0) passed += 1;
|
|
35341
|
+
else failed += 1;
|
|
35342
|
+
turnResults.push({
|
|
35343
|
+
turn: turnIndex + 1,
|
|
35344
|
+
ok: failures.length === 0,
|
|
35345
|
+
message,
|
|
35346
|
+
reply,
|
|
35347
|
+
failures,
|
|
35348
|
+
conversation_id: response.conversationId ?? null,
|
|
35349
|
+
trace_id: response.trace_id ?? null,
|
|
35350
|
+
correlation_id: response.correlation_id ?? null
|
|
35351
|
+
});
|
|
35352
|
+
}
|
|
35353
|
+
scenarioResults.push({
|
|
35354
|
+
id: scenario.id || `scenario-${scenarioIndex + 1}`,
|
|
35355
|
+
name: scenario.name ?? null,
|
|
35356
|
+
ok: turnResults.every((turn) => turn.ok),
|
|
35357
|
+
turns: turnResults
|
|
35358
|
+
});
|
|
35359
|
+
}
|
|
35360
|
+
const report = {
|
|
35361
|
+
schema_version: "foh_local_scenario_suite_report.v1",
|
|
35362
|
+
suite_path: suitePath,
|
|
35363
|
+
agent_id: agentId,
|
|
35364
|
+
ok: failed === 0,
|
|
35365
|
+
passed,
|
|
35366
|
+
failed,
|
|
35367
|
+
scenarios: scenarioResults
|
|
35368
|
+
};
|
|
35369
|
+
if (opts.out) {
|
|
35370
|
+
const out = (0, import_path5.resolve)(String(opts.out));
|
|
35371
|
+
(0, import_fs6.writeFileSync)(out, stableStringify(report), "utf-8");
|
|
35372
|
+
format({ ...report, output_path: out }, { json: opts.json ?? false });
|
|
35373
|
+
} else {
|
|
35374
|
+
format(report, { json: opts.json ?? false });
|
|
35375
|
+
}
|
|
35376
|
+
if (failed > 0) markCommandFailed(1);
|
|
35377
|
+
}));
|
|
35378
|
+
}
|
|
35379
|
+
|
|
34946
35380
|
// src/commands/ops.ts
|
|
34947
35381
|
function parseClientErrorSource(raw) {
|
|
34948
35382
|
if (!raw) return void 0;
|
|
@@ -35357,8 +35791,8 @@ function registerDiag(program3) {
|
|
|
35357
35791
|
}
|
|
35358
35792
|
|
|
35359
35793
|
// src/commands/bug.ts
|
|
35360
|
-
var
|
|
35361
|
-
var
|
|
35794
|
+
var import_fs7 = require("fs");
|
|
35795
|
+
var import_path6 = require("path");
|
|
35362
35796
|
var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
35363
35797
|
var MAX_BODY_PREVIEW_LENGTH = 200;
|
|
35364
35798
|
function parseMethod(raw) {
|
|
@@ -35474,14 +35908,14 @@ function parseRequestBody(raw) {
|
|
|
35474
35908
|
}
|
|
35475
35909
|
}
|
|
35476
35910
|
function writeJsonArtifact(path2, value) {
|
|
35477
|
-
const absolutePath = (0,
|
|
35478
|
-
(0,
|
|
35479
|
-
(0,
|
|
35911
|
+
const absolutePath = (0, import_path6.resolve)(path2);
|
|
35912
|
+
(0, import_fs7.mkdirSync)((0, import_path6.dirname)(absolutePath), { recursive: true });
|
|
35913
|
+
(0, import_fs7.writeFileSync)(absolutePath, stableStringify(value), "utf-8");
|
|
35480
35914
|
return absolutePath;
|
|
35481
35915
|
}
|
|
35482
35916
|
function defaultArtifactPath() {
|
|
35483
35917
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
35484
|
-
return (0,
|
|
35918
|
+
return (0, import_path6.resolve)(`test-results/bug-report.${timestamp2}.json`);
|
|
35485
35919
|
}
|
|
35486
35920
|
async function resolveBugReportWizardInputs(opts) {
|
|
35487
35921
|
if (!opts.wizard) return opts;
|
|
@@ -35673,6 +36107,293 @@ function registerBug(program3) {
|
|
|
35673
36107
|
});
|
|
35674
36108
|
}
|
|
35675
36109
|
|
|
36110
|
+
// src/commands/prove.ts
|
|
36111
|
+
function pass(name, summary, detail) {
|
|
36112
|
+
return { name, status: "pass", reason_code: `${name}_ok`, summary, detail };
|
|
36113
|
+
}
|
|
36114
|
+
function hold(name, reasonCode, summary, nextCommand, detail) {
|
|
36115
|
+
return { name, status: "hold", reason_code: reasonCode, summary, next_command: nextCommand, detail };
|
|
36116
|
+
}
|
|
36117
|
+
function fail(name, reasonCode, error2, nextCommand) {
|
|
36118
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
36119
|
+
return { name, status: "fail", reason_code: reasonCode, summary: message, next_command: nextCommand };
|
|
36120
|
+
}
|
|
36121
|
+
function skipped(name, reasonCode, summary, nextCommand) {
|
|
36122
|
+
return { name, status: "skipped", reason_code: reasonCode, summary, next_command: nextCommand };
|
|
36123
|
+
}
|
|
36124
|
+
function hasBlockingChecks(checks) {
|
|
36125
|
+
return checks.some((check2) => check2.status === "hold" || check2.status === "fail");
|
|
36126
|
+
}
|
|
36127
|
+
function publicKeyFromEnsureResponse(response) {
|
|
36128
|
+
const record2 = response && typeof response === "object" ? response : {};
|
|
36129
|
+
const channel = record2.channel && typeof record2.channel === "object" ? record2.channel : {};
|
|
36130
|
+
const publicKey = channel.public_key ?? record2.widget_public_key ?? record2.public_key;
|
|
36131
|
+
return typeof publicKey === "string" && publicKey.trim() ? publicKey.trim() : void 0;
|
|
36132
|
+
}
|
|
36133
|
+
function agentIdFromList(response) {
|
|
36134
|
+
const agents = Array.isArray(response.agents) ? response.agents : [];
|
|
36135
|
+
const usable = agents.filter((agent) => typeof agent.id === "string" && agent.id.trim());
|
|
36136
|
+
if (usable.length === 1) return { agentId: usable[0].id, count: usable.length };
|
|
36137
|
+
if (usable.length === 0) return { count: 0, reason: "no_agents" };
|
|
36138
|
+
return { count: usable.length, reason: "multiple_agents" };
|
|
36139
|
+
}
|
|
36140
|
+
function firstUsableOrgId(response) {
|
|
36141
|
+
const record2 = response && typeof response === "object" ? response : {};
|
|
36142
|
+
const orgs = Array.isArray(record2.orgs) ? record2.orgs : [];
|
|
36143
|
+
const usable = orgs.map((org) => org && typeof org === "object" ? org : {}).map((org) => String(org.org_id ?? org.id ?? "").trim()).filter(Boolean);
|
|
36144
|
+
return { orgId: usable.length === 1 ? usable[0] : void 0, count: usable.length };
|
|
36145
|
+
}
|
|
36146
|
+
function registerProve(program3) {
|
|
36147
|
+
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("--cert-mode <m>", "Simulation cert mode: quick, full, stress", "quick").option("--cert-adaptive-runs <n>", "Adaptive runs for full/stress certification", "30").option("--cert-max-improvement-rounds <n>", "Max prompt improvement rounds in cert loop (0-5)", "1").option("--require-phone", "Hold proof if no phone/contact number is provisioned").option("--skip-cert", "Skip simulation certification check").option("--skip-smoke", "Skip widget runtime smoke check").option("--skip-voice-health", "Skip realtime voice provider health check").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 () => {
|
|
36148
|
+
const checks = [];
|
|
36149
|
+
const ctx = {
|
|
36150
|
+
tokenPresent: false,
|
|
36151
|
+
traceIds: [],
|
|
36152
|
+
correlationIds: []
|
|
36153
|
+
};
|
|
36154
|
+
try {
|
|
36155
|
+
const creds = loadCredentials(opts.apiUrl);
|
|
36156
|
+
ctx.apiUrl = creds.apiUrl;
|
|
36157
|
+
ctx.tokenPresent = Boolean(creds.token);
|
|
36158
|
+
ctx.orgId = opts.org || creds.orgId;
|
|
36159
|
+
checks.push(pass("auth", "CLI credentials are present and not expired.", {
|
|
36160
|
+
api_url: creds.apiUrl,
|
|
36161
|
+
org_id_from_credentials: creds.orgId ?? null
|
|
36162
|
+
}));
|
|
36163
|
+
} catch (error2) {
|
|
36164
|
+
checks.push(hold("auth", "auth_missing_or_expired", "CLI is not authenticated.", "foh auth login --web", {
|
|
36165
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
36166
|
+
}));
|
|
36167
|
+
}
|
|
36168
|
+
if (ctx.tokenPresent && !ctx.orgId) {
|
|
36169
|
+
try {
|
|
36170
|
+
const orgs = await apiFetch("/v1/console/auth/my-orgs", { apiUrlOverride: opts.apiUrl });
|
|
36171
|
+
const resolved = firstUsableOrgId(orgs);
|
|
36172
|
+
if (resolved.orgId) {
|
|
36173
|
+
ctx.orgId = resolved.orgId;
|
|
36174
|
+
checks.push(pass("org", "Resolved the only available org.", { org_id: resolved.orgId }));
|
|
36175
|
+
} else {
|
|
36176
|
+
checks.push(hold(
|
|
36177
|
+
"org",
|
|
36178
|
+
resolved.count === 0 ? "org_missing" : "org_ambiguous",
|
|
36179
|
+
resolved.count === 0 ? "No usable org was found for this account." : `Found ${resolved.count} orgs; choose one explicitly.`,
|
|
36180
|
+
"foh org list --json && foh org use --org <org-id>",
|
|
36181
|
+
{ org_count: resolved.count }
|
|
36182
|
+
));
|
|
36183
|
+
}
|
|
36184
|
+
} catch (error2) {
|
|
36185
|
+
checks.push(fail("org", "org_resolution_failed", error2, "foh org list --json"));
|
|
36186
|
+
}
|
|
36187
|
+
} else if (ctx.orgId) {
|
|
36188
|
+
checks.push(pass("org", "Org context is selected.", { org_id: ctx.orgId }));
|
|
36189
|
+
} else {
|
|
36190
|
+
checks.push(skipped("org", "auth_required", "Skipped until authentication is fixed.", "foh auth login --web"));
|
|
36191
|
+
}
|
|
36192
|
+
if (ctx.orgId) {
|
|
36193
|
+
if (opts.agent) {
|
|
36194
|
+
ctx.agentId = String(opts.agent);
|
|
36195
|
+
checks.push(pass("agent_selection", "Using explicitly supplied agent.", { agent_id: ctx.agentId }));
|
|
36196
|
+
} else {
|
|
36197
|
+
try {
|
|
36198
|
+
const list = await apiFetch("/v1/console/agents", {
|
|
36199
|
+
orgId: ctx.orgId,
|
|
36200
|
+
apiUrlOverride: opts.apiUrl
|
|
36201
|
+
});
|
|
36202
|
+
const resolved = agentIdFromList(list);
|
|
36203
|
+
if (resolved.agentId) {
|
|
36204
|
+
ctx.agentId = resolved.agentId;
|
|
36205
|
+
checks.push(pass("agent_selection", "Resolved the only available agent.", { agent_id: resolved.agentId }));
|
|
36206
|
+
} else {
|
|
36207
|
+
checks.push(hold(
|
|
36208
|
+
"agent_selection",
|
|
36209
|
+
resolved.reason === "no_agents" ? "agent_missing" : "agent_ambiguous",
|
|
36210
|
+
resolved.reason === "no_agents" ? "No agent exists in this org." : `Found ${resolved.count} agents; choose one explicitly.`,
|
|
36211
|
+
resolved.reason === "no_agents" ? "foh setup --json" : "foh agent list --json && foh prove --agent <agent-id> --json",
|
|
36212
|
+
{ agent_count: resolved.count }
|
|
36213
|
+
));
|
|
36214
|
+
}
|
|
36215
|
+
} catch (error2) {
|
|
36216
|
+
checks.push(fail("agent_selection", "agent_selection_failed", error2, "foh agent list --json"));
|
|
36217
|
+
}
|
|
36218
|
+
}
|
|
36219
|
+
} else {
|
|
36220
|
+
checks.push(skipped("agent_selection", "org_required", "Skipped until org context is fixed.", "foh org use --org <org-id>"));
|
|
36221
|
+
}
|
|
36222
|
+
if (ctx.agentId) {
|
|
36223
|
+
try {
|
|
36224
|
+
const validation = await apiFetch(`/v1/console/agents/${ctx.agentId}/validate`, {
|
|
36225
|
+
method: "POST",
|
|
36226
|
+
orgId: ctx.orgId,
|
|
36227
|
+
apiUrlOverride: opts.apiUrl
|
|
36228
|
+
});
|
|
36229
|
+
const issues = Array.isArray(validation.issues) ? validation.issues : [];
|
|
36230
|
+
if (validation.ok === false || issues.length > 0) {
|
|
36231
|
+
checks.push(hold("agent_validation", "agent_validation_issues", `Agent validation returned ${issues.length} issue(s).`, `foh agent validate --agent ${ctx.agentId} --json`, validation));
|
|
36232
|
+
} else {
|
|
36233
|
+
checks.push(pass("agent_validation", "Agent validation passed.", validation));
|
|
36234
|
+
}
|
|
36235
|
+
} catch (error2) {
|
|
36236
|
+
checks.push(fail("agent_validation", "agent_validation_failed", error2, `foh agent validate --agent ${ctx.agentId} --json`));
|
|
36237
|
+
}
|
|
36238
|
+
if (ctx.orgId) {
|
|
36239
|
+
try {
|
|
36240
|
+
const onboarding = await apiFetch(`/v1/console/org/${ctx.orgId}/onboarding`, {
|
|
36241
|
+
orgId: ctx.orgId,
|
|
36242
|
+
apiUrlOverride: opts.apiUrl
|
|
36243
|
+
});
|
|
36244
|
+
const phoneNumber = typeof onboarding.phone_number === "string" && onboarding.phone_number.trim() ? onboarding.phone_number.trim() : null;
|
|
36245
|
+
if (phoneNumber) {
|
|
36246
|
+
checks.push(pass("contact_channel", "Contact phone number is provisioned.", {
|
|
36247
|
+
phone_number_present: true,
|
|
36248
|
+
provisioning_status: onboarding.provisioning_status ?? null
|
|
36249
|
+
}));
|
|
36250
|
+
} else if (opts.requirePhone) {
|
|
36251
|
+
checks.push(hold("contact_channel", "contact_phone_missing", "No phone/contact number is provisioned for this org.", `foh provision buy --org ${ctx.orgId} --json`, {
|
|
36252
|
+
provisioning_status: onboarding.provisioning_status ?? null
|
|
36253
|
+
}));
|
|
36254
|
+
} else {
|
|
36255
|
+
checks.push(skipped("contact_channel", "contact_phone_not_required", "No phone/contact number is provisioned; pass --require-phone to make this a blocker.", `foh provision buy --org ${ctx.orgId} --json`));
|
|
36256
|
+
}
|
|
36257
|
+
} catch (error2) {
|
|
36258
|
+
checks.push(fail("contact_channel", "contact_channel_check_failed", error2, `foh provision status --org ${ctx.orgId} --json`));
|
|
36259
|
+
}
|
|
36260
|
+
}
|
|
36261
|
+
if (opts.skipVoiceHealth) {
|
|
36262
|
+
checks.push(skipped("voice_realtime_health", "operator_skipped", "Skipped by --skip-voice-health.", "foh voice realtime-health --json"));
|
|
36263
|
+
} else {
|
|
36264
|
+
try {
|
|
36265
|
+
const health = await apiFetch(
|
|
36266
|
+
"/v1/console/realtime/health",
|
|
36267
|
+
{ apiUrlOverride: opts.apiUrl }
|
|
36268
|
+
);
|
|
36269
|
+
const providers = Array.isArray(health.providers) ? health.providers : [];
|
|
36270
|
+
if (providers.length === 0) {
|
|
36271
|
+
checks.push(skipped("voice_realtime_health", "voice_health_no_providers", "Realtime voice health returned no providers.", "foh voice realtime-health --json"));
|
|
36272
|
+
} else if (providers.every((provider) => provider?.ready === true)) {
|
|
36273
|
+
checks.push(pass("voice_realtime_health", "Realtime voice providers are ready.", {
|
|
36274
|
+
provider_count: providers.length
|
|
36275
|
+
}));
|
|
36276
|
+
} else {
|
|
36277
|
+
checks.push(hold("voice_realtime_health", "voice_realtime_provider_not_ready", "One or more realtime voice providers are not ready.", "foh voice realtime-health --json", {
|
|
36278
|
+
providers
|
|
36279
|
+
}));
|
|
36280
|
+
}
|
|
36281
|
+
} catch (error2) {
|
|
36282
|
+
checks.push(fail("voice_realtime_health", "voice_realtime_health_failed", error2, "foh voice realtime-health --json"));
|
|
36283
|
+
}
|
|
36284
|
+
}
|
|
36285
|
+
try {
|
|
36286
|
+
const ensure = await apiFetch("/v1/console/channels/widget/ensure", {
|
|
36287
|
+
method: "POST",
|
|
36288
|
+
body: JSON.stringify({ agentId: ctx.agentId }),
|
|
36289
|
+
orgId: ctx.orgId,
|
|
36290
|
+
apiUrlOverride: opts.apiUrl
|
|
36291
|
+
});
|
|
36292
|
+
const publicKey = publicKeyFromEnsureResponse(ensure);
|
|
36293
|
+
if (!publicKey) {
|
|
36294
|
+
checks.push(hold("widget_channel", "widget_public_key_missing", "Widget channel exists but no public key was returned.", `foh widget ensure --agent ${ctx.agentId} --json`, ensure));
|
|
36295
|
+
} else {
|
|
36296
|
+
ctx.widgetPublicKey = publicKey;
|
|
36297
|
+
checks.push(pass("widget_channel", "Widget channel is available.", { public_key_present: true }));
|
|
36298
|
+
}
|
|
36299
|
+
} catch (error2) {
|
|
36300
|
+
checks.push(fail("widget_channel", "widget_channel_failed", error2, `foh widget ensure --agent ${ctx.agentId} --json`));
|
|
36301
|
+
}
|
|
36302
|
+
try {
|
|
36303
|
+
const embed = await apiFetch("/v1/console/channels/widget/embed-snippet", {
|
|
36304
|
+
orgId: ctx.orgId,
|
|
36305
|
+
apiUrlOverride: opts.apiUrl,
|
|
36306
|
+
headers: { "x-agent-id": ctx.agentId }
|
|
36307
|
+
});
|
|
36308
|
+
if (typeof embed.snippet === "string" && embed.snippet.trim()) {
|
|
36309
|
+
checks.push(pass("widget_embed", "Widget embed snippet is available.", { snippet_present: true }));
|
|
36310
|
+
} else {
|
|
36311
|
+
checks.push(hold("widget_embed", "widget_embed_missing", "Widget embed snippet is missing.", `foh widget embed-snippet --agent ${ctx.agentId}`));
|
|
36312
|
+
}
|
|
36313
|
+
} catch (error2) {
|
|
36314
|
+
checks.push(fail("widget_embed", "widget_embed_failed", error2, `foh widget embed-snippet --agent ${ctx.agentId}`));
|
|
36315
|
+
}
|
|
36316
|
+
if (opts.skipSmoke) {
|
|
36317
|
+
checks.push(skipped("widget_smoke", "operator_skipped", "Skipped by --skip-smoke.", `foh widget smoke --agent ${ctx.agentId} --json`));
|
|
36318
|
+
} else if (!ctx.widgetPublicKey) {
|
|
36319
|
+
checks.push(skipped("widget_smoke", "widget_public_key_required", "Skipped because widget public key is unavailable.", `foh widget ensure --agent ${ctx.agentId} --json`));
|
|
36320
|
+
} else {
|
|
36321
|
+
try {
|
|
36322
|
+
const smoke = await runWidgetSmoke(ctx.widgetPublicKey, opts.apiUrl);
|
|
36323
|
+
ctx.conversationId = smoke.conversation_id;
|
|
36324
|
+
ctx.traceIds = smoke.trace_ids;
|
|
36325
|
+
ctx.correlationIds = smoke.correlation_ids;
|
|
36326
|
+
if (smoke.failed > 0) {
|
|
36327
|
+
checks.push(hold("widget_smoke", "widget_smoke_failed", `${smoke.failed} widget smoke turn(s) failed.`, `foh widget smoke --agent ${ctx.agentId} --json`, smoke));
|
|
36328
|
+
} else {
|
|
36329
|
+
checks.push(pass("widget_smoke", "Widget runtime smoke passed.", smoke));
|
|
36330
|
+
}
|
|
36331
|
+
} catch (error2) {
|
|
36332
|
+
checks.push(fail("widget_smoke", "widget_smoke_failed", error2, `foh widget smoke --agent ${ctx.agentId} --json`));
|
|
36333
|
+
}
|
|
36334
|
+
}
|
|
36335
|
+
if (opts.skipCert) {
|
|
36336
|
+
checks.push(skipped("simulation_certification", "operator_skipped", "Skipped by --skip-cert.", `foh sim certify-loop --agent ${ctx.agentId} --json`));
|
|
36337
|
+
} else {
|
|
36338
|
+
try {
|
|
36339
|
+
const certMode = normalizeAgentCertMode(opts.certMode);
|
|
36340
|
+
const loop = await runSetupCertifyLoop(ctx.agentId, {
|
|
36341
|
+
mode: certMode,
|
|
36342
|
+
adaptiveRuns: Math.max(1, Number(opts.certAdaptiveRuns ?? 30) || 30),
|
|
36343
|
+
maxImprovementRounds: Math.max(0, Math.min(5, Number(opts.certMaxImprovementRounds ?? 1) || 1)),
|
|
36344
|
+
orgId: ctx.orgId,
|
|
36345
|
+
apiUrlOverride: opts.apiUrl
|
|
36346
|
+
});
|
|
36347
|
+
if (!loop.overall_pass) {
|
|
36348
|
+
checks.push(hold("simulation_certification", "simulation_certification_failed", "Simulation certification did not pass.", `foh sim certify-loop --agent ${ctx.agentId} --${certMode === "quick" ? "full" : certMode} --json`, loop));
|
|
36349
|
+
} else {
|
|
36350
|
+
checks.push(pass("simulation_certification", "Simulation certification passed.", {
|
|
36351
|
+
mode: loop.mode,
|
|
36352
|
+
attempts: loop.attempts?.length ?? 0,
|
|
36353
|
+
improvement_runs: loop.improvement_runs,
|
|
36354
|
+
scenario_summary: loop.certificate?.scenario_summary
|
|
36355
|
+
}));
|
|
36356
|
+
}
|
|
36357
|
+
} catch (error2) {
|
|
36358
|
+
checks.push(fail("simulation_certification", "simulation_certification_failed", error2, `foh sim certify-loop --agent ${ctx.agentId} --json`));
|
|
36359
|
+
}
|
|
36360
|
+
}
|
|
36361
|
+
} else {
|
|
36362
|
+
checks.push(skipped("agent_validation", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
|
|
36363
|
+
checks.push(skipped("contact_channel", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
|
|
36364
|
+
checks.push(skipped("voice_realtime_health", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
|
|
36365
|
+
checks.push(skipped("widget_channel", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
|
|
36366
|
+
checks.push(skipped("widget_embed", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
|
|
36367
|
+
checks.push(skipped("widget_smoke", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
|
|
36368
|
+
checks.push(skipped("simulation_certification", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
|
|
36369
|
+
}
|
|
36370
|
+
const status = hasBlockingChecks(checks) ? "hold" : "pass";
|
|
36371
|
+
const nextCommands = Array.from(new Set(checks.map((check2) => check2.next_command).filter((command) => Boolean(command))));
|
|
36372
|
+
if (status === "pass" && ctx.agentId) {
|
|
36373
|
+
nextCommands.push(`foh agent publish --agent ${ctx.agentId} --json`);
|
|
36374
|
+
}
|
|
36375
|
+
const report = signReport({
|
|
36376
|
+
schema_version: "foh_cli_proof_report.v1",
|
|
36377
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
36378
|
+
ok: status === "pass",
|
|
36379
|
+
status,
|
|
36380
|
+
ids: {
|
|
36381
|
+
org_id: ctx.orgId ?? null,
|
|
36382
|
+
agent_id: ctx.agentId ?? null,
|
|
36383
|
+
widget_public_key_present: Boolean(ctx.widgetPublicKey),
|
|
36384
|
+
conversation_id: ctx.conversationId ?? null,
|
|
36385
|
+
trace_ids: ctx.traceIds,
|
|
36386
|
+
correlation_ids: ctx.correlationIds
|
|
36387
|
+
},
|
|
36388
|
+
checks,
|
|
36389
|
+
next_commands: nextCommands
|
|
36390
|
+
});
|
|
36391
|
+
const artifactPath = opts.out ? writeSignedJsonArtifact(String(opts.out), report) : void 0;
|
|
36392
|
+
format(artifactPath ? { ...report, artifact_path: artifactPath } : report, { json: opts.json ?? false });
|
|
36393
|
+
if (opts.strict && status !== "pass") markCommandFailed(1);
|
|
36394
|
+
}));
|
|
36395
|
+
}
|
|
36396
|
+
|
|
35676
36397
|
// src/tui/command-palette.ts
|
|
35677
36398
|
function tokenizeInput(value) {
|
|
35678
36399
|
const tokens = [];
|
|
@@ -36118,7 +36839,7 @@ async function runSelf(args, apiUrlOverride) {
|
|
|
36118
36839
|
if (apiUrlOverride && !spawnArgs.includes("--api-url")) {
|
|
36119
36840
|
spawnArgs.push("--api-url", apiUrlOverride);
|
|
36120
36841
|
}
|
|
36121
|
-
return await new Promise((
|
|
36842
|
+
return await new Promise((resolve7, reject) => {
|
|
36122
36843
|
const child = (0, import_child_process2.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
|
|
36123
36844
|
stdio: "inherit",
|
|
36124
36845
|
env: {
|
|
@@ -36128,7 +36849,7 @@ async function runSelf(args, apiUrlOverride) {
|
|
|
36128
36849
|
}
|
|
36129
36850
|
});
|
|
36130
36851
|
child.once("error", reject);
|
|
36131
|
-
child.once("close", (code) =>
|
|
36852
|
+
child.once("close", (code) => resolve7(typeof code === "number" ? code : 1));
|
|
36132
36853
|
});
|
|
36133
36854
|
}
|
|
36134
36855
|
function shouldUseInteractiveHome(argv) {
|
|
@@ -36418,8 +37139,8 @@ function maybeDefaultToHome(argv = process.argv) {
|
|
|
36418
37139
|
}
|
|
36419
37140
|
|
|
36420
37141
|
// src/lib/update.ts
|
|
36421
|
-
var
|
|
36422
|
-
var
|
|
37142
|
+
var import_fs8 = require("fs");
|
|
37143
|
+
var import_path7 = require("path");
|
|
36423
37144
|
var import_child_process3 = require("child_process");
|
|
36424
37145
|
var import_crypto5 = require("crypto");
|
|
36425
37146
|
function parseSemver(version2) {
|
|
@@ -36440,7 +37161,7 @@ function compareSemver(a, b) {
|
|
|
36440
37161
|
}
|
|
36441
37162
|
function readPackageJsonVersion(path2) {
|
|
36442
37163
|
try {
|
|
36443
|
-
const raw = (0,
|
|
37164
|
+
const raw = (0, import_fs8.readFileSync)(path2, "utf-8");
|
|
36444
37165
|
const parsed = JSON.parse(raw);
|
|
36445
37166
|
const version2 = String(parsed.version ?? "").trim();
|
|
36446
37167
|
return version2 || void 0;
|
|
@@ -36449,13 +37170,13 @@ function readPackageJsonVersion(path2) {
|
|
|
36449
37170
|
}
|
|
36450
37171
|
}
|
|
36451
37172
|
function findRepoRoot(startCwd = process.cwd()) {
|
|
36452
|
-
let current = (0,
|
|
37173
|
+
let current = (0, import_path7.resolve)(startCwd);
|
|
36453
37174
|
while (true) {
|
|
36454
|
-
const rootPackageJsonPath = (0,
|
|
36455
|
-
const cliPackageJsonPath = (0,
|
|
36456
|
-
if ((0,
|
|
37175
|
+
const rootPackageJsonPath = (0, import_path7.join)(current, "package.json");
|
|
37176
|
+
const cliPackageJsonPath = (0, import_path7.join)(current, "packages", "cli", "package.json");
|
|
37177
|
+
if ((0, import_fs8.existsSync)(rootPackageJsonPath) && (0, import_fs8.existsSync)(cliPackageJsonPath)) {
|
|
36457
37178
|
try {
|
|
36458
|
-
const raw = (0,
|
|
37179
|
+
const raw = (0, import_fs8.readFileSync)(rootPackageJsonPath, "utf-8");
|
|
36459
37180
|
const parsed = JSON.parse(raw);
|
|
36460
37181
|
if (String(parsed.name ?? "").trim() === "front-of-house") {
|
|
36461
37182
|
return current;
|
|
@@ -36463,7 +37184,7 @@ function findRepoRoot(startCwd = process.cwd()) {
|
|
|
36463
37184
|
} catch {
|
|
36464
37185
|
}
|
|
36465
37186
|
}
|
|
36466
|
-
const parent = (0,
|
|
37187
|
+
const parent = (0, import_path7.dirname)(current);
|
|
36467
37188
|
if (parent === current) return void 0;
|
|
36468
37189
|
current = parent;
|
|
36469
37190
|
}
|
|
@@ -36477,7 +37198,7 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
|
|
|
36477
37198
|
remediation: "Run this command from the Front Of House repo root to compare/install the latest CLI."
|
|
36478
37199
|
};
|
|
36479
37200
|
}
|
|
36480
|
-
const cliPackageJsonPath = (0,
|
|
37201
|
+
const cliPackageJsonPath = (0, import_path7.join)(repoRoot, "packages", "cli", "package.json");
|
|
36481
37202
|
const latestVersion = readPackageJsonVersion(cliPackageJsonPath);
|
|
36482
37203
|
if (!latestVersion) {
|
|
36483
37204
|
return {
|
|
@@ -36504,19 +37225,19 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
|
|
|
36504
37225
|
};
|
|
36505
37226
|
}
|
|
36506
37227
|
async function applyRepoUpdate(repoRoot) {
|
|
36507
|
-
const scriptPath = (0,
|
|
37228
|
+
const scriptPath = (0, import_path7.join)(repoRoot, "scripts", "Install-FohCli.ps1");
|
|
36508
37229
|
if (process.platform === "win32") {
|
|
36509
|
-
return await new Promise((
|
|
37230
|
+
return await new Promise((resolve7, reject) => {
|
|
36510
37231
|
const child = (0, import_child_process3.spawn)(
|
|
36511
37232
|
"powershell",
|
|
36512
37233
|
["-ExecutionPolicy", "Bypass", "-File", scriptPath],
|
|
36513
37234
|
{ stdio: "inherit" }
|
|
36514
37235
|
);
|
|
36515
37236
|
child.once("error", reject);
|
|
36516
|
-
child.once("close", (code) =>
|
|
37237
|
+
child.once("close", (code) => resolve7(typeof code === "number" ? code : 1));
|
|
36517
37238
|
});
|
|
36518
37239
|
}
|
|
36519
|
-
return await new Promise((
|
|
37240
|
+
return await new Promise((resolve7, reject) => {
|
|
36520
37241
|
const child = (0, import_child_process3.spawn)(
|
|
36521
37242
|
"corepack",
|
|
36522
37243
|
["pnpm", "cli:install:global"],
|
|
@@ -36526,7 +37247,7 @@ async function applyRepoUpdate(repoRoot) {
|
|
|
36526
37247
|
}
|
|
36527
37248
|
);
|
|
36528
37249
|
child.once("error", reject);
|
|
36529
|
-
child.once("close", (code) =>
|
|
37250
|
+
child.once("close", (code) => resolve7(typeof code === "number" ? code : 1));
|
|
36530
37251
|
});
|
|
36531
37252
|
}
|
|
36532
37253
|
function shouldShowUpdateNotice(argv = process.argv) {
|
|
@@ -36540,7 +37261,7 @@ function shouldShowUpdateNotice(argv = process.argv) {
|
|
|
36540
37261
|
}
|
|
36541
37262
|
function hashFileSha256(filePath) {
|
|
36542
37263
|
try {
|
|
36543
|
-
const bytes = (0,
|
|
37264
|
+
const bytes = (0, import_fs8.readFileSync)(filePath);
|
|
36544
37265
|
return (0, import_crypto5.createHash)("sha256").update(bytes).digest("hex");
|
|
36545
37266
|
} catch {
|
|
36546
37267
|
return void 0;
|
|
@@ -36550,10 +37271,10 @@ function verifyCliArtifactIntegrity(params = {}) {
|
|
|
36550
37271
|
const cwd = params.cwd ?? process.cwd();
|
|
36551
37272
|
const argv = params.argv ?? process.argv;
|
|
36552
37273
|
const expectedSha256 = String(params.expectedSha256 ?? "").trim().toLowerCase() || void 0;
|
|
36553
|
-
const runtimePath = (0,
|
|
37274
|
+
const runtimePath = (0, import_path7.resolve)(String(argv[1] || ""));
|
|
36554
37275
|
const runtimeHash = runtimePath ? hashFileSha256(runtimePath) : void 0;
|
|
36555
37276
|
const warnings = [];
|
|
36556
|
-
if (!runtimePath || !(0,
|
|
37277
|
+
if (!runtimePath || !(0, import_fs8.existsSync)(runtimePath)) {
|
|
36557
37278
|
warnings.push("runtime_path_unreadable");
|
|
36558
37279
|
}
|
|
36559
37280
|
if (!runtimeHash) {
|
|
@@ -36571,8 +37292,8 @@ function verifyCliArtifactIntegrity(params = {}) {
|
|
|
36571
37292
|
let repoDistHash;
|
|
36572
37293
|
let runtimeMatchesRepoDist;
|
|
36573
37294
|
if (repoRoot) {
|
|
36574
|
-
repoDistPath = (0,
|
|
36575
|
-
if ((0,
|
|
37295
|
+
repoDistPath = (0, import_path7.join)(repoRoot, "packages", "cli", "dist", "foh.js");
|
|
37296
|
+
if ((0, import_fs8.existsSync)(repoDistPath)) {
|
|
36576
37297
|
repoDistHash = hashFileSha256(repoDistPath);
|
|
36577
37298
|
if (runtimeHash && repoDistHash) {
|
|
36578
37299
|
runtimeMatchesRepoDist = runtimeHash === repoDistHash;
|
|
@@ -36795,6 +37516,9 @@ registerMcp(program2);
|
|
|
36795
37516
|
registerKnowledge(program2);
|
|
36796
37517
|
registerLeads(program2);
|
|
36797
37518
|
registerConversations(program2);
|
|
37519
|
+
registerTranscripts(program2);
|
|
37520
|
+
registerAnalytics(program2);
|
|
37521
|
+
registerTest(program2);
|
|
36798
37522
|
registerTests(program2);
|
|
36799
37523
|
registerOps(program2);
|
|
36800
37524
|
registerSetup(program2);
|
|
@@ -36802,6 +37526,7 @@ registerManifest(program2);
|
|
|
36802
37526
|
registerSim(program2);
|
|
36803
37527
|
registerDiag(program2);
|
|
36804
37528
|
registerBug(program2);
|
|
37529
|
+
registerProve(program2);
|
|
36805
37530
|
registerUpdate(program2);
|
|
36806
37531
|
registerHome(program2);
|
|
36807
37532
|
hideInternalApiUrlOptions(program2);
|