pi-advisor-flow 0.11.2 → 0.12.0
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/CHANGELOG.md +12 -0
- package/README.md +4 -2
- package/dist/index.js +863 -238
- package/package.json +10 -10
- package/src/commands/settings-commands.ts +43 -0
- package/src/commands/settings-persistence.ts +9 -2
- package/src/config/types.ts +6 -1
- package/src/jev/client.ts +88 -22
- package/src/jev/decisions-client.ts +124 -0
- package/src/jev/decisions-response.ts +198 -0
- package/src/jev/key-store.ts +1 -1
- package/src/jev/ledger.ts +28 -6
- package/src/jev/openai-key-store.ts +159 -0
- package/src/jev/transport.ts +103 -25
- package/src/tools/jev-filter.ts +2 -2
- package/src/tools/jev-turn-gate.ts +2 -2
- package/src/ui/jev-setup-submenu-view.ts +96 -0
- package/src/ui/jev-setup-submenu.ts +253 -235
- package/src/ui/jev-setup-support.ts +173 -0
- package/src/ui/settings-items.ts +28 -20
- package/src/ui/settings-mutations.ts +0 -5
- package/src/ui/settings-selector.ts +29 -0
- package/src/ui/types.ts +32 -2
package/dist/index.js
CHANGED
|
@@ -27,7 +27,8 @@ var DEFAULT_JEV_TRANSPORT = "auto";
|
|
|
27
27
|
var JEV_TRANSPORTS = [
|
|
28
28
|
"auto",
|
|
29
29
|
"typesafe",
|
|
30
|
-
"openrouter"
|
|
30
|
+
"openrouter",
|
|
31
|
+
"openai-decisions"
|
|
31
32
|
];
|
|
32
33
|
var ADVISOR_TOOL_POLICIES = [
|
|
33
34
|
"full",
|
|
@@ -4628,7 +4629,9 @@ class AdvisorFollowUpCache {
|
|
|
4628
4629
|
var freshJevUsage = () => ({
|
|
4629
4630
|
cost: 0,
|
|
4630
4631
|
inputTokens: 0,
|
|
4631
|
-
|
|
4632
|
+
knownCostCalls: 0,
|
|
4633
|
+
outputTokens: 0,
|
|
4634
|
+
unavailableCostCalls: 0
|
|
4632
4635
|
});
|
|
4633
4636
|
var freshJevLedger = () => ({
|
|
4634
4637
|
filter: {
|
|
@@ -4643,7 +4646,12 @@ var freshJevLedger = () => ({
|
|
|
4643
4646
|
usage: freshJevUsage()
|
|
4644
4647
|
});
|
|
4645
4648
|
var addJevUsage = (totals, usage) => {
|
|
4646
|
-
|
|
4649
|
+
if (usage.cost === undefined) {
|
|
4650
|
+
totals.unavailableCostCalls += 1;
|
|
4651
|
+
} else {
|
|
4652
|
+
totals.cost += usage.cost;
|
|
4653
|
+
totals.knownCostCalls += 1;
|
|
4654
|
+
}
|
|
4647
4655
|
totals.inputTokens += usage.inputTokens;
|
|
4648
4656
|
totals.outputTokens += usage.outputTokens;
|
|
4649
4657
|
};
|
|
@@ -4655,6 +4663,12 @@ var savingsLine = (costs, skipped) => {
|
|
|
4655
4663
|
return `Estimated saving from skips: ≤ $${(mean * skipped).toFixed(4)} — upper bound; assumes each skipped consultation would have cost this session's mean allowed-consultation cost ($${mean.toFixed(4)}), which the skipped calls would likely have undercut`;
|
|
4656
4664
|
};
|
|
4657
4665
|
var formatJevTokens = (usage) => `↑${formatTokenCount(usage.inputTokens + usage.outputTokens)}`;
|
|
4666
|
+
var jevCostText = (usage) => {
|
|
4667
|
+
if (usage.unavailableCostCalls > 0) {
|
|
4668
|
+
return usage.knownCostCalls > 0 ? `cost unavailable (known-provider estimates $${usage.cost.toFixed(4)}; Decisions billing unconfirmed)` : "cost unavailable (Decisions billing unconfirmed)";
|
|
4669
|
+
}
|
|
4670
|
+
return `$${usage.cost.toFixed(4)}`;
|
|
4671
|
+
};
|
|
4658
4672
|
var markdownCosts = (invocations) => invocations.filter((item) => item.kind === "markdown" && typeof item.cost === "number").map((item) => item.cost);
|
|
4659
4673
|
var filterLine = (filter) => {
|
|
4660
4674
|
const head = `${filter.screened} screened (${filter.allowed} allowed, ${filter.skipped} skipped${filter.repeatSkipped > 0 ? ` [${filter.repeatSkipped} repeat]` : ""})`;
|
|
@@ -4665,12 +4679,12 @@ var filterLine = (filter) => {
|
|
|
4665
4679
|
if (filter.failures > 0) {
|
|
4666
4680
|
parts.push(`${filter.failures} failure${filter.failures === 1 ? "" : "s"}`);
|
|
4667
4681
|
}
|
|
4668
|
-
return `Jev filter: ${parts.join(", ")}`;
|
|
4682
|
+
return `Jev/Decisions filter: ${parts.join(", ")}`;
|
|
4669
4683
|
};
|
|
4670
4684
|
var gateLine = (gate, invocations) => {
|
|
4671
4685
|
const consultationCosts = invocations.filter((item) => item.trigger === "turn-gate" && typeof item.cost === "number").map((item) => item.cost);
|
|
4672
4686
|
const gateSpend = consultationCosts.reduce((sum, cost) => sum + cost, 0);
|
|
4673
|
-
return `Turn gate: ${gate.checks} check${gate.checks === 1 ? "" : "s"} (Jev ${formatJevTokens(gate.usage)} ·
|
|
4687
|
+
return `Turn gate: ${gate.checks} check${gate.checks === 1 ? "" : "s"} (Jev/Decisions ${formatJevTokens(gate.usage)} · ${jevCostText(gate.usage)}), ${gate.consultations} consultation${gate.consultations === 1 ? "" : "s"} ($${gateSpend.toFixed(4)})`;
|
|
4674
4688
|
};
|
|
4675
4689
|
|
|
4676
4690
|
class AdvisorJevLedgerState {
|
|
@@ -4735,8 +4749,9 @@ class AdvisorJevLedgerState {
|
|
|
4735
4749
|
} else if (this.#filterActive()) {
|
|
4736
4750
|
lines.push(filterLine(filter));
|
|
4737
4751
|
const jevTokens = usage.inputTokens + usage.outputTokens;
|
|
4738
|
-
if (jevTokens > 0) {
|
|
4739
|
-
|
|
4752
|
+
if (jevTokens > 0 || usage.unavailableCostCalls > 0) {
|
|
4753
|
+
const cost = usage.unavailableCostCalls > 0 ? jevCostText(usage) : `$${usage.cost.toFixed(4)} (input only; output free)`;
|
|
4754
|
+
lines.push(`Jev/Decisions cost: ${formatJevTokens(usage)} tokens · ${cost}`);
|
|
4740
4755
|
}
|
|
4741
4756
|
if (filter.skipped > 0) {
|
|
4742
4757
|
lines.push(savingsLine(markdownCosts(invocations), filter.skipped));
|
|
@@ -5702,8 +5717,206 @@ class JevFailureError extends Error {
|
|
|
5702
5717
|
}
|
|
5703
5718
|
}
|
|
5704
5719
|
|
|
5720
|
+
// src/jev/decisions-client.ts
|
|
5721
|
+
var OPENAI_DECISIONS_ENDPOINT = "https://api.openai.com/v1/decisions";
|
|
5722
|
+
var OPENAI_DECISIONS_MODEL = "gpt-6-luna";
|
|
5723
|
+
var formatEntry = (entry) => {
|
|
5724
|
+
if (isString(entry)) {
|
|
5725
|
+
return entry;
|
|
5726
|
+
}
|
|
5727
|
+
return entry === undefined ? "" : JSON.stringify(entry);
|
|
5728
|
+
};
|
|
5729
|
+
var predicateInstructions = (question) => {
|
|
5730
|
+
const parts = [formatEntry(question.instructions)];
|
|
5731
|
+
if (question.type === "noul") {
|
|
5732
|
+
if (question.criteria?.true !== undefined) {
|
|
5733
|
+
parts.push(`A true answer means: ${formatEntry(question.criteria.true)}`);
|
|
5734
|
+
}
|
|
5735
|
+
if (question.criteria?.false !== undefined) {
|
|
5736
|
+
parts.push(`A false answer means: ${formatEntry(question.criteria.false)}`);
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5739
|
+
return parts.filter(Boolean).join(`
|
|
5740
|
+
|
|
5741
|
+
`);
|
|
5742
|
+
};
|
|
5743
|
+
var scoreLevelLabel = (description, index) => {
|
|
5744
|
+
const text = formatEntry(description);
|
|
5745
|
+
const colon = text.indexOf(":");
|
|
5746
|
+
const label = (colon === -1 ? text : text.slice(0, colon)).trim();
|
|
5747
|
+
return label || `Level ${index}`;
|
|
5748
|
+
};
|
|
5749
|
+
var decisionQuestion = (name, question) => {
|
|
5750
|
+
if (question.type === "noul") {
|
|
5751
|
+
return {
|
|
5752
|
+
instructions: predicateInstructions(question),
|
|
5753
|
+
name,
|
|
5754
|
+
type: "predicate"
|
|
5755
|
+
};
|
|
5756
|
+
}
|
|
5757
|
+
if (question.type === "score") {
|
|
5758
|
+
const labels = new Set;
|
|
5759
|
+
const levels = question.criteria.map((description, index) => {
|
|
5760
|
+
const label = scoreLevelLabel(description, index);
|
|
5761
|
+
if (labels.has(label)) {
|
|
5762
|
+
throw new JevFailureError("malformed", `Jev score question ${name} has duplicate level labels.`);
|
|
5763
|
+
}
|
|
5764
|
+
labels.add(label);
|
|
5765
|
+
return { description: formatEntry(description), label };
|
|
5766
|
+
});
|
|
5767
|
+
return {
|
|
5768
|
+
instructions: formatEntry(question.instructions),
|
|
5769
|
+
levels,
|
|
5770
|
+
name,
|
|
5771
|
+
type: "score"
|
|
5772
|
+
};
|
|
5773
|
+
}
|
|
5774
|
+
throw new JevFailureError("malformed", `Jev question ${name} has an unsupported type.`);
|
|
5775
|
+
};
|
|
5776
|
+
var formatDecisionsInput = (state) => {
|
|
5777
|
+
if (isString(state)) {
|
|
5778
|
+
return state;
|
|
5779
|
+
}
|
|
5780
|
+
return JSON.stringify(state, null, 2);
|
|
5781
|
+
};
|
|
5782
|
+
var buildDecisionsRequest = (state, questions) => {
|
|
5783
|
+
if (!isRecordOf(questions) || Object.keys(questions).length === 0) {
|
|
5784
|
+
throw new JevFailureError("malformed", "Jev questions are empty.");
|
|
5785
|
+
}
|
|
5786
|
+
return {
|
|
5787
|
+
input: formatDecisionsInput(state),
|
|
5788
|
+
model: OPENAI_DECISIONS_MODEL,
|
|
5789
|
+
questions: Object.entries(questions).map(([name, question]) => decisionQuestion(name, question))
|
|
5790
|
+
};
|
|
5791
|
+
};
|
|
5792
|
+
|
|
5793
|
+
// src/jev/decisions-response.ts
|
|
5794
|
+
var malformedResponse = (detail) => new JevFailureError("malformed", `OpenAI Decisions response ${detail}.`);
|
|
5795
|
+
var boundedProbability = (value) => isNumber(value) && Number.isFinite(value) && value >= 0 && value <= 1 ? value : undefined;
|
|
5796
|
+
var normalizePredicateAnswer = (answer) => {
|
|
5797
|
+
const probability = boundedProbability(answer.probability);
|
|
5798
|
+
if (probability === undefined) {
|
|
5799
|
+
throw malformedResponse("has an invalid predicate probability");
|
|
5800
|
+
}
|
|
5801
|
+
return { noul: probability, type: "noul" };
|
|
5802
|
+
};
|
|
5803
|
+
var normalizeScoreAnswer = (answer, question) => {
|
|
5804
|
+
if (!Array.isArray(answer.probabilities)) {
|
|
5805
|
+
throw malformedResponse("has no score probability array");
|
|
5806
|
+
}
|
|
5807
|
+
if (answer.probabilities.length !== question.levels.length) {
|
|
5808
|
+
throw malformedResponse("has an incomplete score probability array");
|
|
5809
|
+
}
|
|
5810
|
+
const probabilities = {};
|
|
5811
|
+
const legend = {};
|
|
5812
|
+
const seenLabels = new Set;
|
|
5813
|
+
const seenValues = new Set;
|
|
5814
|
+
let probabilityTotal = 0;
|
|
5815
|
+
for (const entry of answer.probabilities) {
|
|
5816
|
+
if (!isRecord(entry)) {
|
|
5817
|
+
throw malformedResponse("has an invalid score probability entry");
|
|
5818
|
+
}
|
|
5819
|
+
const { label, value } = entry;
|
|
5820
|
+
const probability = boundedProbability(entry.probability);
|
|
5821
|
+
if (!isString(label) || !isNumber(value) || !Number.isSafeInteger(value) || value < 0 || value >= question.levels.length || probability === undefined || seenLabels.has(label) || seenValues.has(value)) {
|
|
5822
|
+
throw malformedResponse("has an invalid or duplicate score level");
|
|
5823
|
+
}
|
|
5824
|
+
const level = question.levels[value];
|
|
5825
|
+
if (label !== level.label) {
|
|
5826
|
+
throw malformedResponse("has an ambiguous score label/value pair");
|
|
5827
|
+
}
|
|
5828
|
+
seenLabels.add(label);
|
|
5829
|
+
seenValues.add(value);
|
|
5830
|
+
probabilities[String(value)] = probability;
|
|
5831
|
+
legend[String(value)] = level.description;
|
|
5832
|
+
probabilityTotal += probability;
|
|
5833
|
+
}
|
|
5834
|
+
if (Math.abs(probabilityTotal - 1) > 0.02 || question.levels.some((level, index) => !seenValues.has(index) || !seenLabels.has(level.label))) {
|
|
5835
|
+
throw malformedResponse("has an incomplete or ambiguous score distribution");
|
|
5836
|
+
}
|
|
5837
|
+
return { legend, probabilities, type: "score" };
|
|
5838
|
+
};
|
|
5839
|
+
var usageTokenCount = (value) => isNumber(value) && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
5840
|
+
var parseDecisionsApiResponse = async (response) => {
|
|
5841
|
+
let value;
|
|
5842
|
+
try {
|
|
5843
|
+
value = await response.json();
|
|
5844
|
+
} catch {
|
|
5845
|
+
throw malformedResponse("was not valid JSON");
|
|
5846
|
+
}
|
|
5847
|
+
if (!isRecord(value)) {
|
|
5848
|
+
throw malformedResponse("is not an object");
|
|
5849
|
+
}
|
|
5850
|
+
const { answers: rawAnswers, model, usage } = value;
|
|
5851
|
+
if (!Array.isArray(rawAnswers)) {
|
|
5852
|
+
throw malformedResponse("did not include an answers array");
|
|
5853
|
+
}
|
|
5854
|
+
if (!isRecord(usage)) {
|
|
5855
|
+
throw malformedResponse("has malformed usage");
|
|
5856
|
+
}
|
|
5857
|
+
if (model !== undefined && !isString(model)) {
|
|
5858
|
+
throw malformedResponse("has an invalid model name");
|
|
5859
|
+
}
|
|
5860
|
+
const answers = [];
|
|
5861
|
+
for (const answer of rawAnswers) {
|
|
5862
|
+
if (!isRecord(answer)) {
|
|
5863
|
+
throw malformedResponse("contains an invalid answer");
|
|
5864
|
+
}
|
|
5865
|
+
answers.push(answer);
|
|
5866
|
+
}
|
|
5867
|
+
return { answers, model: isString(model) ? model : undefined, usage };
|
|
5868
|
+
};
|
|
5869
|
+
var normalizeAnswers = (responseAnswers, questions) => {
|
|
5870
|
+
const expected = new Map(questions.map((question) => [question.name, question]));
|
|
5871
|
+
if (expected.size !== questions.length || expected.size === 0) {
|
|
5872
|
+
throw malformedResponse("has duplicate or missing requested question names");
|
|
5873
|
+
}
|
|
5874
|
+
const received = new Map;
|
|
5875
|
+
for (const answer of responseAnswers) {
|
|
5876
|
+
const { name } = answer;
|
|
5877
|
+
if (!isString(name) || !name) {
|
|
5878
|
+
throw malformedResponse("contains an unnamed answer");
|
|
5879
|
+
}
|
|
5880
|
+
if (!expected.has(name)) {
|
|
5881
|
+
throw malformedResponse("contains an unexpected answer");
|
|
5882
|
+
}
|
|
5883
|
+
if (received.has(name)) {
|
|
5884
|
+
throw malformedResponse("contains duplicate answer names");
|
|
5885
|
+
}
|
|
5886
|
+
if (answer.type === "refusal") {
|
|
5887
|
+
throw malformedResponse("contains a refusal");
|
|
5888
|
+
}
|
|
5889
|
+
received.set(name, answer);
|
|
5890
|
+
}
|
|
5891
|
+
if (received.size !== expected.size) {
|
|
5892
|
+
throw malformedResponse("is missing one or more named answers");
|
|
5893
|
+
}
|
|
5894
|
+
const answers = {};
|
|
5895
|
+
for (const question of questions) {
|
|
5896
|
+
const answer = received.get(question.name);
|
|
5897
|
+
if (!answer || answer.type !== question.type) {
|
|
5898
|
+
throw malformedResponse("has an answer with the wrong type");
|
|
5899
|
+
}
|
|
5900
|
+
answers[question.name] = question.type === "predicate" ? normalizePredicateAnswer(answer) : normalizeScoreAnswer(answer, question);
|
|
5901
|
+
}
|
|
5902
|
+
return answers;
|
|
5903
|
+
};
|
|
5904
|
+
var normalizeDecisionsResponse = (response, questions) => {
|
|
5905
|
+
const inputTokens = usageTokenCount(response.usage.input_tokens);
|
|
5906
|
+
const outputTokens = usageTokenCount(response.usage.output_tokens);
|
|
5907
|
+
if (inputTokens === undefined || outputTokens === undefined) {
|
|
5908
|
+
throw malformedResponse("has malformed usage");
|
|
5909
|
+
}
|
|
5910
|
+
return {
|
|
5911
|
+
answers: normalizeAnswers(response.answers, questions),
|
|
5912
|
+
model: response.model || OPENAI_DECISIONS_MODEL,
|
|
5913
|
+
usage: { inputTokens, outputTokens }
|
|
5914
|
+
};
|
|
5915
|
+
};
|
|
5916
|
+
|
|
5705
5917
|
// src/jev/client.ts
|
|
5706
5918
|
var ENDPOINTS = {
|
|
5919
|
+
"openai-decisions": OPENAI_DECISIONS_ENDPOINT,
|
|
5707
5920
|
openrouter: "https://openrouter.ai/api/alpha/decisions",
|
|
5708
5921
|
typesafe: "https://api.typesafe.ai/v1/systemone"
|
|
5709
5922
|
};
|
|
@@ -5730,8 +5943,19 @@ var statusCategory = (status) => {
|
|
|
5730
5943
|
}
|
|
5731
5944
|
return "error";
|
|
5732
5945
|
};
|
|
5733
|
-
var isObjectLike = (value) => typeof value === "object";
|
|
5734
5946
|
var openRouterModelId = (model) => model.includes("/") ? model : `~typesafe/${model}`;
|
|
5947
|
+
var buildClientRequest = (transport, model, state, questions) => {
|
|
5948
|
+
if (transport === "openai-decisions") {
|
|
5949
|
+
const request = buildDecisionsRequest(state, questions);
|
|
5950
|
+
return {
|
|
5951
|
+
body: JSON.stringify(request),
|
|
5952
|
+
decisionQuestions: request.questions
|
|
5953
|
+
};
|
|
5954
|
+
}
|
|
5955
|
+
return {
|
|
5956
|
+
body: JSON.stringify({ model, questions, state })
|
|
5957
|
+
};
|
|
5958
|
+
};
|
|
5735
5959
|
var connectionFailure = (error) => {
|
|
5736
5960
|
const message = redactSecrets(error instanceof Error ? error.message : String(error));
|
|
5737
5961
|
return {
|
|
@@ -5759,6 +5983,7 @@ class JevClient {
|
|
|
5759
5983
|
#model;
|
|
5760
5984
|
#pricePerMtok;
|
|
5761
5985
|
#timeoutMs;
|
|
5986
|
+
#transport;
|
|
5762
5987
|
constructor({
|
|
5763
5988
|
apiKey,
|
|
5764
5989
|
fetch,
|
|
@@ -5770,11 +5995,19 @@ class JevClient {
|
|
|
5770
5995
|
this.#apiKey = apiKey;
|
|
5771
5996
|
this.#endpoint = ENDPOINTS[transport];
|
|
5772
5997
|
this.#fetch = fetch ?? globalThis.fetch.bind(globalThis);
|
|
5773
|
-
|
|
5998
|
+
if (transport === "openai-decisions") {
|
|
5999
|
+
this.#model = OPENAI_DECISIONS_MODEL;
|
|
6000
|
+
} else if (transport === "openrouter") {
|
|
6001
|
+
this.#model = openRouterModelId(model);
|
|
6002
|
+
} else {
|
|
6003
|
+
this.#model = model;
|
|
6004
|
+
}
|
|
5774
6005
|
this.#pricePerMtok = pricePerMtok;
|
|
5775
6006
|
this.#timeoutMs = timeoutMs;
|
|
6007
|
+
this.#transport = transport;
|
|
5776
6008
|
}
|
|
5777
6009
|
async ask(state, questions, signal) {
|
|
6010
|
+
const { body, decisionQuestions } = buildClientRequest(this.#transport, this.#model, state, questions);
|
|
5778
6011
|
const deadline = new AbortController;
|
|
5779
6012
|
const abortFromCaller = () => deadline.abort(signal?.reason);
|
|
5780
6013
|
signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
@@ -5787,15 +6020,10 @@ class JevClient {
|
|
|
5787
6020
|
deadline.abort(new Error("Jev wall-time budget elapsed"));
|
|
5788
6021
|
}, this.#timeoutMs);
|
|
5789
6022
|
timer.unref?.();
|
|
5790
|
-
const body = JSON.stringify({
|
|
5791
|
-
model: this.#model,
|
|
5792
|
-
questions,
|
|
5793
|
-
state
|
|
5794
|
-
});
|
|
5795
6023
|
try {
|
|
5796
6024
|
let outcome = {};
|
|
5797
6025
|
for (let attempt = 1;; attempt += 1) {
|
|
5798
|
-
outcome = await this.#attempt(body, deadline.signal);
|
|
6026
|
+
outcome = await this.#attempt(body, deadline.signal, decisionQuestions);
|
|
5799
6027
|
if (!outcome.retryable || attempt >= MAX_ATTEMPTS || deadline.signal.aborted) {
|
|
5800
6028
|
break;
|
|
5801
6029
|
}
|
|
@@ -5834,7 +6062,7 @@ class JevClient {
|
|
|
5834
6062
|
}
|
|
5835
6063
|
throw new JevFailureError("error", "Jev call failed.");
|
|
5836
6064
|
}
|
|
5837
|
-
async#attempt(body, signal) {
|
|
6065
|
+
async#attempt(body, signal, decisionQuestions) {
|
|
5838
6066
|
if (signal.aborted) {
|
|
5839
6067
|
return { retryable: false };
|
|
5840
6068
|
}
|
|
@@ -5859,7 +6087,7 @@ class JevClient {
|
|
|
5859
6087
|
};
|
|
5860
6088
|
}
|
|
5861
6089
|
if (response.ok) {
|
|
5862
|
-
return this.#parseSuccess(response);
|
|
6090
|
+
return this.#parseSuccess(response, decisionQuestions);
|
|
5863
6091
|
}
|
|
5864
6092
|
const failure = await this.#failureFromStatus(response);
|
|
5865
6093
|
return {
|
|
@@ -5880,9 +6108,23 @@ class JevClient {
|
|
|
5880
6108
|
return new JevFailureError(statusCategory(response.status), message);
|
|
5881
6109
|
}
|
|
5882
6110
|
#transportLabel() {
|
|
5883
|
-
|
|
6111
|
+
if (this.#transport === "openrouter") {
|
|
6112
|
+
return "OpenRouter";
|
|
6113
|
+
}
|
|
6114
|
+
return this.#transport === "openai-decisions" ? "OpenAI Decisions" : "TypeSafe";
|
|
5884
6115
|
}
|
|
5885
|
-
async#parseSuccess(response) {
|
|
6116
|
+
async#parseSuccess(response, decisionQuestions) {
|
|
6117
|
+
if (this.#transport === "openai-decisions") {
|
|
6118
|
+
const normalized = normalizeDecisionsResponse(await parseDecisionsApiResponse(response), decisionQuestions ?? []);
|
|
6119
|
+
return {
|
|
6120
|
+
answers: normalized.answers,
|
|
6121
|
+
model: normalized.model,
|
|
6122
|
+
usage: {
|
|
6123
|
+
input_tokens: normalized.usage.inputTokens,
|
|
6124
|
+
output_tokens: normalized.usage.outputTokens
|
|
6125
|
+
}
|
|
6126
|
+
};
|
|
6127
|
+
}
|
|
5886
6128
|
let parsed;
|
|
5887
6129
|
try {
|
|
5888
6130
|
parsed = await response.json();
|
|
@@ -5891,7 +6133,7 @@ class JevClient {
|
|
|
5891
6133
|
failure: new JevFailureError("malformed", `Jev response was not JSON: ${redactSecrets(error instanceof Error ? error.message : String(error))}`)
|
|
5892
6134
|
};
|
|
5893
6135
|
}
|
|
5894
|
-
if (!isRecord(parsed) || !
|
|
6136
|
+
if (!isRecord(parsed) || !isRecord(parsed.answers)) {
|
|
5895
6137
|
return {
|
|
5896
6138
|
failure: new JevFailureError("malformed", "Jev response did not include an answers object.")
|
|
5897
6139
|
};
|
|
@@ -5914,7 +6156,7 @@ class JevClient {
|
|
|
5914
6156
|
answers: outcome.answers ?? {},
|
|
5915
6157
|
model: outcome.model ?? this.#model,
|
|
5916
6158
|
usage: {
|
|
5917
|
-
cost: inputTokens / 1e6 * price,
|
|
6159
|
+
cost: this.#transport === "openai-decisions" ? undefined : inputTokens / 1e6 * price,
|
|
5918
6160
|
inputTokens,
|
|
5919
6161
|
outputTokens
|
|
5920
6162
|
}
|
|
@@ -6096,7 +6338,7 @@ var consumePlaintextKeyWarning = () => {
|
|
|
6096
6338
|
return;
|
|
6097
6339
|
}
|
|
6098
6340
|
warnedPlaintextKey = true;
|
|
6099
|
-
return `Advisor is using a plaintext ${TYPESAFE_KEY_CONFIG_FIELD} from advisor.json; this is not recommended. Open /advisor-settings → Jev consultation filter to migrate it into a secure store, or use the ${TYPESAFE_KEY_ENV_VAR} environment variable.`;
|
|
6341
|
+
return `Advisor is using a plaintext ${TYPESAFE_KEY_CONFIG_FIELD} from advisor.json; this is not recommended. Open /advisor-settings → Jev/Decisions consultation filter to migrate it into a secure store, or use the ${TYPESAFE_KEY_ENV_VAR} environment variable.`;
|
|
6100
6342
|
};
|
|
6101
6343
|
|
|
6102
6344
|
// src/jev/questions.ts
|
|
@@ -6188,33 +6430,195 @@ var buildJevState = (ctx, input = {}) => {
|
|
|
6188
6430
|
return Object.fromEntries(fields);
|
|
6189
6431
|
};
|
|
6190
6432
|
|
|
6433
|
+
// src/jev/openai-key-store.ts
|
|
6434
|
+
import {
|
|
6435
|
+
chmodSync as chmodSync2,
|
|
6436
|
+
existsSync as existsSync4,
|
|
6437
|
+
readFileSync as readFileSync4,
|
|
6438
|
+
rmSync as rmSync2,
|
|
6439
|
+
writeFileSync as writeFileSync3
|
|
6440
|
+
} from "node:fs";
|
|
6441
|
+
import { join as join5 } from "node:path";
|
|
6442
|
+
import { getAgentDir as getAgentDir5 } from "@earendil-works/pi-coding-agent";
|
|
6443
|
+
var OPENAI_DECISIONS_KEY_SERVICE = "pi-advisor";
|
|
6444
|
+
var OPENAI_DECISIONS_KEY_NAME = "openai-decisions-api-key";
|
|
6445
|
+
var KEY_FILE_MODE2 = 384;
|
|
6446
|
+
var runtimeSecrets2 = () => {
|
|
6447
|
+
const bun = globalThis;
|
|
6448
|
+
return bun.Bun?.secrets;
|
|
6449
|
+
};
|
|
6450
|
+
var keyFilePath2 = () => join5(getAgentDir5(), "openai_api_key");
|
|
6451
|
+
var normalizeKey2 = (value) => value?.trim() || undefined;
|
|
6452
|
+
var readDefaultFileStore = () => {
|
|
6453
|
+
try {
|
|
6454
|
+
return normalizeKey2(readFileSync4(keyFilePath2(), "utf-8"));
|
|
6455
|
+
} catch {
|
|
6456
|
+
return;
|
|
6457
|
+
}
|
|
6458
|
+
};
|
|
6459
|
+
var writeDefaultFileStore = (key) => {
|
|
6460
|
+
const path = keyFilePath2();
|
|
6461
|
+
writeFileSync3(path, `${key}
|
|
6462
|
+
`, { mode: KEY_FILE_MODE2 });
|
|
6463
|
+
chmodSync2(path, KEY_FILE_MODE2);
|
|
6464
|
+
};
|
|
6465
|
+
var deleteDefaultFileStore = () => rmSync2(keyFilePath2(), { force: true });
|
|
6466
|
+
var messageOf2 = (error) => redactSecrets(error instanceof Error ? error.message : String(error));
|
|
6467
|
+
var resolveOpenAIDecisionsKey = async (deps = {}) => {
|
|
6468
|
+
const secrets = deps.secrets === undefined ? runtimeSecrets2() : deps.secrets;
|
|
6469
|
+
if (secrets) {
|
|
6470
|
+
let secretKey;
|
|
6471
|
+
try {
|
|
6472
|
+
secretKey = normalizeKey2(await secrets.get({
|
|
6473
|
+
name: OPENAI_DECISIONS_KEY_NAME,
|
|
6474
|
+
service: OPENAI_DECISIONS_KEY_SERVICE
|
|
6475
|
+
}));
|
|
6476
|
+
} catch {
|
|
6477
|
+
secretKey = undefined;
|
|
6478
|
+
}
|
|
6479
|
+
if (secretKey) {
|
|
6480
|
+
return { key: secretKey, source: "openai-bun-secrets" };
|
|
6481
|
+
}
|
|
6482
|
+
}
|
|
6483
|
+
const key = normalizeKey2((deps.readFileStore ?? readDefaultFileStore)());
|
|
6484
|
+
return key ? { key, source: "openai-file" } : {};
|
|
6485
|
+
};
|
|
6486
|
+
var writeOpenAIDecisionsKey = async (key, deps = {}) => {
|
|
6487
|
+
const normalized = normalizeKey2(key);
|
|
6488
|
+
if (!normalized) {
|
|
6489
|
+
return { message: "The OpenAI API key is empty.", ok: false };
|
|
6490
|
+
}
|
|
6491
|
+
const secrets = deps.secrets === undefined ? runtimeSecrets2() : deps.secrets;
|
|
6492
|
+
if (secrets) {
|
|
6493
|
+
try {
|
|
6494
|
+
await secrets.set({
|
|
6495
|
+
name: OPENAI_DECISIONS_KEY_NAME,
|
|
6496
|
+
service: OPENAI_DECISIONS_KEY_SERVICE,
|
|
6497
|
+
value: normalized
|
|
6498
|
+
});
|
|
6499
|
+
return { message: "OpenAI API key stored in Bun.secrets.", ok: true };
|
|
6500
|
+
} catch (error) {
|
|
6501
|
+
return {
|
|
6502
|
+
message: `Storing the OpenAI API key in Bun.secrets failed: ${messageOf2(error)}. Configure OPENAI_API_KEY in Pi or retry secure storage.`,
|
|
6503
|
+
ok: false
|
|
6504
|
+
};
|
|
6505
|
+
}
|
|
6506
|
+
}
|
|
6507
|
+
try {
|
|
6508
|
+
(deps.writeFileStore ?? writeDefaultFileStore)(normalized);
|
|
6509
|
+
return {
|
|
6510
|
+
message: "OpenAI API key stored in the Pi agent directory (mode 0600).",
|
|
6511
|
+
ok: true
|
|
6512
|
+
};
|
|
6513
|
+
} catch (error) {
|
|
6514
|
+
return {
|
|
6515
|
+
message: `Storing the OpenAI API key failed: ${messageOf2(error)}. Configure OPENAI_API_KEY in Pi or retry secure storage.`,
|
|
6516
|
+
ok: false
|
|
6517
|
+
};
|
|
6518
|
+
}
|
|
6519
|
+
};
|
|
6520
|
+
var clearOpenAIDecisionsKey = async (deps = {}) => {
|
|
6521
|
+
let firstError;
|
|
6522
|
+
const secrets = deps.secrets === undefined ? runtimeSecrets2() : deps.secrets;
|
|
6523
|
+
if (secrets) {
|
|
6524
|
+
try {
|
|
6525
|
+
await secrets.delete({
|
|
6526
|
+
name: OPENAI_DECISIONS_KEY_NAME,
|
|
6527
|
+
service: OPENAI_DECISIONS_KEY_SERVICE
|
|
6528
|
+
});
|
|
6529
|
+
} catch (error) {
|
|
6530
|
+
firstError = messageOf2(error);
|
|
6531
|
+
}
|
|
6532
|
+
}
|
|
6533
|
+
try {
|
|
6534
|
+
if (deps.deleteFileStore) {
|
|
6535
|
+
deps.deleteFileStore();
|
|
6536
|
+
} else if (existsSync4(keyFilePath2())) {
|
|
6537
|
+
deleteDefaultFileStore();
|
|
6538
|
+
}
|
|
6539
|
+
} catch (error) {
|
|
6540
|
+
firstError ??= messageOf2(error);
|
|
6541
|
+
}
|
|
6542
|
+
if (firstError) {
|
|
6543
|
+
return {
|
|
6544
|
+
message: `Clearing the OpenAI API key failed: ${firstError}.`,
|
|
6545
|
+
ok: false
|
|
6546
|
+
};
|
|
6547
|
+
}
|
|
6548
|
+
return { message: "Stored OpenAI API key cleared.", ok: true };
|
|
6549
|
+
};
|
|
6550
|
+
|
|
6191
6551
|
// src/jev/transport.ts
|
|
6192
6552
|
var OPENROUTER_PROVIDER = "openrouter";
|
|
6553
|
+
var OPENAI_PROVIDER = "openai";
|
|
6554
|
+
var OPENAI_API_KEY_SOURCES = new Map([
|
|
6555
|
+
["OPENAI_API_KEY", "openai-env"],
|
|
6556
|
+
["stored credential", "openai-provider-credential"]
|
|
6557
|
+
]);
|
|
6558
|
+
var openAiPlatformCredentials = async (ctx, deps) => {
|
|
6559
|
+
let result;
|
|
6560
|
+
try {
|
|
6561
|
+
result = deps.getProviderAuth ? await deps.getProviderAuth(OPENAI_PROVIDER) : await ctx?.modelRegistry?.getProviderAuth(OPENAI_PROVIDER);
|
|
6562
|
+
} catch {
|
|
6563
|
+
result = undefined;
|
|
6564
|
+
}
|
|
6565
|
+
const source = result?.source;
|
|
6566
|
+
const credentialSource = source ? OPENAI_API_KEY_SOURCES.get(source) : undefined;
|
|
6567
|
+
const apiKey = result?.auth.apiKey?.trim();
|
|
6568
|
+
if (credentialSource && apiKey) {
|
|
6569
|
+
return {
|
|
6570
|
+
apiKey,
|
|
6571
|
+
source: credentialSource,
|
|
6572
|
+
transport: "openai-decisions"
|
|
6573
|
+
};
|
|
6574
|
+
}
|
|
6575
|
+
try {
|
|
6576
|
+
const stored = await (deps.resolveOpenAIKey ?? resolveOpenAIDecisionsKey)();
|
|
6577
|
+
const storedKey = stored.key?.trim();
|
|
6578
|
+
return storedKey && stored.source ? {
|
|
6579
|
+
apiKey: storedKey,
|
|
6580
|
+
source: stored.source,
|
|
6581
|
+
transport: "openai-decisions"
|
|
6582
|
+
} : undefined;
|
|
6583
|
+
} catch {
|
|
6584
|
+
return;
|
|
6585
|
+
}
|
|
6586
|
+
};
|
|
6193
6587
|
var openRouterKey = async (ctx, deps) => {
|
|
6194
6588
|
const key = deps.getProviderKey ? await deps.getProviderKey(OPENROUTER_PROVIDER) : await ctx?.modelRegistry?.getApiKeyForProvider(OPENROUTER_PROVIDER);
|
|
6195
6589
|
return key?.trim() || undefined;
|
|
6196
6590
|
};
|
|
6591
|
+
var typeSafeCredentials = async (deps) => {
|
|
6592
|
+
const resolution = await (deps.resolveTypesafe ?? resolveTypeSafeKey)();
|
|
6593
|
+
if (!resolution.key) {
|
|
6594
|
+
return;
|
|
6595
|
+
}
|
|
6596
|
+
const credentials = {
|
|
6597
|
+
apiKey: resolution.key,
|
|
6598
|
+
transport: "typesafe"
|
|
6599
|
+
};
|
|
6600
|
+
if (resolution.source) {
|
|
6601
|
+
credentials.source = resolution.source;
|
|
6602
|
+
}
|
|
6603
|
+
return credentials;
|
|
6604
|
+
};
|
|
6605
|
+
var resolveJevTransportFor = async (transport, ctx, deps = {}) => {
|
|
6606
|
+
if (transport === "typesafe") {
|
|
6607
|
+
return typeSafeCredentials(deps);
|
|
6608
|
+
}
|
|
6609
|
+
if (transport === "openrouter") {
|
|
6610
|
+
const key = await openRouterKey(ctx, deps);
|
|
6611
|
+
return key ? { apiKey: key, transport } : undefined;
|
|
6612
|
+
}
|
|
6613
|
+
return openAiPlatformCredentials(ctx, deps);
|
|
6614
|
+
};
|
|
6197
6615
|
var resolveJevTransport = async (ctx, deps = {}) => {
|
|
6198
6616
|
const preference = advisorJevTransportRef;
|
|
6199
|
-
if (preference !== "
|
|
6200
|
-
|
|
6201
|
-
const resolution = await resolveTypesafe();
|
|
6202
|
-
if (resolution.key) {
|
|
6203
|
-
const credentials = {
|
|
6204
|
-
apiKey: resolution.key,
|
|
6205
|
-
transport: "typesafe"
|
|
6206
|
-
};
|
|
6207
|
-
if (resolution.source) {
|
|
6208
|
-
credentials.source = resolution.source;
|
|
6209
|
-
}
|
|
6210
|
-
return credentials;
|
|
6211
|
-
}
|
|
6617
|
+
if (preference !== "auto") {
|
|
6618
|
+
return resolveJevTransportFor(preference, ctx, deps);
|
|
6212
6619
|
}
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
}
|
|
6216
|
-
const openrouter = await openRouterKey(ctx, deps);
|
|
6217
|
-
return openrouter ? { apiKey: openrouter, transport: "openrouter" } : undefined;
|
|
6620
|
+
const typesafe = await typeSafeCredentials(deps);
|
|
6621
|
+
return typesafe ?? resolveJevTransportFor("openrouter", ctx, deps);
|
|
6218
6622
|
};
|
|
6219
6623
|
|
|
6220
6624
|
// src/tools/outage-notifier.ts
|
|
@@ -6244,7 +6648,7 @@ var SCREENED_SKIP_TEXT = "Advisor consultation skipped (screened out): the stake
|
|
|
6244
6648
|
var repeatSkipText = (advice) => `Advisor consultation skipped (already answered): this question was answered earlier in this session; the earlier advice is reattached below. Consult again only if the situation has materially changed.
|
|
6245
6649
|
|
|
6246
6650
|
${advice}`;
|
|
6247
|
-
var outageNotifier = createOutageNotifier((category, message) => `Advisor Jev filter failed (${category}); allowing consultations. ${message}`);
|
|
6651
|
+
var outageNotifier = createOutageNotifier((category, message) => `Advisor Jev/Decisions filter failed (${category}); allowing consultations. ${message}`);
|
|
6248
6652
|
var notifyOutageOnce = outageNotifier.notify;
|
|
6249
6653
|
var resetJevOutageNotification = outageNotifier.reset;
|
|
6250
6654
|
var allow = () => ({ decision: "allow" });
|
|
@@ -6266,7 +6670,7 @@ var screenWithJev = async (ctx, session, options, deps, normalizedQuestion) => {
|
|
|
6266
6670
|
const credentials = await (deps.resolveTransport ?? resolveJevTransport)(ctx);
|
|
6267
6671
|
if (!credentials) {
|
|
6268
6672
|
session.recordJevFilterFailure();
|
|
6269
|
-
notifyOutageOnce(ctx, "missing-key", "No
|
|
6673
|
+
notifyOutageOnce(ctx, "missing-key", "No credentials resolved for the selected Jev/Decisions provider.");
|
|
6270
6674
|
return allow();
|
|
6271
6675
|
}
|
|
6272
6676
|
if (credentials.source === "advisor-json") {
|
|
@@ -6604,9 +7008,9 @@ import {
|
|
|
6604
7008
|
unlink,
|
|
6605
7009
|
writeFile
|
|
6606
7010
|
} from "node:fs/promises";
|
|
6607
|
-
import { join as
|
|
7011
|
+
import { join as join6 } from "node:path";
|
|
6608
7012
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
6609
|
-
import { getAgentDir as
|
|
7013
|
+
import { getAgentDir as getAgentDir6 } from "@earendil-works/pi-coding-agent";
|
|
6610
7014
|
var ADOPTIONS = [
|
|
6611
7015
|
"followed",
|
|
6612
7016
|
"not-followed",
|
|
@@ -6626,12 +7030,12 @@ var OUTCOME_TRIGGERS = [
|
|
|
6626
7030
|
];
|
|
6627
7031
|
var MAX_LOG_BYTES = 1024 * 1024;
|
|
6628
7032
|
var OUTCOME_LOG_MAX_BYTES = MAX_LOG_BYTES;
|
|
6629
|
-
var statePath = () =>
|
|
6630
|
-
var outcomeLogPath = () =>
|
|
7033
|
+
var statePath = () => join6(getAgentDir6(), "advisor-outcomes-salt");
|
|
7034
|
+
var outcomeLogPath = () => join6(getAgentDir6(), "advisor-outcomes.jsonl");
|
|
6631
7035
|
var isErrnoException = (error) => error instanceof Error && ("code" in error);
|
|
6632
7036
|
var salt = async () => {
|
|
6633
7037
|
const path = statePath();
|
|
6634
|
-
await mkdir(
|
|
7038
|
+
await mkdir(getAgentDir6(), { mode: 448, recursive: true });
|
|
6635
7039
|
for (let attempt = 0;attempt < 20; attempt += 1) {
|
|
6636
7040
|
try {
|
|
6637
7041
|
const existing = await readFile(path);
|
|
@@ -6708,7 +7112,7 @@ var withOutcomeLock = async (run) => {
|
|
|
6708
7112
|
var adviceDigest = (advice, key) => createHmac("sha256", key).update(advice).digest("hex").slice(0, 16);
|
|
6709
7113
|
var appendOutcome = async (record) => {
|
|
6710
7114
|
const path = outcomeLogPath();
|
|
6711
|
-
await mkdir(
|
|
7115
|
+
await mkdir(getAgentDir6(), { mode: 448, recursive: true });
|
|
6712
7116
|
return withOutcomeLock(async () => {
|
|
6713
7117
|
const next = {
|
|
6714
7118
|
adoption: record.adoption,
|
|
@@ -7096,7 +7500,10 @@ var rainbowGradient = (text, startedAt) => {
|
|
|
7096
7500
|
import { getKeybindings } from "@earendil-works/pi-tui";
|
|
7097
7501
|
|
|
7098
7502
|
// src/ui/jev-setup-submenu.ts
|
|
7099
|
-
import { Key as Key2, matchesKey as matchesKey2
|
|
7503
|
+
import { Key as Key2, matchesKey as matchesKey2 } from "@earendil-works/pi-tui";
|
|
7504
|
+
|
|
7505
|
+
// src/ui/jev-setup-submenu-view.ts
|
|
7506
|
+
import { truncateToWidth as truncateToWidth4 } from "@earendil-works/pi-tui";
|
|
7100
7507
|
|
|
7101
7508
|
// node_modules/@typesafe-ai/sdk/dist/index.mjs
|
|
7102
7509
|
var range2 = (from, to) => Array.from({ length: to - from }, (_, i) => from + i);
|
|
@@ -7141,8 +7548,157 @@ var describeRuntime = () => {
|
|
|
7141
7548
|
};
|
|
7142
7549
|
var RUNTIME = describeRuntime();
|
|
7143
7550
|
|
|
7551
|
+
// src/ui/jev-setup-support.ts
|
|
7552
|
+
var fireAndForget = async (action, onError) => {
|
|
7553
|
+
try {
|
|
7554
|
+
await action;
|
|
7555
|
+
} catch (error) {
|
|
7556
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7557
|
+
onError(redactSecrets(message));
|
|
7558
|
+
}
|
|
7559
|
+
};
|
|
7560
|
+
var providerName = (transport) => {
|
|
7561
|
+
if (transport === "typesafe") {
|
|
7562
|
+
return "TypeSafe Jev";
|
|
7563
|
+
}
|
|
7564
|
+
if (transport === "openrouter") {
|
|
7565
|
+
return "OpenRouter (Pi login)";
|
|
7566
|
+
}
|
|
7567
|
+
return `OpenAI Decisions (${OPENAI_DECISIONS_MODEL})`;
|
|
7568
|
+
};
|
|
7569
|
+
var transportLabel = (credentials) => {
|
|
7570
|
+
if (credentials.transport === "openrouter") {
|
|
7571
|
+
return "OpenRouter (reusing Pi login)";
|
|
7572
|
+
}
|
|
7573
|
+
if (credentials.transport === "openai-decisions") {
|
|
7574
|
+
let source = "verified API key";
|
|
7575
|
+
switch (credentials.source) {
|
|
7576
|
+
case "openai-bun-secrets": {
|
|
7577
|
+
source = "Bun.secrets";
|
|
7578
|
+
break;
|
|
7579
|
+
}
|
|
7580
|
+
case "openai-env": {
|
|
7581
|
+
source = "OPENAI_API_KEY";
|
|
7582
|
+
break;
|
|
7583
|
+
}
|
|
7584
|
+
case "openai-file": {
|
|
7585
|
+
source = "stored file, mode 0600";
|
|
7586
|
+
break;
|
|
7587
|
+
}
|
|
7588
|
+
case "openai-provider-credential": {
|
|
7589
|
+
source = "Pi stored API key";
|
|
7590
|
+
break;
|
|
7591
|
+
}
|
|
7592
|
+
default: {
|
|
7593
|
+
break;
|
|
7594
|
+
}
|
|
7595
|
+
}
|
|
7596
|
+
return `OpenAI Decisions (${OPENAI_DECISIONS_MODEL}; key: ${source})`;
|
|
7597
|
+
}
|
|
7598
|
+
switch (credentials.source) {
|
|
7599
|
+
case "advisor-json": {
|
|
7600
|
+
return "TypeSafe (key: advisor.json — plaintext, not recommended)";
|
|
7601
|
+
}
|
|
7602
|
+
case "bun-secrets": {
|
|
7603
|
+
return "TypeSafe (key: Bun.secrets)";
|
|
7604
|
+
}
|
|
7605
|
+
case "file": {
|
|
7606
|
+
return "TypeSafe (key: stored file, mode 0600)";
|
|
7607
|
+
}
|
|
7608
|
+
default: {
|
|
7609
|
+
return "TypeSafe (key: TYPESAFE_API_KEY)";
|
|
7610
|
+
}
|
|
7611
|
+
}
|
|
7612
|
+
};
|
|
7613
|
+
var canClearCredential = (credentials) => credentials.source === "bun-secrets" || credentials.source === "file" || credentials.source === "openai-bun-secrets" || credentials.source === "openai-file";
|
|
7614
|
+
var canReplaceWithEnteredKey = (credentials) => credentials.transport === "typesafe" || credentials.transport === "openai-decisions" && (credentials.source === "openai-bun-secrets" || credentials.source === "openai-file");
|
|
7615
|
+
var defaultVerify = async (credentials) => {
|
|
7616
|
+
const client = jevClientFromCredentials(credentials);
|
|
7617
|
+
try {
|
|
7618
|
+
await client.ask({ purpose: "pi-advisor setup verification" }, { verified: noul("Answer yes.") });
|
|
7619
|
+
return { ok: true };
|
|
7620
|
+
} catch (error) {
|
|
7621
|
+
return {
|
|
7622
|
+
message: error instanceof Error ? error.message : String(error),
|
|
7623
|
+
ok: false
|
|
7624
|
+
};
|
|
7625
|
+
}
|
|
7626
|
+
};
|
|
7627
|
+
var defaultSetupDeps = {
|
|
7628
|
+
clearStoredKey: (transport) => transport === "openai-decisions" ? clearOpenAIDecisionsKey() : clearKeyTypeSafeKey(),
|
|
7629
|
+
removePlaintextKey: removeTypeSafeKeyFromAdvisorJson,
|
|
7630
|
+
resolveTransport: (transport) => transport ? resolveJevTransportFor(transport) : resolveJevTransport(),
|
|
7631
|
+
verify: defaultVerify,
|
|
7632
|
+
writeKey: (key, transport) => transport === "openai-decisions" ? writeOpenAIDecisionsKey(key) : writeKeyTypeSafeKey(key)
|
|
7633
|
+
};
|
|
7634
|
+
|
|
7635
|
+
// src/ui/jev-setup-submenu-view.ts
|
|
7636
|
+
var setupActions = (state) => {
|
|
7637
|
+
const actions = ["typesafe", "openrouter", "openai-decisions"];
|
|
7638
|
+
if (state.canEnterKey) {
|
|
7639
|
+
actions.push("enter-key");
|
|
7640
|
+
}
|
|
7641
|
+
if (state.filterEnabled) {
|
|
7642
|
+
actions.push("disable");
|
|
7643
|
+
}
|
|
7644
|
+
if (state.credentials && canClearCredential(state.credentials)) {
|
|
7645
|
+
actions.push("clear-stored-key");
|
|
7646
|
+
}
|
|
7647
|
+
actions.push("done");
|
|
7648
|
+
return actions;
|
|
7649
|
+
};
|
|
7650
|
+
var setupLabels = (state) => setupActions(state).map((action) => {
|
|
7651
|
+
if (action === "clear-stored-key") {
|
|
7652
|
+
return state.filterEnabled ? "Disable and clear stored key" : "Clear stored key";
|
|
7653
|
+
}
|
|
7654
|
+
if (action === "disable") {
|
|
7655
|
+
return "Disable";
|
|
7656
|
+
}
|
|
7657
|
+
if (action === "done") {
|
|
7658
|
+
return "Done";
|
|
7659
|
+
}
|
|
7660
|
+
if (action === "enter-key") {
|
|
7661
|
+
return state.selectedTransport === "openai-decisions" ? "Enter an OpenAI Platform API key" : "Enter a TypeSafe API key";
|
|
7662
|
+
}
|
|
7663
|
+
return providerName(action);
|
|
7664
|
+
});
|
|
7665
|
+
var renderJevSetup = (options, state, maskedInput, width) => {
|
|
7666
|
+
const { theme } = options;
|
|
7667
|
+
const lines = [
|
|
7668
|
+
theme.fg("accent", theme.bold(" Jev/Decisions consultation filter")),
|
|
7669
|
+
"",
|
|
7670
|
+
` Filter: ${state.filterEnabled ? "On" : "Off"}`
|
|
7671
|
+
];
|
|
7672
|
+
if (state.credentials) {
|
|
7673
|
+
lines.push(` Credential: ${transportLabel(state.credentials)}`);
|
|
7674
|
+
} else if (state.selectedTransport) {
|
|
7675
|
+
lines.push(` Selected provider: ${providerName(state.selectedTransport)}`);
|
|
7676
|
+
} else {
|
|
7677
|
+
lines.push(` Saved transport: ${options.currentTransport}`);
|
|
7678
|
+
}
|
|
7679
|
+
if (state.notice) {
|
|
7680
|
+
lines.push("", theme.fg("warning", ` ${state.notice}`));
|
|
7681
|
+
}
|
|
7682
|
+
lines.push("");
|
|
7683
|
+
if (state.mode === "resolving") {
|
|
7684
|
+
lines.push(" Checking the selected provider credentials…");
|
|
7685
|
+
} else if (state.mode === "verifying") {
|
|
7686
|
+
lines.push(" Verifying with a live Jev call…");
|
|
7687
|
+
} else if (state.mode === "clearing") {
|
|
7688
|
+
lines.push(" Clearing the selected provider's stored key…");
|
|
7689
|
+
} else if (state.mode === "key-entry") {
|
|
7690
|
+
lines.push(` ${providerName(state.selectedTransport ?? "typesafe")}`, state.selectedTransport === "openai-decisions" ? " No OpenAI Platform API key found. ChatGPT subscription OAuth is not accepted." : " No TypeSafe API key found.", ` ${maskedInput.render(Math.max(10, width - 4))[0] ?? ""}`, theme.fg("dim", " Enter: verify and securely store · Esc: return"));
|
|
7691
|
+
} else {
|
|
7692
|
+
for (const [index, label] of setupLabels(state).entries()) {
|
|
7693
|
+
const prefix = index === state.selectedIndex ? "→ " : " ";
|
|
7694
|
+
lines.push(`${prefix}${label}`);
|
|
7695
|
+
}
|
|
7696
|
+
}
|
|
7697
|
+
return lines.map((line) => truncateToWidth4(line, width));
|
|
7698
|
+
};
|
|
7699
|
+
|
|
7144
7700
|
// src/ui/masked-input.ts
|
|
7145
|
-
import { Input as Input2, truncateToWidth as
|
|
7701
|
+
import { Input as Input2, truncateToWidth as truncateToWidth5 } from "@earendil-works/pi-tui";
|
|
7146
7702
|
|
|
7147
7703
|
class MaskedInput {
|
|
7148
7704
|
input;
|
|
@@ -7179,80 +7735,30 @@ class MaskedInput {
|
|
|
7179
7735
|
const { cursor } = this.input;
|
|
7180
7736
|
const masked = `${"•".repeat(Math.min(cursor, value.length))}█${"•".repeat(Math.max(0, value.length - cursor))}`;
|
|
7181
7737
|
const placeholder = value.length === 0 && this.options.placeholder ? this.options.placeholder : masked;
|
|
7182
|
-
return [
|
|
7738
|
+
return [truncateToWidth5(placeholder, Math.max(1, width))];
|
|
7183
7739
|
}
|
|
7184
7740
|
}
|
|
7185
7741
|
|
|
7186
7742
|
// src/ui/jev-setup-submenu.ts
|
|
7187
|
-
var ACTION_LABELS = {
|
|
7188
|
-
disable: "Disable",
|
|
7189
|
-
"disable-clear": "Disable and clear stored key",
|
|
7190
|
-
done: "Done",
|
|
7191
|
-
"enter-key": "Enter a TypeSafe API key",
|
|
7192
|
-
"verify-again": "Verify again",
|
|
7193
|
-
"verify-enable": "Verify and enable"
|
|
7194
|
-
};
|
|
7195
|
-
var fireAndForget = async (action) => {
|
|
7196
|
-
try {
|
|
7197
|
-
await action;
|
|
7198
|
-
} catch {}
|
|
7199
|
-
};
|
|
7200
|
-
var transportLabel = (credentials) => {
|
|
7201
|
-
if (credentials.transport === "openrouter") {
|
|
7202
|
-
return "OpenRouter (reusing pi login)";
|
|
7203
|
-
}
|
|
7204
|
-
switch (credentials.source) {
|
|
7205
|
-
case "advisor-json": {
|
|
7206
|
-
return "TypeSafe (key: advisor.json — plaintext, not recommended)";
|
|
7207
|
-
}
|
|
7208
|
-
case "bun-secrets": {
|
|
7209
|
-
return "TypeSafe (key: Bun.secrets)";
|
|
7210
|
-
}
|
|
7211
|
-
case "file": {
|
|
7212
|
-
return "TypeSafe (key: stored file, mode 0600)";
|
|
7213
|
-
}
|
|
7214
|
-
default: {
|
|
7215
|
-
return "TypeSafe (key: TYPESAFE_API_KEY)";
|
|
7216
|
-
}
|
|
7217
|
-
}
|
|
7218
|
-
};
|
|
7219
|
-
var defaultVerify = async (credentials) => {
|
|
7220
|
-
const client = jevClientFromCredentials(credentials);
|
|
7221
|
-
try {
|
|
7222
|
-
await client.ask({ purpose: "pi-advisor setup verification" }, { verified: noul("Answer yes.") });
|
|
7223
|
-
return { ok: true };
|
|
7224
|
-
} catch (error) {
|
|
7225
|
-
return {
|
|
7226
|
-
message: error instanceof Error ? error.message : String(error),
|
|
7227
|
-
ok: false
|
|
7228
|
-
};
|
|
7229
|
-
}
|
|
7230
|
-
};
|
|
7231
|
-
|
|
7232
7743
|
class JevSetupSubmenu {
|
|
7233
7744
|
options;
|
|
7234
7745
|
deps;
|
|
7235
7746
|
maskedInput;
|
|
7236
7747
|
credentials;
|
|
7748
|
+
selectedTransport;
|
|
7237
7749
|
mode = "menu";
|
|
7238
7750
|
notice;
|
|
7239
7751
|
selectedIndex = 0;
|
|
7752
|
+
filterEnabled;
|
|
7753
|
+
canEnterKey = false;
|
|
7754
|
+
pendingSelectionRefresh = false;
|
|
7240
7755
|
_focused = true;
|
|
7241
7756
|
constructor(options, deps = {}) {
|
|
7242
7757
|
this.options = options;
|
|
7243
|
-
this.
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
verify: deps.verify ?? defaultVerify,
|
|
7248
|
-
writeKey: deps.writeKey ?? writeKeyTypeSafeKey
|
|
7249
|
-
};
|
|
7250
|
-
this.maskedInput = new MaskedInput({
|
|
7251
|
-
onEscape: () => this.options.done(),
|
|
7252
|
-
onSubmit: (value) => this.submitEnteredKey(value),
|
|
7253
|
-
placeholder: "Paste a TypeSafe API key"
|
|
7254
|
-
});
|
|
7255
|
-
fireAndForget(this.refresh());
|
|
7758
|
+
this.filterEnabled = options.currentValue === "On";
|
|
7759
|
+
this.deps = { ...defaultSetupDeps, ...options.setupDeps, ...deps };
|
|
7760
|
+
this.maskedInput = this.createMaskedInput("Paste a TypeSafe API key");
|
|
7761
|
+
fireAndForget(this.refresh(), this.reportBackgroundError);
|
|
7256
7762
|
}
|
|
7257
7763
|
get focused() {
|
|
7258
7764
|
return this._focused;
|
|
@@ -7273,7 +7779,7 @@ class JevSetupSubmenu {
|
|
|
7273
7779
|
if (this.mode !== "menu") {
|
|
7274
7780
|
return;
|
|
7275
7781
|
}
|
|
7276
|
-
const actions = this.
|
|
7782
|
+
const actions = setupActions(this.viewState());
|
|
7277
7783
|
if (matchesKey2(keyData, Key2.down) || keyData === "\x1B[B" || keyData === "\x1BOB") {
|
|
7278
7784
|
this.selectedIndex = (this.selectedIndex + 1) % actions.length;
|
|
7279
7785
|
} else if (matchesKey2(keyData, Key2.up) || keyData === "\x1B[A" || keyData === "\x1BOA") {
|
|
@@ -7286,157 +7792,228 @@ class JevSetupSubmenu {
|
|
|
7286
7792
|
this.options.tui.requestRender();
|
|
7287
7793
|
}
|
|
7288
7794
|
render(width) {
|
|
7289
|
-
|
|
7290
|
-
const enabled = this.options.currentValue === "On";
|
|
7291
|
-
const lines = [
|
|
7292
|
-
theme.fg("accent", theme.bold(" Jev consultation filter")),
|
|
7293
|
-
""
|
|
7294
|
-
];
|
|
7295
|
-
if (this.credentials) {
|
|
7296
|
-
lines.push(` Transport: ${transportLabel(this.credentials)}`, ` Filter: ${enabled ? "On" : "Off"}`);
|
|
7297
|
-
} else if (this.mode === "verifying") {
|
|
7298
|
-
lines.push(" Checking available Jev credentials…");
|
|
7299
|
-
} else {
|
|
7300
|
-
lines.push(" No Jev credentials found. Enter a TypeSafe API key below, or add", " an OpenRouter login in pi; it is reused automatically.");
|
|
7301
|
-
}
|
|
7302
|
-
if (this.notice) {
|
|
7303
|
-
lines.push("", theme.fg("warning", ` ${this.notice}`));
|
|
7304
|
-
}
|
|
7305
|
-
lines.push("");
|
|
7306
|
-
if (this.mode === "verifying") {
|
|
7307
|
-
lines.push(" Verifying with a live Jev call…");
|
|
7308
|
-
} else if (this.mode === "key-entry") {
|
|
7309
|
-
lines.push(` ${this.maskedInput.render(Math.max(10, width - 4))[0] ?? ""}`, theme.fg("dim", " Enter: verify · Esc: cancel"));
|
|
7310
|
-
} else {
|
|
7311
|
-
for (const [index, label] of this.labels().entries()) {
|
|
7312
|
-
const prefix = index === this.selectedIndex ? "→ " : " ";
|
|
7313
|
-
lines.push(`${prefix}${label}`);
|
|
7314
|
-
}
|
|
7315
|
-
}
|
|
7316
|
-
return lines.map((line) => truncateToWidth5(line, width));
|
|
7795
|
+
return renderJevSetup(this.options, this.viewState(), this.maskedInput, width);
|
|
7317
7796
|
}
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
}
|
|
7328
|
-
actions.push("done");
|
|
7329
|
-
return actions;
|
|
7797
|
+
viewState() {
|
|
7798
|
+
return {
|
|
7799
|
+
canEnterKey: this.canEnterKey,
|
|
7800
|
+
credentials: this.credentials,
|
|
7801
|
+
filterEnabled: this.filterEnabled,
|
|
7802
|
+
mode: this.mode,
|
|
7803
|
+
notice: this.notice,
|
|
7804
|
+
selectedIndex: this.selectedIndex,
|
|
7805
|
+
selectedTransport: this.selectedTransport
|
|
7806
|
+
};
|
|
7330
7807
|
}
|
|
7331
|
-
|
|
7332
|
-
return
|
|
7808
|
+
createMaskedInput(placeholder) {
|
|
7809
|
+
return new MaskedInput({
|
|
7810
|
+
onEscape: () => this.cancelKeyEntry(),
|
|
7811
|
+
onSubmit: (value) => this.submitEnteredKey(value),
|
|
7812
|
+
placeholder
|
|
7813
|
+
});
|
|
7333
7814
|
}
|
|
7334
7815
|
async refresh() {
|
|
7335
|
-
|
|
7336
|
-
this.
|
|
7337
|
-
|
|
7816
|
+
this.mode = "resolving";
|
|
7817
|
+
this.options.tui.requestRender();
|
|
7818
|
+
try {
|
|
7819
|
+
const credentials = await this.deps.resolveTransport(this.options.currentTransport === "auto" ? undefined : this.options.currentTransport);
|
|
7820
|
+
this.credentials = credentials;
|
|
7821
|
+
this.selectedTransport = credentials?.transport;
|
|
7822
|
+
this.mode = "menu";
|
|
7823
|
+
this.selectedIndex = 0;
|
|
7824
|
+
if (credentials?.source === "advisor-json") {
|
|
7825
|
+
this.notice = consumePlaintextKeyWarning();
|
|
7826
|
+
}
|
|
7338
7827
|
this.options.tui.requestRender();
|
|
7828
|
+
} catch (error) {
|
|
7829
|
+
this.reportBackgroundError(error instanceof Error ? error.message : String(error));
|
|
7339
7830
|
}
|
|
7340
|
-
this.credentials = await this.deps.resolveTransport?.();
|
|
7341
|
-
this.mode = "menu";
|
|
7342
|
-
this.selectedIndex = 0;
|
|
7343
|
-
if (this.credentials?.source === "advisor-json") {
|
|
7344
|
-
this.notice ??= consumePlaintextKeyWarning();
|
|
7345
|
-
}
|
|
7346
|
-
this.options.tui.requestRender();
|
|
7347
7831
|
}
|
|
7348
7832
|
activate(action) {
|
|
7349
7833
|
if (action === "done") {
|
|
7350
|
-
this.
|
|
7351
|
-
return;
|
|
7352
|
-
}
|
|
7353
|
-
if (action === "enter-key") {
|
|
7354
|
-
this.mode = "key-entry";
|
|
7834
|
+
this.closeSetup();
|
|
7355
7835
|
return;
|
|
7356
7836
|
}
|
|
7357
7837
|
if (action === "disable") {
|
|
7358
7838
|
this.notice = undefined;
|
|
7359
|
-
this.
|
|
7839
|
+
if (this.commitSelection({
|
|
7840
|
+
enabled: false,
|
|
7841
|
+
transport: this.options.currentTransport
|
|
7842
|
+
}, "Off")) {
|
|
7843
|
+
this.filterEnabled = false;
|
|
7844
|
+
}
|
|
7360
7845
|
return;
|
|
7361
7846
|
}
|
|
7362
|
-
if (action === "
|
|
7363
|
-
fireAndForget(this.disableAndClear());
|
|
7847
|
+
if (action === "clear-stored-key") {
|
|
7848
|
+
fireAndForget(this.disableAndClear(), this.reportBackgroundError);
|
|
7364
7849
|
return;
|
|
7365
7850
|
}
|
|
7366
|
-
if (action === "
|
|
7367
|
-
|
|
7851
|
+
if (action === "enter-key") {
|
|
7852
|
+
if (this.selectedTransport) {
|
|
7853
|
+
this.openKeyEntry(this.selectedTransport);
|
|
7854
|
+
}
|
|
7855
|
+
return;
|
|
7368
7856
|
}
|
|
7857
|
+
fireAndForget(this.selectProvider(action), this.reportBackgroundError);
|
|
7369
7858
|
}
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7859
|
+
reportBackgroundError = (message) => {
|
|
7860
|
+
this.mode = "menu";
|
|
7861
|
+
this.notice = `Setup failed: ${redactSecrets(message)}`;
|
|
7862
|
+
this.options.tui.requestRender();
|
|
7863
|
+
};
|
|
7864
|
+
async selectProvider(transport) {
|
|
7865
|
+
this.selectedTransport = transport;
|
|
7866
|
+
this.mode = "resolving";
|
|
7867
|
+
this.notice = undefined;
|
|
7868
|
+
this.canEnterKey = false;
|
|
7869
|
+
this.options.tui.requestRender();
|
|
7870
|
+
let credentials;
|
|
7871
|
+
try {
|
|
7872
|
+
credentials = await this.deps.resolveTransport(transport);
|
|
7873
|
+
} catch (error) {
|
|
7874
|
+
this.mode = "menu";
|
|
7875
|
+
this.notice = `Credential lookup failed: ${redactSecrets(error instanceof Error ? error.message : String(error))}`;
|
|
7876
|
+
this.options.tui.requestRender();
|
|
7373
7877
|
return;
|
|
7374
7878
|
}
|
|
7375
|
-
|
|
7376
|
-
this.
|
|
7377
|
-
if (!
|
|
7879
|
+
this.credentials = credentials;
|
|
7880
|
+
this.mode = "menu";
|
|
7881
|
+
if (!credentials) {
|
|
7882
|
+
if (transport === "typesafe" || transport === "openai-decisions") {
|
|
7883
|
+
this.notice = undefined;
|
|
7884
|
+
this.openKeyEntry(transport);
|
|
7885
|
+
} else {
|
|
7886
|
+
this.notice = "No OpenRouter Pi login found. Add one in Pi and retry.";
|
|
7887
|
+
}
|
|
7378
7888
|
this.options.tui.requestRender();
|
|
7379
7889
|
return;
|
|
7380
7890
|
}
|
|
7381
|
-
|
|
7382
|
-
|
|
7891
|
+
if (credentials.transport !== transport) {
|
|
7892
|
+
this.notice = "The selected provider did not resolve matching credentials.";
|
|
7893
|
+
this.options.tui.requestRender();
|
|
7894
|
+
return;
|
|
7895
|
+
}
|
|
7896
|
+
await this.verifyAndEnable(credentials);
|
|
7383
7897
|
}
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7898
|
+
openKeyEntry(transport) {
|
|
7899
|
+
this.selectedTransport = transport;
|
|
7900
|
+
this.mode = "key-entry";
|
|
7901
|
+
const placeholder = transport === "openai-decisions" ? "Paste an OpenAI Platform API key" : "Paste a TypeSafe API key";
|
|
7902
|
+
this.maskedInput = this.createMaskedInput(placeholder);
|
|
7903
|
+
this.maskedInput.focused = this._focused;
|
|
7904
|
+
this.options.tui.requestRender();
|
|
7905
|
+
}
|
|
7906
|
+
cancelKeyEntry() {
|
|
7907
|
+
this.mode = "menu";
|
|
7908
|
+
this.selectedTransport = undefined;
|
|
7909
|
+
this.maskedInput.setValue("");
|
|
7910
|
+
this.options.tui.requestRender();
|
|
7911
|
+
}
|
|
7912
|
+
async submitEnteredKey(value) {
|
|
7913
|
+
const transport = this.selectedTransport;
|
|
7914
|
+
const key = value.trim();
|
|
7915
|
+
this.maskedInput.setValue("");
|
|
7916
|
+
if (!transport || !key) {
|
|
7387
7917
|
return;
|
|
7388
7918
|
}
|
|
7919
|
+
await this.verifyAndEnable({ apiKey: key, transport }, key);
|
|
7920
|
+
}
|
|
7921
|
+
async verifyAndEnable(credentials, enteredKey) {
|
|
7389
7922
|
this.mode = "verifying";
|
|
7390
7923
|
this.notice = undefined;
|
|
7391
7924
|
this.options.tui.requestRender();
|
|
7392
7925
|
const outcome = await this.deps.verify(credentials);
|
|
7393
|
-
this.mode = "menu";
|
|
7394
7926
|
if (!outcome.ok) {
|
|
7395
7927
|
this.notice = `Verification failed: ${outcome.message ?? "unknown error"}`;
|
|
7928
|
+
this.mode = enteredKey ? "key-entry" : "menu";
|
|
7929
|
+
this.canEnterKey = !enteredKey && canReplaceWithEnteredKey(credentials);
|
|
7396
7930
|
this.options.tui.requestRender();
|
|
7397
7931
|
return;
|
|
7398
7932
|
}
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7933
|
+
const keyToStore = enteredKey ?? (credentials.transport === "typesafe" && credentials.source === "advisor-json" ? credentials.apiKey : undefined);
|
|
7934
|
+
if (keyToStore) {
|
|
7935
|
+
const stored = await this.deps.writeKey(keyToStore, credentials.transport);
|
|
7936
|
+
if (!stored.ok) {
|
|
7937
|
+
this.notice = stored.message;
|
|
7938
|
+
this.mode = "key-entry";
|
|
7939
|
+
this.canEnterKey = false;
|
|
7403
7940
|
this.options.tui.requestRender();
|
|
7404
7941
|
return;
|
|
7405
7942
|
}
|
|
7406
|
-
|
|
7407
|
-
|
|
7943
|
+
if (credentials.transport === "typesafe" && credentials.source === "advisor-json") {
|
|
7944
|
+
const removed = this.deps.removePlaintextKey();
|
|
7945
|
+
this.notice = removed.ok ? "Key moved from advisor.json into the secure store." : `Stored securely, but ${removed.message}`;
|
|
7946
|
+
} else {
|
|
7947
|
+
this.notice = `${stored.message} Verification succeeded.`;
|
|
7948
|
+
}
|
|
7408
7949
|
}
|
|
7409
|
-
this.
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7950
|
+
this.credentials = credentials;
|
|
7951
|
+
const saved = this.commitSelection({ enabled: true, transport: credentials.transport }, "On");
|
|
7952
|
+
if (!saved) {
|
|
7953
|
+
this.mode = "menu";
|
|
7954
|
+
this.notice ??= "Provider verified, but settings could not be saved.";
|
|
7955
|
+
this.options.tui.requestRender();
|
|
7414
7956
|
return;
|
|
7415
7957
|
}
|
|
7416
|
-
|
|
7958
|
+
this.filterEnabled = true;
|
|
7959
|
+
this.mode = "menu";
|
|
7960
|
+
this.canEnterKey = false;
|
|
7961
|
+
this.options.tui.requestRender();
|
|
7962
|
+
}
|
|
7963
|
+
commitSelection(selection, legacyValue) {
|
|
7964
|
+
if (this.options.onSelection) {
|
|
7965
|
+
if (!this.options.onSelection(selection)) {
|
|
7966
|
+
return false;
|
|
7967
|
+
}
|
|
7968
|
+
this.closeSetup(true);
|
|
7969
|
+
return true;
|
|
7970
|
+
}
|
|
7971
|
+
this.options.done(legacyValue);
|
|
7972
|
+
return true;
|
|
7973
|
+
}
|
|
7974
|
+
closeSetup(refreshSettings = false) {
|
|
7975
|
+
this.options.done();
|
|
7976
|
+
if (refreshSettings || this.pendingSelectionRefresh) {
|
|
7977
|
+
this.options.afterSelection?.();
|
|
7978
|
+
}
|
|
7979
|
+
this.pendingSelectionRefresh = false;
|
|
7980
|
+
}
|
|
7981
|
+
async disableAndClear() {
|
|
7982
|
+
const { credentials } = this;
|
|
7983
|
+
if (!(credentials && canClearCredential(credentials))) {
|
|
7417
7984
|
return;
|
|
7418
7985
|
}
|
|
7419
|
-
this.
|
|
7986
|
+
if (this.options.onSelection) {
|
|
7987
|
+
const saved = this.options.onSelection({
|
|
7988
|
+
enabled: false,
|
|
7989
|
+
transport: this.options.currentTransport
|
|
7990
|
+
});
|
|
7991
|
+
if (!saved) {
|
|
7992
|
+
this.mode = "menu";
|
|
7993
|
+
this.notice = "Could not save the disabled state; the key was not cleared.";
|
|
7994
|
+
this.options.tui.requestRender();
|
|
7995
|
+
return;
|
|
7996
|
+
}
|
|
7997
|
+
this.filterEnabled = false;
|
|
7998
|
+
this.pendingSelectionRefresh = true;
|
|
7999
|
+
}
|
|
8000
|
+
this.mode = "clearing";
|
|
8001
|
+
this.notice = undefined;
|
|
7420
8002
|
this.options.tui.requestRender();
|
|
7421
|
-
const
|
|
7422
|
-
apiKey: key,
|
|
7423
|
-
transport: "typesafe"
|
|
7424
|
-
});
|
|
8003
|
+
const result = await this.deps.clearStoredKey(credentials.transport);
|
|
7425
8004
|
this.mode = "menu";
|
|
7426
|
-
|
|
7427
|
-
|
|
8005
|
+
this.notice = result.message;
|
|
8006
|
+
if (!result.ok) {
|
|
7428
8007
|
this.options.tui.requestRender();
|
|
7429
8008
|
return;
|
|
7430
8009
|
}
|
|
7431
|
-
|
|
7432
|
-
if (
|
|
7433
|
-
this.
|
|
7434
|
-
|
|
7435
|
-
|
|
8010
|
+
this.credentials = undefined;
|
|
8011
|
+
if (this.options.onSelection) {
|
|
8012
|
+
this.closeSetup();
|
|
8013
|
+
} else if (this.commitSelection({ enabled: false, transport: this.options.currentTransport }, "Off")) {
|
|
8014
|
+
this.filterEnabled = false;
|
|
7436
8015
|
}
|
|
7437
|
-
this.
|
|
7438
|
-
await this.refresh();
|
|
7439
|
-
this.options.done("On");
|
|
8016
|
+
this.options.tui.requestRender();
|
|
7440
8017
|
}
|
|
7441
8018
|
}
|
|
7442
8019
|
|
|
@@ -7524,13 +8101,22 @@ var scoutTimeoutItem = (settings) => {
|
|
|
7524
8101
|
values: numericValues(timeoutMs, [5000, 1e4, 15000, 30000, 45000, 60000, 90000, 120000])
|
|
7525
8102
|
};
|
|
7526
8103
|
};
|
|
7527
|
-
var jevItems = (settings, theme, tui) => [
|
|
8104
|
+
var jevItems = (settings, theme, tui, jevSetupDeps, onJevSetup, afterJevSetup) => [
|
|
7528
8105
|
{
|
|
7529
8106
|
currentValue: settingValue(settings.jevFilterEnabled, false),
|
|
7530
|
-
description: "Screen low-stakes ask_advisor consultations with Jev; guided setup verifies credentials.",
|
|
8107
|
+
description: "Screen low-stakes ask_advisor consultations with Jev/Decisions; guided setup verifies credentials.",
|
|
7531
8108
|
id: "jevFilter",
|
|
7532
|
-
label: "Jev consultation filter",
|
|
7533
|
-
submenu: (currentValue, done) => new JevSetupSubmenu({
|
|
8109
|
+
label: "Jev/Decisions consultation filter",
|
|
8110
|
+
submenu: (currentValue, done) => new JevSetupSubmenu({
|
|
8111
|
+
afterSelection: afterJevSetup,
|
|
8112
|
+
currentTransport: settings.jevTransport ?? DEFAULT_JEV_TRANSPORT,
|
|
8113
|
+
currentValue,
|
|
8114
|
+
done,
|
|
8115
|
+
onSelection: onJevSetup,
|
|
8116
|
+
setupDeps: jevSetupDeps,
|
|
8117
|
+
theme,
|
|
8118
|
+
tui
|
|
8119
|
+
})
|
|
7534
8120
|
},
|
|
7535
8121
|
{
|
|
7536
8122
|
currentValue: String(settings.jevFilterSkipConfidence ?? DEFAULT_JEV_FILTER_SKIP_CONFIDENCE),
|
|
@@ -7584,7 +8170,7 @@ var jevItems = (settings, theme, tui) => [
|
|
|
7584
8170
|
},
|
|
7585
8171
|
{
|
|
7586
8172
|
currentValue: String(settings.jevPricePerMtok ?? DEFAULT_JEV_PRICE_PER_MTOK),
|
|
7587
|
-
description: "
|
|
8173
|
+
description: "TypeSafe/OpenRouter estimated input price per million tokens; ignored for OpenAI Decisions, whose cost is unavailable.",
|
|
7588
8174
|
id: "jevPricePerMtok",
|
|
7589
8175
|
label: "Jev price/Mtok",
|
|
7590
8176
|
values: numericValues(settings.jevPricePerMtok ?? DEFAULT_JEV_PRICE_PER_MTOK, [0.01, 0.02, 0.042, 0.05, 0.1])
|
|
@@ -7593,7 +8179,7 @@ var jevItems = (settings, theme, tui) => [
|
|
|
7593
8179
|
currentValue: settings.jevTurnGateEveryTurns === 0 || settings.jevTurnGateEveryTurns === undefined ? "Off" : `every ${settings.jevTurnGateEveryTurns} turns`,
|
|
7594
8180
|
description: "Proactively consult the Advisor every N turns without a consultation (0 = off).",
|
|
7595
8181
|
id: "jevTurnGateEveryTurns",
|
|
7596
|
-
label: "Jev turn gate",
|
|
8182
|
+
label: "Jev/Decisions turn gate",
|
|
7597
8183
|
values: [
|
|
7598
8184
|
"Off",
|
|
7599
8185
|
"every 3 turns",
|
|
@@ -7606,19 +8192,8 @@ var jevItems = (settings, theme, tui) => [
|
|
|
7606
8192
|
currentValue: String(settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD),
|
|
7607
8193
|
description: "Jev confidence required before the turn gate interrupts with advice.",
|
|
7608
8194
|
id: "jevTurnGateNoulThreshold",
|
|
7609
|
-
label: "Jev turn-gate threshold",
|
|
8195
|
+
label: "Jev/Decisions turn-gate threshold",
|
|
7610
8196
|
values: numericValues(settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD, [0.6, 0.7, 0.8, 0.85, 0.9, 0.95])
|
|
7611
|
-
},
|
|
7612
|
-
{
|
|
7613
|
-
currentValue: settings.jevTransport ?? DEFAULT_JEV_TRANSPORT,
|
|
7614
|
-
description: "How Jev calls travel: auto reuses an OpenRouter login when no TypeSafe key is set.",
|
|
7615
|
-
id: "jevTransport",
|
|
7616
|
-
label: "Jev transport",
|
|
7617
|
-
values: withCurrentValue(settings.jevTransport ?? DEFAULT_JEV_TRANSPORT, [
|
|
7618
|
-
"auto",
|
|
7619
|
-
"typesafe",
|
|
7620
|
-
"openrouter"
|
|
7621
|
-
])
|
|
7622
8197
|
}
|
|
7623
8198
|
];
|
|
7624
8199
|
var advisorFallbackModelItem = (settings, modelRefs, keybindings, theme, tui) => ({
|
|
@@ -7665,6 +8240,9 @@ var createSettingsItems = ({
|
|
|
7665
8240
|
modelWhitelist,
|
|
7666
8241
|
presets,
|
|
7667
8242
|
settings,
|
|
8243
|
+
afterJevSetup,
|
|
8244
|
+
jevSetupDeps,
|
|
8245
|
+
onJevSetup,
|
|
7668
8246
|
theme,
|
|
7669
8247
|
tui
|
|
7670
8248
|
}) => {
|
|
@@ -7799,7 +8377,7 @@ var createSettingsItems = ({
|
|
|
7799
8377
|
title: "Tool disclosure policies",
|
|
7800
8378
|
tui
|
|
7801
8379
|
})
|
|
7802
|
-
}, toggle("trackedFileContent", "Tracked file content", "Allow tracked file contents to be sent with Advisor context.", settings.trackedFileContent, false), toggle("untrackedContent", "Untracked file content", "Allow untracked file contents to be sent with Advisor context.", settings.untrackedContent, false), toggle("outcomeLogging", "Outcome logging (global)", "Allow anonymized Advisor outcomes to be logged globally.", settings.outcomeLogging, false), ...jevItems(settings, theme, tui));
|
|
8380
|
+
}, toggle("trackedFileContent", "Tracked file content", "Allow tracked file contents to be sent with Advisor context.", settings.trackedFileContent, false), toggle("untrackedContent", "Untracked file content", "Allow untracked file contents to be sent with Advisor context.", settings.untrackedContent, false), toggle("outcomeLogging", "Outcome logging (global)", "Allow anonymized Advisor outcomes to be logged globally.", settings.outcomeLogging, false), ...jevItems(settings, theme, tui, jevSetupDeps, onJevSetup, afterJevSetup));
|
|
7803
8381
|
return items;
|
|
7804
8382
|
};
|
|
7805
8383
|
|
|
@@ -8018,10 +8596,6 @@ var applyJevMutation = (settings, id, value) => {
|
|
|
8018
8596
|
settings.jevPricePerMtok = Number(value);
|
|
8019
8597
|
return true;
|
|
8020
8598
|
}
|
|
8021
|
-
case "jevTransport": {
|
|
8022
|
-
settings.jevTransport = value;
|
|
8023
|
-
return true;
|
|
8024
|
-
}
|
|
8025
8599
|
default: {
|
|
8026
8600
|
return false;
|
|
8027
8601
|
}
|
|
@@ -8103,9 +8677,15 @@ class AdvisorSettingsSelector {
|
|
|
8103
8677
|
const fallbackModel = advisorFallbackModelItem(this.settings, this.options.modelRefs, this.options.keybindings, this.options.theme, this.options.tui);
|
|
8104
8678
|
const modelWhitelist = advisorModelWhitelistItem(this.settings, this.options.modelRefs, this.options.keybindings, this.options.theme, this.options.tui);
|
|
8105
8679
|
const items = createSettingsItems({
|
|
8680
|
+
afterJevSetup: () => {
|
|
8681
|
+
this.settingsList = this.createSettingsList("jevFilter");
|
|
8682
|
+
this.options.tui.requestRender();
|
|
8683
|
+
},
|
|
8106
8684
|
effortLevels: this.options.effortLevels,
|
|
8107
8685
|
fallbackModel,
|
|
8686
|
+
jevSetupDeps: this.options.jevSetupDeps,
|
|
8108
8687
|
modelWhitelist,
|
|
8688
|
+
onJevSetup: (selection) => this.applyJevSetup(selection),
|
|
8109
8689
|
presets: this.presets,
|
|
8110
8690
|
settings: this.settings,
|
|
8111
8691
|
theme: this.options.theme,
|
|
@@ -8118,6 +8698,25 @@ class AdvisorSettingsSelector {
|
|
|
8118
8698
|
}
|
|
8119
8699
|
return adapter;
|
|
8120
8700
|
}
|
|
8701
|
+
applyJevSetup(selection) {
|
|
8702
|
+
const updated = {
|
|
8703
|
+
...this.settings,
|
|
8704
|
+
jevFilterEnabled: selection.enabled,
|
|
8705
|
+
jevTransport: selection.transport,
|
|
8706
|
+
showUsageDetails: this.settings.showUsageDetails ?? true,
|
|
8707
|
+
toolPolicies: { ...this.settings.toolPolicies }
|
|
8708
|
+
};
|
|
8709
|
+
try {
|
|
8710
|
+
const result = this.options.onJevSetup ? this.options.onJevSetup(selection, updated) : (this.options.onChange ?? this.options.onSave)?.(updated);
|
|
8711
|
+
if (result === false) {
|
|
8712
|
+
return false;
|
|
8713
|
+
}
|
|
8714
|
+
} catch {
|
|
8715
|
+
return false;
|
|
8716
|
+
}
|
|
8717
|
+
Object.assign(this.settings, updated);
|
|
8718
|
+
return true;
|
|
8719
|
+
}
|
|
8121
8720
|
startSimpleModeGradient() {
|
|
8122
8721
|
this.stopSimpleModeGradient();
|
|
8123
8722
|
this.simpleModeGradientStartedAt = Date.now();
|
|
@@ -8209,7 +8808,7 @@ var applyAdvisorSettings = (settings) => {
|
|
|
8209
8808
|
applyJevSettings(settings);
|
|
8210
8809
|
applyDisclosureSettings(settings);
|
|
8211
8810
|
};
|
|
8212
|
-
var saveAdvisorSettings = (ctx, settings) => {
|
|
8811
|
+
var saveAdvisorSettings = (ctx, settings, options = {}) => {
|
|
8213
8812
|
const previous = getAdvisorSettings();
|
|
8214
8813
|
try {
|
|
8215
8814
|
applyAdvisorSettings(settings);
|
|
@@ -8218,7 +8817,9 @@ var saveAdvisorSettings = (ctx, settings) => {
|
|
|
8218
8817
|
persistAdvisor: Boolean(persisted.advisor),
|
|
8219
8818
|
persistExecutor: Boolean(persisted.executor)
|
|
8220
8819
|
});
|
|
8221
|
-
|
|
8820
|
+
if (!options.skipOutcomeLogging) {
|
|
8821
|
+
saveGlobalOutcomeLogging(settings.outcomeLogging ?? false);
|
|
8822
|
+
}
|
|
8222
8823
|
} catch (error) {
|
|
8223
8824
|
applyAdvisorSettings(previous);
|
|
8224
8825
|
throw error;
|
|
@@ -8237,6 +8838,9 @@ var registerSettingsCommands = (runtime) => {
|
|
|
8237
8838
|
await ctx.ui.custom((tui, theme, keybindings, done) => new AdvisorSettingsSelector({
|
|
8238
8839
|
effortLevels: EFFORT_LEVELS,
|
|
8239
8840
|
initial,
|
|
8841
|
+
jevSetupDeps: {
|
|
8842
|
+
resolveTransport: (transport) => transport ? resolveJevTransportFor(transport, ctx) : resolveJevTransport(ctx)
|
|
8843
|
+
},
|
|
8240
8844
|
keybindings,
|
|
8241
8845
|
modelRefs: getConfiguredModelRefs(ctx),
|
|
8242
8846
|
onCancel: () => done(undefined),
|
|
@@ -8250,6 +8854,27 @@ var registerSettingsCommands = (runtime) => {
|
|
|
8250
8854
|
ctx.ui.notify(`Could not save Advisor settings: ${message}`, "error");
|
|
8251
8855
|
}
|
|
8252
8856
|
},
|
|
8857
|
+
onJevSetup: (selection, settings) => {
|
|
8858
|
+
try {
|
|
8859
|
+
saveAdvisorSettings(ctx, {
|
|
8860
|
+
...settings,
|
|
8861
|
+
jevFilterEnabled: selection.enabled,
|
|
8862
|
+
jevTransport: selection.transport
|
|
8863
|
+
}, { skipOutcomeLogging: true });
|
|
8864
|
+
} catch (error) {
|
|
8865
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8866
|
+
ctx.ui.notify(`Could not save Advisor settings: ${message}`, "error");
|
|
8867
|
+
return false;
|
|
8868
|
+
}
|
|
8869
|
+
try {
|
|
8870
|
+
runtime.updateSameModelNotice(ctx);
|
|
8871
|
+
runtime.updateAdvisorUsageStatus(ctx);
|
|
8872
|
+
} catch (error) {
|
|
8873
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8874
|
+
ctx.ui.notify(`Advisor settings were saved, but the status refresh failed: ${message}`, "warning");
|
|
8875
|
+
}
|
|
8876
|
+
return true;
|
|
8877
|
+
},
|
|
8253
8878
|
presets: CONTEXT_PRESETS,
|
|
8254
8879
|
theme,
|
|
8255
8880
|
tui
|
|
@@ -8370,7 +8995,7 @@ var turnGateQuestion = {
|
|
|
8370
8995
|
instructions: "Should a senior engineering advisor be consulted right now, before the executor continues? Judge from `recent_conversation`.",
|
|
8371
8996
|
type: "noul"
|
|
8372
8997
|
};
|
|
8373
|
-
var outageNotifier2 = createOutageNotifier((category, message) => `Advisor Jev turn gate failed (${category}); continuing without a proactive consultation. ${message}`);
|
|
8998
|
+
var outageNotifier2 = createOutageNotifier((category, message) => `Advisor Jev/Decisions turn gate failed (${category}); continuing without a proactive consultation. ${message}`);
|
|
8374
8999
|
var notifyFailureOnce = outageNotifier2.notify;
|
|
8375
9000
|
var shouldRunTurnGate = (registration, ctx, session) => {
|
|
8376
9001
|
const interval = advisorJevTurnGateEveryTurnsRef;
|
|
@@ -8396,7 +9021,7 @@ var handleJevTurnEnd = async (registration, ctx) => {
|
|
|
8396
9021
|
const credentials = await (deps.resolveTransport ?? resolveJevTransport)(ctx);
|
|
8397
9022
|
if (!credentials) {
|
|
8398
9023
|
session.recordJevGateFailure();
|
|
8399
|
-
notifyFailureOnce(ctx, "missing-key", "No
|
|
9024
|
+
notifyFailureOnce(ctx, "missing-key", "No credentials resolved for the selected Jev/Decisions provider.");
|
|
8400
9025
|
return;
|
|
8401
9026
|
}
|
|
8402
9027
|
const client = jevClientFromCredentials(credentials, deps.fetch);
|