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/src/jev/key-store.ts
CHANGED
|
@@ -258,7 +258,7 @@ export const consumePlaintextKeyWarning = (): string | undefined => {
|
|
|
258
258
|
return undefined;
|
|
259
259
|
}
|
|
260
260
|
warnedPlaintextKey = true;
|
|
261
|
-
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.`;
|
|
261
|
+
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.`;
|
|
262
262
|
};
|
|
263
263
|
|
|
264
264
|
/** Test-only: re-arms the one-time plaintext warning. */
|
package/src/jev/ledger.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface AdvisorJevUsageTotals {
|
|
|
4
4
|
cost: number;
|
|
5
5
|
inputTokens: number;
|
|
6
6
|
outputTokens: number;
|
|
7
|
+
unavailableCostCalls: number;
|
|
8
|
+
knownCostCalls: number;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export interface AdvisorJevFilterLedger {
|
|
@@ -29,7 +31,7 @@ export interface AdvisorJevLedger {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export interface AdvisorJevUsage {
|
|
32
|
-
cost
|
|
34
|
+
cost?: number;
|
|
33
35
|
inputTokens: number;
|
|
34
36
|
outputTokens: number;
|
|
35
37
|
}
|
|
@@ -49,7 +51,9 @@ export interface JevInvocationView {
|
|
|
49
51
|
const freshJevUsage = (): AdvisorJevUsageTotals => ({
|
|
50
52
|
cost: 0,
|
|
51
53
|
inputTokens: 0,
|
|
54
|
+
knownCostCalls: 0,
|
|
52
55
|
outputTokens: 0,
|
|
56
|
+
unavailableCostCalls: 0,
|
|
53
57
|
});
|
|
54
58
|
|
|
55
59
|
const freshJevLedger = (): AdvisorJevLedger => ({
|
|
@@ -66,7 +70,12 @@ const freshJevLedger = (): AdvisorJevLedger => ({
|
|
|
66
70
|
});
|
|
67
71
|
|
|
68
72
|
const addJevUsage = (totals: AdvisorJevUsageTotals, usage: AdvisorJevUsage) => {
|
|
69
|
-
|
|
73
|
+
if (usage.cost === undefined) {
|
|
74
|
+
totals.unavailableCostCalls += 1;
|
|
75
|
+
} else {
|
|
76
|
+
totals.cost += usage.cost;
|
|
77
|
+
totals.knownCostCalls += 1;
|
|
78
|
+
}
|
|
70
79
|
totals.inputTokens += usage.inputTokens;
|
|
71
80
|
totals.outputTokens += usage.outputTokens;
|
|
72
81
|
};
|
|
@@ -82,6 +91,15 @@ const savingsLine = (costs: number[], skipped: number) => {
|
|
|
82
91
|
const formatJevTokens = (usage: AdvisorJevUsageTotals) =>
|
|
83
92
|
`↑${formatTokenCount(usage.inputTokens + usage.outputTokens)}`;
|
|
84
93
|
|
|
94
|
+
const jevCostText = (usage: AdvisorJevUsageTotals) => {
|
|
95
|
+
if (usage.unavailableCostCalls > 0) {
|
|
96
|
+
return usage.knownCostCalls > 0
|
|
97
|
+
? `cost unavailable (known-provider estimates $${usage.cost.toFixed(4)}; Decisions billing unconfirmed)`
|
|
98
|
+
: "cost unavailable (Decisions billing unconfirmed)";
|
|
99
|
+
}
|
|
100
|
+
return `$${usage.cost.toFixed(4)}`;
|
|
101
|
+
};
|
|
102
|
+
|
|
85
103
|
const markdownCosts = (invocations: readonly JevInvocationView[]): number[] =>
|
|
86
104
|
invocations
|
|
87
105
|
.filter(
|
|
@@ -101,7 +119,7 @@ const filterLine = (filter: AdvisorJevFilterLedger) => {
|
|
|
101
119
|
if (filter.failures > 0) {
|
|
102
120
|
parts.push(`${filter.failures} failure${filter.failures === 1 ? "" : "s"}`);
|
|
103
121
|
}
|
|
104
|
-
return `Jev filter: ${parts.join(", ")}`;
|
|
122
|
+
return `Jev/Decisions filter: ${parts.join(", ")}`;
|
|
105
123
|
};
|
|
106
124
|
|
|
107
125
|
const gateLine = (
|
|
@@ -115,7 +133,7 @@ const gateLine = (
|
|
|
115
133
|
)
|
|
116
134
|
.map((item) => item.cost);
|
|
117
135
|
const gateSpend = consultationCosts.reduce((sum, cost) => sum + cost, 0);
|
|
118
|
-
return `Turn gate: ${gate.checks} check${gate.checks === 1 ? "" : "s"} (Jev ${formatJevTokens(gate.usage)} ·
|
|
136
|
+
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)})`;
|
|
119
137
|
};
|
|
120
138
|
|
|
121
139
|
/** One session's Jev screening and turn-gate accounting plus summary lines. */
|
|
@@ -206,9 +224,13 @@ export class AdvisorJevLedgerState {
|
|
|
206
224
|
} else if (this.#filterActive()) {
|
|
207
225
|
lines.push(filterLine(filter));
|
|
208
226
|
const jevTokens = usage.inputTokens + usage.outputTokens;
|
|
209
|
-
if (jevTokens > 0) {
|
|
227
|
+
if (jevTokens > 0 || usage.unavailableCostCalls > 0) {
|
|
228
|
+
const cost =
|
|
229
|
+
usage.unavailableCostCalls > 0
|
|
230
|
+
? jevCostText(usage)
|
|
231
|
+
: `$${usage.cost.toFixed(4)} (input only; output free)`;
|
|
210
232
|
lines.push(
|
|
211
|
-
`Jev cost: ${formatJevTokens(usage)} tokens ·
|
|
233
|
+
`Jev/Decisions cost: ${formatJevTokens(usage)} tokens · ${cost}`
|
|
212
234
|
);
|
|
213
235
|
}
|
|
214
236
|
if (filter.skipped > 0) {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
|
|
12
|
+
import { redactSecrets } from "../redaction.ts";
|
|
13
|
+
import type { JevKeyStoreResult, JevSecretsLike } from "./key-store.ts";
|
|
14
|
+
|
|
15
|
+
export const OPENAI_DECISIONS_KEY_SERVICE = "pi-advisor";
|
|
16
|
+
export const OPENAI_DECISIONS_KEY_NAME = "openai-decisions-api-key";
|
|
17
|
+
const KEY_FILE_MODE = 0o600;
|
|
18
|
+
|
|
19
|
+
export type OpenAIDecisionsKeySource = "openai-bun-secrets" | "openai-file";
|
|
20
|
+
|
|
21
|
+
export interface OpenAIDecisionsKeyResolution {
|
|
22
|
+
key?: string;
|
|
23
|
+
source?: OpenAIDecisionsKeySource;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface OpenAIDecisionsKeyStoreDeps {
|
|
27
|
+
deleteFileStore?: () => void;
|
|
28
|
+
readFileStore?: () => string | undefined;
|
|
29
|
+
secrets?: JevSecretsLike | null;
|
|
30
|
+
writeFileStore?: (key: string) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface BunGlobal {
|
|
34
|
+
Bun?: { secrets?: JevSecretsLike };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const runtimeSecrets = (): JevSecretsLike | undefined => {
|
|
38
|
+
// SAFETY: only Bun runtimes expose Bun.secrets; other hosts leave the property absent.
|
|
39
|
+
const bun = globalThis as BunGlobal;
|
|
40
|
+
return bun.Bun?.secrets;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const keyFilePath = () => join(getAgentDir(), "openai_api_key");
|
|
44
|
+
const normalizeKey = (value: string | null | undefined) =>
|
|
45
|
+
value?.trim() || undefined;
|
|
46
|
+
|
|
47
|
+
const readDefaultFileStore = (): string | undefined => {
|
|
48
|
+
try {
|
|
49
|
+
return normalizeKey(readFileSync(keyFilePath(), "utf-8"));
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const writeDefaultFileStore = (key: string) => {
|
|
56
|
+
const path = keyFilePath();
|
|
57
|
+
writeFileSync(path, `${key}\n`, { mode: KEY_FILE_MODE });
|
|
58
|
+
chmodSync(path, KEY_FILE_MODE);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const deleteDefaultFileStore = () => rmSync(keyFilePath(), { force: true });
|
|
62
|
+
|
|
63
|
+
const messageOf = <Error>(error: Error) =>
|
|
64
|
+
redactSecrets(error instanceof Error ? error.message : String(error));
|
|
65
|
+
|
|
66
|
+
export const resolveOpenAIDecisionsKey = async (
|
|
67
|
+
deps: OpenAIDecisionsKeyStoreDeps = {}
|
|
68
|
+
): Promise<OpenAIDecisionsKeyResolution> => {
|
|
69
|
+
const secrets = deps.secrets === undefined ? runtimeSecrets() : deps.secrets;
|
|
70
|
+
if (secrets) {
|
|
71
|
+
let secretKey: string | undefined;
|
|
72
|
+
try {
|
|
73
|
+
secretKey = normalizeKey(
|
|
74
|
+
await secrets.get({
|
|
75
|
+
name: OPENAI_DECISIONS_KEY_NAME,
|
|
76
|
+
service: OPENAI_DECISIONS_KEY_SERVICE,
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
} catch {
|
|
80
|
+
secretKey = undefined;
|
|
81
|
+
}
|
|
82
|
+
if (secretKey) {
|
|
83
|
+
return { key: secretKey, source: "openai-bun-secrets" };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const key = normalizeKey((deps.readFileStore ?? readDefaultFileStore)());
|
|
87
|
+
return key ? { key, source: "openai-file" } : {};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const writeOpenAIDecisionsKey = async (
|
|
91
|
+
key: string,
|
|
92
|
+
deps: OpenAIDecisionsKeyStoreDeps = {}
|
|
93
|
+
): Promise<JevKeyStoreResult> => {
|
|
94
|
+
const normalized = normalizeKey(key);
|
|
95
|
+
if (!normalized) {
|
|
96
|
+
return { message: "The OpenAI API key is empty.", ok: false };
|
|
97
|
+
}
|
|
98
|
+
const secrets = deps.secrets === undefined ? runtimeSecrets() : deps.secrets;
|
|
99
|
+
if (secrets) {
|
|
100
|
+
try {
|
|
101
|
+
await secrets.set({
|
|
102
|
+
name: OPENAI_DECISIONS_KEY_NAME,
|
|
103
|
+
service: OPENAI_DECISIONS_KEY_SERVICE,
|
|
104
|
+
value: normalized,
|
|
105
|
+
});
|
|
106
|
+
return { message: "OpenAI API key stored in Bun.secrets.", ok: true };
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return {
|
|
109
|
+
message: `Storing the OpenAI API key in Bun.secrets failed: ${messageOf(error)}. Configure OPENAI_API_KEY in Pi or retry secure storage.`,
|
|
110
|
+
ok: false,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
(deps.writeFileStore ?? writeDefaultFileStore)(normalized);
|
|
116
|
+
return {
|
|
117
|
+
message: "OpenAI API key stored in the Pi agent directory (mode 0600).",
|
|
118
|
+
ok: true,
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
message: `Storing the OpenAI API key failed: ${messageOf(error)}. Configure OPENAI_API_KEY in Pi or retry secure storage.`,
|
|
123
|
+
ok: false,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const clearOpenAIDecisionsKey = async (
|
|
129
|
+
deps: OpenAIDecisionsKeyStoreDeps = {}
|
|
130
|
+
): Promise<JevKeyStoreResult> => {
|
|
131
|
+
let firstError: string | undefined;
|
|
132
|
+
const secrets = deps.secrets === undefined ? runtimeSecrets() : deps.secrets;
|
|
133
|
+
if (secrets) {
|
|
134
|
+
try {
|
|
135
|
+
await secrets.delete({
|
|
136
|
+
name: OPENAI_DECISIONS_KEY_NAME,
|
|
137
|
+
service: OPENAI_DECISIONS_KEY_SERVICE,
|
|
138
|
+
});
|
|
139
|
+
} catch (error) {
|
|
140
|
+
firstError = messageOf(error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
if (deps.deleteFileStore) {
|
|
145
|
+
deps.deleteFileStore();
|
|
146
|
+
} else if (existsSync(keyFilePath())) {
|
|
147
|
+
deleteDefaultFileStore();
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
firstError ??= messageOf(error);
|
|
151
|
+
}
|
|
152
|
+
if (firstError) {
|
|
153
|
+
return {
|
|
154
|
+
message: `Clearing the OpenAI API key failed: ${firstError}.`,
|
|
155
|
+
ok: false,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return { message: "Stored OpenAI API key cleared.", ok: true };
|
|
159
|
+
};
|
package/src/jev/transport.ts
CHANGED
|
@@ -3,24 +3,87 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import { advisorJevTransportRef } from "../config/state.ts";
|
|
4
4
|
import { resolveTypeSafeKey } from "./key-store.ts";
|
|
5
5
|
import type { JevKeySource } from "./key-store.ts";
|
|
6
|
+
import { resolveOpenAIDecisionsKey } from "./openai-key-store.ts";
|
|
7
|
+
import type { OpenAIDecisionsKeyResolution } from "./openai-key-store.ts";
|
|
6
8
|
|
|
7
|
-
export type JevTransportKind = "typesafe" | "openrouter";
|
|
9
|
+
export type JevTransportKind = "typesafe" | "openrouter" | "openai-decisions";
|
|
10
|
+
|
|
11
|
+
export type JevCredentialSource =
|
|
12
|
+
| JevKeySource
|
|
13
|
+
| "openai-env"
|
|
14
|
+
| "openai-provider-credential"
|
|
15
|
+
| "openai-bun-secrets"
|
|
16
|
+
| "openai-file";
|
|
8
17
|
|
|
9
18
|
export interface JevCredentials {
|
|
10
19
|
apiKey: string;
|
|
11
|
-
|
|
12
|
-
source?: JevKeySource;
|
|
20
|
+
source?: JevCredentialSource;
|
|
13
21
|
transport: JevTransportKind;
|
|
14
22
|
}
|
|
15
23
|
|
|
24
|
+
export interface JevProviderAuthResult {
|
|
25
|
+
auth: { apiKey?: string };
|
|
26
|
+
source?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
export interface JevTransportDeps {
|
|
17
30
|
/** Reads a pi-stored provider login; injectable for tests. */
|
|
18
31
|
getProviderKey?: (provider: string) => Promise<string | undefined>;
|
|
32
|
+
/** Resolves Pi provider auth with its source label; injectable for tests. */
|
|
33
|
+
getProviderAuth?: (
|
|
34
|
+
provider: string
|
|
35
|
+
) => Promise<JevProviderAuthResult | undefined>;
|
|
36
|
+
/** Resolves the extension-owned OpenAI Decisions key; injectable for tests. */
|
|
37
|
+
resolveOpenAIKey?: () => Promise<OpenAIDecisionsKeyResolution>;
|
|
19
38
|
/** Resolves the TypeSafe key chain; injectable for tests. */
|
|
20
39
|
resolveTypesafe?: () => ReturnType<typeof resolveTypeSafeKey>;
|
|
21
40
|
}
|
|
22
41
|
|
|
23
42
|
const OPENROUTER_PROVIDER = "openrouter";
|
|
43
|
+
const OPENAI_PROVIDER = "openai";
|
|
44
|
+
const OPENAI_API_KEY_SOURCES = new Map<string, JevCredentialSource>([
|
|
45
|
+
["OPENAI_API_KEY", "openai-env"],
|
|
46
|
+
["stored credential", "openai-provider-credential"],
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const openAiPlatformCredentials = async (
|
|
50
|
+
ctx: ExtensionContext | undefined,
|
|
51
|
+
deps: JevTransportDeps
|
|
52
|
+
): Promise<JevCredentials | undefined> => {
|
|
53
|
+
let result: JevProviderAuthResult | undefined;
|
|
54
|
+
try {
|
|
55
|
+
result = deps.getProviderAuth
|
|
56
|
+
? await deps.getProviderAuth(OPENAI_PROVIDER)
|
|
57
|
+
: await ctx?.modelRegistry?.getProviderAuth(OPENAI_PROVIDER);
|
|
58
|
+
} catch {
|
|
59
|
+
result = undefined;
|
|
60
|
+
}
|
|
61
|
+
const source = result?.source;
|
|
62
|
+
const credentialSource = source
|
|
63
|
+
? OPENAI_API_KEY_SOURCES.get(source)
|
|
64
|
+
: undefined;
|
|
65
|
+
const apiKey = result?.auth.apiKey?.trim();
|
|
66
|
+
if (credentialSource && apiKey) {
|
|
67
|
+
return {
|
|
68
|
+
apiKey,
|
|
69
|
+
source: credentialSource,
|
|
70
|
+
transport: "openai-decisions",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const stored = await (deps.resolveOpenAIKey ?? resolveOpenAIDecisionsKey)();
|
|
75
|
+
const storedKey = stored.key?.trim();
|
|
76
|
+
return storedKey && stored.source
|
|
77
|
+
? {
|
|
78
|
+
apiKey: storedKey,
|
|
79
|
+
source: stored.source,
|
|
80
|
+
transport: "openai-decisions",
|
|
81
|
+
}
|
|
82
|
+
: undefined;
|
|
83
|
+
} catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
24
87
|
|
|
25
88
|
const openRouterKey = async (
|
|
26
89
|
ctx: ExtensionContext | undefined,
|
|
@@ -32,32 +95,47 @@ const openRouterKey = async (
|
|
|
32
95
|
return key?.trim() || undefined;
|
|
33
96
|
};
|
|
34
97
|
|
|
35
|
-
|
|
36
|
-
|
|
98
|
+
const typeSafeCredentials = async (
|
|
99
|
+
deps: JevTransportDeps
|
|
100
|
+
): Promise<JevCredentials | undefined> => {
|
|
101
|
+
const resolution = await (deps.resolveTypesafe ?? resolveTypeSafeKey)();
|
|
102
|
+
if (!resolution.key) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const credentials: JevCredentials = {
|
|
106
|
+
apiKey: resolution.key,
|
|
107
|
+
transport: "typesafe",
|
|
108
|
+
};
|
|
109
|
+
if (resolution.source) {
|
|
110
|
+
credentials.source = resolution.source;
|
|
111
|
+
}
|
|
112
|
+
return credentials;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const resolveJevTransportFor = async (
|
|
116
|
+
transport: JevTransportKind,
|
|
117
|
+
ctx?: ExtensionContext,
|
|
118
|
+
deps: JevTransportDeps = {}
|
|
119
|
+
): Promise<JevCredentials | undefined> => {
|
|
120
|
+
if (transport === "typesafe") {
|
|
121
|
+
return typeSafeCredentials(deps);
|
|
122
|
+
}
|
|
123
|
+
if (transport === "openrouter") {
|
|
124
|
+
const key = await openRouterKey(ctx, deps);
|
|
125
|
+
return key ? { apiKey: key, transport } : undefined;
|
|
126
|
+
}
|
|
127
|
+
return openAiPlatformCredentials(ctx, deps);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** Resolves `auto` as TypeSafe then OpenRouter; explicit choices never fall back. */
|
|
37
131
|
export const resolveJevTransport = async (
|
|
38
132
|
ctx?: ExtensionContext,
|
|
39
133
|
deps: JevTransportDeps = {}
|
|
40
134
|
): Promise<JevCredentials | undefined> => {
|
|
41
135
|
const preference = advisorJevTransportRef;
|
|
42
|
-
if (preference !== "
|
|
43
|
-
|
|
44
|
-
const resolution = await resolveTypesafe();
|
|
45
|
-
if (resolution.key) {
|
|
46
|
-
const credentials: JevCredentials = {
|
|
47
|
-
apiKey: resolution.key,
|
|
48
|
-
transport: "typesafe",
|
|
49
|
-
};
|
|
50
|
-
if (resolution.source) {
|
|
51
|
-
credentials.source = resolution.source;
|
|
52
|
-
}
|
|
53
|
-
return credentials;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (preference === "typesafe") {
|
|
57
|
-
return undefined;
|
|
136
|
+
if (preference !== "auto") {
|
|
137
|
+
return resolveJevTransportFor(preference, ctx, deps);
|
|
58
138
|
}
|
|
59
|
-
const
|
|
60
|
-
return openrouter
|
|
61
|
-
? { apiKey: openrouter, transport: "openrouter" }
|
|
62
|
-
: undefined;
|
|
139
|
+
const typesafe = await typeSafeCredentials(deps);
|
|
140
|
+
return typesafe ?? resolveJevTransportFor("openrouter", ctx, deps);
|
|
63
141
|
};
|
package/src/tools/jev-filter.ts
CHANGED
|
@@ -57,7 +57,7 @@ const repeatSkipText = (advice: string) =>
|
|
|
57
57
|
|
|
58
58
|
const outageNotifier = createOutageNotifier(
|
|
59
59
|
(category, message) =>
|
|
60
|
-
`Advisor Jev filter failed (${category}); allowing consultations. ${message}`
|
|
60
|
+
`Advisor Jev/Decisions filter failed (${category}); allowing consultations. ${message}`
|
|
61
61
|
);
|
|
62
62
|
|
|
63
63
|
const notifyOutageOnce = outageNotifier.notify;
|
|
@@ -107,7 +107,7 @@ const screenWithJev = async (
|
|
|
107
107
|
notifyOutageOnce(
|
|
108
108
|
ctx,
|
|
109
109
|
"missing-key",
|
|
110
|
-
"No
|
|
110
|
+
"No credentials resolved for the selected Jev/Decisions provider."
|
|
111
111
|
);
|
|
112
112
|
return allow();
|
|
113
113
|
}
|
|
@@ -69,7 +69,7 @@ export interface JevTurnGateRegistration {
|
|
|
69
69
|
|
|
70
70
|
const outageNotifier = createOutageNotifier(
|
|
71
71
|
(category, message) =>
|
|
72
|
-
`Advisor Jev turn gate failed (${category}); continuing without a proactive consultation. ${message}`
|
|
72
|
+
`Advisor Jev/Decisions turn gate failed (${category}); continuing without a proactive consultation. ${message}`
|
|
73
73
|
);
|
|
74
74
|
|
|
75
75
|
const notifyFailureOnce = outageNotifier.notify;
|
|
@@ -133,7 +133,7 @@ export const handleJevTurnEnd = async (
|
|
|
133
133
|
notifyFailureOnce(
|
|
134
134
|
ctx,
|
|
135
135
|
"missing-key",
|
|
136
|
-
"No
|
|
136
|
+
"No credentials resolved for the selected Jev/Decisions provider."
|
|
137
137
|
);
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
canClearCredential,
|
|
5
|
+
providerName,
|
|
6
|
+
transportLabel,
|
|
7
|
+
} from "./jev-setup-support.ts";
|
|
8
|
+
import type {
|
|
9
|
+
JevSetupSubmenuOptions,
|
|
10
|
+
JevSetupViewState,
|
|
11
|
+
SetupAction,
|
|
12
|
+
} from "./jev-setup-support.ts";
|
|
13
|
+
import type { MaskedInput } from "./masked-input.ts";
|
|
14
|
+
|
|
15
|
+
export const setupActions = (state: JevSetupViewState): SetupAction[] => {
|
|
16
|
+
const actions: SetupAction[] = ["typesafe", "openrouter", "openai-decisions"];
|
|
17
|
+
if (state.canEnterKey) {
|
|
18
|
+
actions.push("enter-key");
|
|
19
|
+
}
|
|
20
|
+
if (state.filterEnabled) {
|
|
21
|
+
actions.push("disable");
|
|
22
|
+
}
|
|
23
|
+
if (state.credentials && canClearCredential(state.credentials)) {
|
|
24
|
+
actions.push("clear-stored-key");
|
|
25
|
+
}
|
|
26
|
+
actions.push("done");
|
|
27
|
+
return actions;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const setupLabels = (state: JevSetupViewState): string[] =>
|
|
31
|
+
setupActions(state).map((action) => {
|
|
32
|
+
if (action === "clear-stored-key") {
|
|
33
|
+
return state.filterEnabled
|
|
34
|
+
? "Disable and clear stored key"
|
|
35
|
+
: "Clear stored key";
|
|
36
|
+
}
|
|
37
|
+
if (action === "disable") {
|
|
38
|
+
return "Disable";
|
|
39
|
+
}
|
|
40
|
+
if (action === "done") {
|
|
41
|
+
return "Done";
|
|
42
|
+
}
|
|
43
|
+
if (action === "enter-key") {
|
|
44
|
+
return state.selectedTransport === "openai-decisions"
|
|
45
|
+
? "Enter an OpenAI Platform API key"
|
|
46
|
+
: "Enter a TypeSafe API key";
|
|
47
|
+
}
|
|
48
|
+
return providerName(action);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const renderJevSetup = (
|
|
52
|
+
options: JevSetupSubmenuOptions,
|
|
53
|
+
state: JevSetupViewState,
|
|
54
|
+
maskedInput: MaskedInput,
|
|
55
|
+
width: number
|
|
56
|
+
): string[] => {
|
|
57
|
+
const { theme } = options;
|
|
58
|
+
const lines = [
|
|
59
|
+
theme.fg("accent", theme.bold(" Jev/Decisions consultation filter")),
|
|
60
|
+
"",
|
|
61
|
+
` Filter: ${state.filterEnabled ? "On" : "Off"}`,
|
|
62
|
+
];
|
|
63
|
+
if (state.credentials) {
|
|
64
|
+
lines.push(` Credential: ${transportLabel(state.credentials)}`);
|
|
65
|
+
} else if (state.selectedTransport) {
|
|
66
|
+
lines.push(` Selected provider: ${providerName(state.selectedTransport)}`);
|
|
67
|
+
} else {
|
|
68
|
+
lines.push(` Saved transport: ${options.currentTransport}`);
|
|
69
|
+
}
|
|
70
|
+
if (state.notice) {
|
|
71
|
+
lines.push("", theme.fg("warning", ` ${state.notice}`));
|
|
72
|
+
}
|
|
73
|
+
lines.push("");
|
|
74
|
+
if (state.mode === "resolving") {
|
|
75
|
+
lines.push(" Checking the selected provider credentials…");
|
|
76
|
+
} else if (state.mode === "verifying") {
|
|
77
|
+
lines.push(" Verifying with a live Jev call…");
|
|
78
|
+
} else if (state.mode === "clearing") {
|
|
79
|
+
lines.push(" Clearing the selected provider's stored key…");
|
|
80
|
+
} else if (state.mode === "key-entry") {
|
|
81
|
+
lines.push(
|
|
82
|
+
` ${providerName(state.selectedTransport ?? "typesafe")}`,
|
|
83
|
+
state.selectedTransport === "openai-decisions"
|
|
84
|
+
? " No OpenAI Platform API key found. ChatGPT subscription OAuth is not accepted."
|
|
85
|
+
: " No TypeSafe API key found.",
|
|
86
|
+
` ${maskedInput.render(Math.max(10, width - 4))[0] ?? ""}`,
|
|
87
|
+
theme.fg("dim", " Enter: verify and securely store · Esc: return")
|
|
88
|
+
);
|
|
89
|
+
} else {
|
|
90
|
+
for (const [index, label] of setupLabels(state).entries()) {
|
|
91
|
+
const prefix = index === state.selectedIndex ? "→ " : " ";
|
|
92
|
+
lines.push(`${prefix}${label}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return lines.map((line) => truncateToWidth(line, width));
|
|
96
|
+
};
|