@f-o-h/cli 0.1.4 → 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 +799 -84
- 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();
|
|
@@ -10377,7 +10377,7 @@ async function storeAuthenticatedSession(params) {
|
|
|
10377
10377
|
return output;
|
|
10378
10378
|
}
|
|
10379
10379
|
function sleep(ms) {
|
|
10380
|
-
return new Promise((
|
|
10380
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
10381
10381
|
}
|
|
10382
10382
|
async function runDeviceLogin(opts) {
|
|
10383
10383
|
const jsonMode = Boolean(opts.json);
|
|
@@ -10915,7 +10915,7 @@ async function pollUntil(check2, opts) {
|
|
|
10915
10915
|
}
|
|
10916
10916
|
}
|
|
10917
10917
|
function sleep2(ms) {
|
|
10918
|
-
return new Promise((
|
|
10918
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
10919
10919
|
}
|
|
10920
10920
|
|
|
10921
10921
|
// src/commands/compliance.ts
|
|
@@ -13783,8 +13783,8 @@ function registerAgentGuardrailCommands(agent) {
|
|
|
13783
13783
|
try {
|
|
13784
13784
|
rule = JSON.parse(opts.rule);
|
|
13785
13785
|
} catch {
|
|
13786
|
-
const { readFileSync:
|
|
13787
|
-
rule = JSON.parse(
|
|
13786
|
+
const { readFileSync: readFileSync7 } = await import("fs");
|
|
13787
|
+
rule = JSON.parse(readFileSync7(opts.rule, "utf-8"));
|
|
13788
13788
|
}
|
|
13789
13789
|
const data = await apiFetch(`/v1/console/agents/${opts.agent}/guardrails`, {
|
|
13790
13790
|
method: "POST",
|
|
@@ -14190,6 +14190,58 @@ function registerAgent(program3) {
|
|
|
14190
14190
|
const data = await apiFetch(`/v1/console/agents/${opts.agent}`, { apiUrlOverride: opts.apiUrl });
|
|
14191
14191
|
format(data, { json: opts.json ?? false });
|
|
14192
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
|
+
}));
|
|
14193
14245
|
const blueprint = agent.command("blueprint").description("Compile or apply Conversation Blueprint v1");
|
|
14194
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 () => {
|
|
14195
14247
|
const parsedBlueprint = await parseJsonOption(opts.blueprint, "--blueprint");
|
|
@@ -14316,9 +14368,9 @@ function registerAgent(program3) {
|
|
|
14316
14368
|
process.stdout.write(yaml);
|
|
14317
14369
|
return;
|
|
14318
14370
|
}
|
|
14319
|
-
const { writeFileSync:
|
|
14371
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
14320
14372
|
const outputPath = opts.output ?? "tenant.yaml";
|
|
14321
|
-
|
|
14373
|
+
writeFileSync6(
|
|
14322
14374
|
outputPath,
|
|
14323
14375
|
`# tenant.yaml - Front Of House agent manifest
|
|
14324
14376
|
# Edit this file and run: foh plan tenant.yaml
|
|
@@ -15753,11 +15805,11 @@ function registerVoice(program3) {
|
|
|
15753
15805
|
}
|
|
15754
15806
|
const outputPath = String(opts.out || `foh-voice-preview-${provider}-${voiceId}.mp3`).trim();
|
|
15755
15807
|
const audio = Buffer.from(await res.arrayBuffer());
|
|
15756
|
-
const { mkdirSync: mkdirSync4, writeFileSync:
|
|
15757
|
-
const { dirname: dirname5, resolve:
|
|
15758
|
-
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);
|
|
15759
15811
|
mkdirSync4(dirname5(absolutePath), { recursive: true });
|
|
15760
|
-
|
|
15812
|
+
writeFileSync6(absolutePath, audio);
|
|
15761
15813
|
format({
|
|
15762
15814
|
status: "ok",
|
|
15763
15815
|
provider,
|
|
@@ -30238,7 +30290,7 @@ var Protocol = class {
|
|
|
30238
30290
|
return;
|
|
30239
30291
|
}
|
|
30240
30292
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
30241
|
-
await new Promise((
|
|
30293
|
+
await new Promise((resolve7) => setTimeout(resolve7, pollInterval));
|
|
30242
30294
|
options?.signal?.throwIfAborted();
|
|
30243
30295
|
}
|
|
30244
30296
|
} catch (error2) {
|
|
@@ -30255,7 +30307,7 @@ var Protocol = class {
|
|
|
30255
30307
|
*/
|
|
30256
30308
|
request(request, resultSchema, options) {
|
|
30257
30309
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
30258
|
-
return new Promise((
|
|
30310
|
+
return new Promise((resolve7, reject) => {
|
|
30259
30311
|
const earlyReject = (error2) => {
|
|
30260
30312
|
reject(error2);
|
|
30261
30313
|
};
|
|
@@ -30333,7 +30385,7 @@ var Protocol = class {
|
|
|
30333
30385
|
if (!parseResult.success) {
|
|
30334
30386
|
reject(parseResult.error);
|
|
30335
30387
|
} else {
|
|
30336
|
-
|
|
30388
|
+
resolve7(parseResult.data);
|
|
30337
30389
|
}
|
|
30338
30390
|
} catch (error2) {
|
|
30339
30391
|
reject(error2);
|
|
@@ -30594,12 +30646,12 @@ var Protocol = class {
|
|
|
30594
30646
|
}
|
|
30595
30647
|
} catch {
|
|
30596
30648
|
}
|
|
30597
|
-
return new Promise((
|
|
30649
|
+
return new Promise((resolve7, reject) => {
|
|
30598
30650
|
if (signal.aborted) {
|
|
30599
30651
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
30600
30652
|
return;
|
|
30601
30653
|
}
|
|
30602
|
-
const timeoutId = setTimeout(
|
|
30654
|
+
const timeoutId = setTimeout(resolve7, interval);
|
|
30603
30655
|
signal.addEventListener("abort", () => {
|
|
30604
30656
|
clearTimeout(timeoutId);
|
|
30605
30657
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -31699,7 +31751,7 @@ var McpServer = class {
|
|
|
31699
31751
|
let task = createTaskResult.task;
|
|
31700
31752
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
31701
31753
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
31702
|
-
await new Promise((
|
|
31754
|
+
await new Promise((resolve7) => setTimeout(resolve7, pollInterval));
|
|
31703
31755
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
31704
31756
|
if (!updatedTask) {
|
|
31705
31757
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -32348,19 +32400,19 @@ var StdioServerTransport = class {
|
|
|
32348
32400
|
this.onclose?.();
|
|
32349
32401
|
}
|
|
32350
32402
|
send(message) {
|
|
32351
|
-
return new Promise((
|
|
32403
|
+
return new Promise((resolve7) => {
|
|
32352
32404
|
const json3 = serializeMessage(message);
|
|
32353
32405
|
if (this._stdout.write(json3)) {
|
|
32354
|
-
|
|
32406
|
+
resolve7();
|
|
32355
32407
|
} else {
|
|
32356
|
-
this._stdout.once("drain",
|
|
32408
|
+
this._stdout.once("drain", resolve7);
|
|
32357
32409
|
}
|
|
32358
32410
|
});
|
|
32359
32411
|
}
|
|
32360
32412
|
};
|
|
32361
32413
|
|
|
32362
32414
|
// src/lib/cli-version.ts
|
|
32363
|
-
var CLI_VERSION = "0.1.
|
|
32415
|
+
var CLI_VERSION = "0.1.5";
|
|
32364
32416
|
|
|
32365
32417
|
// src/commands/mcp-serve.ts
|
|
32366
32418
|
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
@@ -32545,7 +32597,7 @@ async function runFohCli(params) {
|
|
|
32545
32597
|
effectiveArgv.push("--json");
|
|
32546
32598
|
}
|
|
32547
32599
|
const command = `foh ${effectiveArgv.join(" ")}`;
|
|
32548
|
-
return await new Promise((
|
|
32600
|
+
return await new Promise((resolve7) => {
|
|
32549
32601
|
const child = (0, import_node_child_process.spawn)(process.execPath, [cliEntry, ...effectiveArgv], {
|
|
32550
32602
|
stdio: ["ignore", "pipe", "pipe"],
|
|
32551
32603
|
env: {
|
|
@@ -32570,7 +32622,7 @@ async function runFohCli(params) {
|
|
|
32570
32622
|
});
|
|
32571
32623
|
child.once("error", (error2) => {
|
|
32572
32624
|
clearTimeout(timeoutHandle);
|
|
32573
|
-
|
|
32625
|
+
resolve7({
|
|
32574
32626
|
ok: false,
|
|
32575
32627
|
command,
|
|
32576
32628
|
argv: effectiveArgv,
|
|
@@ -32586,7 +32638,7 @@ async function runFohCli(params) {
|
|
|
32586
32638
|
const stderrText = finalizeBoundedText(stderrBuffer);
|
|
32587
32639
|
const exitCode = Number.isFinite(code ?? NaN) ? Number(code) : 1;
|
|
32588
32640
|
const stdoutJson = tryParseJson(stdoutText);
|
|
32589
|
-
|
|
32641
|
+
resolve7({
|
|
32590
32642
|
ok: !timedOut && exitCode === 0,
|
|
32591
32643
|
command,
|
|
32592
32644
|
argv: effectiveArgv,
|
|
@@ -33361,14 +33413,107 @@ function registerMcp(program3) {
|
|
|
33361
33413
|
// src/commands/knowledge.ts
|
|
33362
33414
|
var import_fs2 = require("fs");
|
|
33363
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
|
|
33364
33429
|
function readDraftKnowledgeText(draft) {
|
|
33365
33430
|
const fromRaw = typeof draft.knowledge_base_raw === "string" ? draft.knowledge_base_raw : "";
|
|
33366
33431
|
if (fromRaw.trim().length > 0) return fromRaw;
|
|
33367
33432
|
const fromLegacy = typeof draft.knowledge_base === "string" ? draft.knowledge_base : "";
|
|
33368
33433
|
return fromLegacy;
|
|
33369
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
|
+
}
|
|
33370
33450
|
function registerKnowledge(program3) {
|
|
33371
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
|
+
}));
|
|
33372
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 () => {
|
|
33373
33518
|
const content = (0, import_fs2.readFileSync)(opts.file, "utf-8");
|
|
33374
33519
|
let data;
|
|
@@ -33997,7 +34142,7 @@ function registerSetup(program3) {
|
|
|
33997
34142
|
const startedAtIso = nowIso();
|
|
33998
34143
|
const startedAtMs = Date.now();
|
|
33999
34144
|
if (shouldResumeSkip(name)) {
|
|
34000
|
-
const
|
|
34145
|
+
const skipped2 = timedStepResult(
|
|
34001
34146
|
{
|
|
34002
34147
|
step: name,
|
|
34003
34148
|
status: "skipped",
|
|
@@ -34006,10 +34151,10 @@ function registerSetup(program3) {
|
|
|
34006
34151
|
startedAtIso,
|
|
34007
34152
|
startedAtMs
|
|
34008
34153
|
);
|
|
34009
|
-
completed.push(
|
|
34154
|
+
completed.push(skipped2);
|
|
34010
34155
|
process.stderr.write(import_picocolors4.default.dim(` [RESUME] ${name}: skipped (resume-from ${resumeState.resumeFrom})
|
|
34011
34156
|
`));
|
|
34012
|
-
return
|
|
34157
|
+
return skipped2;
|
|
34013
34158
|
}
|
|
34014
34159
|
if (opts.dryRun) {
|
|
34015
34160
|
const dryRunResult = timedStepResult(
|
|
@@ -34365,8 +34510,8 @@ function registerSetup(program3) {
|
|
|
34365
34510
|
}
|
|
34366
34511
|
try {
|
|
34367
34512
|
const manifest = await agentExport(resolvedAgentId, { apiUrlOverride: opts.apiUrl });
|
|
34368
|
-
const { writeFileSync:
|
|
34369
|
-
|
|
34513
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
34514
|
+
writeFileSync6(
|
|
34370
34515
|
"tenant.yaml",
|
|
34371
34516
|
`# tenant.yaml - Front Of House agent manifest
|
|
34372
34517
|
# Edit this file and run: foh plan tenant.yaml
|
|
@@ -34506,8 +34651,8 @@ function registerSim(program3) {
|
|
|
34506
34651
|
}
|
|
34507
34652
|
const cert = response.certificate;
|
|
34508
34653
|
if (opts.out) {
|
|
34509
|
-
const { writeFileSync:
|
|
34510
|
-
|
|
34654
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
34655
|
+
writeFileSync6(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
|
|
34511
34656
|
process.stderr.write(` Certificate written to ${opts.out}
|
|
34512
34657
|
`);
|
|
34513
34658
|
}
|
|
@@ -34557,8 +34702,8 @@ function registerSim(program3) {
|
|
|
34557
34702
|
});
|
|
34558
34703
|
}
|
|
34559
34704
|
if (opts.out) {
|
|
34560
|
-
const { writeFileSync:
|
|
34561
|
-
|
|
34705
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
34706
|
+
writeFileSync6(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
|
|
34562
34707
|
process.stderr.write(` Final certificate written to ${opts.out}
|
|
34563
34708
|
`);
|
|
34564
34709
|
}
|
|
@@ -34595,19 +34740,6 @@ ${passIcon} Certification loop summary
|
|
|
34595
34740
|
|
|
34596
34741
|
// src/commands/conversations.ts
|
|
34597
34742
|
var import_crypto4 = require("crypto");
|
|
34598
|
-
|
|
34599
|
-
// src/lib/query-options.ts
|
|
34600
|
-
function parsePositiveInt(value, fallback, min, max) {
|
|
34601
|
-
const parsed = Number(value ?? fallback);
|
|
34602
|
-
if (!Number.isFinite(parsed)) return fallback;
|
|
34603
|
-
return Math.max(min, Math.min(max, Math.trunc(parsed)));
|
|
34604
|
-
}
|
|
34605
|
-
function withQuery(path2, params) {
|
|
34606
|
-
const query = params.toString();
|
|
34607
|
-
return query ? `${path2}?${query}` : path2;
|
|
34608
|
-
}
|
|
34609
|
-
|
|
34610
|
-
// src/commands/conversations.ts
|
|
34611
34743
|
function registerConversations(program3) {
|
|
34612
34744
|
const conversations = program3.command("conversations").description("Search and operate on conversation traces and lead data");
|
|
34613
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 () => {
|
|
@@ -34769,6 +34901,140 @@ function registerConversations(program3) {
|
|
|
34769
34901
|
}));
|
|
34770
34902
|
}
|
|
34771
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
|
+
|
|
34772
35038
|
// src/commands/tests.ts
|
|
34773
35039
|
function registerTests(program3) {
|
|
34774
35040
|
const tests = program3.command("tests").description("Manage agent test catalog and test-run lifecycle");
|
|
@@ -34953,6 +35219,164 @@ function registerTests(program3) {
|
|
|
34953
35219
|
}));
|
|
34954
35220
|
}
|
|
34955
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
|
+
|
|
34956
35380
|
// src/commands/ops.ts
|
|
34957
35381
|
function parseClientErrorSource(raw) {
|
|
34958
35382
|
if (!raw) return void 0;
|
|
@@ -35367,8 +35791,8 @@ function registerDiag(program3) {
|
|
|
35367
35791
|
}
|
|
35368
35792
|
|
|
35369
35793
|
// src/commands/bug.ts
|
|
35370
|
-
var
|
|
35371
|
-
var
|
|
35794
|
+
var import_fs7 = require("fs");
|
|
35795
|
+
var import_path6 = require("path");
|
|
35372
35796
|
var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
35373
35797
|
var MAX_BODY_PREVIEW_LENGTH = 200;
|
|
35374
35798
|
function parseMethod(raw) {
|
|
@@ -35484,14 +35908,14 @@ function parseRequestBody(raw) {
|
|
|
35484
35908
|
}
|
|
35485
35909
|
}
|
|
35486
35910
|
function writeJsonArtifact(path2, value) {
|
|
35487
|
-
const absolutePath = (0,
|
|
35488
|
-
(0,
|
|
35489
|
-
(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");
|
|
35490
35914
|
return absolutePath;
|
|
35491
35915
|
}
|
|
35492
35916
|
function defaultArtifactPath() {
|
|
35493
35917
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
35494
|
-
return (0,
|
|
35918
|
+
return (0, import_path6.resolve)(`test-results/bug-report.${timestamp2}.json`);
|
|
35495
35919
|
}
|
|
35496
35920
|
async function resolveBugReportWizardInputs(opts) {
|
|
35497
35921
|
if (!opts.wizard) return opts;
|
|
@@ -35683,6 +36107,293 @@ function registerBug(program3) {
|
|
|
35683
36107
|
});
|
|
35684
36108
|
}
|
|
35685
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
|
+
|
|
35686
36397
|
// src/tui/command-palette.ts
|
|
35687
36398
|
function tokenizeInput(value) {
|
|
35688
36399
|
const tokens = [];
|
|
@@ -36128,7 +36839,7 @@ async function runSelf(args, apiUrlOverride) {
|
|
|
36128
36839
|
if (apiUrlOverride && !spawnArgs.includes("--api-url")) {
|
|
36129
36840
|
spawnArgs.push("--api-url", apiUrlOverride);
|
|
36130
36841
|
}
|
|
36131
|
-
return await new Promise((
|
|
36842
|
+
return await new Promise((resolve7, reject) => {
|
|
36132
36843
|
const child = (0, import_child_process2.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
|
|
36133
36844
|
stdio: "inherit",
|
|
36134
36845
|
env: {
|
|
@@ -36138,7 +36849,7 @@ async function runSelf(args, apiUrlOverride) {
|
|
|
36138
36849
|
}
|
|
36139
36850
|
});
|
|
36140
36851
|
child.once("error", reject);
|
|
36141
|
-
child.once("close", (code) =>
|
|
36852
|
+
child.once("close", (code) => resolve7(typeof code === "number" ? code : 1));
|
|
36142
36853
|
});
|
|
36143
36854
|
}
|
|
36144
36855
|
function shouldUseInteractiveHome(argv) {
|
|
@@ -36428,8 +37139,8 @@ function maybeDefaultToHome(argv = process.argv) {
|
|
|
36428
37139
|
}
|
|
36429
37140
|
|
|
36430
37141
|
// src/lib/update.ts
|
|
36431
|
-
var
|
|
36432
|
-
var
|
|
37142
|
+
var import_fs8 = require("fs");
|
|
37143
|
+
var import_path7 = require("path");
|
|
36433
37144
|
var import_child_process3 = require("child_process");
|
|
36434
37145
|
var import_crypto5 = require("crypto");
|
|
36435
37146
|
function parseSemver(version2) {
|
|
@@ -36450,7 +37161,7 @@ function compareSemver(a, b) {
|
|
|
36450
37161
|
}
|
|
36451
37162
|
function readPackageJsonVersion(path2) {
|
|
36452
37163
|
try {
|
|
36453
|
-
const raw = (0,
|
|
37164
|
+
const raw = (0, import_fs8.readFileSync)(path2, "utf-8");
|
|
36454
37165
|
const parsed = JSON.parse(raw);
|
|
36455
37166
|
const version2 = String(parsed.version ?? "").trim();
|
|
36456
37167
|
return version2 || void 0;
|
|
@@ -36459,13 +37170,13 @@ function readPackageJsonVersion(path2) {
|
|
|
36459
37170
|
}
|
|
36460
37171
|
}
|
|
36461
37172
|
function findRepoRoot(startCwd = process.cwd()) {
|
|
36462
|
-
let current = (0,
|
|
37173
|
+
let current = (0, import_path7.resolve)(startCwd);
|
|
36463
37174
|
while (true) {
|
|
36464
|
-
const rootPackageJsonPath = (0,
|
|
36465
|
-
const cliPackageJsonPath = (0,
|
|
36466
|
-
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)) {
|
|
36467
37178
|
try {
|
|
36468
|
-
const raw = (0,
|
|
37179
|
+
const raw = (0, import_fs8.readFileSync)(rootPackageJsonPath, "utf-8");
|
|
36469
37180
|
const parsed = JSON.parse(raw);
|
|
36470
37181
|
if (String(parsed.name ?? "").trim() === "front-of-house") {
|
|
36471
37182
|
return current;
|
|
@@ -36473,7 +37184,7 @@ function findRepoRoot(startCwd = process.cwd()) {
|
|
|
36473
37184
|
} catch {
|
|
36474
37185
|
}
|
|
36475
37186
|
}
|
|
36476
|
-
const parent = (0,
|
|
37187
|
+
const parent = (0, import_path7.dirname)(current);
|
|
36477
37188
|
if (parent === current) return void 0;
|
|
36478
37189
|
current = parent;
|
|
36479
37190
|
}
|
|
@@ -36487,7 +37198,7 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
|
|
|
36487
37198
|
remediation: "Run this command from the Front Of House repo root to compare/install the latest CLI."
|
|
36488
37199
|
};
|
|
36489
37200
|
}
|
|
36490
|
-
const cliPackageJsonPath = (0,
|
|
37201
|
+
const cliPackageJsonPath = (0, import_path7.join)(repoRoot, "packages", "cli", "package.json");
|
|
36491
37202
|
const latestVersion = readPackageJsonVersion(cliPackageJsonPath);
|
|
36492
37203
|
if (!latestVersion) {
|
|
36493
37204
|
return {
|
|
@@ -36514,19 +37225,19 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
|
|
|
36514
37225
|
};
|
|
36515
37226
|
}
|
|
36516
37227
|
async function applyRepoUpdate(repoRoot) {
|
|
36517
|
-
const scriptPath = (0,
|
|
37228
|
+
const scriptPath = (0, import_path7.join)(repoRoot, "scripts", "Install-FohCli.ps1");
|
|
36518
37229
|
if (process.platform === "win32") {
|
|
36519
|
-
return await new Promise((
|
|
37230
|
+
return await new Promise((resolve7, reject) => {
|
|
36520
37231
|
const child = (0, import_child_process3.spawn)(
|
|
36521
37232
|
"powershell",
|
|
36522
37233
|
["-ExecutionPolicy", "Bypass", "-File", scriptPath],
|
|
36523
37234
|
{ stdio: "inherit" }
|
|
36524
37235
|
);
|
|
36525
37236
|
child.once("error", reject);
|
|
36526
|
-
child.once("close", (code) =>
|
|
37237
|
+
child.once("close", (code) => resolve7(typeof code === "number" ? code : 1));
|
|
36527
37238
|
});
|
|
36528
37239
|
}
|
|
36529
|
-
return await new Promise((
|
|
37240
|
+
return await new Promise((resolve7, reject) => {
|
|
36530
37241
|
const child = (0, import_child_process3.spawn)(
|
|
36531
37242
|
"corepack",
|
|
36532
37243
|
["pnpm", "cli:install:global"],
|
|
@@ -36536,7 +37247,7 @@ async function applyRepoUpdate(repoRoot) {
|
|
|
36536
37247
|
}
|
|
36537
37248
|
);
|
|
36538
37249
|
child.once("error", reject);
|
|
36539
|
-
child.once("close", (code) =>
|
|
37250
|
+
child.once("close", (code) => resolve7(typeof code === "number" ? code : 1));
|
|
36540
37251
|
});
|
|
36541
37252
|
}
|
|
36542
37253
|
function shouldShowUpdateNotice(argv = process.argv) {
|
|
@@ -36550,7 +37261,7 @@ function shouldShowUpdateNotice(argv = process.argv) {
|
|
|
36550
37261
|
}
|
|
36551
37262
|
function hashFileSha256(filePath) {
|
|
36552
37263
|
try {
|
|
36553
|
-
const bytes = (0,
|
|
37264
|
+
const bytes = (0, import_fs8.readFileSync)(filePath);
|
|
36554
37265
|
return (0, import_crypto5.createHash)("sha256").update(bytes).digest("hex");
|
|
36555
37266
|
} catch {
|
|
36556
37267
|
return void 0;
|
|
@@ -36560,10 +37271,10 @@ function verifyCliArtifactIntegrity(params = {}) {
|
|
|
36560
37271
|
const cwd = params.cwd ?? process.cwd();
|
|
36561
37272
|
const argv = params.argv ?? process.argv;
|
|
36562
37273
|
const expectedSha256 = String(params.expectedSha256 ?? "").trim().toLowerCase() || void 0;
|
|
36563
|
-
const runtimePath = (0,
|
|
37274
|
+
const runtimePath = (0, import_path7.resolve)(String(argv[1] || ""));
|
|
36564
37275
|
const runtimeHash = runtimePath ? hashFileSha256(runtimePath) : void 0;
|
|
36565
37276
|
const warnings = [];
|
|
36566
|
-
if (!runtimePath || !(0,
|
|
37277
|
+
if (!runtimePath || !(0, import_fs8.existsSync)(runtimePath)) {
|
|
36567
37278
|
warnings.push("runtime_path_unreadable");
|
|
36568
37279
|
}
|
|
36569
37280
|
if (!runtimeHash) {
|
|
@@ -36581,8 +37292,8 @@ function verifyCliArtifactIntegrity(params = {}) {
|
|
|
36581
37292
|
let repoDistHash;
|
|
36582
37293
|
let runtimeMatchesRepoDist;
|
|
36583
37294
|
if (repoRoot) {
|
|
36584
|
-
repoDistPath = (0,
|
|
36585
|
-
if ((0,
|
|
37295
|
+
repoDistPath = (0, import_path7.join)(repoRoot, "packages", "cli", "dist", "foh.js");
|
|
37296
|
+
if ((0, import_fs8.existsSync)(repoDistPath)) {
|
|
36586
37297
|
repoDistHash = hashFileSha256(repoDistPath);
|
|
36587
37298
|
if (runtimeHash && repoDistHash) {
|
|
36588
37299
|
runtimeMatchesRepoDist = runtimeHash === repoDistHash;
|
|
@@ -36805,6 +37516,9 @@ registerMcp(program2);
|
|
|
36805
37516
|
registerKnowledge(program2);
|
|
36806
37517
|
registerLeads(program2);
|
|
36807
37518
|
registerConversations(program2);
|
|
37519
|
+
registerTranscripts(program2);
|
|
37520
|
+
registerAnalytics(program2);
|
|
37521
|
+
registerTest(program2);
|
|
36808
37522
|
registerTests(program2);
|
|
36809
37523
|
registerOps(program2);
|
|
36810
37524
|
registerSetup(program2);
|
|
@@ -36812,6 +37526,7 @@ registerManifest(program2);
|
|
|
36812
37526
|
registerSim(program2);
|
|
36813
37527
|
registerDiag(program2);
|
|
36814
37528
|
registerBug(program2);
|
|
37529
|
+
registerProve(program2);
|
|
36815
37530
|
registerUpdate(program2);
|
|
36816
37531
|
registerHome(program2);
|
|
36817
37532
|
hideInternalApiUrlOptions(program2);
|