@f-o-h/cli 0.1.4 → 0.1.6
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 +99 -1
- package/dist/foh.js +1724 -138
- package/examples/improvement-packet.example.json +35 -0
- package/examples/proof-report.example.json +32 -0
- package/examples/scenario-suite.viewing.yml +26 -0
- package/examples/transcript-export.example.json +39 -0
- package/package.json +3 -1
- package/schemas/cli-envelope.schema.json +22 -0
- package/schemas/improvement-packet.schema.json +76 -0
- package/schemas/scenario-suite.schema.json +42 -0
- package/schemas/transcript-export.schema.json +54 -0
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 = resolve8.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 resolve8(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 resolve8(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: resolve8,
|
|
6879
6879
|
resolveComponent,
|
|
6880
6880
|
equal,
|
|
6881
6881
|
serialize,
|
|
@@ -9889,6 +9889,8 @@ var FohError = class extends Error {
|
|
|
9889
9889
|
remediation;
|
|
9890
9890
|
statusCode;
|
|
9891
9891
|
detail;
|
|
9892
|
+
reasonCode;
|
|
9893
|
+
nextCommands;
|
|
9892
9894
|
constructor(opts) {
|
|
9893
9895
|
super(opts.error);
|
|
9894
9896
|
this.name = "FohError";
|
|
@@ -9898,6 +9900,8 @@ var FohError = class extends Error {
|
|
|
9898
9900
|
this.remediation = opts.remediation;
|
|
9899
9901
|
this.statusCode = opts.statusCode;
|
|
9900
9902
|
this.detail = opts.detail;
|
|
9903
|
+
this.reasonCode = opts.reasonCode;
|
|
9904
|
+
this.nextCommands = opts.nextCommands;
|
|
9901
9905
|
}
|
|
9902
9906
|
toJSON() {
|
|
9903
9907
|
return {
|
|
@@ -9905,6 +9909,8 @@ var FohError = class extends Error {
|
|
|
9905
9909
|
status: this.status,
|
|
9906
9910
|
error: this.error,
|
|
9907
9911
|
remediation: this.remediation,
|
|
9912
|
+
reason_code: this.reasonCode,
|
|
9913
|
+
next_commands: this.nextCommands,
|
|
9908
9914
|
status_code: this.statusCode,
|
|
9909
9915
|
detail: this.detail
|
|
9910
9916
|
};
|
|
@@ -9913,6 +9919,34 @@ var FohError = class extends Error {
|
|
|
9913
9919
|
|
|
9914
9920
|
// src/lib/output.ts
|
|
9915
9921
|
var import_picocolors = __toESM(require_picocolors());
|
|
9922
|
+
|
|
9923
|
+
// src/lib/cli-envelope.ts
|
|
9924
|
+
function envelopeOk(status) {
|
|
9925
|
+
return status === "pass" || status === "success" || status === "exported";
|
|
9926
|
+
}
|
|
9927
|
+
function dedupeCommands(commands = []) {
|
|
9928
|
+
return Array.from(new Set(commands.map((command) => String(command || "").trim()).filter(Boolean)));
|
|
9929
|
+
}
|
|
9930
|
+
function cliEnvelope(input) {
|
|
9931
|
+
return {
|
|
9932
|
+
schema_version: input.schemaVersion ?? "foh_cli_envelope.v1",
|
|
9933
|
+
ok: envelopeOk(input.status),
|
|
9934
|
+
status: input.status,
|
|
9935
|
+
reason_code: input.reasonCode,
|
|
9936
|
+
summary: input.summary,
|
|
9937
|
+
ids: input.ids ?? {},
|
|
9938
|
+
checks: input.checks ?? [],
|
|
9939
|
+
artifacts: input.artifacts ?? {},
|
|
9940
|
+
next_commands: dedupeCommands(input.nextCommands),
|
|
9941
|
+
...input.extra ?? {}
|
|
9942
|
+
};
|
|
9943
|
+
}
|
|
9944
|
+
function reasonCodeFromStep(step, fallback = "cli_command_failed") {
|
|
9945
|
+
const normalized = step.trim().toLowerCase().replace(/^\/+/, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
9946
|
+
return normalized ? `${normalized}_failed` : fallback;
|
|
9947
|
+
}
|
|
9948
|
+
|
|
9949
|
+
// src/lib/output.ts
|
|
9916
9950
|
function resolveJsonMode(opts = {}) {
|
|
9917
9951
|
return Boolean(opts.json || process.argv.includes("--json"));
|
|
9918
9952
|
}
|
|
@@ -9927,13 +9961,21 @@ function formatError(e, opts = {}) {
|
|
|
9927
9961
|
if (resolveJsonMode(opts)) {
|
|
9928
9962
|
process.stderr.write(
|
|
9929
9963
|
JSON.stringify(
|
|
9930
|
-
{
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9964
|
+
cliEnvelope({
|
|
9965
|
+
status: "fail",
|
|
9966
|
+
reasonCode: e.reasonCode ?? reasonCodeFromStep(e.step),
|
|
9967
|
+
summary: e.error,
|
|
9968
|
+
nextCommands: e.nextCommands,
|
|
9969
|
+
extra: {
|
|
9970
|
+
error: {
|
|
9971
|
+
step: e.step,
|
|
9972
|
+
message: e.error,
|
|
9973
|
+
remediation: e.remediation,
|
|
9974
|
+
status_code: e.statusCode,
|
|
9975
|
+
detail: e.detail
|
|
9976
|
+
}
|
|
9935
9977
|
}
|
|
9936
|
-
},
|
|
9978
|
+
}),
|
|
9937
9979
|
null,
|
|
9938
9980
|
2
|
|
9939
9981
|
) + "\n"
|
|
@@ -10063,21 +10105,21 @@ async function promptLine(label, {
|
|
|
10063
10105
|
allowEmpty = false,
|
|
10064
10106
|
defaultValue
|
|
10065
10107
|
} = {}) {
|
|
10066
|
-
return await new Promise((
|
|
10108
|
+
return await new Promise((resolve8) => {
|
|
10067
10109
|
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
10068
10110
|
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout, terminal: true });
|
|
10069
10111
|
rl.question(`${label}${suffix}: `, (answer) => {
|
|
10070
10112
|
rl.close();
|
|
10071
10113
|
const value = String(answer ?? "").trim();
|
|
10072
10114
|
if (!value && typeof defaultValue === "string") {
|
|
10073
|
-
|
|
10115
|
+
resolve8(defaultValue);
|
|
10074
10116
|
return;
|
|
10075
10117
|
}
|
|
10076
10118
|
if (!value && !allowEmpty) {
|
|
10077
|
-
|
|
10119
|
+
resolve8("");
|
|
10078
10120
|
return;
|
|
10079
10121
|
}
|
|
10080
|
-
|
|
10122
|
+
resolve8(value);
|
|
10081
10123
|
});
|
|
10082
10124
|
});
|
|
10083
10125
|
}
|
|
@@ -10085,7 +10127,7 @@ async function promptSecret(label) {
|
|
|
10085
10127
|
if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== "function") {
|
|
10086
10128
|
return await promptLine(label);
|
|
10087
10129
|
}
|
|
10088
|
-
return await new Promise((
|
|
10130
|
+
return await new Promise((resolve8) => {
|
|
10089
10131
|
const stdin = process.stdin;
|
|
10090
10132
|
const stdout = process.stdout;
|
|
10091
10133
|
const wasRaw = Boolean(stdin.isRaw);
|
|
@@ -10099,7 +10141,7 @@ async function promptSecret(label) {
|
|
|
10099
10141
|
const finish = () => {
|
|
10100
10142
|
cleanup();
|
|
10101
10143
|
stdout.write("\n");
|
|
10102
|
-
|
|
10144
|
+
resolve8(value);
|
|
10103
10145
|
};
|
|
10104
10146
|
const onData = (chunk) => {
|
|
10105
10147
|
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
@@ -10108,7 +10150,7 @@ async function promptSecret(label) {
|
|
|
10108
10150
|
cleanup();
|
|
10109
10151
|
process.exitCode = 130;
|
|
10110
10152
|
stdout.write("\n");
|
|
10111
|
-
return
|
|
10153
|
+
return resolve8("");
|
|
10112
10154
|
}
|
|
10113
10155
|
if (char === "\r" || char === "\n") {
|
|
10114
10156
|
finish();
|
|
@@ -10377,7 +10419,7 @@ async function storeAuthenticatedSession(params) {
|
|
|
10377
10419
|
return output;
|
|
10378
10420
|
}
|
|
10379
10421
|
function sleep(ms) {
|
|
10380
|
-
return new Promise((
|
|
10422
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
10381
10423
|
}
|
|
10382
10424
|
async function runDeviceLogin(opts) {
|
|
10383
10425
|
const jsonMode = Boolean(opts.json);
|
|
@@ -10915,7 +10957,7 @@ async function pollUntil(check2, opts) {
|
|
|
10915
10957
|
}
|
|
10916
10958
|
}
|
|
10917
10959
|
function sleep2(ms) {
|
|
10918
|
-
return new Promise((
|
|
10960
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
10919
10961
|
}
|
|
10920
10962
|
|
|
10921
10963
|
// src/commands/compliance.ts
|
|
@@ -13718,6 +13760,176 @@ function agentDraftFromManifest(manifest) {
|
|
|
13718
13760
|
return patch;
|
|
13719
13761
|
}
|
|
13720
13762
|
|
|
13763
|
+
// src/lib/local-replay-packet.ts
|
|
13764
|
+
var import_fs2 = require("fs");
|
|
13765
|
+
|
|
13766
|
+
// src/lib/transcript-export.ts
|
|
13767
|
+
var EMAIL_RE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
|
|
13768
|
+
var PHONE_RE = /(?<!\w)(?:\+?\d[\d\s().-]{7,}\d)(?!\w)/g;
|
|
13769
|
+
var SECRET_RE = /\b(?:sk|pk|xai|whsec|EAAN)[A-Za-z0-9_\-]{16,}\b/g;
|
|
13770
|
+
function redactString(value) {
|
|
13771
|
+
return value.replace(EMAIL_RE, "[redacted_email]").replace(SECRET_RE, "[redacted_secret]").replace(PHONE_RE, "[redacted_phone]");
|
|
13772
|
+
}
|
|
13773
|
+
function redactObject(value) {
|
|
13774
|
+
if (typeof value === "string") return redactString(value);
|
|
13775
|
+
if (Array.isArray(value)) return value.map((entry) => redactObject(entry));
|
|
13776
|
+
if (!value || typeof value !== "object") return value;
|
|
13777
|
+
const result = {};
|
|
13778
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
13779
|
+
result[key] = redactObject(entry);
|
|
13780
|
+
}
|
|
13781
|
+
return result;
|
|
13782
|
+
}
|
|
13783
|
+
function extractUserTurns(transcriptText) {
|
|
13784
|
+
const turns = [];
|
|
13785
|
+
for (const line of transcriptText.split(/\r?\n/)) {
|
|
13786
|
+
const match = line.match(/^\s*(?:user|customer|lead)\s*:\s*(.+)\s*$/i);
|
|
13787
|
+
if (match?.[1]?.trim()) turns.push({ user: match[1].trim() });
|
|
13788
|
+
}
|
|
13789
|
+
if (turns.length === 0 && transcriptText.trim()) {
|
|
13790
|
+
turns.push({ user: transcriptText.trim().slice(0, 500) });
|
|
13791
|
+
}
|
|
13792
|
+
return turns;
|
|
13793
|
+
}
|
|
13794
|
+
function buildReplayReadyConversation(input) {
|
|
13795
|
+
const conversationId = String(input.conversation.id || "");
|
|
13796
|
+
const transcriptText = String(input.conversation.transcript_text || "");
|
|
13797
|
+
return {
|
|
13798
|
+
...input.conversation,
|
|
13799
|
+
replay_command: conversationId ? `foh agent replay --agent ${input.agentId} --conversation ${conversationId} --json` : null,
|
|
13800
|
+
test_fixture: {
|
|
13801
|
+
schema_version: "foh_scenario_fixture.v1",
|
|
13802
|
+
conversation_id: conversationId || null,
|
|
13803
|
+
turns: extractUserTurns(transcriptText)
|
|
13804
|
+
}
|
|
13805
|
+
};
|
|
13806
|
+
}
|
|
13807
|
+
|
|
13808
|
+
// src/lib/local-replay-packet.ts
|
|
13809
|
+
function asRecord(value) {
|
|
13810
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
13811
|
+
}
|
|
13812
|
+
function nonEmpty(value) {
|
|
13813
|
+
const text = String(value ?? "").trim();
|
|
13814
|
+
return text.length > 0 ? text : void 0;
|
|
13815
|
+
}
|
|
13816
|
+
function arrayOfRecords(value) {
|
|
13817
|
+
return Array.isArray(value) ? value.map(asRecord).filter(Boolean) : [];
|
|
13818
|
+
}
|
|
13819
|
+
function collectStrings(value, keys) {
|
|
13820
|
+
const found = /* @__PURE__ */ new Set();
|
|
13821
|
+
const visit = (entry) => {
|
|
13822
|
+
if (Array.isArray(entry)) {
|
|
13823
|
+
for (const item of entry) visit(item);
|
|
13824
|
+
return;
|
|
13825
|
+
}
|
|
13826
|
+
const record2 = asRecord(entry);
|
|
13827
|
+
if (!record2) return;
|
|
13828
|
+
for (const key of keys) {
|
|
13829
|
+
const text = nonEmpty(record2[key]);
|
|
13830
|
+
if (text) found.add(text);
|
|
13831
|
+
}
|
|
13832
|
+
for (const nested of Object.values(record2)) visit(nested);
|
|
13833
|
+
};
|
|
13834
|
+
visit(value);
|
|
13835
|
+
return Array.from(found);
|
|
13836
|
+
}
|
|
13837
|
+
function selectConversation(artifact, agentId) {
|
|
13838
|
+
const conversations = arrayOfRecords(artifact.conversations);
|
|
13839
|
+
if (conversations.length > 0) {
|
|
13840
|
+
if (agentId) {
|
|
13841
|
+
const byAgent = conversations.find((conversation) => nonEmpty(conversation.agent_id) === agentId);
|
|
13842
|
+
if (byAgent) return byAgent;
|
|
13843
|
+
}
|
|
13844
|
+
return conversations[0] ?? null;
|
|
13845
|
+
}
|
|
13846
|
+
if (artifact.transcript_text || artifact.test_fixture || artifact.traces) return artifact;
|
|
13847
|
+
return null;
|
|
13848
|
+
}
|
|
13849
|
+
function buildDiff(conversation) {
|
|
13850
|
+
const expectedTurns = arrayOfRecords(conversation.expected_turns);
|
|
13851
|
+
const fixtureTurns = arrayOfRecords((asRecord(conversation.test_fixture) ?? {}).turns);
|
|
13852
|
+
const actualTurns = fixtureTurns.length > 0 ? fixtureTurns : extractUserTurns(String(conversation.transcript_text ?? ""));
|
|
13853
|
+
if (expectedTurns.length === 0) {
|
|
13854
|
+
return {
|
|
13855
|
+
status: "not_evaluated",
|
|
13856
|
+
reason_code: "expected_turns_missing",
|
|
13857
|
+
expected_turn_count: 0,
|
|
13858
|
+
actual_turn_count: actualTurns.length,
|
|
13859
|
+
differences: []
|
|
13860
|
+
};
|
|
13861
|
+
}
|
|
13862
|
+
const differences = [];
|
|
13863
|
+
const max = Math.max(expectedTurns.length, actualTurns.length);
|
|
13864
|
+
for (let i = 0; i < max; i += 1) {
|
|
13865
|
+
const expected = nonEmpty(expectedTurns[i]?.user);
|
|
13866
|
+
const actual = nonEmpty(actualTurns[i]?.user);
|
|
13867
|
+
if (expected !== actual) differences.push(`turn_${i + 1}: expected "${expected ?? ""}" got "${actual ?? ""}"`);
|
|
13868
|
+
}
|
|
13869
|
+
return {
|
|
13870
|
+
status: differences.length === 0 ? "match" : "diff",
|
|
13871
|
+
reason_code: differences.length === 0 ? "replay_diff_match" : "replay_diff_mismatch",
|
|
13872
|
+
expected_turn_count: expectedTurns.length,
|
|
13873
|
+
actual_turn_count: actualTurns.length,
|
|
13874
|
+
differences
|
|
13875
|
+
};
|
|
13876
|
+
}
|
|
13877
|
+
function buildLocalReplayPacket(input) {
|
|
13878
|
+
const artifact = JSON.parse((0, import_fs2.readFileSync)(input.filePath, "utf-8"));
|
|
13879
|
+
const conversation = selectConversation(artifact, input.agentId);
|
|
13880
|
+
if (!conversation) {
|
|
13881
|
+
return {
|
|
13882
|
+
schema_version: "foh_agent_replay_packet.v1",
|
|
13883
|
+
status: "local_artifact_not_replayable",
|
|
13884
|
+
source: { type: "file", file: input.filePath, agent_id: input.agentId ?? null },
|
|
13885
|
+
not_replayable_reason: "file_has_no_conversation_payload",
|
|
13886
|
+
next_commands: ["foh transcripts export --agent <agent-id> --hydrate --include-traces --format json --out transcript-export.json --json"]
|
|
13887
|
+
};
|
|
13888
|
+
}
|
|
13889
|
+
const traces = arrayOfRecords(conversation.traces ?? artifact.traces);
|
|
13890
|
+
const traceIds = traces.map((trace) => nonEmpty(trace.id)).filter(Boolean);
|
|
13891
|
+
const agentId = input.agentId ?? nonEmpty(conversation.agent_id) ?? nonEmpty(artifact.agent_id);
|
|
13892
|
+
const conversationId = nonEmpty(conversation.id) ?? nonEmpty((asRecord(conversation.test_fixture) ?? {}).conversation_id);
|
|
13893
|
+
const firstTraceId = traceIds[0];
|
|
13894
|
+
return {
|
|
13895
|
+
schema_version: "foh_agent_replay_packet.v1",
|
|
13896
|
+
status: "local_replay_packet_created",
|
|
13897
|
+
source: {
|
|
13898
|
+
type: "file",
|
|
13899
|
+
file: input.filePath,
|
|
13900
|
+
agent_id: agentId ?? null,
|
|
13901
|
+
conversation_id: conversationId ?? null
|
|
13902
|
+
},
|
|
13903
|
+
replay_runtime: {
|
|
13904
|
+
mode: "local_artifact",
|
|
13905
|
+
version: "foh_agent_replay_local_v1"
|
|
13906
|
+
},
|
|
13907
|
+
agent_reference: {
|
|
13908
|
+
agent_id: agentId ?? null,
|
|
13909
|
+
agent_version: conversation.agent_version ?? artifact.agent_version ?? null,
|
|
13910
|
+
config_ref: conversation.config_ref ?? artifact.config_ref ?? null
|
|
13911
|
+
},
|
|
13912
|
+
trace_ids: traceIds,
|
|
13913
|
+
correlation_ids: collectStrings([conversation, artifact], ["correlation_id", "correlationId"]),
|
|
13914
|
+
tool_references: collectStrings(traces, ["tool_name", "toolName", "name"]),
|
|
13915
|
+
knowledge_snapshot_refs: collectStrings([conversation, artifact], ["knowledge_snapshot_id", "knowledgeSnapshotId", "knowledge_ref"]),
|
|
13916
|
+
conversation,
|
|
13917
|
+
trace_count: traces.length,
|
|
13918
|
+
traces,
|
|
13919
|
+
diff: buildDiff(conversation),
|
|
13920
|
+
not_replayable_reason: null,
|
|
13921
|
+
next_commands: [
|
|
13922
|
+
...agentId && firstTraceId ? [
|
|
13923
|
+
`foh agent replay --agent ${agentId} --trace ${firstTraceId} --json`,
|
|
13924
|
+
`foh tests from-trace --agent ${agentId} --trace ${firstTraceId} --json`
|
|
13925
|
+
] : [],
|
|
13926
|
+
...agentId && conversationId ? [
|
|
13927
|
+
`foh test run --suite <suite.yml> --agent ${agentId} --json`
|
|
13928
|
+
] : []
|
|
13929
|
+
]
|
|
13930
|
+
};
|
|
13931
|
+
}
|
|
13932
|
+
|
|
13721
13933
|
// src/commands/agent-candidate-decision.ts
|
|
13722
13934
|
function registerAgentCandidateDecisionCommands(agent) {
|
|
13723
13935
|
const candidateDecision = agent.command("candidate-decision").description("Read/write candidate promotion decisions");
|
|
@@ -13783,8 +13995,8 @@ function registerAgentGuardrailCommands(agent) {
|
|
|
13783
13995
|
try {
|
|
13784
13996
|
rule = JSON.parse(opts.rule);
|
|
13785
13997
|
} catch {
|
|
13786
|
-
const { readFileSync:
|
|
13787
|
-
rule = JSON.parse(
|
|
13998
|
+
const { readFileSync: readFileSync9 } = await import("fs");
|
|
13999
|
+
rule = JSON.parse(readFileSync9(opts.rule, "utf-8"));
|
|
13788
14000
|
}
|
|
13789
14001
|
const data = await apiFetch(`/v1/console/agents/${opts.agent}/guardrails`, {
|
|
13790
14002
|
method: "POST",
|
|
@@ -14190,6 +14402,74 @@ function registerAgent(program3) {
|
|
|
14190
14402
|
const data = await apiFetch(`/v1/console/agents/${opts.agent}`, { apiUrlOverride: opts.apiUrl });
|
|
14191
14403
|
format(data, { json: opts.json ?? false });
|
|
14192
14404
|
}));
|
|
14405
|
+
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").option("--file <path>", "Local transcript/replay artifact JSON to package without API access").option("--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 () => {
|
|
14406
|
+
const sourceCount = [opts.trace, opts.conversation, opts.file].filter(Boolean).length;
|
|
14407
|
+
if (sourceCount === 0) {
|
|
14408
|
+
throw new FohError({
|
|
14409
|
+
step: "agent.replay",
|
|
14410
|
+
error: "Missing replay source",
|
|
14411
|
+
remediation: "Pass --trace <id>, --conversation <id>, or --file <transcript.json>.",
|
|
14412
|
+
statusCode: 400
|
|
14413
|
+
});
|
|
14414
|
+
}
|
|
14415
|
+
if (sourceCount > 1) {
|
|
14416
|
+
throw new FohError({
|
|
14417
|
+
step: "agent.replay",
|
|
14418
|
+
error: "Ambiguous replay source",
|
|
14419
|
+
remediation: "Pass only one of --trace, --conversation, or --file.",
|
|
14420
|
+
statusCode: 400
|
|
14421
|
+
});
|
|
14422
|
+
}
|
|
14423
|
+
if ((opts.trace || opts.conversation) && !opts.agent) {
|
|
14424
|
+
throw new FohError({
|
|
14425
|
+
step: "agent.replay",
|
|
14426
|
+
error: "Missing agent id for server replay source",
|
|
14427
|
+
remediation: "Pass --agent <id>, or use --file with a local transcript artifact that includes agent_id.",
|
|
14428
|
+
statusCode: 400
|
|
14429
|
+
});
|
|
14430
|
+
}
|
|
14431
|
+
if (opts.file) {
|
|
14432
|
+
format(buildLocalReplayPacket({
|
|
14433
|
+
filePath: String(opts.file),
|
|
14434
|
+
agentId: opts.agent ? String(opts.agent) : void 0
|
|
14435
|
+
}), { json: opts.json ?? false });
|
|
14436
|
+
return;
|
|
14437
|
+
}
|
|
14438
|
+
if (opts.trace) {
|
|
14439
|
+
const data2 = await apiFetch(`/v1/console/traces/${opts.trace}/replay`, {
|
|
14440
|
+
method: "POST",
|
|
14441
|
+
orgId: opts.org,
|
|
14442
|
+
apiUrlOverride: opts.apiUrl
|
|
14443
|
+
});
|
|
14444
|
+
format({
|
|
14445
|
+
schema_version: "foh_agent_replay_packet.v1",
|
|
14446
|
+
status: "server_replay_completed",
|
|
14447
|
+
source: { type: "trace", trace_id: opts.trace, agent_id: opts.agent },
|
|
14448
|
+
replay: data2,
|
|
14449
|
+
next_commands: [`foh tests from-trace --agent ${opts.agent} --trace ${opts.trace} --json`]
|
|
14450
|
+
}, { json: opts.json ?? false });
|
|
14451
|
+
return;
|
|
14452
|
+
}
|
|
14453
|
+
const data = await apiFetch(`/v1/console/agents/${opts.agent}/conversations/${opts.conversation}?include_traces=true`, {
|
|
14454
|
+
orgId: opts.org,
|
|
14455
|
+
apiUrlOverride: opts.apiUrl
|
|
14456
|
+
});
|
|
14457
|
+
const traces = Array.isArray(data.traces) ? data.traces : [];
|
|
14458
|
+
const firstTraceId = traces.map((trace) => String(trace.id || "").trim()).find(Boolean);
|
|
14459
|
+
format({
|
|
14460
|
+
schema_version: "foh_agent_replay_packet.v1",
|
|
14461
|
+
status: firstTraceId ? "conversation_replay_packet_created" : "conversation_not_replayable",
|
|
14462
|
+
source: { type: "conversation", conversation_id: opts.conversation, agent_id: opts.agent },
|
|
14463
|
+
conversation: data.conversation ?? null,
|
|
14464
|
+
trace_count: traces.length,
|
|
14465
|
+
traces,
|
|
14466
|
+
not_replayable_reason: firstTraceId ? null : "conversation_has_no_trace_events",
|
|
14467
|
+
next_commands: firstTraceId ? [
|
|
14468
|
+
`foh agent replay --agent ${opts.agent} --trace ${firstTraceId} --json`,
|
|
14469
|
+
`foh tests from-trace --agent ${opts.agent} --trace ${firstTraceId} --json`
|
|
14470
|
+
] : [`foh transcripts get --agent ${opts.agent} --conversation ${opts.conversation} --include-traces --json`]
|
|
14471
|
+
}, { json: opts.json ?? false });
|
|
14472
|
+
}));
|
|
14193
14473
|
const blueprint = agent.command("blueprint").description("Compile or apply Conversation Blueprint v1");
|
|
14194
14474
|
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
14475
|
const parsedBlueprint = await parseJsonOption(opts.blueprint, "--blueprint");
|
|
@@ -14316,9 +14596,9 @@ function registerAgent(program3) {
|
|
|
14316
14596
|
process.stdout.write(yaml);
|
|
14317
14597
|
return;
|
|
14318
14598
|
}
|
|
14319
|
-
const { writeFileSync:
|
|
14599
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
14320
14600
|
const outputPath = opts.output ?? "tenant.yaml";
|
|
14321
|
-
|
|
14601
|
+
writeFileSync6(
|
|
14322
14602
|
outputPath,
|
|
14323
14603
|
`# tenant.yaml - Front Of House agent manifest
|
|
14324
14604
|
# Edit this file and run: foh plan tenant.yaml
|
|
@@ -15753,11 +16033,11 @@ function registerVoice(program3) {
|
|
|
15753
16033
|
}
|
|
15754
16034
|
const outputPath = String(opts.out || `foh-voice-preview-${provider}-${voiceId}.mp3`).trim();
|
|
15755
16035
|
const audio = Buffer.from(await res.arrayBuffer());
|
|
15756
|
-
const { mkdirSync: mkdirSync4, writeFileSync:
|
|
15757
|
-
const { dirname: dirname5, resolve:
|
|
15758
|
-
const absolutePath =
|
|
16036
|
+
const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync6 } = await import("fs");
|
|
16037
|
+
const { dirname: dirname5, resolve: resolve8 } = await import("path");
|
|
16038
|
+
const absolutePath = resolve8(outputPath);
|
|
15759
16039
|
mkdirSync4(dirname5(absolutePath), { recursive: true });
|
|
15760
|
-
|
|
16040
|
+
writeFileSync6(absolutePath, audio);
|
|
15761
16041
|
format({
|
|
15762
16042
|
status: "ok",
|
|
15763
16043
|
provider,
|
|
@@ -30238,7 +30518,7 @@ var Protocol = class {
|
|
|
30238
30518
|
return;
|
|
30239
30519
|
}
|
|
30240
30520
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
30241
|
-
await new Promise((
|
|
30521
|
+
await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
|
|
30242
30522
|
options?.signal?.throwIfAborted();
|
|
30243
30523
|
}
|
|
30244
30524
|
} catch (error2) {
|
|
@@ -30255,7 +30535,7 @@ var Protocol = class {
|
|
|
30255
30535
|
*/
|
|
30256
30536
|
request(request, resultSchema, options) {
|
|
30257
30537
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
30258
|
-
return new Promise((
|
|
30538
|
+
return new Promise((resolve8, reject) => {
|
|
30259
30539
|
const earlyReject = (error2) => {
|
|
30260
30540
|
reject(error2);
|
|
30261
30541
|
};
|
|
@@ -30333,7 +30613,7 @@ var Protocol = class {
|
|
|
30333
30613
|
if (!parseResult.success) {
|
|
30334
30614
|
reject(parseResult.error);
|
|
30335
30615
|
} else {
|
|
30336
|
-
|
|
30616
|
+
resolve8(parseResult.data);
|
|
30337
30617
|
}
|
|
30338
30618
|
} catch (error2) {
|
|
30339
30619
|
reject(error2);
|
|
@@ -30594,12 +30874,12 @@ var Protocol = class {
|
|
|
30594
30874
|
}
|
|
30595
30875
|
} catch {
|
|
30596
30876
|
}
|
|
30597
|
-
return new Promise((
|
|
30877
|
+
return new Promise((resolve8, reject) => {
|
|
30598
30878
|
if (signal.aborted) {
|
|
30599
30879
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
30600
30880
|
return;
|
|
30601
30881
|
}
|
|
30602
|
-
const timeoutId = setTimeout(
|
|
30882
|
+
const timeoutId = setTimeout(resolve8, interval);
|
|
30603
30883
|
signal.addEventListener("abort", () => {
|
|
30604
30884
|
clearTimeout(timeoutId);
|
|
30605
30885
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -31699,7 +31979,7 @@ var McpServer = class {
|
|
|
31699
31979
|
let task = createTaskResult.task;
|
|
31700
31980
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
31701
31981
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
31702
|
-
await new Promise((
|
|
31982
|
+
await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
|
|
31703
31983
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
31704
31984
|
if (!updatedTask) {
|
|
31705
31985
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -32348,19 +32628,19 @@ var StdioServerTransport = class {
|
|
|
32348
32628
|
this.onclose?.();
|
|
32349
32629
|
}
|
|
32350
32630
|
send(message) {
|
|
32351
|
-
return new Promise((
|
|
32631
|
+
return new Promise((resolve8) => {
|
|
32352
32632
|
const json3 = serializeMessage(message);
|
|
32353
32633
|
if (this._stdout.write(json3)) {
|
|
32354
|
-
|
|
32634
|
+
resolve8();
|
|
32355
32635
|
} else {
|
|
32356
|
-
this._stdout.once("drain",
|
|
32636
|
+
this._stdout.once("drain", resolve8);
|
|
32357
32637
|
}
|
|
32358
32638
|
});
|
|
32359
32639
|
}
|
|
32360
32640
|
};
|
|
32361
32641
|
|
|
32362
32642
|
// src/lib/cli-version.ts
|
|
32363
|
-
var CLI_VERSION = "0.1.
|
|
32643
|
+
var CLI_VERSION = "0.1.6";
|
|
32364
32644
|
|
|
32365
32645
|
// src/commands/mcp-serve.ts
|
|
32366
32646
|
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
@@ -32545,7 +32825,7 @@ async function runFohCli(params) {
|
|
|
32545
32825
|
effectiveArgv.push("--json");
|
|
32546
32826
|
}
|
|
32547
32827
|
const command = `foh ${effectiveArgv.join(" ")}`;
|
|
32548
|
-
return await new Promise((
|
|
32828
|
+
return await new Promise((resolve8) => {
|
|
32549
32829
|
const child = (0, import_node_child_process.spawn)(process.execPath, [cliEntry, ...effectiveArgv], {
|
|
32550
32830
|
stdio: ["ignore", "pipe", "pipe"],
|
|
32551
32831
|
env: {
|
|
@@ -32570,7 +32850,7 @@ async function runFohCli(params) {
|
|
|
32570
32850
|
});
|
|
32571
32851
|
child.once("error", (error2) => {
|
|
32572
32852
|
clearTimeout(timeoutHandle);
|
|
32573
|
-
|
|
32853
|
+
resolve8({
|
|
32574
32854
|
ok: false,
|
|
32575
32855
|
command,
|
|
32576
32856
|
argv: effectiveArgv,
|
|
@@ -32586,7 +32866,7 @@ async function runFohCli(params) {
|
|
|
32586
32866
|
const stderrText = finalizeBoundedText(stderrBuffer);
|
|
32587
32867
|
const exitCode = Number.isFinite(code ?? NaN) ? Number(code) : 1;
|
|
32588
32868
|
const stdoutJson = tryParseJson(stdoutText);
|
|
32589
|
-
|
|
32869
|
+
resolve8({
|
|
32590
32870
|
ok: !timedOut && exitCode === 0,
|
|
32591
32871
|
command,
|
|
32592
32872
|
argv: effectiveArgv,
|
|
@@ -33359,18 +33639,143 @@ function registerMcp(program3) {
|
|
|
33359
33639
|
}
|
|
33360
33640
|
|
|
33361
33641
|
// src/commands/knowledge.ts
|
|
33362
|
-
var
|
|
33642
|
+
var import_fs3 = require("fs");
|
|
33363
33643
|
var import_path2 = require("path");
|
|
33644
|
+
|
|
33645
|
+
// src/lib/query-options.ts
|
|
33646
|
+
function parsePositiveInt(value, fallback, min, max) {
|
|
33647
|
+
const parsed = Number(value ?? fallback);
|
|
33648
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
33649
|
+
return Math.max(min, Math.min(max, Math.trunc(parsed)));
|
|
33650
|
+
}
|
|
33651
|
+
function withQuery(path2, params) {
|
|
33652
|
+
const query = params.toString();
|
|
33653
|
+
return query ? `${path2}?${query}` : path2;
|
|
33654
|
+
}
|
|
33655
|
+
|
|
33656
|
+
// src/commands/knowledge.ts
|
|
33364
33657
|
function readDraftKnowledgeText(draft) {
|
|
33365
33658
|
const fromRaw = typeof draft.knowledge_base_raw === "string" ? draft.knowledge_base_raw : "";
|
|
33366
33659
|
if (fromRaw.trim().length > 0) return fromRaw;
|
|
33367
33660
|
const fromLegacy = typeof draft.knowledge_base === "string" ? draft.knowledge_base : "";
|
|
33368
33661
|
return fromLegacy;
|
|
33369
33662
|
}
|
|
33663
|
+
function tokenize(value) {
|
|
33664
|
+
return value.toLowerCase().split(/[^a-z0-9]+/g).map((token) => token.trim()).filter((token) => token.length >= 3);
|
|
33665
|
+
}
|
|
33666
|
+
function chunkKnowledgeText(text) {
|
|
33667
|
+
return text.split(/\n\s*\n|---+/g).map((chunk) => chunk.trim()).filter(Boolean).map((chunk, index) => ({ index, text: chunk }));
|
|
33668
|
+
}
|
|
33669
|
+
function scoreChunk(queryTokens, chunkText) {
|
|
33670
|
+
if (queryTokens.size === 0) return 0;
|
|
33671
|
+
const chunkTokens = new Set(tokenize(chunkText));
|
|
33672
|
+
let overlap = 0;
|
|
33673
|
+
for (const token of queryTokens) {
|
|
33674
|
+
if (chunkTokens.has(token)) overlap += 1;
|
|
33675
|
+
}
|
|
33676
|
+
return Number((overlap / queryTokens.size).toFixed(6));
|
|
33677
|
+
}
|
|
33370
33678
|
function registerKnowledge(program3) {
|
|
33371
33679
|
const knowledge = program3.command("knowledge").description("Manage agent knowledge base");
|
|
33680
|
+
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 () => {
|
|
33681
|
+
const query = String(opts.text || "").trim();
|
|
33682
|
+
if (query.length < 3) {
|
|
33683
|
+
throw new FohError({
|
|
33684
|
+
step: "knowledge.query",
|
|
33685
|
+
error: "Query text must be at least 3 characters",
|
|
33686
|
+
remediation: 'Pass --text "<question>" with 3+ characters.',
|
|
33687
|
+
statusCode: 400
|
|
33688
|
+
});
|
|
33689
|
+
}
|
|
33690
|
+
const draft = await apiFetch(`/v1/console/agents/${opts.agent}/draft`, {
|
|
33691
|
+
orgId: opts.org,
|
|
33692
|
+
apiUrlOverride: opts.apiUrl
|
|
33693
|
+
});
|
|
33694
|
+
const knowledgeText = readDraftKnowledgeText(draft);
|
|
33695
|
+
const chunks = chunkKnowledgeText(knowledgeText);
|
|
33696
|
+
const queryTokens = new Set(tokenize(query));
|
|
33697
|
+
const minScore = Math.max(0, Math.min(1, Number(opts.minScore ?? 0.2) || 0.2));
|
|
33698
|
+
const limit = parsePositiveInt(opts.limit, 5, 1, 20);
|
|
33699
|
+
const matches = chunks.map((chunk) => ({
|
|
33700
|
+
chunk_id: `agent-draft-${opts.agent}#${chunk.index + 1}`,
|
|
33701
|
+
source: "agent_draft_knowledge",
|
|
33702
|
+
citation: `agent:${opts.agent}:chunk:${chunk.index + 1}`,
|
|
33703
|
+
score: scoreChunk(queryTokens, chunk.text),
|
|
33704
|
+
lineage: {
|
|
33705
|
+
source: "agent_draft_direct",
|
|
33706
|
+
agent_id: opts.agent,
|
|
33707
|
+
chunk_index: chunk.index + 1
|
|
33708
|
+
},
|
|
33709
|
+
text: chunk.text.slice(0, 1200)
|
|
33710
|
+
})).filter((chunk) => chunk.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
33711
|
+
const topScore = matches[0]?.score ?? 0;
|
|
33712
|
+
const status = matches.length === 0 ? "no_match" : topScore >= minScore ? "pass" : "low_confidence";
|
|
33713
|
+
const reasonCode = status === "pass" ? "knowledge_query_match" : status === "low_confidence" ? "knowledge_query_low_confidence" : "knowledge_query_no_match";
|
|
33714
|
+
const packet = status === "pass" ? null : {
|
|
33715
|
+
schema_version: "foh_knowledge_query_failure_packet.v1",
|
|
33716
|
+
agent_id: opts.agent,
|
|
33717
|
+
query,
|
|
33718
|
+
reason_code: reasonCode,
|
|
33719
|
+
top_score: topScore,
|
|
33720
|
+
chunk_count: chunks.length,
|
|
33721
|
+
lineage: {
|
|
33722
|
+
source: "agent_draft_direct",
|
|
33723
|
+
agent_id: opts.agent,
|
|
33724
|
+
candidate_chunk_count: chunks.length
|
|
33725
|
+
},
|
|
33726
|
+
next_commands: [
|
|
33727
|
+
`foh knowledge ingest-file --agent ${opts.agent} --file <path> --json`,
|
|
33728
|
+
`foh knowledge query --agent ${opts.agent} --text "${query.replace(/"/g, '\\"')}" --explain --json`
|
|
33729
|
+
]
|
|
33730
|
+
};
|
|
33731
|
+
format({
|
|
33732
|
+
schema_version: "foh_knowledge_query_debug.v1",
|
|
33733
|
+
ok: status === "pass",
|
|
33734
|
+
status,
|
|
33735
|
+
reason_code: reasonCode,
|
|
33736
|
+
summary: status === "pass" ? `Knowledge query matched ${matches.length} chunk(s).` : status === "low_confidence" ? `Knowledge query matched weakly; top score ${topScore}.` : "Knowledge query returned no usable matches.",
|
|
33737
|
+
ids: {
|
|
33738
|
+
agent_id: opts.agent
|
|
33739
|
+
},
|
|
33740
|
+
checks: [{
|
|
33741
|
+
name: "knowledge_retrieval",
|
|
33742
|
+
status,
|
|
33743
|
+
reason_code: reasonCode,
|
|
33744
|
+
top_score: topScore,
|
|
33745
|
+
min_score: minScore,
|
|
33746
|
+
match_count: matches.length
|
|
33747
|
+
}],
|
|
33748
|
+
artifacts: {},
|
|
33749
|
+
next_commands: status === "pass" ? [] : [
|
|
33750
|
+
`foh knowledge ingest-file --agent ${opts.agent} --file <path> --json`,
|
|
33751
|
+
`foh knowledge query --agent ${opts.agent} --text "${query.replace(/"/g, '\\"')}" --explain --json`
|
|
33752
|
+
],
|
|
33753
|
+
agent_id: opts.agent,
|
|
33754
|
+
query,
|
|
33755
|
+
retrieval: {
|
|
33756
|
+
source: "agent_draft_direct",
|
|
33757
|
+
chunk_count: chunks.length,
|
|
33758
|
+
match_count: matches.length,
|
|
33759
|
+
top_score: topScore,
|
|
33760
|
+
min_score: minScore,
|
|
33761
|
+
lineage: {
|
|
33762
|
+
agent_id: opts.agent,
|
|
33763
|
+
source: "agent_draft_direct",
|
|
33764
|
+
candidate_chunk_count: chunks.length
|
|
33765
|
+
}
|
|
33766
|
+
},
|
|
33767
|
+
matches,
|
|
33768
|
+
failure_packet: packet,
|
|
33769
|
+
...opts.explain ? {
|
|
33770
|
+
explain: {
|
|
33771
|
+
query_tokens: Array.from(queryTokens),
|
|
33772
|
+
scoring: "token_overlap_ratio_v1"
|
|
33773
|
+
}
|
|
33774
|
+
} : {}
|
|
33775
|
+
}, { json: opts.json ?? false });
|
|
33776
|
+
}));
|
|
33372
33777
|
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
|
-
const content = (0,
|
|
33778
|
+
const content = (0, import_fs3.readFileSync)(opts.file, "utf-8");
|
|
33374
33779
|
let data;
|
|
33375
33780
|
if (opts.agent) {
|
|
33376
33781
|
const draft = await apiFetch(`/v1/console/agents/${opts.agent}/draft`, {
|
|
@@ -33529,7 +33934,7 @@ var import_crypto3 = require("crypto");
|
|
|
33529
33934
|
|
|
33530
33935
|
// src/lib/signed-report.ts
|
|
33531
33936
|
var import_crypto2 = require("crypto");
|
|
33532
|
-
var
|
|
33937
|
+
var import_fs4 = require("fs");
|
|
33533
33938
|
var import_path3 = require("path");
|
|
33534
33939
|
function canonicalize(value) {
|
|
33535
33940
|
if (value === null || value === void 0) return null;
|
|
@@ -33561,13 +33966,13 @@ function signReport(reportPayload) {
|
|
|
33561
33966
|
}
|
|
33562
33967
|
function writeSignedJsonArtifact(path2, value) {
|
|
33563
33968
|
const absolutePath = (0, import_path3.resolve)(path2);
|
|
33564
|
-
(0,
|
|
33565
|
-
(0,
|
|
33969
|
+
(0, import_fs4.mkdirSync)((0, import_path3.dirname)(absolutePath), { recursive: true });
|
|
33970
|
+
(0, import_fs4.writeFileSync)(absolutePath, stableStringify(value), "utf-8");
|
|
33566
33971
|
return absolutePath;
|
|
33567
33972
|
}
|
|
33568
33973
|
|
|
33569
33974
|
// src/commands/manifest.ts
|
|
33570
|
-
var
|
|
33975
|
+
var import_fs5 = require("fs");
|
|
33571
33976
|
var import_picocolors3 = __toESM(require_picocolors());
|
|
33572
33977
|
function formatDiff(diffs) {
|
|
33573
33978
|
if (diffs.length === 0) return "No changes";
|
|
@@ -33603,7 +34008,7 @@ function formatDiff(diffs) {
|
|
|
33603
34008
|
function loadManifestFile(filePath) {
|
|
33604
34009
|
let raw;
|
|
33605
34010
|
try {
|
|
33606
|
-
raw = (0,
|
|
34011
|
+
raw = (0, import_fs5.readFileSync)(filePath, "utf-8");
|
|
33607
34012
|
} catch {
|
|
33608
34013
|
throw new FohError({
|
|
33609
34014
|
step: "manifest.load",
|
|
@@ -33871,27 +34276,32 @@ function optionNameToFlag(key) {
|
|
|
33871
34276
|
function buildMissingOptionsPlan(missing, opts) {
|
|
33872
34277
|
const missingFlags = missing.map(optionNameToFlag);
|
|
33873
34278
|
const signInUrl = buildConsoleSignInUrl(resolveConsoleBaseUrl(opts.consoleUrl));
|
|
33874
|
-
return {
|
|
34279
|
+
return cliEnvelope({
|
|
33875
34280
|
status: "blocked",
|
|
33876
|
-
|
|
33877
|
-
|
|
33878
|
-
|
|
33879
|
-
|
|
33880
|
-
next_commands: [
|
|
34281
|
+
reasonCode: "setup_required_options_missing",
|
|
34282
|
+
summary: "Setup requires an authenticated org, an agent template, and an agent name before it can mutate customer resources.",
|
|
34283
|
+
ids: {},
|
|
34284
|
+
nextCommands: [
|
|
33881
34285
|
"foh auth signup --web --json",
|
|
33882
34286
|
"foh auth login --web --json",
|
|
33883
34287
|
...buildCliAuthFallbackCommands(),
|
|
33884
34288
|
"foh templates list --json",
|
|
33885
34289
|
'foh setup --org <org-id> --agent-template <template-id> --agent-name "Demo Agent" --widget-domains <domain> --report-out setup-report.json --json'
|
|
33886
34290
|
],
|
|
33887
|
-
|
|
33888
|
-
|
|
33889
|
-
|
|
33890
|
-
"
|
|
33891
|
-
|
|
33892
|
-
|
|
33893
|
-
|
|
33894
|
-
|
|
34291
|
+
extra: {
|
|
34292
|
+
code: "setup_required_options_missing",
|
|
34293
|
+
missing_options: missingFlags,
|
|
34294
|
+
reason: "setup requires an authenticated org, an agent template, and an agent name before it can mutate customer resources",
|
|
34295
|
+
sign_in_url: signInUrl,
|
|
34296
|
+
text_fallback: buildCliAuthFallbackInstructions(signInUrl),
|
|
34297
|
+
ai_agent_instruction: [
|
|
34298
|
+
"Do not guess org IDs, template IDs, or customer domains.",
|
|
34299
|
+
"If no browser is available, print sign_in_url and ask the user to sign in.",
|
|
34300
|
+
"After auth, discover orgs and templates with the listed commands.",
|
|
34301
|
+
"Rerun setup only after all missing_options are resolved."
|
|
34302
|
+
]
|
|
34303
|
+
}
|
|
34304
|
+
});
|
|
33895
34305
|
}
|
|
33896
34306
|
function emitMissingOptionsPlan(missing, opts) {
|
|
33897
34307
|
const plan = buildMissingOptionsPlan(missing, { consoleUrl: opts.consoleUrl });
|
|
@@ -33997,7 +34407,7 @@ function registerSetup(program3) {
|
|
|
33997
34407
|
const startedAtIso = nowIso();
|
|
33998
34408
|
const startedAtMs = Date.now();
|
|
33999
34409
|
if (shouldResumeSkip(name)) {
|
|
34000
|
-
const
|
|
34410
|
+
const skipped2 = timedStepResult(
|
|
34001
34411
|
{
|
|
34002
34412
|
step: name,
|
|
34003
34413
|
status: "skipped",
|
|
@@ -34006,10 +34416,10 @@ function registerSetup(program3) {
|
|
|
34006
34416
|
startedAtIso,
|
|
34007
34417
|
startedAtMs
|
|
34008
34418
|
);
|
|
34009
|
-
completed.push(
|
|
34419
|
+
completed.push(skipped2);
|
|
34010
34420
|
process.stderr.write(import_picocolors4.default.dim(` [RESUME] ${name}: skipped (resume-from ${resumeState.resumeFrom})
|
|
34011
34421
|
`));
|
|
34012
|
-
return
|
|
34422
|
+
return skipped2;
|
|
34013
34423
|
}
|
|
34014
34424
|
if (opts.dryRun) {
|
|
34015
34425
|
const dryRunResult = timedStepResult(
|
|
@@ -34365,8 +34775,8 @@ function registerSetup(program3) {
|
|
|
34365
34775
|
}
|
|
34366
34776
|
try {
|
|
34367
34777
|
const manifest = await agentExport(resolvedAgentId, { apiUrlOverride: opts.apiUrl });
|
|
34368
|
-
const { writeFileSync:
|
|
34369
|
-
|
|
34778
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
34779
|
+
writeFileSync6(
|
|
34370
34780
|
"tenant.yaml",
|
|
34371
34781
|
`# tenant.yaml - Front Of House agent manifest
|
|
34372
34782
|
# Edit this file and run: foh plan tenant.yaml
|
|
@@ -34382,7 +34792,15 @@ ${serialiseManifest(manifest)}`,
|
|
|
34382
34792
|
});
|
|
34383
34793
|
const reportMeta = emitSetupReport("success");
|
|
34384
34794
|
const summary = {
|
|
34795
|
+
schema_version: "foh_cli_setup_summary.v1",
|
|
34796
|
+
ok: true,
|
|
34385
34797
|
status: "success",
|
|
34798
|
+
reason_code: "setup_completed",
|
|
34799
|
+
summary: "Setup completed and produced a signed setup report.",
|
|
34800
|
+
ids: {
|
|
34801
|
+
org_id: opts.org,
|
|
34802
|
+
agent_id: agentId ?? null
|
|
34803
|
+
},
|
|
34386
34804
|
org_id: opts.org,
|
|
34387
34805
|
agent_id: agentId,
|
|
34388
34806
|
phone_number: phoneNumber,
|
|
@@ -34394,7 +34812,15 @@ ${serialiseManifest(manifest)}`,
|
|
|
34394
34812
|
manifest_written: manifestWritten ? "tenant.yaml" : null,
|
|
34395
34813
|
resume_from: resumeState.resumeFrom,
|
|
34396
34814
|
setup_report_hash: reportMeta.reportHash,
|
|
34397
|
-
setup_report_path: reportMeta.reportPath
|
|
34815
|
+
setup_report_path: reportMeta.reportPath,
|
|
34816
|
+
artifacts: {
|
|
34817
|
+
setup_report_path: reportMeta.reportPath,
|
|
34818
|
+
tenant_manifest_path: manifestWritten ? "tenant.yaml" : null
|
|
34819
|
+
},
|
|
34820
|
+
next_commands: agentId ? [
|
|
34821
|
+
`foh prove --agent ${agentId} --json --out test-results/foh-proof.latest.json`,
|
|
34822
|
+
`foh agent publish --agent ${agentId} --json`
|
|
34823
|
+
] : []
|
|
34398
34824
|
};
|
|
34399
34825
|
format(summary, { json: opts.json ?? false });
|
|
34400
34826
|
} catch (error2) {
|
|
@@ -34407,13 +34833,25 @@ ${serialiseManifest(manifest)}`,
|
|
|
34407
34833
|
});
|
|
34408
34834
|
format(
|
|
34409
34835
|
{
|
|
34836
|
+
schema_version: "foh_cli_setup_failure.v1",
|
|
34837
|
+
ok: false,
|
|
34410
34838
|
status: "failed",
|
|
34839
|
+
reason_code: error2.reasonCode ?? "setup_failed",
|
|
34840
|
+
summary: error2.error,
|
|
34841
|
+
ids: {
|
|
34842
|
+
org_id: opts.org,
|
|
34843
|
+
agent_id: agentId ?? null
|
|
34844
|
+
},
|
|
34411
34845
|
step: error2.step,
|
|
34412
34846
|
completed_steps: completed.map((stepResult) => stepResult.step),
|
|
34413
34847
|
error: error2.error,
|
|
34414
34848
|
remediation: error2.remediation,
|
|
34415
34849
|
setup_report_hash: reportMeta.reportHash,
|
|
34416
|
-
setup_report_path: reportMeta.reportPath
|
|
34850
|
+
setup_report_path: reportMeta.reportPath,
|
|
34851
|
+
artifacts: {
|
|
34852
|
+
setup_report_path: reportMeta.reportPath
|
|
34853
|
+
},
|
|
34854
|
+
next_commands: error2.nextCommands ?? [error2.remediation.replace(/^Run:\s*/, "")].filter(Boolean)
|
|
34417
34855
|
},
|
|
34418
34856
|
{ json: opts.json ?? false }
|
|
34419
34857
|
);
|
|
@@ -34506,8 +34944,8 @@ function registerSim(program3) {
|
|
|
34506
34944
|
}
|
|
34507
34945
|
const cert = response.certificate;
|
|
34508
34946
|
if (opts.out) {
|
|
34509
|
-
const { writeFileSync:
|
|
34510
|
-
|
|
34947
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
34948
|
+
writeFileSync6(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
|
|
34511
34949
|
process.stderr.write(` Certificate written to ${opts.out}
|
|
34512
34950
|
`);
|
|
34513
34951
|
}
|
|
@@ -34557,8 +34995,8 @@ function registerSim(program3) {
|
|
|
34557
34995
|
});
|
|
34558
34996
|
}
|
|
34559
34997
|
if (opts.out) {
|
|
34560
|
-
const { writeFileSync:
|
|
34561
|
-
|
|
34998
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
34999
|
+
writeFileSync6(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
|
|
34562
35000
|
process.stderr.write(` Final certificate written to ${opts.out}
|
|
34563
35001
|
`);
|
|
34564
35002
|
}
|
|
@@ -34595,19 +35033,6 @@ ${passIcon} Certification loop summary
|
|
|
34595
35033
|
|
|
34596
35034
|
// src/commands/conversations.ts
|
|
34597
35035
|
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
35036
|
function registerConversations(program3) {
|
|
34612
35037
|
const conversations = program3.command("conversations").description("Search and operate on conversation traces and lead data");
|
|
34613
35038
|
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 +35194,227 @@ function registerConversations(program3) {
|
|
|
34769
35194
|
}));
|
|
34770
35195
|
}
|
|
34771
35196
|
|
|
35197
|
+
// src/commands/transcripts.ts
|
|
35198
|
+
var import_fs6 = require("fs");
|
|
35199
|
+
var import_path4 = require("path");
|
|
35200
|
+
function listPath(agentId, opts) {
|
|
35201
|
+
const params = new URLSearchParams();
|
|
35202
|
+
if (opts.q) params.set("q", String(opts.q));
|
|
35203
|
+
if (opts.from) params.set("from", String(opts.from));
|
|
35204
|
+
if (opts.to) params.set("to", String(opts.to));
|
|
35205
|
+
params.set("page", String(parsePositiveInt(opts.page, 1, 1, 1e4)));
|
|
35206
|
+
params.set("limit", String(parsePositiveInt(opts.limit, 20, 1, 100)));
|
|
35207
|
+
return withQuery(`/v1/console/agents/${agentId}/conversations`, params);
|
|
35208
|
+
}
|
|
35209
|
+
function jsonl(rows) {
|
|
35210
|
+
return rows.map((row) => JSON.stringify(row)).join("\n") + (rows.length > 0 ? "\n" : "");
|
|
35211
|
+
}
|
|
35212
|
+
async function hydrateRows(input) {
|
|
35213
|
+
const hydrated = [];
|
|
35214
|
+
for (const row of input.rows) {
|
|
35215
|
+
const conversationId = String(row.id || "").trim();
|
|
35216
|
+
if (!conversationId) {
|
|
35217
|
+
hydrated.push(row);
|
|
35218
|
+
continue;
|
|
35219
|
+
}
|
|
35220
|
+
const params = new URLSearchParams();
|
|
35221
|
+
if (input.includeTraces) params.set("include_traces", "true");
|
|
35222
|
+
const detail = await apiFetch(
|
|
35223
|
+
withQuery(`/v1/console/agents/${input.agentId}/conversations/${conversationId}`, params),
|
|
35224
|
+
{ orgId: input.orgId, apiUrlOverride: input.apiUrl }
|
|
35225
|
+
);
|
|
35226
|
+
hydrated.push({
|
|
35227
|
+
...detail.conversation || row,
|
|
35228
|
+
...input.includeTraces ? { traces: Array.isArray(detail.traces) ? detail.traces : [] } : {}
|
|
35229
|
+
});
|
|
35230
|
+
}
|
|
35231
|
+
return hydrated;
|
|
35232
|
+
}
|
|
35233
|
+
function registerTranscripts(program3) {
|
|
35234
|
+
const transcripts = program3.command("transcripts").description("List, fetch, and export conversation transcripts");
|
|
35235
|
+
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 () => {
|
|
35236
|
+
const data = await apiFetch(listPath(opts.agent, opts), { orgId: opts.org, apiUrlOverride: opts.apiUrl });
|
|
35237
|
+
format(data, { json: opts.json ?? false });
|
|
35238
|
+
}));
|
|
35239
|
+
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 () => {
|
|
35240
|
+
const params = new URLSearchParams();
|
|
35241
|
+
if (opts.includeTraces) params.set("include_traces", "true");
|
|
35242
|
+
const path2 = withQuery(`/v1/console/agents/${opts.agent}/conversations/${opts.conversation}`, params);
|
|
35243
|
+
const data = await apiFetch(path2, { orgId: opts.org, apiUrlOverride: opts.apiUrl });
|
|
35244
|
+
format(data, { json: opts.json ?? false });
|
|
35245
|
+
}));
|
|
35246
|
+
transcripts.command("export").description("Export recent transcripts as JSON or JSONL").requiredOption("--agent <id>", "Agent ID").option("--q <query>", "Full-text transcript query").option("--from <iso-date>", "Start datetime (ISO8601)").option("--to <iso-date>", "End datetime (ISO8601)").option("--limit <n>", "Rows to export (1-100)", "100").option("--format <value>", "Export format: jsonl or json", "jsonl").option("--hydrate", "Fetch full conversation detail for every exported row").option("--include-traces", "Hydrate each conversation with ordered trace events").option("--no-redact", "Disable default redaction of emails, phone numbers, and obvious secrets").option("--out <path>", "Output file path").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
|
|
35247
|
+
const exportFormat = String(opts.format || "jsonl").trim().toLowerCase();
|
|
35248
|
+
if (!["jsonl", "json"].includes(exportFormat)) {
|
|
35249
|
+
throw new FohError({
|
|
35250
|
+
step: "transcripts.export",
|
|
35251
|
+
error: `Invalid format: ${opts.format}`,
|
|
35252
|
+
remediation: "Use --format jsonl or --format json.",
|
|
35253
|
+
statusCode: 400
|
|
35254
|
+
});
|
|
35255
|
+
}
|
|
35256
|
+
const data = await apiFetch(listPath(opts.agent, { ...opts, page: "1" }), {
|
|
35257
|
+
orgId: opts.org,
|
|
35258
|
+
apiUrlOverride: opts.apiUrl
|
|
35259
|
+
});
|
|
35260
|
+
const listRows = Array.isArray(data.conversations) ? data.conversations : [];
|
|
35261
|
+
const shouldHydrate = Boolean(opts.hydrate || opts.includeTraces);
|
|
35262
|
+
const includeTraces = Boolean(opts.includeTraces);
|
|
35263
|
+
const rawRows = shouldHydrate ? await hydrateRows({
|
|
35264
|
+
agentId: opts.agent,
|
|
35265
|
+
rows: listRows,
|
|
35266
|
+
includeTraces,
|
|
35267
|
+
orgId: opts.org,
|
|
35268
|
+
apiUrl: opts.apiUrl
|
|
35269
|
+
}) : listRows;
|
|
35270
|
+
const replayReadyRows = rawRows.map((conversation) => buildReplayReadyConversation({
|
|
35271
|
+
agentId: opts.agent,
|
|
35272
|
+
conversation
|
|
35273
|
+
}));
|
|
35274
|
+
const rows = opts.redact === false ? replayReadyRows : redactObject(replayReadyRows);
|
|
35275
|
+
const exportPacket = {
|
|
35276
|
+
schema_version: "foh_transcript_export.v2",
|
|
35277
|
+
redaction: {
|
|
35278
|
+
enabled: opts.redact !== false,
|
|
35279
|
+
fields: opts.redact === false ? [] : ["email", "phone", "secret-like-token"]
|
|
35280
|
+
},
|
|
35281
|
+
hydration: {
|
|
35282
|
+
enabled: shouldHydrate,
|
|
35283
|
+
include_traces: includeTraces
|
|
35284
|
+
},
|
|
35285
|
+
conversations: rows
|
|
35286
|
+
};
|
|
35287
|
+
const content = exportFormat === "json" ? stableStringify(exportPacket) : jsonl(rows);
|
|
35288
|
+
if (opts.out) {
|
|
35289
|
+
const outputPath = (0, import_path4.resolve)(String(opts.out));
|
|
35290
|
+
(0, import_fs6.writeFileSync)(outputPath, content, "utf-8");
|
|
35291
|
+
format({
|
|
35292
|
+
schema_version: "foh_transcript_export_result.v1",
|
|
35293
|
+
ok: true,
|
|
35294
|
+
status: "exported",
|
|
35295
|
+
reason_code: "transcripts_exported",
|
|
35296
|
+
summary: `Exported ${rows.length} transcript row(s).`,
|
|
35297
|
+
ids: { agent_id: opts.agent },
|
|
35298
|
+
artifacts: { output_path: outputPath },
|
|
35299
|
+
next_commands: [
|
|
35300
|
+
`foh agent replay --agent ${opts.agent} --conversation <conversation-id> --json`,
|
|
35301
|
+
`foh test run --suite <suite.yml> --agent ${opts.agent} --json`
|
|
35302
|
+
],
|
|
35303
|
+
format: exportFormat,
|
|
35304
|
+
count: rows.length,
|
|
35305
|
+
redaction: exportPacket.redaction,
|
|
35306
|
+
hydration: exportPacket.hydration,
|
|
35307
|
+
output_path: outputPath
|
|
35308
|
+
}, { json: opts.json ?? false });
|
|
35309
|
+
return;
|
|
35310
|
+
}
|
|
35311
|
+
if (opts.json || exportFormat === "json") {
|
|
35312
|
+
format({
|
|
35313
|
+
schema_version: "foh_transcript_export.v1",
|
|
35314
|
+
ok: true,
|
|
35315
|
+
status: "pass",
|
|
35316
|
+
reason_code: "transcripts_exported",
|
|
35317
|
+
summary: `Prepared ${rows.length} transcript row(s).`,
|
|
35318
|
+
ids: { agent_id: opts.agent },
|
|
35319
|
+
artifacts: {},
|
|
35320
|
+
next_commands: [
|
|
35321
|
+
`foh agent replay --agent ${opts.agent} --conversation <conversation-id> --json`,
|
|
35322
|
+
`foh test run --suite <suite.yml> --agent ${opts.agent} --json`
|
|
35323
|
+
],
|
|
35324
|
+
redaction: exportPacket.redaction,
|
|
35325
|
+
hydration: exportPacket.hydration,
|
|
35326
|
+
conversations: exportPacket.conversations
|
|
35327
|
+
}, { json: opts.json ?? false });
|
|
35328
|
+
return;
|
|
35329
|
+
}
|
|
35330
|
+
process.stdout.write(content);
|
|
35331
|
+
}));
|
|
35332
|
+
}
|
|
35333
|
+
|
|
35334
|
+
// src/commands/analytics.ts
|
|
35335
|
+
function parsePreset(raw) {
|
|
35336
|
+
const value = String(raw || "7d").trim().toLowerCase();
|
|
35337
|
+
if (value === "today" || value === "7d" || value === "failed" || value === "lead-capture") return value;
|
|
35338
|
+
throw new FohError({
|
|
35339
|
+
step: "analytics.fetch",
|
|
35340
|
+
error: `Invalid preset: ${raw}`,
|
|
35341
|
+
remediation: "Use --preset today, 7d, failed, or lead-capture.",
|
|
35342
|
+
statusCode: 400
|
|
35343
|
+
});
|
|
35344
|
+
}
|
|
35345
|
+
function presetWindowDays(preset, rawWindowDays) {
|
|
35346
|
+
if (rawWindowDays !== void 0) return parsePositiveInt(String(rawWindowDays), preset === "today" ? 1 : 7, 1, 90);
|
|
35347
|
+
if (preset === "today") return 1;
|
|
35348
|
+
return 7;
|
|
35349
|
+
}
|
|
35350
|
+
function registerAnalytics(program3) {
|
|
35351
|
+
const analytics = program3.command("analytics").description("Fetch runtime analytics and inspection summaries");
|
|
35352
|
+
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 () => {
|
|
35353
|
+
const preset = parsePreset(opts.preset);
|
|
35354
|
+
const windowDays = presetWindowDays(preset, opts.windowDays);
|
|
35355
|
+
const qualityParams = new URLSearchParams({ windowDays: String(windowDays), environment: "production" });
|
|
35356
|
+
const conversationParams = new URLSearchParams({
|
|
35357
|
+
page: "1",
|
|
35358
|
+
limit: "10"
|
|
35359
|
+
});
|
|
35360
|
+
if (preset === "failed") conversationParams.set("terminal_state", "failed_visible");
|
|
35361
|
+
if (preset === "lead-capture") conversationParams.set("journey", "lead_capture");
|
|
35362
|
+
const [qualityScorecard, leadDataTrends, loopKpis, conversations, voiceSlo] = await Promise.all([
|
|
35363
|
+
apiFetch(`/v1/console/agents/${opts.agent}/quality-scorecard?${qualityParams.toString()}`, {
|
|
35364
|
+
orgId: opts.org,
|
|
35365
|
+
apiUrlOverride: opts.apiUrl
|
|
35366
|
+
}),
|
|
35367
|
+
apiFetch(`/v1/console/agents/${opts.agent}/lead-data-trends?windowDays=${windowDays}`, {
|
|
35368
|
+
orgId: opts.org,
|
|
35369
|
+
apiUrlOverride: opts.apiUrl
|
|
35370
|
+
}),
|
|
35371
|
+
apiFetch(`/v1/console/agents/${opts.agent}/loop-kpis?windowDays=${windowDays}`, {
|
|
35372
|
+
orgId: opts.org,
|
|
35373
|
+
apiUrlOverride: opts.apiUrl
|
|
35374
|
+
}),
|
|
35375
|
+
apiFetch(withQuery(`/v1/console/agents/${opts.agent}/conversations`, conversationParams), {
|
|
35376
|
+
orgId: opts.org,
|
|
35377
|
+
apiUrlOverride: opts.apiUrl
|
|
35378
|
+
}),
|
|
35379
|
+
apiFetch(`/v1/console/voice-slo?agentId=${opts.agent}&days=${windowDays}`, {
|
|
35380
|
+
orgId: opts.org,
|
|
35381
|
+
apiUrlOverride: opts.apiUrl
|
|
35382
|
+
}).catch((error2) => ({
|
|
35383
|
+
ok: false,
|
|
35384
|
+
skipped: true,
|
|
35385
|
+
reason_code: "voice_slo_unavailable",
|
|
35386
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
35387
|
+
}))
|
|
35388
|
+
]);
|
|
35389
|
+
format({
|
|
35390
|
+
schema_version: "foh_analytics_fetch.v1",
|
|
35391
|
+
ok: true,
|
|
35392
|
+
status: "pass",
|
|
35393
|
+
reason_code: "analytics_fetch_completed",
|
|
35394
|
+
summary: `Fetched analytics summary for preset ${preset}.`,
|
|
35395
|
+
ids: {
|
|
35396
|
+
agent_id: opts.agent
|
|
35397
|
+
},
|
|
35398
|
+
checks: [],
|
|
35399
|
+
artifacts: {},
|
|
35400
|
+
agent_id: opts.agent,
|
|
35401
|
+
preset,
|
|
35402
|
+
window_days: windowDays,
|
|
35403
|
+
summaries: {
|
|
35404
|
+
quality_scorecard: qualityScorecard,
|
|
35405
|
+
lead_data_trends: leadDataTrends,
|
|
35406
|
+
loop_kpis: loopKpis,
|
|
35407
|
+
conversations,
|
|
35408
|
+
voice_slo: voiceSlo
|
|
35409
|
+
},
|
|
35410
|
+
next_commands: [
|
|
35411
|
+
`foh transcripts list --agent ${opts.agent} --limit 10 --json`,
|
|
35412
|
+
`foh ops reporting weekly-report --agent ${opts.agent} --window-days ${Math.min(30, windowDays)} --json`
|
|
35413
|
+
]
|
|
35414
|
+
}, { json: opts.json ?? false });
|
|
35415
|
+
}));
|
|
35416
|
+
}
|
|
35417
|
+
|
|
34772
35418
|
// src/commands/tests.ts
|
|
34773
35419
|
function registerTests(program3) {
|
|
34774
35420
|
const tests = program3.command("tests").description("Manage agent test catalog and test-run lifecycle");
|
|
@@ -34953,19 +35599,361 @@ function registerTests(program3) {
|
|
|
34953
35599
|
}));
|
|
34954
35600
|
}
|
|
34955
35601
|
|
|
34956
|
-
// src/commands/
|
|
34957
|
-
|
|
34958
|
-
|
|
34959
|
-
|
|
34960
|
-
|
|
34961
|
-
|
|
34962
|
-
|
|
34963
|
-
|
|
34964
|
-
|
|
34965
|
-
|
|
34966
|
-
|
|
34967
|
-
|
|
34968
|
-
|
|
35602
|
+
// src/commands/test.ts
|
|
35603
|
+
var import_fs8 = require("fs");
|
|
35604
|
+
var import_path6 = require("path");
|
|
35605
|
+
|
|
35606
|
+
// src/lib/scenario-suite.ts
|
|
35607
|
+
var import_fs7 = require("fs");
|
|
35608
|
+
var import_path5 = require("path");
|
|
35609
|
+
function asStringList(value) {
|
|
35610
|
+
if (typeof value === "string" && value.trim()) return [value.trim()];
|
|
35611
|
+
if (Array.isArray(value)) return value.map((entry) => String(entry || "").trim()).filter(Boolean);
|
|
35612
|
+
return [];
|
|
35613
|
+
}
|
|
35614
|
+
function getPath(source, path2) {
|
|
35615
|
+
if (!source || typeof source !== "object") return void 0;
|
|
35616
|
+
return path2.split(".").reduce((current, part) => {
|
|
35617
|
+
if (!current || typeof current !== "object") return void 0;
|
|
35618
|
+
return current[part];
|
|
35619
|
+
}, source);
|
|
35620
|
+
}
|
|
35621
|
+
function valuesEqual(actual, expected) {
|
|
35622
|
+
if (Array.isArray(expected)) {
|
|
35623
|
+
return expected.some((entry) => valuesEqual(actual, entry));
|
|
35624
|
+
}
|
|
35625
|
+
if (expected && typeof expected === "object") {
|
|
35626
|
+
return JSON.stringify(actual) === JSON.stringify(expected);
|
|
35627
|
+
}
|
|
35628
|
+
return String(actual ?? "") === String(expected ?? "");
|
|
35629
|
+
}
|
|
35630
|
+
function parseStructuredFile(path2) {
|
|
35631
|
+
const raw = (0, import_fs7.readFileSync)(path2, "utf-8");
|
|
35632
|
+
return path2.toLowerCase().endsWith(".json") ? JSON.parse(raw) : load(raw);
|
|
35633
|
+
}
|
|
35634
|
+
function parseSuiteFile(path2) {
|
|
35635
|
+
const parsed = parseStructuredFile(path2);
|
|
35636
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
35637
|
+
throw new FohError({
|
|
35638
|
+
step: "test.run",
|
|
35639
|
+
error: "Suite file must contain an object",
|
|
35640
|
+
remediation: "Use a JSON/YAML object with scenarios[].turns[].",
|
|
35641
|
+
statusCode: 400
|
|
35642
|
+
});
|
|
35643
|
+
}
|
|
35644
|
+
return parsed;
|
|
35645
|
+
}
|
|
35646
|
+
function turnFromFixtureEntry(entry) {
|
|
35647
|
+
if (typeof entry === "string" && entry.trim()) return { user: entry.trim() };
|
|
35648
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
35649
|
+
const row = entry;
|
|
35650
|
+
const role = typeof row.role === "string" ? row.role.toLowerCase() : "";
|
|
35651
|
+
if (role && role !== "user") return null;
|
|
35652
|
+
const content = row.user ?? row.message ?? row.content ?? row.text;
|
|
35653
|
+
if (typeof content !== "string" || !content.trim()) return null;
|
|
35654
|
+
return {
|
|
35655
|
+
user: content.trim(),
|
|
35656
|
+
expect: row.expect && typeof row.expect === "object" && !Array.isArray(row.expect) ? row.expect : void 0
|
|
35657
|
+
};
|
|
35658
|
+
}
|
|
35659
|
+
function loadFixtureTurns(path2) {
|
|
35660
|
+
const parsed = parseStructuredFile(path2);
|
|
35661
|
+
const entries = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.turns) ? parsed.turns : parsed && typeof parsed === "object" && Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
35662
|
+
return entries.map(turnFromFixtureEntry).filter((entry) => !!entry);
|
|
35663
|
+
}
|
|
35664
|
+
function resolveScenarioTurns(scenario, suitePath) {
|
|
35665
|
+
const inlineTurns = Array.isArray(scenario.turns) ? scenario.turns : [];
|
|
35666
|
+
const fixture = scenario.transcript_fixture || scenario.fixture_transcript || scenario.fixture;
|
|
35667
|
+
if (!fixture) return inlineTurns;
|
|
35668
|
+
const fixturePath = (0, import_path5.resolve)(suitePath, "..", fixture);
|
|
35669
|
+
const fixtureTurns = loadFixtureTurns(fixturePath);
|
|
35670
|
+
return [...fixtureTurns, ...inlineTurns];
|
|
35671
|
+
}
|
|
35672
|
+
function validateSuite(suite, suitePath) {
|
|
35673
|
+
const scenarios = Array.isArray(suite.scenarios) ? suite.scenarios : [];
|
|
35674
|
+
if (scenarios.length === 0) {
|
|
35675
|
+
throw new FohError({
|
|
35676
|
+
step: "test.run",
|
|
35677
|
+
error: "Suite contains no scenarios",
|
|
35678
|
+
remediation: "Add at least one scenarios[] entry with turns[].",
|
|
35679
|
+
statusCode: 400
|
|
35680
|
+
});
|
|
35681
|
+
}
|
|
35682
|
+
return scenarios.map((scenario) => {
|
|
35683
|
+
const turns = resolveScenarioTurns(scenario, suitePath);
|
|
35684
|
+
if (turns.length === 0) {
|
|
35685
|
+
throw new FohError({
|
|
35686
|
+
step: "test.run",
|
|
35687
|
+
error: `Scenario "${scenario.id || scenario.name || "(unnamed)"}" has no turns`,
|
|
35688
|
+
remediation: "Add inline turns or a fixture_transcript file with user messages.",
|
|
35689
|
+
statusCode: 400
|
|
35690
|
+
});
|
|
35691
|
+
}
|
|
35692
|
+
return { ...scenario, turns };
|
|
35693
|
+
});
|
|
35694
|
+
}
|
|
35695
|
+
function evaluateReply(reply, expect) {
|
|
35696
|
+
const lowerReply = reply.toLowerCase();
|
|
35697
|
+
const contains = asStringList(expect?.contains);
|
|
35698
|
+
const notContains = asStringList(expect?.not_contains);
|
|
35699
|
+
const failures = [];
|
|
35700
|
+
for (const expected of contains) {
|
|
35701
|
+
if (!lowerReply.includes(expected.toLowerCase())) {
|
|
35702
|
+
failures.push(`missing expected text: ${expected}`);
|
|
35703
|
+
}
|
|
35704
|
+
}
|
|
35705
|
+
for (const forbidden of notContains) {
|
|
35706
|
+
if (lowerReply.includes(forbidden.toLowerCase())) {
|
|
35707
|
+
failures.push(`contained forbidden text: ${forbidden}`);
|
|
35708
|
+
}
|
|
35709
|
+
}
|
|
35710
|
+
return failures;
|
|
35711
|
+
}
|
|
35712
|
+
function pickVariables(response) {
|
|
35713
|
+
for (const key of ["variables", "context_variables", "lead_data", "extracted_variables"]) {
|
|
35714
|
+
const value = response[key];
|
|
35715
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
35716
|
+
}
|
|
35717
|
+
return null;
|
|
35718
|
+
}
|
|
35719
|
+
function pickToolCalls(response) {
|
|
35720
|
+
for (const path2 of ["tool_calls", "toolCalls", "telemetry.tool_calls", "trace.tool_calls"]) {
|
|
35721
|
+
const value = getPath(response, path2);
|
|
35722
|
+
if (Array.isArray(value)) return value;
|
|
35723
|
+
}
|
|
35724
|
+
return [];
|
|
35725
|
+
}
|
|
35726
|
+
function toolCallName(call) {
|
|
35727
|
+
if (typeof call === "string") return call;
|
|
35728
|
+
if (!call || typeof call !== "object") return "";
|
|
35729
|
+
const row = call;
|
|
35730
|
+
return String(row.name ?? row.tool ?? row.tool_id ?? row.id ?? getPath(row, "function.name") ?? "");
|
|
35731
|
+
}
|
|
35732
|
+
function pickLeadCapture(response) {
|
|
35733
|
+
for (const key of ["lead_capture", "lead", "lead_data"]) {
|
|
35734
|
+
const value = response[key];
|
|
35735
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
35736
|
+
}
|
|
35737
|
+
if (response.lead_id || response.leadId) {
|
|
35738
|
+
return { id: response.lead_id ?? response.leadId };
|
|
35739
|
+
}
|
|
35740
|
+
return null;
|
|
35741
|
+
}
|
|
35742
|
+
function evaluateStructuredExpectations(response, expect, latencyMs) {
|
|
35743
|
+
if (!expect) return [];
|
|
35744
|
+
const failures = [];
|
|
35745
|
+
if (typeof expect.trace_present === "boolean") {
|
|
35746
|
+
const present = Boolean(response.trace_id);
|
|
35747
|
+
if (present !== expect.trace_present) failures.push(`trace presence expected ${expect.trace_present} but got ${present}`);
|
|
35748
|
+
}
|
|
35749
|
+
if (typeof expect.correlation_present === "boolean") {
|
|
35750
|
+
const present = Boolean(response.correlation_id);
|
|
35751
|
+
if (present !== expect.correlation_present) failures.push(`correlation presence expected ${expect.correlation_present} but got ${present}`);
|
|
35752
|
+
}
|
|
35753
|
+
const expectedActions = asStringList(expect.action);
|
|
35754
|
+
if (expectedActions.length > 0) {
|
|
35755
|
+
const actual = String(response.action ?? "");
|
|
35756
|
+
if (!expectedActions.includes(actual)) failures.push(`action expected ${expectedActions.join("|")} but got ${actual || "(empty)"}`);
|
|
35757
|
+
}
|
|
35758
|
+
const expectedTerminalStates = asStringList(expect.terminal_state);
|
|
35759
|
+
if (expectedTerminalStates.length > 0) {
|
|
35760
|
+
const actual = String(response.terminal_state ?? response.terminalState ?? response.action ?? "");
|
|
35761
|
+
if (!expectedTerminalStates.includes(actual)) {
|
|
35762
|
+
failures.push(`terminal_state expected ${expectedTerminalStates.join("|")} but got ${actual || "(empty)"}`);
|
|
35763
|
+
}
|
|
35764
|
+
}
|
|
35765
|
+
const minLatency = expect.latency_ms?.min;
|
|
35766
|
+
const maxLatency = expect.latency_ms?.max ?? expect.max_latency_ms;
|
|
35767
|
+
if (typeof minLatency === "number" && latencyMs < minLatency) {
|
|
35768
|
+
failures.push(`latency_ms expected >=${minLatency} but got ${latencyMs}`);
|
|
35769
|
+
}
|
|
35770
|
+
if (typeof maxLatency === "number" && latencyMs > maxLatency) {
|
|
35771
|
+
failures.push(`latency_ms expected <=${maxLatency} but got ${latencyMs}`);
|
|
35772
|
+
}
|
|
35773
|
+
if (expect.variables && typeof expect.variables === "object") {
|
|
35774
|
+
const variables = pickVariables(response);
|
|
35775
|
+
if (!variables) failures.push("variables expected but response had none");
|
|
35776
|
+
else {
|
|
35777
|
+
for (const [path2, expected] of Object.entries(expect.variables)) {
|
|
35778
|
+
const actual = getPath(variables, path2);
|
|
35779
|
+
if (!valuesEqual(actual, expected)) failures.push(`variables.${path2} expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`);
|
|
35780
|
+
}
|
|
35781
|
+
}
|
|
35782
|
+
}
|
|
35783
|
+
if (expect.tool_calls) {
|
|
35784
|
+
const calls = pickToolCalls(response);
|
|
35785
|
+
const names = calls.map(toolCallName).filter(Boolean);
|
|
35786
|
+
const includes = asStringList(expect.tool_calls.includes);
|
|
35787
|
+
const excludes = asStringList(expect.tool_calls.excludes);
|
|
35788
|
+
for (const name of includes) {
|
|
35789
|
+
if (!names.includes(name)) failures.push(`tool_calls missing expected tool: ${name}`);
|
|
35790
|
+
}
|
|
35791
|
+
for (const name of excludes) {
|
|
35792
|
+
if (names.includes(name)) failures.push(`tool_calls contained forbidden tool: ${name}`);
|
|
35793
|
+
}
|
|
35794
|
+
if (typeof expect.tool_calls.min_count === "number" && calls.length < expect.tool_calls.min_count) {
|
|
35795
|
+
failures.push(`tool_calls expected >=${expect.tool_calls.min_count} but got ${calls.length}`);
|
|
35796
|
+
}
|
|
35797
|
+
if (typeof expect.tool_calls.max_count === "number" && calls.length > expect.tool_calls.max_count) {
|
|
35798
|
+
failures.push(`tool_calls expected <=${expect.tool_calls.max_count} but got ${calls.length}`);
|
|
35799
|
+
}
|
|
35800
|
+
}
|
|
35801
|
+
if (expect.escalation) {
|
|
35802
|
+
const requested = Boolean(getPath(response, "escalation.requested") ?? response.handoff?.requested);
|
|
35803
|
+
if (typeof expect.escalation.requested === "boolean" && requested !== expect.escalation.requested) {
|
|
35804
|
+
failures.push(`escalation.requested expected ${expect.escalation.requested} but got ${requested}`);
|
|
35805
|
+
}
|
|
35806
|
+
const expectedReasons = asStringList(expect.escalation.reason);
|
|
35807
|
+
if (expectedReasons.length > 0) {
|
|
35808
|
+
const actual = String(getPath(response, "escalation.reason") ?? response.handoff?.reason ?? "");
|
|
35809
|
+
if (!expectedReasons.includes(actual)) failures.push(`escalation.reason expected ${expectedReasons.join("|")} but got ${actual || "(empty)"}`);
|
|
35810
|
+
}
|
|
35811
|
+
}
|
|
35812
|
+
if (expect.lead_capture) {
|
|
35813
|
+
const lead = pickLeadCapture(response);
|
|
35814
|
+
if (expect.lead_capture.required === true && !lead) failures.push("lead_capture expected but response had none");
|
|
35815
|
+
for (const field of expect.lead_capture.fields || []) {
|
|
35816
|
+
const actual = lead ? getPath(lead, field) : void 0;
|
|
35817
|
+
if (actual == null || actual === "") failures.push(`lead_capture.${field} missing`);
|
|
35818
|
+
}
|
|
35819
|
+
}
|
|
35820
|
+
if (expect.fields && typeof expect.fields === "object") {
|
|
35821
|
+
for (const [path2, expected] of Object.entries(expect.fields)) {
|
|
35822
|
+
const actual = getPath(response, path2);
|
|
35823
|
+
if (!valuesEqual(actual, expected)) failures.push(`fields.${path2} expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`);
|
|
35824
|
+
}
|
|
35825
|
+
}
|
|
35826
|
+
return failures;
|
|
35827
|
+
}
|
|
35828
|
+
|
|
35829
|
+
// src/commands/test.ts
|
|
35830
|
+
function registerTest(program3) {
|
|
35831
|
+
const test = program3.command("test").description("Run local scenario suites against runtime channels");
|
|
35832
|
+
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 () => {
|
|
35833
|
+
const suitePath = (0, import_path6.resolve)(String(opts.suite));
|
|
35834
|
+
const suite = parseSuiteFile(suitePath);
|
|
35835
|
+
const agentId = String(opts.agent || suite.agent || "").trim();
|
|
35836
|
+
if (!agentId) {
|
|
35837
|
+
throw new FohError({
|
|
35838
|
+
step: "test.run",
|
|
35839
|
+
error: "Missing agent ID",
|
|
35840
|
+
remediation: "Pass --agent <id> or include agent in the suite file.",
|
|
35841
|
+
statusCode: 400
|
|
35842
|
+
});
|
|
35843
|
+
}
|
|
35844
|
+
const scenarios = validateSuite(suite, suitePath);
|
|
35845
|
+
const ensure = await apiFetch("/v1/console/channels/widget/ensure", {
|
|
35846
|
+
method: "POST",
|
|
35847
|
+
body: JSON.stringify({ agentId }),
|
|
35848
|
+
orgId: opts.org,
|
|
35849
|
+
apiUrlOverride: opts.apiUrl
|
|
35850
|
+
});
|
|
35851
|
+
const publicKey = ensure.channel?.public_key;
|
|
35852
|
+
if (!publicKey) {
|
|
35853
|
+
throw new FohError({
|
|
35854
|
+
step: "test.run",
|
|
35855
|
+
error: "Widget channel public key missing",
|
|
35856
|
+
remediation: `Run: foh widget ensure --agent ${agentId} --json`,
|
|
35857
|
+
statusCode: 409
|
|
35858
|
+
});
|
|
35859
|
+
}
|
|
35860
|
+
let passed = 0;
|
|
35861
|
+
let failed = 0;
|
|
35862
|
+
const scenarioResults = [];
|
|
35863
|
+
for (let scenarioIndex = 0; scenarioIndex < scenarios.length; scenarioIndex += 1) {
|
|
35864
|
+
const scenario = scenarios[scenarioIndex];
|
|
35865
|
+
let conversationId;
|
|
35866
|
+
const turnResults = [];
|
|
35867
|
+
for (let turnIndex = 0; turnIndex < scenario.turns.length; turnIndex += 1) {
|
|
35868
|
+
const turn = scenario.turns[turnIndex];
|
|
35869
|
+
const message = String(turn.user || turn.message || "").trim();
|
|
35870
|
+
if (!message) {
|
|
35871
|
+
failed += 1;
|
|
35872
|
+
turnResults.push({ turn: turnIndex + 1, ok: false, failures: ["missing user/message"] });
|
|
35873
|
+
continue;
|
|
35874
|
+
}
|
|
35875
|
+
const start = Date.now();
|
|
35876
|
+
const response = await apiFetch("/v1/widget/inbound", {
|
|
35877
|
+
method: "POST",
|
|
35878
|
+
body: JSON.stringify({
|
|
35879
|
+
channel_public_key: publicKey,
|
|
35880
|
+
message_body: message,
|
|
35881
|
+
preview: true,
|
|
35882
|
+
...conversationId ? { conversation_id: conversationId } : {}
|
|
35883
|
+
}),
|
|
35884
|
+
apiUrlOverride: opts.apiUrl
|
|
35885
|
+
});
|
|
35886
|
+
const latencyMs = Date.now() - start;
|
|
35887
|
+
conversationId = response.conversationId || conversationId;
|
|
35888
|
+
const reply = String(response.reply || "");
|
|
35889
|
+
const failures = reply ? [
|
|
35890
|
+
...evaluateReply(reply, turn.expect),
|
|
35891
|
+
...evaluateStructuredExpectations(response, turn.expect, latencyMs)
|
|
35892
|
+
] : ["empty reply"];
|
|
35893
|
+
if (failures.length === 0) passed += 1;
|
|
35894
|
+
else failed += 1;
|
|
35895
|
+
turnResults.push({
|
|
35896
|
+
turn: turnIndex + 1,
|
|
35897
|
+
ok: failures.length === 0,
|
|
35898
|
+
message,
|
|
35899
|
+
reply,
|
|
35900
|
+
failures,
|
|
35901
|
+
latency_ms: latencyMs,
|
|
35902
|
+
conversation_id: response.conversationId ?? null,
|
|
35903
|
+
trace_id: response.trace_id ?? null,
|
|
35904
|
+
correlation_id: response.correlation_id ?? null,
|
|
35905
|
+
action: response.action ?? null,
|
|
35906
|
+
handoff: response.handoff ?? null
|
|
35907
|
+
});
|
|
35908
|
+
}
|
|
35909
|
+
scenarioResults.push({
|
|
35910
|
+
id: scenario.id || `scenario-${scenarioIndex + 1}`,
|
|
35911
|
+
name: scenario.name ?? null,
|
|
35912
|
+
ok: turnResults.every((turn) => turn.ok),
|
|
35913
|
+
turns: turnResults
|
|
35914
|
+
});
|
|
35915
|
+
}
|
|
35916
|
+
const report = {
|
|
35917
|
+
schema_version: "foh_local_scenario_suite_report.v1",
|
|
35918
|
+
ok: failed === 0,
|
|
35919
|
+
status: failed === 0 ? "pass" : "fail",
|
|
35920
|
+
reason_code: failed === 0 ? "scenario_suite_passed" : "scenario_suite_failed",
|
|
35921
|
+
summary: failed === 0 ? "All scenario-suite assertions passed." : `${failed} scenario-suite assertion(s) failed.`,
|
|
35922
|
+
ids: {
|
|
35923
|
+
agent_id: agentId
|
|
35924
|
+
},
|
|
35925
|
+
artifacts: {},
|
|
35926
|
+
next_commands: failed === 0 ? [`foh prove --agent ${agentId} --json`] : [`foh transcripts list --agent ${agentId} --limit 10 --json`],
|
|
35927
|
+
suite_path: suitePath,
|
|
35928
|
+
agent_id: agentId,
|
|
35929
|
+
passed,
|
|
35930
|
+
failed,
|
|
35931
|
+
scenarios: scenarioResults
|
|
35932
|
+
};
|
|
35933
|
+
if (opts.out) {
|
|
35934
|
+
const out = (0, import_path6.resolve)(String(opts.out));
|
|
35935
|
+
(0, import_fs8.writeFileSync)(out, stableStringify(report), "utf-8");
|
|
35936
|
+
format({ ...report, output_path: out }, { json: opts.json ?? false });
|
|
35937
|
+
} else {
|
|
35938
|
+
format(report, { json: opts.json ?? false });
|
|
35939
|
+
}
|
|
35940
|
+
if (failed > 0) markCommandFailed(1);
|
|
35941
|
+
}));
|
|
35942
|
+
}
|
|
35943
|
+
|
|
35944
|
+
// src/commands/ops.ts
|
|
35945
|
+
function parseClientErrorSource(raw) {
|
|
35946
|
+
if (!raw) return void 0;
|
|
35947
|
+
const source = String(raw).trim().toLowerCase();
|
|
35948
|
+
if (source === "window" || source === "react" || source === "unhandledrejection") {
|
|
35949
|
+
return source;
|
|
35950
|
+
}
|
|
35951
|
+
throw new FohError({
|
|
35952
|
+
step: "ops.client-errors.report",
|
|
35953
|
+
error: `Invalid --source "${raw}"`,
|
|
35954
|
+
remediation: "Use one of: window, react, unhandledrejection.",
|
|
35955
|
+
statusCode: 400
|
|
35956
|
+
});
|
|
34969
35957
|
}
|
|
34970
35958
|
function registerOps(program3) {
|
|
34971
35959
|
const ops = program3.command("ops").description("Operational/reporting workflows (incidents, recommendations, traces, evidence)");
|
|
@@ -35367,8 +36355,210 @@ function registerDiag(program3) {
|
|
|
35367
36355
|
}
|
|
35368
36356
|
|
|
35369
36357
|
// src/commands/bug.ts
|
|
35370
|
-
var
|
|
35371
|
-
var
|
|
36358
|
+
var import_fs10 = require("fs");
|
|
36359
|
+
var import_path8 = require("path");
|
|
36360
|
+
|
|
36361
|
+
// src/lib/improvement-packet.ts
|
|
36362
|
+
var import_fs9 = require("fs");
|
|
36363
|
+
var import_path7 = require("path");
|
|
36364
|
+
var IMPROVEMENT_SOURCE_TYPES = [
|
|
36365
|
+
"setup_failure",
|
|
36366
|
+
"proof_failure",
|
|
36367
|
+
"replay_failure",
|
|
36368
|
+
"knowledge_miss",
|
|
36369
|
+
"runtime_miss",
|
|
36370
|
+
"live_proof_failure"
|
|
36371
|
+
];
|
|
36372
|
+
var IMPROVEMENT_DECISIONS = [
|
|
36373
|
+
"ignore",
|
|
36374
|
+
"fix_docs",
|
|
36375
|
+
"fix_config",
|
|
36376
|
+
"fix_runtime",
|
|
36377
|
+
"add_test"
|
|
36378
|
+
];
|
|
36379
|
+
var SECRET_LEAK_RE = /\b(?:sk|pk|xai|whsec|EAAN)[A-Za-z0-9_\-]{16,}\b/i;
|
|
36380
|
+
var MAX_REDACTED_SOURCE_BYTES = 4e3;
|
|
36381
|
+
function asRecord2(value) {
|
|
36382
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
36383
|
+
}
|
|
36384
|
+
function nonEmpty2(value) {
|
|
36385
|
+
const text = String(value ?? "").trim();
|
|
36386
|
+
return text.length > 0 ? text : void 0;
|
|
36387
|
+
}
|
|
36388
|
+
function getPath2(value, path2) {
|
|
36389
|
+
let current = value;
|
|
36390
|
+
for (const segment of path2.split(".")) {
|
|
36391
|
+
const record2 = asRecord2(current);
|
|
36392
|
+
if (!record2) return void 0;
|
|
36393
|
+
current = record2[segment];
|
|
36394
|
+
}
|
|
36395
|
+
return current;
|
|
36396
|
+
}
|
|
36397
|
+
function parseEnum(raw, allowed, label) {
|
|
36398
|
+
const value = nonEmpty2(raw);
|
|
36399
|
+
if (!value) return void 0;
|
|
36400
|
+
if (allowed.includes(value)) return value;
|
|
36401
|
+
throw new FohError({
|
|
36402
|
+
step: "bug.improve",
|
|
36403
|
+
error: `Invalid ${label} "${value}"`,
|
|
36404
|
+
remediation: `Use one of: ${allowed.join(", ")}.`,
|
|
36405
|
+
statusCode: 400
|
|
36406
|
+
});
|
|
36407
|
+
}
|
|
36408
|
+
function inferSourceType(artifact) {
|
|
36409
|
+
const schema2 = nonEmpty2(getPath2(artifact, "schema_version")) || "";
|
|
36410
|
+
const status = nonEmpty2(getPath2(artifact, "status")) || "";
|
|
36411
|
+
if (schema2.includes("knowledge_query") || nonEmpty2(getPath2(artifact, "failure_packet.schema_version"))?.includes("knowledge_query")) {
|
|
36412
|
+
return "knowledge_miss";
|
|
36413
|
+
}
|
|
36414
|
+
if (schema2.includes("agent_replay") || status.includes("replay")) return "replay_failure";
|
|
36415
|
+
if (schema2.includes("proof") || schema2.includes("live_proof")) return schema2.includes("live") ? "live_proof_failure" : "proof_failure";
|
|
36416
|
+
if (schema2.includes("setup")) return "setup_failure";
|
|
36417
|
+
return "runtime_miss";
|
|
36418
|
+
}
|
|
36419
|
+
function inferReasonCode(artifact) {
|
|
36420
|
+
const direct = nonEmpty2(getPath2(artifact, "reason_code"));
|
|
36421
|
+
if (direct) return direct;
|
|
36422
|
+
const nested = nonEmpty2(getPath2(artifact, "failure_packet.reason_code"));
|
|
36423
|
+
if (nested) return nested;
|
|
36424
|
+
const checks = getPath2(artifact, "checks");
|
|
36425
|
+
if (Array.isArray(checks)) {
|
|
36426
|
+
for (const check2 of checks) {
|
|
36427
|
+
const reason = nonEmpty2(getPath2(check2, "reason_code"));
|
|
36428
|
+
const status = nonEmpty2(getPath2(check2, "status"));
|
|
36429
|
+
if (reason && status !== "pass" && status !== "success") return reason;
|
|
36430
|
+
}
|
|
36431
|
+
}
|
|
36432
|
+
return nonEmpty2(getPath2(artifact, "status"));
|
|
36433
|
+
}
|
|
36434
|
+
function inferPromotionDecision(sourceType) {
|
|
36435
|
+
if (sourceType === "knowledge_miss") return "fix_docs";
|
|
36436
|
+
if (sourceType === "setup_failure" || sourceType === "proof_failure" || sourceType === "live_proof_failure") return "fix_config";
|
|
36437
|
+
if (sourceType === "replay_failure" || sourceType === "runtime_miss") return "add_test";
|
|
36438
|
+
return "fix_runtime";
|
|
36439
|
+
}
|
|
36440
|
+
function collectIds(artifact, explicit = {}) {
|
|
36441
|
+
const source = getPath2(artifact, "source");
|
|
36442
|
+
const ids = {
|
|
36443
|
+
org_id: explicit.org_id ?? nonEmpty2(getPath2(artifact, "ids.org_id")) ?? nonEmpty2(getPath2(artifact, "org_id")),
|
|
36444
|
+
agent_id: explicit.agent_id ?? nonEmpty2(getPath2(source, "agent_id")) ?? nonEmpty2(getPath2(artifact, "ids.agent_id")) ?? nonEmpty2(getPath2(artifact, "agent_id")),
|
|
36445
|
+
conversation_id: explicit.conversation_id ?? nonEmpty2(getPath2(source, "conversation_id")) ?? nonEmpty2(getPath2(artifact, "ids.conversation_id")) ?? nonEmpty2(getPath2(artifact, "conversation_id")),
|
|
36446
|
+
trace_id: explicit.trace_id ?? nonEmpty2(getPath2(source, "trace_id")) ?? nonEmpty2(getPath2(artifact, "ids.trace_id")) ?? nonEmpty2(getPath2(artifact, "trace_id")),
|
|
36447
|
+
correlation_id: explicit.correlation_id ?? nonEmpty2(getPath2(artifact, "ids.correlation_id")) ?? nonEmpty2(getPath2(artifact, "correlation_id")),
|
|
36448
|
+
proof_artifact: explicit.proof_artifact
|
|
36449
|
+
};
|
|
36450
|
+
return Object.fromEntries(Object.entries(ids).filter(([, value]) => value !== void 0));
|
|
36451
|
+
}
|
|
36452
|
+
function compactSourceArtifact(artifact) {
|
|
36453
|
+
const redacted = redactObject(artifact);
|
|
36454
|
+
const text = JSON.stringify(redacted);
|
|
36455
|
+
if (text.length <= MAX_REDACTED_SOURCE_BYTES) {
|
|
36456
|
+
return { truncated: false, value: redacted };
|
|
36457
|
+
}
|
|
36458
|
+
return {
|
|
36459
|
+
truncated: true,
|
|
36460
|
+
bytes_before_truncate: text.length,
|
|
36461
|
+
value_preview: text.slice(0, MAX_REDACTED_SOURCE_BYTES)
|
|
36462
|
+
};
|
|
36463
|
+
}
|
|
36464
|
+
function defaultNextCommands(input) {
|
|
36465
|
+
const commands = [];
|
|
36466
|
+
if (input.ids.trace_id && input.ids.agent_id) {
|
|
36467
|
+
commands.push(`foh tests from-trace --agent ${input.ids.agent_id} --trace ${input.ids.trace_id} --json`);
|
|
36468
|
+
commands.push(`foh agent replay --agent ${input.ids.agent_id} --trace ${input.ids.trace_id} --json`);
|
|
36469
|
+
}
|
|
36470
|
+
if (input.ids.conversation_id && input.ids.agent_id) {
|
|
36471
|
+
commands.push(`foh agent replay --agent ${input.ids.agent_id} --conversation ${input.ids.conversation_id} --json`);
|
|
36472
|
+
}
|
|
36473
|
+
if (input.sourceType === "knowledge_miss" && input.ids.agent_id) {
|
|
36474
|
+
commands.push(`foh knowledge query --agent ${input.ids.agent_id} --text "<question>" --explain --json`);
|
|
36475
|
+
}
|
|
36476
|
+
if (input.sourceArtifactPath) {
|
|
36477
|
+
commands.push(`foh bug report --out test-results/bug-report.from-improvement.json --command "investigate ${input.reasonCode}" --request-url https://front-of-house-api.stldocs.app/api --response-status 500 --next-check "Review ${(0, import_path7.basename)(input.sourceArtifactPath)}" --json`);
|
|
36478
|
+
}
|
|
36479
|
+
return commands;
|
|
36480
|
+
}
|
|
36481
|
+
function assertOrgBoundary(artifact, explicitOrgId) {
|
|
36482
|
+
const artifactOrgId = nonEmpty2(getPath2(artifact, "ids.org_id")) ?? nonEmpty2(getPath2(artifact, "org_id"));
|
|
36483
|
+
if (explicitOrgId && artifactOrgId && explicitOrgId !== artifactOrgId) {
|
|
36484
|
+
throw new FohError({
|
|
36485
|
+
step: "bug.improve",
|
|
36486
|
+
error: "Org boundary check failed for improvement packet.",
|
|
36487
|
+
remediation: "Use the org id from the source artifact, or omit --org when building a local redacted packet.",
|
|
36488
|
+
statusCode: 403
|
|
36489
|
+
});
|
|
36490
|
+
}
|
|
36491
|
+
}
|
|
36492
|
+
function assertRedacted(value) {
|
|
36493
|
+
const text = JSON.stringify(value);
|
|
36494
|
+
if (SECRET_LEAK_RE.test(text)) {
|
|
36495
|
+
throw new FohError({
|
|
36496
|
+
step: "bug.improve",
|
|
36497
|
+
error: "Improvement packet still contains a secret-like value after redaction.",
|
|
36498
|
+
remediation: "Remove raw credentials from the source artifact and rebuild the packet.",
|
|
36499
|
+
statusCode: 400
|
|
36500
|
+
});
|
|
36501
|
+
}
|
|
36502
|
+
}
|
|
36503
|
+
function readSourceArtifact(path2) {
|
|
36504
|
+
if (!path2) return null;
|
|
36505
|
+
try {
|
|
36506
|
+
return JSON.parse((0, import_fs9.readFileSync)(path2, "utf-8"));
|
|
36507
|
+
} catch (error2) {
|
|
36508
|
+
throw new FohError({
|
|
36509
|
+
step: "bug.improve",
|
|
36510
|
+
error: `Failed to read source artifact: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
36511
|
+
remediation: "Pass --from-file with a readable JSON artifact.",
|
|
36512
|
+
statusCode: 400
|
|
36513
|
+
});
|
|
36514
|
+
}
|
|
36515
|
+
}
|
|
36516
|
+
function buildImprovementPacket(input) {
|
|
36517
|
+
const artifact = input.sourceArtifact ?? null;
|
|
36518
|
+
const sourceType = parseEnum(input.sourceType, IMPROVEMENT_SOURCE_TYPES, "--source-type") ?? inferSourceType(artifact);
|
|
36519
|
+
const promotionDecision = parseEnum(input.promotionDecision, IMPROVEMENT_DECISIONS, "--recommendation") ?? inferPromotionDecision(sourceType);
|
|
36520
|
+
const ids = collectIds(artifact, input.ids);
|
|
36521
|
+
assertOrgBoundary(artifact, input.ids?.org_id);
|
|
36522
|
+
const reasonCode = nonEmpty2(input.reasonCode) ?? inferReasonCode(artifact);
|
|
36523
|
+
if (!reasonCode) {
|
|
36524
|
+
throw new FohError({
|
|
36525
|
+
step: "bug.improve",
|
|
36526
|
+
error: "Missing improvement reason code.",
|
|
36527
|
+
remediation: "Pass --reason-code <code> or build from a source artifact that includes reason_code/status.",
|
|
36528
|
+
statusCode: 400
|
|
36529
|
+
});
|
|
36530
|
+
}
|
|
36531
|
+
const evidenceSummary = redactString(
|
|
36532
|
+
nonEmpty2(input.evidenceSummary) ?? nonEmpty2(getPath2(artifact, "summary")) ?? `Improvement candidate generated from ${sourceType} with reason ${reasonCode}.`
|
|
36533
|
+
);
|
|
36534
|
+
const nextCommands = Array.from(new Set([
|
|
36535
|
+
...input.nextCommands ?? [],
|
|
36536
|
+
...defaultNextCommands({ sourceType, ids, sourceArtifactPath: input.sourceArtifactPath, reasonCode })
|
|
36537
|
+
].map((command) => command.trim()).filter(Boolean)));
|
|
36538
|
+
const packet = {
|
|
36539
|
+
schema_version: "foh_improvement_packet.v1",
|
|
36540
|
+
created_at: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
36541
|
+
source_type: sourceType,
|
|
36542
|
+
reason_code: reasonCode,
|
|
36543
|
+
promotion_decision: promotionDecision,
|
|
36544
|
+
ids,
|
|
36545
|
+
evidence: {
|
|
36546
|
+
summary: evidenceSummary,
|
|
36547
|
+
source_artifact_path: input.sourceArtifactPath ?? null,
|
|
36548
|
+
source_command: input.sourceCommand ?? null,
|
|
36549
|
+
redaction: {
|
|
36550
|
+
enabled: true,
|
|
36551
|
+
fields: ["email", "phone", "secret-like-token"]
|
|
36552
|
+
},
|
|
36553
|
+
redacted_source: artifact ? compactSourceArtifact(artifact) : null
|
|
36554
|
+
},
|
|
36555
|
+
next_commands: nextCommands
|
|
36556
|
+
};
|
|
36557
|
+
assertRedacted(packet);
|
|
36558
|
+
return packet;
|
|
36559
|
+
}
|
|
36560
|
+
|
|
36561
|
+
// src/commands/bug.ts
|
|
35372
36562
|
var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
35373
36563
|
var MAX_BODY_PREVIEW_LENGTH = 200;
|
|
35374
36564
|
function parseMethod(raw) {
|
|
@@ -35484,14 +36674,18 @@ function parseRequestBody(raw) {
|
|
|
35484
36674
|
}
|
|
35485
36675
|
}
|
|
35486
36676
|
function writeJsonArtifact(path2, value) {
|
|
35487
|
-
const absolutePath = (0,
|
|
35488
|
-
(0,
|
|
35489
|
-
(0,
|
|
36677
|
+
const absolutePath = (0, import_path8.resolve)(path2);
|
|
36678
|
+
(0, import_fs10.mkdirSync)((0, import_path8.dirname)(absolutePath), { recursive: true });
|
|
36679
|
+
(0, import_fs10.writeFileSync)(absolutePath, stableStringify(value), "utf-8");
|
|
35490
36680
|
return absolutePath;
|
|
35491
36681
|
}
|
|
35492
36682
|
function defaultArtifactPath() {
|
|
35493
36683
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
35494
|
-
return (0,
|
|
36684
|
+
return (0, import_path8.resolve)(`test-results/bug-report.${timestamp2}.json`);
|
|
36685
|
+
}
|
|
36686
|
+
function defaultImprovementArtifactPath() {
|
|
36687
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
36688
|
+
return (0, import_path8.resolve)(`test-results/improvement-packet.${timestamp2}.json`);
|
|
35495
36689
|
}
|
|
35496
36690
|
async function resolveBugReportWizardInputs(opts) {
|
|
35497
36691
|
if (!opts.wizard) return opts;
|
|
@@ -35641,6 +36835,44 @@ function registerBug(program3) {
|
|
|
35641
36835
|
throw e;
|
|
35642
36836
|
}
|
|
35643
36837
|
});
|
|
36838
|
+
bug.command("improve").description("Write a redacted failure-to-improvement packet from a setup/proof/replay/knowledge/runtime artifact").option("--from-file <path>", "Source JSON artifact to convert into an improvement packet").option("--out <path>", "Output JSON file path for improvement packet").option("--source-type <type>", "setup_failure|proof_failure|replay_failure|knowledge_miss|runtime_miss|live_proof_failure").option("--reason-code <code>", "Deterministic failure reason code").option("--recommendation <decision>", "ignore|fix_docs|fix_config|fix_runtime|add_test").option("--evidence-summary <text>", "Compact redacted evidence summary").option("--source-command <text>", "Command or operation that produced the source artifact").option("--org <id>", "Org ID for boundary check and packet IDs").option("--agent <id>", "Agent ID to attach").option("--conversation <id>", "Conversation ID to attach").option("--trace <id>", "Trace ID to attach").option("--correlation <id>", "Correlation ID to attach").option("--proof-artifact <path>", "Proof artifact path to attach").option(
|
|
36839
|
+
"--next-command <text>",
|
|
36840
|
+
"Deterministic next command (repeat this flag for multiple commands)",
|
|
36841
|
+
(value, previous = []) => [...previous, value],
|
|
36842
|
+
[]
|
|
36843
|
+
).option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
|
|
36844
|
+
const sourceArtifact = readSourceArtifact(opts.fromFile);
|
|
36845
|
+
const outPath = String(opts.out || defaultImprovementArtifactPath());
|
|
36846
|
+
const packet = buildImprovementPacket({
|
|
36847
|
+
sourceArtifact,
|
|
36848
|
+
sourceArtifactPath: opts.fromFile,
|
|
36849
|
+
sourceType: opts.sourceType,
|
|
36850
|
+
reasonCode: opts.reasonCode,
|
|
36851
|
+
evidenceSummary: opts.evidenceSummary,
|
|
36852
|
+
promotionDecision: opts.recommendation,
|
|
36853
|
+
sourceCommand: opts.sourceCommand,
|
|
36854
|
+
ids: {
|
|
36855
|
+
org_id: opts.org,
|
|
36856
|
+
agent_id: opts.agent,
|
|
36857
|
+
conversation_id: opts.conversation,
|
|
36858
|
+
trace_id: opts.trace,
|
|
36859
|
+
correlation_id: opts.correlation,
|
|
36860
|
+
proof_artifact: opts.proofArtifact
|
|
36861
|
+
},
|
|
36862
|
+
nextCommands: Array.isArray(opts.nextCommand) ? opts.nextCommand : []
|
|
36863
|
+
});
|
|
36864
|
+
const artifactPath = writeJsonArtifact(outPath, packet);
|
|
36865
|
+
format(cliEnvelope({
|
|
36866
|
+
schemaVersion: "foh_improvement_packet_result.v1",
|
|
36867
|
+
status: "exported",
|
|
36868
|
+
reasonCode: "improvement_packet_created",
|
|
36869
|
+
summary: "Improvement packet created.",
|
|
36870
|
+
ids: packet.ids,
|
|
36871
|
+
artifacts: { improvement_packet: artifactPath },
|
|
36872
|
+
nextCommands: packet.next_commands,
|
|
36873
|
+
extra: { packet }
|
|
36874
|
+
}), { json: opts.json ?? false });
|
|
36875
|
+
}));
|
|
35644
36876
|
bug.command("list").description("List bug reports from the FOH bug inbox").option("--state <state>", "Filter by state: open, triaged, closed").option("--severity <level>", "Filter by severity: low, medium, high, critical").option("--source <source>", "Filter by source: cli, eval, api").option("--request-id <id>", "Filter by request id").option("--search <text>", "Case-insensitive command search").option("--limit <n>", "Result limit (1-200)", "50").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "Internal API base URL override (operators only)").option("--json", "Output as JSON").action(async (opts) => {
|
|
35645
36877
|
try {
|
|
35646
36878
|
const params = new URLSearchParams();
|
|
@@ -35683,6 +36915,344 @@ function registerBug(program3) {
|
|
|
35683
36915
|
});
|
|
35684
36916
|
}
|
|
35685
36917
|
|
|
36918
|
+
// src/commands/prove.ts
|
|
36919
|
+
function categoryForCheck(name) {
|
|
36920
|
+
if (name === "auth") return "auth";
|
|
36921
|
+
if (name === "contact_channel" || name === "voice_realtime_health") return "voice";
|
|
36922
|
+
if (name.startsWith("widget_")) return "widget";
|
|
36923
|
+
if (name === "simulation_certification") return "certification";
|
|
36924
|
+
if (name === "agent_validation") return "publish_readiness";
|
|
36925
|
+
return "setup";
|
|
36926
|
+
}
|
|
36927
|
+
function pass(name, summary, detail) {
|
|
36928
|
+
return { name, category: categoryForCheck(name), status: "pass", reason_code: `${name}_ok`, summary, detail };
|
|
36929
|
+
}
|
|
36930
|
+
function hold(name, reasonCode, summary, nextCommand, detail) {
|
|
36931
|
+
return { name, category: categoryForCheck(name), status: "hold", reason_code: reasonCode, summary, next_command: nextCommand, detail };
|
|
36932
|
+
}
|
|
36933
|
+
function fail(name, reasonCode, error2, nextCommand) {
|
|
36934
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
36935
|
+
return { name, category: categoryForCheck(name), status: "fail", reason_code: reasonCode, summary: message, next_command: nextCommand };
|
|
36936
|
+
}
|
|
36937
|
+
function skipped(name, reasonCode, summary, nextCommand) {
|
|
36938
|
+
return { name, category: categoryForCheck(name), status: "skipped", reason_code: reasonCode, summary, next_command: nextCommand };
|
|
36939
|
+
}
|
|
36940
|
+
function hasBlockingChecks(checks) {
|
|
36941
|
+
return checks.some((check2) => check2.status === "hold" || check2.status === "fail");
|
|
36942
|
+
}
|
|
36943
|
+
function publicKeyFromEnsureResponse(response) {
|
|
36944
|
+
const record2 = response && typeof response === "object" ? response : {};
|
|
36945
|
+
const channel = record2.channel && typeof record2.channel === "object" ? record2.channel : {};
|
|
36946
|
+
const publicKey = channel.public_key ?? record2.widget_public_key ?? record2.public_key;
|
|
36947
|
+
return typeof publicKey === "string" && publicKey.trim() ? publicKey.trim() : void 0;
|
|
36948
|
+
}
|
|
36949
|
+
function publicKeyFromEmbedResponse(response) {
|
|
36950
|
+
const record2 = response && typeof response === "object" ? response : {};
|
|
36951
|
+
const publicKey = record2.widget_public_key ?? record2.public_key;
|
|
36952
|
+
return typeof publicKey === "string" && publicKey.trim() ? publicKey.trim() : void 0;
|
|
36953
|
+
}
|
|
36954
|
+
function normalizeMission(raw) {
|
|
36955
|
+
const value = String(raw || "setup").trim().toLowerCase();
|
|
36956
|
+
if (value === "setup" || value === "widget" || value === "voice" || value === "publish") return value;
|
|
36957
|
+
return "setup";
|
|
36958
|
+
}
|
|
36959
|
+
function normalizeMutationMode(raw, repair) {
|
|
36960
|
+
if (repair) return "ensure";
|
|
36961
|
+
const value = String(raw || "read-only").trim().toLowerCase();
|
|
36962
|
+
return value === "ensure" ? "ensure" : "read-only";
|
|
36963
|
+
}
|
|
36964
|
+
function agentIdFromList(response) {
|
|
36965
|
+
const agents = Array.isArray(response.agents) ? response.agents : [];
|
|
36966
|
+
const usable = agents.filter((agent) => typeof agent.id === "string" && agent.id.trim());
|
|
36967
|
+
if (usable.length === 1) return { agentId: usable[0].id, count: usable.length };
|
|
36968
|
+
if (usable.length === 0) return { count: 0, reason: "no_agents" };
|
|
36969
|
+
return { count: usable.length, reason: "multiple_agents" };
|
|
36970
|
+
}
|
|
36971
|
+
function firstUsableOrgId(response) {
|
|
36972
|
+
const record2 = response && typeof response === "object" ? response : {};
|
|
36973
|
+
const orgs = Array.isArray(record2.orgs) ? record2.orgs : [];
|
|
36974
|
+
const usable = orgs.map((org) => org && typeof org === "object" ? org : {}).map((org) => String(org.org_id ?? org.id ?? "").trim()).filter(Boolean);
|
|
36975
|
+
return { orgId: usable.length === 1 ? usable[0] : void 0, count: usable.length };
|
|
36976
|
+
}
|
|
36977
|
+
function registerProve(program3) {
|
|
36978
|
+
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("--mission <mission>", "Proof mission: setup, widget, voice, publish", "setup").option("--mutation-mode <mode>", "Proof mutation mode: read-only or ensure", "read-only").option("--repair", "Alias for --mutation-mode ensure").option("--require-phone", "Hold proof if no phone/contact number is provisioned").option("--skip-cert", "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 () => {
|
|
36979
|
+
const checks = [];
|
|
36980
|
+
const mission = normalizeMission(opts.mission);
|
|
36981
|
+
const mutationMode = normalizeMutationMode(opts.mutationMode, Boolean(opts.repair));
|
|
36982
|
+
const ctx = {
|
|
36983
|
+
tokenPresent: false,
|
|
36984
|
+
traceIds: [],
|
|
36985
|
+
correlationIds: []
|
|
36986
|
+
};
|
|
36987
|
+
try {
|
|
36988
|
+
const creds = loadCredentials(opts.apiUrl);
|
|
36989
|
+
ctx.apiUrl = creds.apiUrl;
|
|
36990
|
+
ctx.tokenPresent = Boolean(creds.token);
|
|
36991
|
+
ctx.orgId = opts.org || creds.orgId;
|
|
36992
|
+
checks.push(pass("auth", "CLI credentials are present and not expired.", {
|
|
36993
|
+
api_url: creds.apiUrl,
|
|
36994
|
+
org_id_from_credentials: creds.orgId ?? null
|
|
36995
|
+
}));
|
|
36996
|
+
} catch (error2) {
|
|
36997
|
+
checks.push(hold("auth", "auth_missing_or_expired", "CLI is not authenticated.", "foh auth login --web", {
|
|
36998
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
36999
|
+
}));
|
|
37000
|
+
}
|
|
37001
|
+
if (ctx.tokenPresent && !ctx.orgId) {
|
|
37002
|
+
try {
|
|
37003
|
+
const orgs = await apiFetch("/v1/console/auth/my-orgs", { apiUrlOverride: opts.apiUrl });
|
|
37004
|
+
const resolved = firstUsableOrgId(orgs);
|
|
37005
|
+
if (resolved.orgId) {
|
|
37006
|
+
ctx.orgId = resolved.orgId;
|
|
37007
|
+
checks.push(pass("org", "Resolved the only available org.", { org_id: resolved.orgId }));
|
|
37008
|
+
} else {
|
|
37009
|
+
checks.push(hold(
|
|
37010
|
+
"org",
|
|
37011
|
+
resolved.count === 0 ? "org_missing" : "org_ambiguous",
|
|
37012
|
+
resolved.count === 0 ? "No usable org was found for this account." : `Found ${resolved.count} orgs; choose one explicitly.`,
|
|
37013
|
+
"foh org list --json && foh org use --org <org-id>",
|
|
37014
|
+
{ org_count: resolved.count }
|
|
37015
|
+
));
|
|
37016
|
+
}
|
|
37017
|
+
} catch (error2) {
|
|
37018
|
+
checks.push(fail("org", "org_resolution_failed", error2, "foh org list --json"));
|
|
37019
|
+
}
|
|
37020
|
+
} else if (ctx.orgId) {
|
|
37021
|
+
checks.push(pass("org", "Org context is selected.", { org_id: ctx.orgId }));
|
|
37022
|
+
} else {
|
|
37023
|
+
checks.push(skipped("org", "auth_required", "Skipped until authentication is fixed.", "foh auth login --web"));
|
|
37024
|
+
}
|
|
37025
|
+
if (ctx.orgId) {
|
|
37026
|
+
if (opts.agent) {
|
|
37027
|
+
ctx.agentId = String(opts.agent);
|
|
37028
|
+
checks.push(pass("agent_selection", "Using explicitly supplied agent.", { agent_id: ctx.agentId }));
|
|
37029
|
+
} else {
|
|
37030
|
+
try {
|
|
37031
|
+
const list = await apiFetch("/v1/console/agents", {
|
|
37032
|
+
orgId: ctx.orgId,
|
|
37033
|
+
apiUrlOverride: opts.apiUrl
|
|
37034
|
+
});
|
|
37035
|
+
const resolved = agentIdFromList(list);
|
|
37036
|
+
if (resolved.agentId) {
|
|
37037
|
+
ctx.agentId = resolved.agentId;
|
|
37038
|
+
checks.push(pass("agent_selection", "Resolved the only available agent.", { agent_id: resolved.agentId }));
|
|
37039
|
+
} else {
|
|
37040
|
+
checks.push(hold(
|
|
37041
|
+
"agent_selection",
|
|
37042
|
+
resolved.reason === "no_agents" ? "agent_missing" : "agent_ambiguous",
|
|
37043
|
+
resolved.reason === "no_agents" ? "No agent exists in this org." : `Found ${resolved.count} agents; choose one explicitly.`,
|
|
37044
|
+
resolved.reason === "no_agents" ? "foh setup --json" : "foh agent list --json && foh prove --agent <agent-id> --json",
|
|
37045
|
+
{ agent_count: resolved.count }
|
|
37046
|
+
));
|
|
37047
|
+
}
|
|
37048
|
+
} catch (error2) {
|
|
37049
|
+
checks.push(fail("agent_selection", "agent_selection_failed", error2, "foh agent list --json"));
|
|
37050
|
+
}
|
|
37051
|
+
}
|
|
37052
|
+
} else {
|
|
37053
|
+
checks.push(skipped("agent_selection", "org_required", "Skipped until org context is fixed.", "foh org use --org <org-id>"));
|
|
37054
|
+
}
|
|
37055
|
+
if (ctx.agentId) {
|
|
37056
|
+
try {
|
|
37057
|
+
const validation = await apiFetch(`/v1/console/agents/${ctx.agentId}/validate`, {
|
|
37058
|
+
method: "POST",
|
|
37059
|
+
orgId: ctx.orgId,
|
|
37060
|
+
apiUrlOverride: opts.apiUrl
|
|
37061
|
+
});
|
|
37062
|
+
const issues = Array.isArray(validation.issues) ? validation.issues : [];
|
|
37063
|
+
if (validation.ok === false || issues.length > 0) {
|
|
37064
|
+
checks.push(hold("agent_validation", "agent_validation_issues", `Agent validation returned ${issues.length} issue(s).`, `foh agent validate --agent ${ctx.agentId} --json`, validation));
|
|
37065
|
+
} else {
|
|
37066
|
+
checks.push(pass("agent_validation", "Agent validation passed.", validation));
|
|
37067
|
+
}
|
|
37068
|
+
} catch (error2) {
|
|
37069
|
+
checks.push(fail("agent_validation", "agent_validation_failed", error2, `foh agent validate --agent ${ctx.agentId} --json`));
|
|
37070
|
+
}
|
|
37071
|
+
if (ctx.orgId) {
|
|
37072
|
+
try {
|
|
37073
|
+
const onboarding = await apiFetch(`/v1/console/org/${ctx.orgId}/onboarding`, {
|
|
37074
|
+
orgId: ctx.orgId,
|
|
37075
|
+
apiUrlOverride: opts.apiUrl
|
|
37076
|
+
});
|
|
37077
|
+
const phoneNumber = typeof onboarding.phone_number === "string" && onboarding.phone_number.trim() ? onboarding.phone_number.trim() : null;
|
|
37078
|
+
if (phoneNumber) {
|
|
37079
|
+
checks.push(pass("contact_channel", "Contact phone number is provisioned.", {
|
|
37080
|
+
phone_number_present: true,
|
|
37081
|
+
provisioning_status: onboarding.provisioning_status ?? null
|
|
37082
|
+
}));
|
|
37083
|
+
} else if (opts.requirePhone || mission === "voice") {
|
|
37084
|
+
checks.push(hold("contact_channel", "contact_phone_missing", "No phone/contact number is provisioned for this org.", `foh provision buy --org ${ctx.orgId} --json`, {
|
|
37085
|
+
provisioning_status: onboarding.provisioning_status ?? null,
|
|
37086
|
+
mission
|
|
37087
|
+
}));
|
|
37088
|
+
} else {
|
|
37089
|
+
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`));
|
|
37090
|
+
}
|
|
37091
|
+
} catch (error2) {
|
|
37092
|
+
checks.push(fail("contact_channel", "contact_channel_check_failed", error2, `foh provision status --org ${ctx.orgId} --json`));
|
|
37093
|
+
}
|
|
37094
|
+
}
|
|
37095
|
+
if (opts.skipVoiceHealth) {
|
|
37096
|
+
checks.push(skipped("voice_realtime_health", "operator_skipped", "Skipped by --skip-voice-health.", "foh voice realtime-health --json"));
|
|
37097
|
+
} else {
|
|
37098
|
+
try {
|
|
37099
|
+
const health = await apiFetch(
|
|
37100
|
+
"/v1/console/realtime/health",
|
|
37101
|
+
{ apiUrlOverride: opts.apiUrl }
|
|
37102
|
+
);
|
|
37103
|
+
const providers = Array.isArray(health.providers) ? health.providers : [];
|
|
37104
|
+
if (providers.length === 0) {
|
|
37105
|
+
checks.push(skipped("voice_realtime_health", "voice_health_no_providers", "Realtime voice health returned no providers.", "foh voice realtime-health --json"));
|
|
37106
|
+
} else if (providers.every((provider) => provider?.ready === true)) {
|
|
37107
|
+
checks.push(pass("voice_realtime_health", "Realtime voice providers are ready.", {
|
|
37108
|
+
provider_count: providers.length
|
|
37109
|
+
}));
|
|
37110
|
+
} else {
|
|
37111
|
+
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", {
|
|
37112
|
+
providers
|
|
37113
|
+
}));
|
|
37114
|
+
}
|
|
37115
|
+
} catch (error2) {
|
|
37116
|
+
checks.push(fail("voice_realtime_health", "voice_realtime_health_failed", error2, "foh voice realtime-health --json"));
|
|
37117
|
+
}
|
|
37118
|
+
}
|
|
37119
|
+
try {
|
|
37120
|
+
const embed = await apiFetch("/v1/console/channels/widget/embed-snippet", {
|
|
37121
|
+
orgId: ctx.orgId,
|
|
37122
|
+
apiUrlOverride: opts.apiUrl,
|
|
37123
|
+
headers: { "x-agent-id": ctx.agentId }
|
|
37124
|
+
});
|
|
37125
|
+
const publicKey = publicKeyFromEmbedResponse(embed);
|
|
37126
|
+
if (publicKey) {
|
|
37127
|
+
ctx.widgetPublicKey = publicKey;
|
|
37128
|
+
checks.push(pass("widget_channel", "Widget channel is available in read-only proof mode.", {
|
|
37129
|
+
public_key_present: true,
|
|
37130
|
+
mutation_mode: mutationMode
|
|
37131
|
+
}));
|
|
37132
|
+
}
|
|
37133
|
+
if (typeof embed.snippet === "string" && embed.snippet.trim()) {
|
|
37134
|
+
checks.push(pass("widget_embed", "Widget embed snippet is available.", { snippet_present: true }));
|
|
37135
|
+
} else {
|
|
37136
|
+
checks.push(hold("widget_embed", "widget_embed_missing", "Widget embed snippet is missing.", `foh widget embed-snippet --agent ${ctx.agentId}`));
|
|
37137
|
+
}
|
|
37138
|
+
} catch (error2) {
|
|
37139
|
+
if (mutationMode === "ensure") {
|
|
37140
|
+
try {
|
|
37141
|
+
const ensure = await apiFetch("/v1/console/channels/widget/ensure", {
|
|
37142
|
+
method: "POST",
|
|
37143
|
+
body: JSON.stringify({ agentId: ctx.agentId }),
|
|
37144
|
+
orgId: ctx.orgId,
|
|
37145
|
+
apiUrlOverride: opts.apiUrl
|
|
37146
|
+
});
|
|
37147
|
+
const publicKey = publicKeyFromEnsureResponse(ensure);
|
|
37148
|
+
if (!publicKey) {
|
|
37149
|
+
checks.push(hold("widget_channel", "widget_public_key_missing", "Widget channel ensure returned no public key.", `foh widget ensure --agent ${ctx.agentId} --json`, ensure));
|
|
37150
|
+
} else {
|
|
37151
|
+
ctx.widgetPublicKey = publicKey;
|
|
37152
|
+
checks.push(pass("widget_channel", "Widget channel was ensured explicitly.", {
|
|
37153
|
+
public_key_present: true,
|
|
37154
|
+
mutation_mode: mutationMode
|
|
37155
|
+
}));
|
|
37156
|
+
}
|
|
37157
|
+
checks.push(skipped("widget_embed", "widget_embed_recheck_required", "Widget channel was ensured; rerun read-only proof to verify embed snippet.", `foh prove --agent ${ctx.agentId} --mutation-mode read-only --json`));
|
|
37158
|
+
} catch (ensureError) {
|
|
37159
|
+
checks.push(fail("widget_channel", "widget_channel_ensure_failed", ensureError, `foh widget ensure --agent ${ctx.agentId} --json`));
|
|
37160
|
+
checks.push(skipped("widget_embed", "widget_channel_required", "Skipped because widget channel could not be ensured.", `foh widget ensure --agent ${ctx.agentId} --json`));
|
|
37161
|
+
}
|
|
37162
|
+
} else {
|
|
37163
|
+
checks.push(hold("widget_channel", "widget_channel_missing_read_only", "Widget channel/embed was not observable in read-only proof mode.", `foh prove --agent ${ctx.agentId} --mutation-mode ensure --json`, {
|
|
37164
|
+
message: error2 instanceof Error ? error2.message : String(error2),
|
|
37165
|
+
mutation_mode: mutationMode
|
|
37166
|
+
}));
|
|
37167
|
+
checks.push(skipped("widget_embed", "widget_channel_required", "Skipped because widget channel was not observable.", `foh widget ensure --agent ${ctx.agentId} --json`));
|
|
37168
|
+
}
|
|
37169
|
+
}
|
|
37170
|
+
if (opts.skipSmoke) {
|
|
37171
|
+
checks.push(skipped("widget_smoke", "operator_skipped", "Skipped by --skip-smoke.", `foh widget smoke --agent ${ctx.agentId} --json`));
|
|
37172
|
+
} else if (!ctx.widgetPublicKey) {
|
|
37173
|
+
checks.push(skipped("widget_smoke", "widget_public_key_required", "Skipped because widget public key is unavailable.", `foh widget ensure --agent ${ctx.agentId} --json`));
|
|
37174
|
+
} else {
|
|
37175
|
+
try {
|
|
37176
|
+
const smoke = await runWidgetSmoke(ctx.widgetPublicKey, opts.apiUrl);
|
|
37177
|
+
ctx.conversationId = smoke.conversation_id;
|
|
37178
|
+
ctx.traceIds = smoke.trace_ids;
|
|
37179
|
+
ctx.correlationIds = smoke.correlation_ids;
|
|
37180
|
+
if (smoke.failed > 0) {
|
|
37181
|
+
checks.push(hold("widget_smoke", "widget_smoke_failed", `${smoke.failed} widget smoke turn(s) failed.`, `foh widget smoke --agent ${ctx.agentId} --json`, smoke));
|
|
37182
|
+
} else {
|
|
37183
|
+
checks.push(pass("widget_smoke", "Widget runtime smoke passed.", smoke));
|
|
37184
|
+
}
|
|
37185
|
+
} catch (error2) {
|
|
37186
|
+
checks.push(fail("widget_smoke", "widget_smoke_failed", error2, `foh widget smoke --agent ${ctx.agentId} --json`));
|
|
37187
|
+
}
|
|
37188
|
+
}
|
|
37189
|
+
if (opts.skipCert) {
|
|
37190
|
+
checks.push(skipped("simulation_certification", "operator_skipped", "Skipped by --skip-cert.", `foh sim certify-loop --agent ${ctx.agentId} --json`));
|
|
37191
|
+
} else {
|
|
37192
|
+
try {
|
|
37193
|
+
const certMode = normalizeAgentCertMode(opts.certMode);
|
|
37194
|
+
const loop = await runSetupCertifyLoop(ctx.agentId, {
|
|
37195
|
+
mode: certMode,
|
|
37196
|
+
adaptiveRuns: Math.max(1, Number(opts.certAdaptiveRuns ?? 30) || 30),
|
|
37197
|
+
maxImprovementRounds: Math.max(0, Math.min(5, Number(opts.certMaxImprovementRounds ?? 1) || 1)),
|
|
37198
|
+
orgId: ctx.orgId,
|
|
37199
|
+
apiUrlOverride: opts.apiUrl
|
|
37200
|
+
});
|
|
37201
|
+
if (!loop.overall_pass) {
|
|
37202
|
+
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));
|
|
37203
|
+
} else {
|
|
37204
|
+
checks.push(pass("simulation_certification", "Simulation certification passed.", {
|
|
37205
|
+
mode: loop.mode,
|
|
37206
|
+
attempts: loop.attempts?.length ?? 0,
|
|
37207
|
+
improvement_runs: loop.improvement_runs,
|
|
37208
|
+
scenario_summary: loop.certificate?.scenario_summary
|
|
37209
|
+
}));
|
|
37210
|
+
}
|
|
37211
|
+
} catch (error2) {
|
|
37212
|
+
checks.push(fail("simulation_certification", "simulation_certification_failed", error2, `foh sim certify-loop --agent ${ctx.agentId} --json`));
|
|
37213
|
+
}
|
|
37214
|
+
}
|
|
37215
|
+
} else {
|
|
37216
|
+
checks.push(skipped("agent_validation", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
|
|
37217
|
+
checks.push(skipped("contact_channel", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
|
|
37218
|
+
checks.push(skipped("voice_realtime_health", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
|
|
37219
|
+
checks.push(skipped("widget_channel", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
|
|
37220
|
+
checks.push(skipped("widget_embed", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
|
|
37221
|
+
checks.push(skipped("widget_smoke", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
|
|
37222
|
+
checks.push(skipped("simulation_certification", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
|
|
37223
|
+
}
|
|
37224
|
+
const status = hasBlockingChecks(checks) ? "hold" : "pass";
|
|
37225
|
+
const nextCommands = Array.from(new Set(checks.map((check2) => check2.next_command).filter((command) => Boolean(command))));
|
|
37226
|
+
if (status === "pass" && ctx.agentId) {
|
|
37227
|
+
nextCommands.push(`foh agent publish --agent ${ctx.agentId} --json`);
|
|
37228
|
+
}
|
|
37229
|
+
const report = signReport(cliEnvelope({
|
|
37230
|
+
schemaVersion: "foh_cli_proof_report.v1",
|
|
37231
|
+
status,
|
|
37232
|
+
reasonCode: status === "pass" ? "proof_passed" : "proof_held",
|
|
37233
|
+
summary: status === "pass" ? "All non-skipped proof checks passed." : "One or more proof checks require operator action.",
|
|
37234
|
+
ids: {
|
|
37235
|
+
org_id: ctx.orgId ?? null,
|
|
37236
|
+
agent_id: ctx.agentId ?? null,
|
|
37237
|
+
mission,
|
|
37238
|
+
mutation_mode: mutationMode,
|
|
37239
|
+
widget_public_key_present: Boolean(ctx.widgetPublicKey),
|
|
37240
|
+
conversation_id: ctx.conversationId ?? null,
|
|
37241
|
+
trace_ids: ctx.traceIds,
|
|
37242
|
+
correlation_ids: ctx.correlationIds
|
|
37243
|
+
},
|
|
37244
|
+
checks,
|
|
37245
|
+
nextCommands,
|
|
37246
|
+
extra: {
|
|
37247
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
37248
|
+
}
|
|
37249
|
+
}));
|
|
37250
|
+
const artifactPath = opts.out ? writeSignedJsonArtifact(String(opts.out), report) : void 0;
|
|
37251
|
+
format(artifactPath ? { ...report, artifact_path: artifactPath } : report, { json: opts.json ?? false });
|
|
37252
|
+
if (opts.strict && status !== "pass") markCommandFailed(1);
|
|
37253
|
+
}));
|
|
37254
|
+
}
|
|
37255
|
+
|
|
35686
37256
|
// src/tui/command-palette.ts
|
|
35687
37257
|
function tokenizeInput(value) {
|
|
35688
37258
|
const tokens = [];
|
|
@@ -36128,7 +37698,7 @@ async function runSelf(args, apiUrlOverride) {
|
|
|
36128
37698
|
if (apiUrlOverride && !spawnArgs.includes("--api-url")) {
|
|
36129
37699
|
spawnArgs.push("--api-url", apiUrlOverride);
|
|
36130
37700
|
}
|
|
36131
|
-
return await new Promise((
|
|
37701
|
+
return await new Promise((resolve8, reject) => {
|
|
36132
37702
|
const child = (0, import_child_process2.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
|
|
36133
37703
|
stdio: "inherit",
|
|
36134
37704
|
env: {
|
|
@@ -36138,7 +37708,7 @@ async function runSelf(args, apiUrlOverride) {
|
|
|
36138
37708
|
}
|
|
36139
37709
|
});
|
|
36140
37710
|
child.once("error", reject);
|
|
36141
|
-
child.once("close", (code) =>
|
|
37711
|
+
child.once("close", (code) => resolve8(typeof code === "number" ? code : 1));
|
|
36142
37712
|
});
|
|
36143
37713
|
}
|
|
36144
37714
|
function shouldUseInteractiveHome(argv) {
|
|
@@ -36428,8 +37998,8 @@ function maybeDefaultToHome(argv = process.argv) {
|
|
|
36428
37998
|
}
|
|
36429
37999
|
|
|
36430
38000
|
// src/lib/update.ts
|
|
36431
|
-
var
|
|
36432
|
-
var
|
|
38001
|
+
var import_fs11 = require("fs");
|
|
38002
|
+
var import_path9 = require("path");
|
|
36433
38003
|
var import_child_process3 = require("child_process");
|
|
36434
38004
|
var import_crypto5 = require("crypto");
|
|
36435
38005
|
function parseSemver(version2) {
|
|
@@ -36450,7 +38020,7 @@ function compareSemver(a, b) {
|
|
|
36450
38020
|
}
|
|
36451
38021
|
function readPackageJsonVersion(path2) {
|
|
36452
38022
|
try {
|
|
36453
|
-
const raw = (0,
|
|
38023
|
+
const raw = (0, import_fs11.readFileSync)(path2, "utf-8");
|
|
36454
38024
|
const parsed = JSON.parse(raw);
|
|
36455
38025
|
const version2 = String(parsed.version ?? "").trim();
|
|
36456
38026
|
return version2 || void 0;
|
|
@@ -36459,13 +38029,13 @@ function readPackageJsonVersion(path2) {
|
|
|
36459
38029
|
}
|
|
36460
38030
|
}
|
|
36461
38031
|
function findRepoRoot(startCwd = process.cwd()) {
|
|
36462
|
-
let current = (0,
|
|
38032
|
+
let current = (0, import_path9.resolve)(startCwd);
|
|
36463
38033
|
while (true) {
|
|
36464
|
-
const rootPackageJsonPath = (0,
|
|
36465
|
-
const cliPackageJsonPath = (0,
|
|
36466
|
-
if ((0,
|
|
38034
|
+
const rootPackageJsonPath = (0, import_path9.join)(current, "package.json");
|
|
38035
|
+
const cliPackageJsonPath = (0, import_path9.join)(current, "packages", "cli", "package.json");
|
|
38036
|
+
if ((0, import_fs11.existsSync)(rootPackageJsonPath) && (0, import_fs11.existsSync)(cliPackageJsonPath)) {
|
|
36467
38037
|
try {
|
|
36468
|
-
const raw = (0,
|
|
38038
|
+
const raw = (0, import_fs11.readFileSync)(rootPackageJsonPath, "utf-8");
|
|
36469
38039
|
const parsed = JSON.parse(raw);
|
|
36470
38040
|
if (String(parsed.name ?? "").trim() === "front-of-house") {
|
|
36471
38041
|
return current;
|
|
@@ -36473,7 +38043,7 @@ function findRepoRoot(startCwd = process.cwd()) {
|
|
|
36473
38043
|
} catch {
|
|
36474
38044
|
}
|
|
36475
38045
|
}
|
|
36476
|
-
const parent = (0,
|
|
38046
|
+
const parent = (0, import_path9.dirname)(current);
|
|
36477
38047
|
if (parent === current) return void 0;
|
|
36478
38048
|
current = parent;
|
|
36479
38049
|
}
|
|
@@ -36487,7 +38057,7 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
|
|
|
36487
38057
|
remediation: "Run this command from the Front Of House repo root to compare/install the latest CLI."
|
|
36488
38058
|
};
|
|
36489
38059
|
}
|
|
36490
|
-
const cliPackageJsonPath = (0,
|
|
38060
|
+
const cliPackageJsonPath = (0, import_path9.join)(repoRoot, "packages", "cli", "package.json");
|
|
36491
38061
|
const latestVersion = readPackageJsonVersion(cliPackageJsonPath);
|
|
36492
38062
|
if (!latestVersion) {
|
|
36493
38063
|
return {
|
|
@@ -36514,19 +38084,19 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
|
|
|
36514
38084
|
};
|
|
36515
38085
|
}
|
|
36516
38086
|
async function applyRepoUpdate(repoRoot) {
|
|
36517
|
-
const scriptPath = (0,
|
|
38087
|
+
const scriptPath = (0, import_path9.join)(repoRoot, "scripts", "Install-FohCli.ps1");
|
|
36518
38088
|
if (process.platform === "win32") {
|
|
36519
|
-
return await new Promise((
|
|
38089
|
+
return await new Promise((resolve8, reject) => {
|
|
36520
38090
|
const child = (0, import_child_process3.spawn)(
|
|
36521
38091
|
"powershell",
|
|
36522
38092
|
["-ExecutionPolicy", "Bypass", "-File", scriptPath],
|
|
36523
38093
|
{ stdio: "inherit" }
|
|
36524
38094
|
);
|
|
36525
38095
|
child.once("error", reject);
|
|
36526
|
-
child.once("close", (code) =>
|
|
38096
|
+
child.once("close", (code) => resolve8(typeof code === "number" ? code : 1));
|
|
36527
38097
|
});
|
|
36528
38098
|
}
|
|
36529
|
-
return await new Promise((
|
|
38099
|
+
return await new Promise((resolve8, reject) => {
|
|
36530
38100
|
const child = (0, import_child_process3.spawn)(
|
|
36531
38101
|
"corepack",
|
|
36532
38102
|
["pnpm", "cli:install:global"],
|
|
@@ -36536,7 +38106,7 @@ async function applyRepoUpdate(repoRoot) {
|
|
|
36536
38106
|
}
|
|
36537
38107
|
);
|
|
36538
38108
|
child.once("error", reject);
|
|
36539
|
-
child.once("close", (code) =>
|
|
38109
|
+
child.once("close", (code) => resolve8(typeof code === "number" ? code : 1));
|
|
36540
38110
|
});
|
|
36541
38111
|
}
|
|
36542
38112
|
function shouldShowUpdateNotice(argv = process.argv) {
|
|
@@ -36550,7 +38120,7 @@ function shouldShowUpdateNotice(argv = process.argv) {
|
|
|
36550
38120
|
}
|
|
36551
38121
|
function hashFileSha256(filePath) {
|
|
36552
38122
|
try {
|
|
36553
|
-
const bytes = (0,
|
|
38123
|
+
const bytes = (0, import_fs11.readFileSync)(filePath);
|
|
36554
38124
|
return (0, import_crypto5.createHash)("sha256").update(bytes).digest("hex");
|
|
36555
38125
|
} catch {
|
|
36556
38126
|
return void 0;
|
|
@@ -36560,10 +38130,10 @@ function verifyCliArtifactIntegrity(params = {}) {
|
|
|
36560
38130
|
const cwd = params.cwd ?? process.cwd();
|
|
36561
38131
|
const argv = params.argv ?? process.argv;
|
|
36562
38132
|
const expectedSha256 = String(params.expectedSha256 ?? "").trim().toLowerCase() || void 0;
|
|
36563
|
-
const runtimePath = (0,
|
|
38133
|
+
const runtimePath = (0, import_path9.resolve)(String(argv[1] || ""));
|
|
36564
38134
|
const runtimeHash = runtimePath ? hashFileSha256(runtimePath) : void 0;
|
|
36565
38135
|
const warnings = [];
|
|
36566
|
-
if (!runtimePath || !(0,
|
|
38136
|
+
if (!runtimePath || !(0, import_fs11.existsSync)(runtimePath)) {
|
|
36567
38137
|
warnings.push("runtime_path_unreadable");
|
|
36568
38138
|
}
|
|
36569
38139
|
if (!runtimeHash) {
|
|
@@ -36581,8 +38151,8 @@ function verifyCliArtifactIntegrity(params = {}) {
|
|
|
36581
38151
|
let repoDistHash;
|
|
36582
38152
|
let runtimeMatchesRepoDist;
|
|
36583
38153
|
if (repoRoot) {
|
|
36584
|
-
repoDistPath = (0,
|
|
36585
|
-
if ((0,
|
|
38154
|
+
repoDistPath = (0, import_path9.join)(repoRoot, "packages", "cli", "dist", "foh.js");
|
|
38155
|
+
if ((0, import_fs11.existsSync)(repoDistPath)) {
|
|
36586
38156
|
repoDistHash = hashFileSha256(repoDistPath);
|
|
36587
38157
|
if (runtimeHash && repoDistHash) {
|
|
36588
38158
|
runtimeMatchesRepoDist = runtimeHash === repoDistHash;
|
|
@@ -36770,13 +38340,19 @@ function writeKnownError(message, remediation) {
|
|
|
36770
38340
|
if (jsonRequested()) {
|
|
36771
38341
|
process.stderr.write(
|
|
36772
38342
|
JSON.stringify(
|
|
36773
|
-
{
|
|
36774
|
-
|
|
36775
|
-
|
|
36776
|
-
|
|
36777
|
-
|
|
38343
|
+
cliEnvelope({
|
|
38344
|
+
status: "fail",
|
|
38345
|
+
reasonCode: reasonCodeFromStep(message, "cli_run_failed"),
|
|
38346
|
+
summary: message,
|
|
38347
|
+
nextCommands: remediation.startsWith("Run: ") ? [remediation.slice("Run: ".length)] : [],
|
|
38348
|
+
extra: {
|
|
38349
|
+
error: {
|
|
38350
|
+
step: "cli.run",
|
|
38351
|
+
message,
|
|
38352
|
+
remediation
|
|
38353
|
+
}
|
|
36778
38354
|
}
|
|
36779
|
-
},
|
|
38355
|
+
}),
|
|
36780
38356
|
null,
|
|
36781
38357
|
2
|
|
36782
38358
|
) + "\n"
|
|
@@ -36805,6 +38381,9 @@ registerMcp(program2);
|
|
|
36805
38381
|
registerKnowledge(program2);
|
|
36806
38382
|
registerLeads(program2);
|
|
36807
38383
|
registerConversations(program2);
|
|
38384
|
+
registerTranscripts(program2);
|
|
38385
|
+
registerAnalytics(program2);
|
|
38386
|
+
registerTest(program2);
|
|
36808
38387
|
registerTests(program2);
|
|
36809
38388
|
registerOps(program2);
|
|
36810
38389
|
registerSetup(program2);
|
|
@@ -36812,6 +38391,7 @@ registerManifest(program2);
|
|
|
36812
38391
|
registerSim(program2);
|
|
36813
38392
|
registerDiag(program2);
|
|
36814
38393
|
registerBug(program2);
|
|
38394
|
+
registerProve(program2);
|
|
36815
38395
|
registerUpdate(program2);
|
|
36816
38396
|
registerHome(program2);
|
|
36817
38397
|
hideInternalApiUrlOptions(program2);
|
|
@@ -36849,13 +38429,19 @@ program2.parseAsync(process.argv).catch((e) => {
|
|
|
36849
38429
|
if (jsonRequested()) {
|
|
36850
38430
|
process.stderr.write(
|
|
36851
38431
|
JSON.stringify(
|
|
36852
|
-
{
|
|
36853
|
-
|
|
36854
|
-
|
|
36855
|
-
|
|
36856
|
-
|
|
38432
|
+
cliEnvelope({
|
|
38433
|
+
status: "fail",
|
|
38434
|
+
reasonCode: "cli_unhandled_failed",
|
|
38435
|
+
summary: e instanceof Error ? e.message : String(e),
|
|
38436
|
+
nextCommands: [],
|
|
38437
|
+
extra: {
|
|
38438
|
+
error: {
|
|
38439
|
+
step: "cli.unhandled",
|
|
38440
|
+
message: e instanceof Error ? e.message : String(e),
|
|
38441
|
+
remediation: `Report this as a bug at ${BUG_REPORT_URL}`
|
|
38442
|
+
}
|
|
36857
38443
|
}
|
|
36858
|
-
},
|
|
38444
|
+
}),
|
|
36859
38445
|
null,
|
|
36860
38446
|
2
|
|
36861
38447
|
) + "\n"
|