@f-o-h/cli 0.1.5 → 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 +92 -2
- package/dist/foh.js +1031 -160
- 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,23 +14402,39 @@ 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
|
}));
|
|
14193
|
-
agent.command("replay").description("Create a replay/debug packet from a trace or conversation").option("--trace <id>", "Trace event ID to replay through the server trace replay endpoint").option("--conversation <id>", "Conversation ID to package with transcript and traces").
|
|
14194
|
-
|
|
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) {
|
|
14195
14408
|
throw new FohError({
|
|
14196
14409
|
step: "agent.replay",
|
|
14197
14410
|
error: "Missing replay source",
|
|
14198
|
-
remediation: "Pass --trace <id
|
|
14411
|
+
remediation: "Pass --trace <id>, --conversation <id>, or --file <transcript.json>.",
|
|
14199
14412
|
statusCode: 400
|
|
14200
14413
|
});
|
|
14201
14414
|
}
|
|
14202
|
-
if (
|
|
14415
|
+
if (sourceCount > 1) {
|
|
14203
14416
|
throw new FohError({
|
|
14204
14417
|
step: "agent.replay",
|
|
14205
14418
|
error: "Ambiguous replay source",
|
|
14206
|
-
remediation: "Pass only one of --trace or --
|
|
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.",
|
|
14207
14428
|
statusCode: 400
|
|
14208
14429
|
});
|
|
14209
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
|
+
}
|
|
14210
14438
|
if (opts.trace) {
|
|
14211
14439
|
const data2 = await apiFetch(`/v1/console/traces/${opts.trace}/replay`, {
|
|
14212
14440
|
method: "POST",
|
|
@@ -15806,8 +16034,8 @@ function registerVoice(program3) {
|
|
|
15806
16034
|
const outputPath = String(opts.out || `foh-voice-preview-${provider}-${voiceId}.mp3`).trim();
|
|
15807
16035
|
const audio = Buffer.from(await res.arrayBuffer());
|
|
15808
16036
|
const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync6 } = await import("fs");
|
|
15809
|
-
const { dirname: dirname5, resolve:
|
|
15810
|
-
const absolutePath =
|
|
16037
|
+
const { dirname: dirname5, resolve: resolve8 } = await import("path");
|
|
16038
|
+
const absolutePath = resolve8(outputPath);
|
|
15811
16039
|
mkdirSync4(dirname5(absolutePath), { recursive: true });
|
|
15812
16040
|
writeFileSync6(absolutePath, audio);
|
|
15813
16041
|
format({
|
|
@@ -30290,7 +30518,7 @@ var Protocol = class {
|
|
|
30290
30518
|
return;
|
|
30291
30519
|
}
|
|
30292
30520
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
30293
|
-
await new Promise((
|
|
30521
|
+
await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
|
|
30294
30522
|
options?.signal?.throwIfAborted();
|
|
30295
30523
|
}
|
|
30296
30524
|
} catch (error2) {
|
|
@@ -30307,7 +30535,7 @@ var Protocol = class {
|
|
|
30307
30535
|
*/
|
|
30308
30536
|
request(request, resultSchema, options) {
|
|
30309
30537
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
30310
|
-
return new Promise((
|
|
30538
|
+
return new Promise((resolve8, reject) => {
|
|
30311
30539
|
const earlyReject = (error2) => {
|
|
30312
30540
|
reject(error2);
|
|
30313
30541
|
};
|
|
@@ -30385,7 +30613,7 @@ var Protocol = class {
|
|
|
30385
30613
|
if (!parseResult.success) {
|
|
30386
30614
|
reject(parseResult.error);
|
|
30387
30615
|
} else {
|
|
30388
|
-
|
|
30616
|
+
resolve8(parseResult.data);
|
|
30389
30617
|
}
|
|
30390
30618
|
} catch (error2) {
|
|
30391
30619
|
reject(error2);
|
|
@@ -30646,12 +30874,12 @@ var Protocol = class {
|
|
|
30646
30874
|
}
|
|
30647
30875
|
} catch {
|
|
30648
30876
|
}
|
|
30649
|
-
return new Promise((
|
|
30877
|
+
return new Promise((resolve8, reject) => {
|
|
30650
30878
|
if (signal.aborted) {
|
|
30651
30879
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
30652
30880
|
return;
|
|
30653
30881
|
}
|
|
30654
|
-
const timeoutId = setTimeout(
|
|
30882
|
+
const timeoutId = setTimeout(resolve8, interval);
|
|
30655
30883
|
signal.addEventListener("abort", () => {
|
|
30656
30884
|
clearTimeout(timeoutId);
|
|
30657
30885
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -31751,7 +31979,7 @@ var McpServer = class {
|
|
|
31751
31979
|
let task = createTaskResult.task;
|
|
31752
31980
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
31753
31981
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
31754
|
-
await new Promise((
|
|
31982
|
+
await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
|
|
31755
31983
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
31756
31984
|
if (!updatedTask) {
|
|
31757
31985
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -32400,19 +32628,19 @@ var StdioServerTransport = class {
|
|
|
32400
32628
|
this.onclose?.();
|
|
32401
32629
|
}
|
|
32402
32630
|
send(message) {
|
|
32403
|
-
return new Promise((
|
|
32631
|
+
return new Promise((resolve8) => {
|
|
32404
32632
|
const json3 = serializeMessage(message);
|
|
32405
32633
|
if (this._stdout.write(json3)) {
|
|
32406
|
-
|
|
32634
|
+
resolve8();
|
|
32407
32635
|
} else {
|
|
32408
|
-
this._stdout.once("drain",
|
|
32636
|
+
this._stdout.once("drain", resolve8);
|
|
32409
32637
|
}
|
|
32410
32638
|
});
|
|
32411
32639
|
}
|
|
32412
32640
|
};
|
|
32413
32641
|
|
|
32414
32642
|
// src/lib/cli-version.ts
|
|
32415
|
-
var CLI_VERSION = "0.1.
|
|
32643
|
+
var CLI_VERSION = "0.1.6";
|
|
32416
32644
|
|
|
32417
32645
|
// src/commands/mcp-serve.ts
|
|
32418
32646
|
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
@@ -32597,7 +32825,7 @@ async function runFohCli(params) {
|
|
|
32597
32825
|
effectiveArgv.push("--json");
|
|
32598
32826
|
}
|
|
32599
32827
|
const command = `foh ${effectiveArgv.join(" ")}`;
|
|
32600
|
-
return await new Promise((
|
|
32828
|
+
return await new Promise((resolve8) => {
|
|
32601
32829
|
const child = (0, import_node_child_process.spawn)(process.execPath, [cliEntry, ...effectiveArgv], {
|
|
32602
32830
|
stdio: ["ignore", "pipe", "pipe"],
|
|
32603
32831
|
env: {
|
|
@@ -32622,7 +32850,7 @@ async function runFohCli(params) {
|
|
|
32622
32850
|
});
|
|
32623
32851
|
child.once("error", (error2) => {
|
|
32624
32852
|
clearTimeout(timeoutHandle);
|
|
32625
|
-
|
|
32853
|
+
resolve8({
|
|
32626
32854
|
ok: false,
|
|
32627
32855
|
command,
|
|
32628
32856
|
argv: effectiveArgv,
|
|
@@ -32638,7 +32866,7 @@ async function runFohCli(params) {
|
|
|
32638
32866
|
const stderrText = finalizeBoundedText(stderrBuffer);
|
|
32639
32867
|
const exitCode = Number.isFinite(code ?? NaN) ? Number(code) : 1;
|
|
32640
32868
|
const stdoutJson = tryParseJson(stdoutText);
|
|
32641
|
-
|
|
32869
|
+
resolve8({
|
|
32642
32870
|
ok: !timedOut && exitCode === 0,
|
|
32643
32871
|
command,
|
|
32644
32872
|
argv: effectiveArgv,
|
|
@@ -33411,7 +33639,7 @@ function registerMcp(program3) {
|
|
|
33411
33639
|
}
|
|
33412
33640
|
|
|
33413
33641
|
// src/commands/knowledge.ts
|
|
33414
|
-
var
|
|
33642
|
+
var import_fs3 = require("fs");
|
|
33415
33643
|
var import_path2 = require("path");
|
|
33416
33644
|
|
|
33417
33645
|
// src/lib/query-options.ts
|
|
@@ -33473,6 +33701,11 @@ function registerKnowledge(program3) {
|
|
|
33473
33701
|
source: "agent_draft_knowledge",
|
|
33474
33702
|
citation: `agent:${opts.agent}:chunk:${chunk.index + 1}`,
|
|
33475
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
|
+
},
|
|
33476
33709
|
text: chunk.text.slice(0, 1200)
|
|
33477
33710
|
})).filter((chunk) => chunk.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
33478
33711
|
const topScore = matches[0]?.score ?? 0;
|
|
@@ -33485,6 +33718,11 @@ function registerKnowledge(program3) {
|
|
|
33485
33718
|
reason_code: reasonCode,
|
|
33486
33719
|
top_score: topScore,
|
|
33487
33720
|
chunk_count: chunks.length,
|
|
33721
|
+
lineage: {
|
|
33722
|
+
source: "agent_draft_direct",
|
|
33723
|
+
agent_id: opts.agent,
|
|
33724
|
+
candidate_chunk_count: chunks.length
|
|
33725
|
+
},
|
|
33488
33726
|
next_commands: [
|
|
33489
33727
|
`foh knowledge ingest-file --agent ${opts.agent} --file <path> --json`,
|
|
33490
33728
|
`foh knowledge query --agent ${opts.agent} --text "${query.replace(/"/g, '\\"')}" --explain --json`
|
|
@@ -33495,6 +33733,23 @@ function registerKnowledge(program3) {
|
|
|
33495
33733
|
ok: status === "pass",
|
|
33496
33734
|
status,
|
|
33497
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
|
+
],
|
|
33498
33753
|
agent_id: opts.agent,
|
|
33499
33754
|
query,
|
|
33500
33755
|
retrieval: {
|
|
@@ -33502,7 +33757,12 @@ function registerKnowledge(program3) {
|
|
|
33502
33757
|
chunk_count: chunks.length,
|
|
33503
33758
|
match_count: matches.length,
|
|
33504
33759
|
top_score: topScore,
|
|
33505
|
-
min_score: minScore
|
|
33760
|
+
min_score: minScore,
|
|
33761
|
+
lineage: {
|
|
33762
|
+
agent_id: opts.agent,
|
|
33763
|
+
source: "agent_draft_direct",
|
|
33764
|
+
candidate_chunk_count: chunks.length
|
|
33765
|
+
}
|
|
33506
33766
|
},
|
|
33507
33767
|
matches,
|
|
33508
33768
|
failure_packet: packet,
|
|
@@ -33515,7 +33775,7 @@ function registerKnowledge(program3) {
|
|
|
33515
33775
|
}, { json: opts.json ?? false });
|
|
33516
33776
|
}));
|
|
33517
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 () => {
|
|
33518
|
-
const content = (0,
|
|
33778
|
+
const content = (0, import_fs3.readFileSync)(opts.file, "utf-8");
|
|
33519
33779
|
let data;
|
|
33520
33780
|
if (opts.agent) {
|
|
33521
33781
|
const draft = await apiFetch(`/v1/console/agents/${opts.agent}/draft`, {
|
|
@@ -33674,7 +33934,7 @@ var import_crypto3 = require("crypto");
|
|
|
33674
33934
|
|
|
33675
33935
|
// src/lib/signed-report.ts
|
|
33676
33936
|
var import_crypto2 = require("crypto");
|
|
33677
|
-
var
|
|
33937
|
+
var import_fs4 = require("fs");
|
|
33678
33938
|
var import_path3 = require("path");
|
|
33679
33939
|
function canonicalize(value) {
|
|
33680
33940
|
if (value === null || value === void 0) return null;
|
|
@@ -33706,13 +33966,13 @@ function signReport(reportPayload) {
|
|
|
33706
33966
|
}
|
|
33707
33967
|
function writeSignedJsonArtifact(path2, value) {
|
|
33708
33968
|
const absolutePath = (0, import_path3.resolve)(path2);
|
|
33709
|
-
(0,
|
|
33710
|
-
(0,
|
|
33969
|
+
(0, import_fs4.mkdirSync)((0, import_path3.dirname)(absolutePath), { recursive: true });
|
|
33970
|
+
(0, import_fs4.writeFileSync)(absolutePath, stableStringify(value), "utf-8");
|
|
33711
33971
|
return absolutePath;
|
|
33712
33972
|
}
|
|
33713
33973
|
|
|
33714
33974
|
// src/commands/manifest.ts
|
|
33715
|
-
var
|
|
33975
|
+
var import_fs5 = require("fs");
|
|
33716
33976
|
var import_picocolors3 = __toESM(require_picocolors());
|
|
33717
33977
|
function formatDiff(diffs) {
|
|
33718
33978
|
if (diffs.length === 0) return "No changes";
|
|
@@ -33748,7 +34008,7 @@ function formatDiff(diffs) {
|
|
|
33748
34008
|
function loadManifestFile(filePath) {
|
|
33749
34009
|
let raw;
|
|
33750
34010
|
try {
|
|
33751
|
-
raw = (0,
|
|
34011
|
+
raw = (0, import_fs5.readFileSync)(filePath, "utf-8");
|
|
33752
34012
|
} catch {
|
|
33753
34013
|
throw new FohError({
|
|
33754
34014
|
step: "manifest.load",
|
|
@@ -34016,27 +34276,32 @@ function optionNameToFlag(key) {
|
|
|
34016
34276
|
function buildMissingOptionsPlan(missing, opts) {
|
|
34017
34277
|
const missingFlags = missing.map(optionNameToFlag);
|
|
34018
34278
|
const signInUrl = buildConsoleSignInUrl(resolveConsoleBaseUrl(opts.consoleUrl));
|
|
34019
|
-
return {
|
|
34279
|
+
return cliEnvelope({
|
|
34020
34280
|
status: "blocked",
|
|
34021
|
-
|
|
34022
|
-
|
|
34023
|
-
|
|
34024
|
-
|
|
34025
|
-
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: [
|
|
34026
34285
|
"foh auth signup --web --json",
|
|
34027
34286
|
"foh auth login --web --json",
|
|
34028
34287
|
...buildCliAuthFallbackCommands(),
|
|
34029
34288
|
"foh templates list --json",
|
|
34030
34289
|
'foh setup --org <org-id> --agent-template <template-id> --agent-name "Demo Agent" --widget-domains <domain> --report-out setup-report.json --json'
|
|
34031
34290
|
],
|
|
34032
|
-
|
|
34033
|
-
|
|
34034
|
-
|
|
34035
|
-
"
|
|
34036
|
-
|
|
34037
|
-
|
|
34038
|
-
|
|
34039
|
-
|
|
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
|
+
});
|
|
34040
34305
|
}
|
|
34041
34306
|
function emitMissingOptionsPlan(missing, opts) {
|
|
34042
34307
|
const plan = buildMissingOptionsPlan(missing, { consoleUrl: opts.consoleUrl });
|
|
@@ -34527,7 +34792,15 @@ ${serialiseManifest(manifest)}`,
|
|
|
34527
34792
|
});
|
|
34528
34793
|
const reportMeta = emitSetupReport("success");
|
|
34529
34794
|
const summary = {
|
|
34795
|
+
schema_version: "foh_cli_setup_summary.v1",
|
|
34796
|
+
ok: true,
|
|
34530
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
|
+
},
|
|
34531
34804
|
org_id: opts.org,
|
|
34532
34805
|
agent_id: agentId,
|
|
34533
34806
|
phone_number: phoneNumber,
|
|
@@ -34539,7 +34812,15 @@ ${serialiseManifest(manifest)}`,
|
|
|
34539
34812
|
manifest_written: manifestWritten ? "tenant.yaml" : null,
|
|
34540
34813
|
resume_from: resumeState.resumeFrom,
|
|
34541
34814
|
setup_report_hash: reportMeta.reportHash,
|
|
34542
|
-
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
|
+
] : []
|
|
34543
34824
|
};
|
|
34544
34825
|
format(summary, { json: opts.json ?? false });
|
|
34545
34826
|
} catch (error2) {
|
|
@@ -34552,13 +34833,25 @@ ${serialiseManifest(manifest)}`,
|
|
|
34552
34833
|
});
|
|
34553
34834
|
format(
|
|
34554
34835
|
{
|
|
34836
|
+
schema_version: "foh_cli_setup_failure.v1",
|
|
34837
|
+
ok: false,
|
|
34555
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
|
+
},
|
|
34556
34845
|
step: error2.step,
|
|
34557
34846
|
completed_steps: completed.map((stepResult) => stepResult.step),
|
|
34558
34847
|
error: error2.error,
|
|
34559
34848
|
remediation: error2.remediation,
|
|
34560
34849
|
setup_report_hash: reportMeta.reportHash,
|
|
34561
|
-
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)
|
|
34562
34855
|
},
|
|
34563
34856
|
{ json: opts.json ?? false }
|
|
34564
34857
|
);
|
|
@@ -34902,7 +35195,7 @@ function registerConversations(program3) {
|
|
|
34902
35195
|
}
|
|
34903
35196
|
|
|
34904
35197
|
// src/commands/transcripts.ts
|
|
34905
|
-
var
|
|
35198
|
+
var import_fs6 = require("fs");
|
|
34906
35199
|
var import_path4 = require("path");
|
|
34907
35200
|
function listPath(agentId, opts) {
|
|
34908
35201
|
const params = new URLSearchParams();
|
|
@@ -34916,6 +35209,27 @@ function listPath(agentId, opts) {
|
|
|
34916
35209
|
function jsonl(rows) {
|
|
34917
35210
|
return rows.map((row) => JSON.stringify(row)).join("\n") + (rows.length > 0 ? "\n" : "");
|
|
34918
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
|
+
}
|
|
34919
35233
|
function registerTranscripts(program3) {
|
|
34920
35234
|
const transcripts = program3.command("transcripts").description("List, fetch, and export conversation transcripts");
|
|
34921
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 () => {
|
|
@@ -34929,7 +35243,7 @@ function registerTranscripts(program3) {
|
|
|
34929
35243
|
const data = await apiFetch(path2, { orgId: opts.org, apiUrlOverride: opts.apiUrl });
|
|
34930
35244
|
format(data, { json: opts.json ?? false });
|
|
34931
35245
|
}));
|
|
34932
|
-
transcripts.command("export").description("Export recent transcripts as JSON or JSONL").requiredOption("--agent <id>", "Agent ID").option("--q <query>", "Full-text transcript query").option("--from <iso-date>", "Start datetime (ISO8601)").option("--to <iso-date>", "End datetime (ISO8601)").option("--limit <n>", "Rows to export (1-100)", "100").option("--format <value>", "Export format: jsonl or json", "jsonl").option("--out <path>", "Output file path").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
|
|
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 () => {
|
|
34933
35247
|
const exportFormat = String(opts.format || "jsonl").trim().toLowerCase();
|
|
34934
35248
|
if (!["jsonl", "json"].includes(exportFormat)) {
|
|
34935
35249
|
throw new FohError({
|
|
@@ -34943,16 +35257,74 @@ function registerTranscripts(program3) {
|
|
|
34943
35257
|
orgId: opts.org,
|
|
34944
35258
|
apiUrlOverride: opts.apiUrl
|
|
34945
35259
|
});
|
|
34946
|
-
const
|
|
34947
|
-
const
|
|
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);
|
|
34948
35288
|
if (opts.out) {
|
|
34949
35289
|
const outputPath = (0, import_path4.resolve)(String(opts.out));
|
|
34950
|
-
(0,
|
|
34951
|
-
format({
|
|
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 });
|
|
34952
35309
|
return;
|
|
34953
35310
|
}
|
|
34954
35311
|
if (opts.json || exportFormat === "json") {
|
|
34955
|
-
format({
|
|
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 });
|
|
34956
35328
|
return;
|
|
34957
35329
|
}
|
|
34958
35330
|
process.stdout.write(content);
|
|
@@ -35017,6 +35389,14 @@ function registerAnalytics(program3) {
|
|
|
35017
35389
|
format({
|
|
35018
35390
|
schema_version: "foh_analytics_fetch.v1",
|
|
35019
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: {},
|
|
35020
35400
|
agent_id: opts.agent,
|
|
35021
35401
|
preset,
|
|
35022
35402
|
window_days: windowDays,
|
|
@@ -35220,16 +35600,39 @@ function registerTests(program3) {
|
|
|
35220
35600
|
}
|
|
35221
35601
|
|
|
35222
35602
|
// src/commands/test.ts
|
|
35223
|
-
var
|
|
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");
|
|
35224
35608
|
var import_path5 = require("path");
|
|
35225
35609
|
function asStringList(value) {
|
|
35226
35610
|
if (typeof value === "string" && value.trim()) return [value.trim()];
|
|
35227
35611
|
if (Array.isArray(value)) return value.map((entry) => String(entry || "").trim()).filter(Boolean);
|
|
35228
35612
|
return [];
|
|
35229
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
|
+
}
|
|
35230
35634
|
function parseSuiteFile(path2) {
|
|
35231
|
-
const
|
|
35232
|
-
const parsed = path2.toLowerCase().endsWith(".json") ? JSON.parse(raw) : load(raw);
|
|
35635
|
+
const parsed = parseStructuredFile(path2);
|
|
35233
35636
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
35234
35637
|
throw new FohError({
|
|
35235
35638
|
step: "test.run",
|
|
@@ -35240,7 +35643,33 @@ function parseSuiteFile(path2) {
|
|
|
35240
35643
|
}
|
|
35241
35644
|
return parsed;
|
|
35242
35645
|
}
|
|
35243
|
-
function
|
|
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) {
|
|
35244
35673
|
const scenarios = Array.isArray(suite.scenarios) ? suite.scenarios : [];
|
|
35245
35674
|
if (scenarios.length === 0) {
|
|
35246
35675
|
throw new FohError({
|
|
@@ -35250,17 +35679,18 @@ function validateSuite(suite) {
|
|
|
35250
35679
|
statusCode: 400
|
|
35251
35680
|
});
|
|
35252
35681
|
}
|
|
35253
|
-
|
|
35254
|
-
|
|
35682
|
+
return scenarios.map((scenario) => {
|
|
35683
|
+
const turns = resolveScenarioTurns(scenario, suitePath);
|
|
35684
|
+
if (turns.length === 0) {
|
|
35255
35685
|
throw new FohError({
|
|
35256
35686
|
step: "test.run",
|
|
35257
35687
|
error: `Scenario "${scenario.id || scenario.name || "(unnamed)"}" has no turns`,
|
|
35258
|
-
remediation: "Add turns with user
|
|
35688
|
+
remediation: "Add inline turns or a fixture_transcript file with user messages.",
|
|
35259
35689
|
statusCode: 400
|
|
35260
35690
|
});
|
|
35261
35691
|
}
|
|
35262
|
-
|
|
35263
|
-
|
|
35692
|
+
return { ...scenario, turns };
|
|
35693
|
+
});
|
|
35264
35694
|
}
|
|
35265
35695
|
function evaluateReply(reply, expect) {
|
|
35266
35696
|
const lowerReply = reply.toLowerCase();
|
|
@@ -35279,10 +35709,128 @@ function evaluateReply(reply, expect) {
|
|
|
35279
35709
|
}
|
|
35280
35710
|
return failures;
|
|
35281
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
|
|
35282
35830
|
function registerTest(program3) {
|
|
35283
35831
|
const test = program3.command("test").description("Run local scenario suites against runtime channels");
|
|
35284
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 () => {
|
|
35285
|
-
const suitePath = (0,
|
|
35833
|
+
const suitePath = (0, import_path6.resolve)(String(opts.suite));
|
|
35286
35834
|
const suite = parseSuiteFile(suitePath);
|
|
35287
35835
|
const agentId = String(opts.agent || suite.agent || "").trim();
|
|
35288
35836
|
if (!agentId) {
|
|
@@ -35293,7 +35841,7 @@ function registerTest(program3) {
|
|
|
35293
35841
|
statusCode: 400
|
|
35294
35842
|
});
|
|
35295
35843
|
}
|
|
35296
|
-
const scenarios = validateSuite(suite);
|
|
35844
|
+
const scenarios = validateSuite(suite, suitePath);
|
|
35297
35845
|
const ensure = await apiFetch("/v1/console/channels/widget/ensure", {
|
|
35298
35846
|
method: "POST",
|
|
35299
35847
|
body: JSON.stringify({ agentId }),
|
|
@@ -35316,7 +35864,7 @@ function registerTest(program3) {
|
|
|
35316
35864
|
const scenario = scenarios[scenarioIndex];
|
|
35317
35865
|
let conversationId;
|
|
35318
35866
|
const turnResults = [];
|
|
35319
|
-
for (let turnIndex = 0; turnIndex <
|
|
35867
|
+
for (let turnIndex = 0; turnIndex < scenario.turns.length; turnIndex += 1) {
|
|
35320
35868
|
const turn = scenario.turns[turnIndex];
|
|
35321
35869
|
const message = String(turn.user || turn.message || "").trim();
|
|
35322
35870
|
if (!message) {
|
|
@@ -35324,6 +35872,7 @@ function registerTest(program3) {
|
|
|
35324
35872
|
turnResults.push({ turn: turnIndex + 1, ok: false, failures: ["missing user/message"] });
|
|
35325
35873
|
continue;
|
|
35326
35874
|
}
|
|
35875
|
+
const start = Date.now();
|
|
35327
35876
|
const response = await apiFetch("/v1/widget/inbound", {
|
|
35328
35877
|
method: "POST",
|
|
35329
35878
|
body: JSON.stringify({
|
|
@@ -35334,9 +35883,13 @@ function registerTest(program3) {
|
|
|
35334
35883
|
}),
|
|
35335
35884
|
apiUrlOverride: opts.apiUrl
|
|
35336
35885
|
});
|
|
35886
|
+
const latencyMs = Date.now() - start;
|
|
35337
35887
|
conversationId = response.conversationId || conversationId;
|
|
35338
35888
|
const reply = String(response.reply || "");
|
|
35339
|
-
const failures = reply ?
|
|
35889
|
+
const failures = reply ? [
|
|
35890
|
+
...evaluateReply(reply, turn.expect),
|
|
35891
|
+
...evaluateStructuredExpectations(response, turn.expect, latencyMs)
|
|
35892
|
+
] : ["empty reply"];
|
|
35340
35893
|
if (failures.length === 0) passed += 1;
|
|
35341
35894
|
else failed += 1;
|
|
35342
35895
|
turnResults.push({
|
|
@@ -35345,9 +35898,12 @@ function registerTest(program3) {
|
|
|
35345
35898
|
message,
|
|
35346
35899
|
reply,
|
|
35347
35900
|
failures,
|
|
35901
|
+
latency_ms: latencyMs,
|
|
35348
35902
|
conversation_id: response.conversationId ?? null,
|
|
35349
35903
|
trace_id: response.trace_id ?? null,
|
|
35350
|
-
correlation_id: response.correlation_id ?? null
|
|
35904
|
+
correlation_id: response.correlation_id ?? null,
|
|
35905
|
+
action: response.action ?? null,
|
|
35906
|
+
handoff: response.handoff ?? null
|
|
35351
35907
|
});
|
|
35352
35908
|
}
|
|
35353
35909
|
scenarioResults.push({
|
|
@@ -35359,16 +35915,24 @@ function registerTest(program3) {
|
|
|
35359
35915
|
}
|
|
35360
35916
|
const report = {
|
|
35361
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`],
|
|
35362
35927
|
suite_path: suitePath,
|
|
35363
35928
|
agent_id: agentId,
|
|
35364
|
-
ok: failed === 0,
|
|
35365
35929
|
passed,
|
|
35366
35930
|
failed,
|
|
35367
35931
|
scenarios: scenarioResults
|
|
35368
35932
|
};
|
|
35369
35933
|
if (opts.out) {
|
|
35370
|
-
const out = (0,
|
|
35371
|
-
(0,
|
|
35934
|
+
const out = (0, import_path6.resolve)(String(opts.out));
|
|
35935
|
+
(0, import_fs8.writeFileSync)(out, stableStringify(report), "utf-8");
|
|
35372
35936
|
format({ ...report, output_path: out }, { json: opts.json ?? false });
|
|
35373
35937
|
} else {
|
|
35374
35938
|
format(report, { json: opts.json ?? false });
|
|
@@ -35791,8 +36355,210 @@ function registerDiag(program3) {
|
|
|
35791
36355
|
}
|
|
35792
36356
|
|
|
35793
36357
|
// src/commands/bug.ts
|
|
35794
|
-
var
|
|
35795
|
-
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
|
|
35796
36562
|
var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
35797
36563
|
var MAX_BODY_PREVIEW_LENGTH = 200;
|
|
35798
36564
|
function parseMethod(raw) {
|
|
@@ -35908,14 +36674,18 @@ function parseRequestBody(raw) {
|
|
|
35908
36674
|
}
|
|
35909
36675
|
}
|
|
35910
36676
|
function writeJsonArtifact(path2, value) {
|
|
35911
|
-
const absolutePath = (0,
|
|
35912
|
-
(0,
|
|
35913
|
-
(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");
|
|
35914
36680
|
return absolutePath;
|
|
35915
36681
|
}
|
|
35916
36682
|
function defaultArtifactPath() {
|
|
35917
36683
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
35918
|
-
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`);
|
|
35919
36689
|
}
|
|
35920
36690
|
async function resolveBugReportWizardInputs(opts) {
|
|
35921
36691
|
if (!opts.wizard) return opts;
|
|
@@ -36065,6 +36835,44 @@ function registerBug(program3) {
|
|
|
36065
36835
|
throw e;
|
|
36066
36836
|
}
|
|
36067
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
|
+
}));
|
|
36068
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) => {
|
|
36069
36877
|
try {
|
|
36070
36878
|
const params = new URLSearchParams();
|
|
@@ -36108,18 +36916,26 @@ function registerBug(program3) {
|
|
|
36108
36916
|
}
|
|
36109
36917
|
|
|
36110
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
|
+
}
|
|
36111
36927
|
function pass(name, summary, detail) {
|
|
36112
|
-
return { name, status: "pass", reason_code: `${name}_ok`, summary, detail };
|
|
36928
|
+
return { name, category: categoryForCheck(name), status: "pass", reason_code: `${name}_ok`, summary, detail };
|
|
36113
36929
|
}
|
|
36114
36930
|
function hold(name, reasonCode, summary, nextCommand, detail) {
|
|
36115
|
-
return { name, status: "hold", reason_code: reasonCode, summary, next_command: nextCommand, detail };
|
|
36931
|
+
return { name, category: categoryForCheck(name), status: "hold", reason_code: reasonCode, summary, next_command: nextCommand, detail };
|
|
36116
36932
|
}
|
|
36117
36933
|
function fail(name, reasonCode, error2, nextCommand) {
|
|
36118
36934
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
36119
|
-
return { name, status: "fail", reason_code: reasonCode, summary: message, next_command: nextCommand };
|
|
36935
|
+
return { name, category: categoryForCheck(name), status: "fail", reason_code: reasonCode, summary: message, next_command: nextCommand };
|
|
36120
36936
|
}
|
|
36121
36937
|
function skipped(name, reasonCode, summary, nextCommand) {
|
|
36122
|
-
return { name, status: "skipped", reason_code: reasonCode, summary, next_command: nextCommand };
|
|
36938
|
+
return { name, category: categoryForCheck(name), status: "skipped", reason_code: reasonCode, summary, next_command: nextCommand };
|
|
36123
36939
|
}
|
|
36124
36940
|
function hasBlockingChecks(checks) {
|
|
36125
36941
|
return checks.some((check2) => check2.status === "hold" || check2.status === "fail");
|
|
@@ -36130,6 +36946,21 @@ function publicKeyFromEnsureResponse(response) {
|
|
|
36130
36946
|
const publicKey = channel.public_key ?? record2.widget_public_key ?? record2.public_key;
|
|
36131
36947
|
return typeof publicKey === "string" && publicKey.trim() ? publicKey.trim() : void 0;
|
|
36132
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
|
+
}
|
|
36133
36964
|
function agentIdFromList(response) {
|
|
36134
36965
|
const agents = Array.isArray(response.agents) ? response.agents : [];
|
|
36135
36966
|
const usable = agents.filter((agent) => typeof agent.id === "string" && agent.id.trim());
|
|
@@ -36144,8 +36975,10 @@ function firstUsableOrgId(response) {
|
|
|
36144
36975
|
return { orgId: usable.length === 1 ? usable[0] : void 0, count: usable.length };
|
|
36145
36976
|
}
|
|
36146
36977
|
function registerProve(program3) {
|
|
36147
|
-
program3.command("prove").description("Produce one setup/runtime proof bundle for an agent").option("--agent <id>", "Agent ID to prove").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--cert-mode <m>", "Simulation cert mode: quick, full, stress", "quick").option("--cert-adaptive-runs <n>", "Adaptive runs for full/stress certification", "30").option("--cert-max-improvement-rounds <n>", "Max prompt improvement rounds in cert loop (0-5)", "1").option("--require-phone", "Hold proof if no phone/contact number is provisioned").option("--skip-cert", "Skip simulation certification check").option("--skip-smoke", "Skip widget runtime smoke check").option("--skip-voice-health", "Skip realtime voice provider health check").option("--out <path>", "Write signed proof report JSON to this path").option("--strict", "Exit non-zero unless all non-skipped checks pass").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
|
|
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 () => {
|
|
36148
36979
|
const checks = [];
|
|
36980
|
+
const mission = normalizeMission(opts.mission);
|
|
36981
|
+
const mutationMode = normalizeMutationMode(opts.mutationMode, Boolean(opts.repair));
|
|
36149
36982
|
const ctx = {
|
|
36150
36983
|
tokenPresent: false,
|
|
36151
36984
|
traceIds: [],
|
|
@@ -36247,9 +37080,10 @@ function registerProve(program3) {
|
|
|
36247
37080
|
phone_number_present: true,
|
|
36248
37081
|
provisioning_status: onboarding.provisioning_status ?? null
|
|
36249
37082
|
}));
|
|
36250
|
-
} else if (opts.requirePhone) {
|
|
37083
|
+
} else if (opts.requirePhone || mission === "voice") {
|
|
36251
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`, {
|
|
36252
|
-
provisioning_status: onboarding.provisioning_status ?? null
|
|
37085
|
+
provisioning_status: onboarding.provisioning_status ?? null,
|
|
37086
|
+
mission
|
|
36253
37087
|
}));
|
|
36254
37088
|
} else {
|
|
36255
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`));
|
|
@@ -36282,36 +37116,56 @@ function registerProve(program3) {
|
|
|
36282
37116
|
checks.push(fail("voice_realtime_health", "voice_realtime_health_failed", error2, "foh voice realtime-health --json"));
|
|
36283
37117
|
}
|
|
36284
37118
|
}
|
|
36285
|
-
try {
|
|
36286
|
-
const ensure = await apiFetch("/v1/console/channels/widget/ensure", {
|
|
36287
|
-
method: "POST",
|
|
36288
|
-
body: JSON.stringify({ agentId: ctx.agentId }),
|
|
36289
|
-
orgId: ctx.orgId,
|
|
36290
|
-
apiUrlOverride: opts.apiUrl
|
|
36291
|
-
});
|
|
36292
|
-
const publicKey = publicKeyFromEnsureResponse(ensure);
|
|
36293
|
-
if (!publicKey) {
|
|
36294
|
-
checks.push(hold("widget_channel", "widget_public_key_missing", "Widget channel exists but no public key was returned.", `foh widget ensure --agent ${ctx.agentId} --json`, ensure));
|
|
36295
|
-
} else {
|
|
36296
|
-
ctx.widgetPublicKey = publicKey;
|
|
36297
|
-
checks.push(pass("widget_channel", "Widget channel is available.", { public_key_present: true }));
|
|
36298
|
-
}
|
|
36299
|
-
} catch (error2) {
|
|
36300
|
-
checks.push(fail("widget_channel", "widget_channel_failed", error2, `foh widget ensure --agent ${ctx.agentId} --json`));
|
|
36301
|
-
}
|
|
36302
37119
|
try {
|
|
36303
37120
|
const embed = await apiFetch("/v1/console/channels/widget/embed-snippet", {
|
|
36304
37121
|
orgId: ctx.orgId,
|
|
36305
37122
|
apiUrlOverride: opts.apiUrl,
|
|
36306
37123
|
headers: { "x-agent-id": ctx.agentId }
|
|
36307
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
|
+
}
|
|
36308
37133
|
if (typeof embed.snippet === "string" && embed.snippet.trim()) {
|
|
36309
37134
|
checks.push(pass("widget_embed", "Widget embed snippet is available.", { snippet_present: true }));
|
|
36310
37135
|
} else {
|
|
36311
37136
|
checks.push(hold("widget_embed", "widget_embed_missing", "Widget embed snippet is missing.", `foh widget embed-snippet --agent ${ctx.agentId}`));
|
|
36312
37137
|
}
|
|
36313
37138
|
} catch (error2) {
|
|
36314
|
-
|
|
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
|
+
}
|
|
36315
37169
|
}
|
|
36316
37170
|
if (opts.skipSmoke) {
|
|
36317
37171
|
checks.push(skipped("widget_smoke", "operator_skipped", "Skipped by --skip-smoke.", `foh widget smoke --agent ${ctx.agentId} --json`));
|
|
@@ -36372,22 +37226,27 @@ function registerProve(program3) {
|
|
|
36372
37226
|
if (status === "pass" && ctx.agentId) {
|
|
36373
37227
|
nextCommands.push(`foh agent publish --agent ${ctx.agentId} --json`);
|
|
36374
37228
|
}
|
|
36375
|
-
const report = signReport({
|
|
36376
|
-
|
|
36377
|
-
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
36378
|
-
ok: status === "pass",
|
|
37229
|
+
const report = signReport(cliEnvelope({
|
|
37230
|
+
schemaVersion: "foh_cli_proof_report.v1",
|
|
36379
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.",
|
|
36380
37234
|
ids: {
|
|
36381
37235
|
org_id: ctx.orgId ?? null,
|
|
36382
37236
|
agent_id: ctx.agentId ?? null,
|
|
37237
|
+
mission,
|
|
37238
|
+
mutation_mode: mutationMode,
|
|
36383
37239
|
widget_public_key_present: Boolean(ctx.widgetPublicKey),
|
|
36384
37240
|
conversation_id: ctx.conversationId ?? null,
|
|
36385
37241
|
trace_ids: ctx.traceIds,
|
|
36386
37242
|
correlation_ids: ctx.correlationIds
|
|
36387
37243
|
},
|
|
36388
37244
|
checks,
|
|
36389
|
-
|
|
36390
|
-
|
|
37245
|
+
nextCommands,
|
|
37246
|
+
extra: {
|
|
37247
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
37248
|
+
}
|
|
37249
|
+
}));
|
|
36391
37250
|
const artifactPath = opts.out ? writeSignedJsonArtifact(String(opts.out), report) : void 0;
|
|
36392
37251
|
format(artifactPath ? { ...report, artifact_path: artifactPath } : report, { json: opts.json ?? false });
|
|
36393
37252
|
if (opts.strict && status !== "pass") markCommandFailed(1);
|
|
@@ -36839,7 +37698,7 @@ async function runSelf(args, apiUrlOverride) {
|
|
|
36839
37698
|
if (apiUrlOverride && !spawnArgs.includes("--api-url")) {
|
|
36840
37699
|
spawnArgs.push("--api-url", apiUrlOverride);
|
|
36841
37700
|
}
|
|
36842
|
-
return await new Promise((
|
|
37701
|
+
return await new Promise((resolve8, reject) => {
|
|
36843
37702
|
const child = (0, import_child_process2.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
|
|
36844
37703
|
stdio: "inherit",
|
|
36845
37704
|
env: {
|
|
@@ -36849,7 +37708,7 @@ async function runSelf(args, apiUrlOverride) {
|
|
|
36849
37708
|
}
|
|
36850
37709
|
});
|
|
36851
37710
|
child.once("error", reject);
|
|
36852
|
-
child.once("close", (code) =>
|
|
37711
|
+
child.once("close", (code) => resolve8(typeof code === "number" ? code : 1));
|
|
36853
37712
|
});
|
|
36854
37713
|
}
|
|
36855
37714
|
function shouldUseInteractiveHome(argv) {
|
|
@@ -37139,8 +37998,8 @@ function maybeDefaultToHome(argv = process.argv) {
|
|
|
37139
37998
|
}
|
|
37140
37999
|
|
|
37141
38000
|
// src/lib/update.ts
|
|
37142
|
-
var
|
|
37143
|
-
var
|
|
38001
|
+
var import_fs11 = require("fs");
|
|
38002
|
+
var import_path9 = require("path");
|
|
37144
38003
|
var import_child_process3 = require("child_process");
|
|
37145
38004
|
var import_crypto5 = require("crypto");
|
|
37146
38005
|
function parseSemver(version2) {
|
|
@@ -37161,7 +38020,7 @@ function compareSemver(a, b) {
|
|
|
37161
38020
|
}
|
|
37162
38021
|
function readPackageJsonVersion(path2) {
|
|
37163
38022
|
try {
|
|
37164
|
-
const raw = (0,
|
|
38023
|
+
const raw = (0, import_fs11.readFileSync)(path2, "utf-8");
|
|
37165
38024
|
const parsed = JSON.parse(raw);
|
|
37166
38025
|
const version2 = String(parsed.version ?? "").trim();
|
|
37167
38026
|
return version2 || void 0;
|
|
@@ -37170,13 +38029,13 @@ function readPackageJsonVersion(path2) {
|
|
|
37170
38029
|
}
|
|
37171
38030
|
}
|
|
37172
38031
|
function findRepoRoot(startCwd = process.cwd()) {
|
|
37173
|
-
let current = (0,
|
|
38032
|
+
let current = (0, import_path9.resolve)(startCwd);
|
|
37174
38033
|
while (true) {
|
|
37175
|
-
const rootPackageJsonPath = (0,
|
|
37176
|
-
const cliPackageJsonPath = (0,
|
|
37177
|
-
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)) {
|
|
37178
38037
|
try {
|
|
37179
|
-
const raw = (0,
|
|
38038
|
+
const raw = (0, import_fs11.readFileSync)(rootPackageJsonPath, "utf-8");
|
|
37180
38039
|
const parsed = JSON.parse(raw);
|
|
37181
38040
|
if (String(parsed.name ?? "").trim() === "front-of-house") {
|
|
37182
38041
|
return current;
|
|
@@ -37184,7 +38043,7 @@ function findRepoRoot(startCwd = process.cwd()) {
|
|
|
37184
38043
|
} catch {
|
|
37185
38044
|
}
|
|
37186
38045
|
}
|
|
37187
|
-
const parent = (0,
|
|
38046
|
+
const parent = (0, import_path9.dirname)(current);
|
|
37188
38047
|
if (parent === current) return void 0;
|
|
37189
38048
|
current = parent;
|
|
37190
38049
|
}
|
|
@@ -37198,7 +38057,7 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
|
|
|
37198
38057
|
remediation: "Run this command from the Front Of House repo root to compare/install the latest CLI."
|
|
37199
38058
|
};
|
|
37200
38059
|
}
|
|
37201
|
-
const cliPackageJsonPath = (0,
|
|
38060
|
+
const cliPackageJsonPath = (0, import_path9.join)(repoRoot, "packages", "cli", "package.json");
|
|
37202
38061
|
const latestVersion = readPackageJsonVersion(cliPackageJsonPath);
|
|
37203
38062
|
if (!latestVersion) {
|
|
37204
38063
|
return {
|
|
@@ -37225,19 +38084,19 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
|
|
|
37225
38084
|
};
|
|
37226
38085
|
}
|
|
37227
38086
|
async function applyRepoUpdate(repoRoot) {
|
|
37228
|
-
const scriptPath = (0,
|
|
38087
|
+
const scriptPath = (0, import_path9.join)(repoRoot, "scripts", "Install-FohCli.ps1");
|
|
37229
38088
|
if (process.platform === "win32") {
|
|
37230
|
-
return await new Promise((
|
|
38089
|
+
return await new Promise((resolve8, reject) => {
|
|
37231
38090
|
const child = (0, import_child_process3.spawn)(
|
|
37232
38091
|
"powershell",
|
|
37233
38092
|
["-ExecutionPolicy", "Bypass", "-File", scriptPath],
|
|
37234
38093
|
{ stdio: "inherit" }
|
|
37235
38094
|
);
|
|
37236
38095
|
child.once("error", reject);
|
|
37237
|
-
child.once("close", (code) =>
|
|
38096
|
+
child.once("close", (code) => resolve8(typeof code === "number" ? code : 1));
|
|
37238
38097
|
});
|
|
37239
38098
|
}
|
|
37240
|
-
return await new Promise((
|
|
38099
|
+
return await new Promise((resolve8, reject) => {
|
|
37241
38100
|
const child = (0, import_child_process3.spawn)(
|
|
37242
38101
|
"corepack",
|
|
37243
38102
|
["pnpm", "cli:install:global"],
|
|
@@ -37247,7 +38106,7 @@ async function applyRepoUpdate(repoRoot) {
|
|
|
37247
38106
|
}
|
|
37248
38107
|
);
|
|
37249
38108
|
child.once("error", reject);
|
|
37250
|
-
child.once("close", (code) =>
|
|
38109
|
+
child.once("close", (code) => resolve8(typeof code === "number" ? code : 1));
|
|
37251
38110
|
});
|
|
37252
38111
|
}
|
|
37253
38112
|
function shouldShowUpdateNotice(argv = process.argv) {
|
|
@@ -37261,7 +38120,7 @@ function shouldShowUpdateNotice(argv = process.argv) {
|
|
|
37261
38120
|
}
|
|
37262
38121
|
function hashFileSha256(filePath) {
|
|
37263
38122
|
try {
|
|
37264
|
-
const bytes = (0,
|
|
38123
|
+
const bytes = (0, import_fs11.readFileSync)(filePath);
|
|
37265
38124
|
return (0, import_crypto5.createHash)("sha256").update(bytes).digest("hex");
|
|
37266
38125
|
} catch {
|
|
37267
38126
|
return void 0;
|
|
@@ -37271,10 +38130,10 @@ function verifyCliArtifactIntegrity(params = {}) {
|
|
|
37271
38130
|
const cwd = params.cwd ?? process.cwd();
|
|
37272
38131
|
const argv = params.argv ?? process.argv;
|
|
37273
38132
|
const expectedSha256 = String(params.expectedSha256 ?? "").trim().toLowerCase() || void 0;
|
|
37274
|
-
const runtimePath = (0,
|
|
38133
|
+
const runtimePath = (0, import_path9.resolve)(String(argv[1] || ""));
|
|
37275
38134
|
const runtimeHash = runtimePath ? hashFileSha256(runtimePath) : void 0;
|
|
37276
38135
|
const warnings = [];
|
|
37277
|
-
if (!runtimePath || !(0,
|
|
38136
|
+
if (!runtimePath || !(0, import_fs11.existsSync)(runtimePath)) {
|
|
37278
38137
|
warnings.push("runtime_path_unreadable");
|
|
37279
38138
|
}
|
|
37280
38139
|
if (!runtimeHash) {
|
|
@@ -37292,8 +38151,8 @@ function verifyCliArtifactIntegrity(params = {}) {
|
|
|
37292
38151
|
let repoDistHash;
|
|
37293
38152
|
let runtimeMatchesRepoDist;
|
|
37294
38153
|
if (repoRoot) {
|
|
37295
|
-
repoDistPath = (0,
|
|
37296
|
-
if ((0,
|
|
38154
|
+
repoDistPath = (0, import_path9.join)(repoRoot, "packages", "cli", "dist", "foh.js");
|
|
38155
|
+
if ((0, import_fs11.existsSync)(repoDistPath)) {
|
|
37297
38156
|
repoDistHash = hashFileSha256(repoDistPath);
|
|
37298
38157
|
if (runtimeHash && repoDistHash) {
|
|
37299
38158
|
runtimeMatchesRepoDist = runtimeHash === repoDistHash;
|
|
@@ -37481,13 +38340,19 @@ function writeKnownError(message, remediation) {
|
|
|
37481
38340
|
if (jsonRequested()) {
|
|
37482
38341
|
process.stderr.write(
|
|
37483
38342
|
JSON.stringify(
|
|
37484
|
-
{
|
|
37485
|
-
|
|
37486
|
-
|
|
37487
|
-
|
|
37488
|
-
|
|
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
|
+
}
|
|
37489
38354
|
}
|
|
37490
|
-
},
|
|
38355
|
+
}),
|
|
37491
38356
|
null,
|
|
37492
38357
|
2
|
|
37493
38358
|
) + "\n"
|
|
@@ -37564,13 +38429,19 @@ program2.parseAsync(process.argv).catch((e) => {
|
|
|
37564
38429
|
if (jsonRequested()) {
|
|
37565
38430
|
process.stderr.write(
|
|
37566
38431
|
JSON.stringify(
|
|
37567
|
-
{
|
|
37568
|
-
|
|
37569
|
-
|
|
37570
|
-
|
|
37571
|
-
|
|
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
|
+
}
|
|
37572
38443
|
}
|
|
37573
|
-
},
|
|
38444
|
+
}),
|
|
37574
38445
|
null,
|
|
37575
38446
|
2
|
|
37576
38447
|
) + "\n"
|