pi-advisor-flow 0.11.2 → 0.11.3-dev.1

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.
@@ -0,0 +1,173 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { noul } from "@typesafe-ai/sdk";
3
+
4
+ import type { JevTransport } from "../config/types.ts";
5
+ import { jevClientFromCredentials } from "../jev/client.ts";
6
+ import { OPENAI_DECISIONS_MODEL } from "../jev/decisions-client.ts";
7
+ import {
8
+ clearKeyTypeSafeKey,
9
+ removeTypeSafeKeyFromAdvisorJson,
10
+ writeKeyTypeSafeKey,
11
+ } from "../jev/key-store.ts";
12
+ import {
13
+ clearOpenAIDecisionsKey,
14
+ writeOpenAIDecisionsKey,
15
+ } from "../jev/openai-key-store.ts";
16
+ import {
17
+ resolveJevTransport,
18
+ resolveJevTransportFor,
19
+ } from "../jev/transport.ts";
20
+ import type { JevCredentials, JevTransportKind } from "../jev/transport.ts";
21
+ import { redactSecrets } from "../redaction.ts";
22
+ import type {
23
+ JevSetupDeps,
24
+ JevSetupSelection,
25
+ RenderRequester,
26
+ } from "./types.ts";
27
+
28
+ export interface JevSetupSubmenuOptions {
29
+ afterSelection?: () => void;
30
+ currentTransport: JevTransport;
31
+ currentValue: string;
32
+ done: (selectedValue?: string) => void;
33
+ onSelection?: (selection: JevSetupSelection) => boolean;
34
+ setupDeps?: JevSetupDeps;
35
+ theme: Theme;
36
+ tui: RenderRequester;
37
+ }
38
+
39
+ export type JevSetupOverrides = JevSetupDeps;
40
+ export type SetupMode =
41
+ | "menu"
42
+ | "key-entry"
43
+ | "resolving"
44
+ | "verifying"
45
+ | "clearing";
46
+ export type SetupAction =
47
+ | JevTransportKind
48
+ | "clear-stored-key"
49
+ | "disable"
50
+ | "done"
51
+ | "enter-key";
52
+
53
+ export interface JevSetupViewState {
54
+ canEnterKey: boolean;
55
+ credentials: JevCredentials | undefined;
56
+ filterEnabled: boolean;
57
+ mode: SetupMode;
58
+ notice: string | undefined;
59
+ selectedIndex: number;
60
+ selectedTransport: JevTransportKind | undefined;
61
+ }
62
+
63
+ export const fireAndForget = async (
64
+ action: Promise<unknown>,
65
+ onError: (message: string) => void
66
+ ): Promise<void> => {
67
+ try {
68
+ await action;
69
+ } catch (error) {
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ onError(redactSecrets(message));
72
+ }
73
+ };
74
+
75
+ export const providerName = (transport: JevTransportKind): string => {
76
+ if (transport === "typesafe") {
77
+ return "TypeSafe Jev";
78
+ }
79
+ if (transport === "openrouter") {
80
+ return "OpenRouter (Pi login)";
81
+ }
82
+ return `OpenAI Decisions (${OPENAI_DECISIONS_MODEL})`;
83
+ };
84
+
85
+ export const transportLabel = (credentials: JevCredentials): string => {
86
+ if (credentials.transport === "openrouter") {
87
+ return "OpenRouter (reusing Pi login)";
88
+ }
89
+ if (credentials.transport === "openai-decisions") {
90
+ let source = "verified API key";
91
+ switch (credentials.source) {
92
+ case "openai-bun-secrets": {
93
+ source = "Bun.secrets";
94
+ break;
95
+ }
96
+ case "openai-env": {
97
+ source = "OPENAI_API_KEY";
98
+ break;
99
+ }
100
+ case "openai-file": {
101
+ source = "stored file, mode 0600";
102
+ break;
103
+ }
104
+ case "openai-provider-credential": {
105
+ source = "Pi stored API key";
106
+ break;
107
+ }
108
+ default: {
109
+ break;
110
+ }
111
+ }
112
+ return `OpenAI Decisions (${OPENAI_DECISIONS_MODEL}; key: ${source})`;
113
+ }
114
+ switch (credentials.source) {
115
+ case "advisor-json": {
116
+ return "TypeSafe (key: advisor.json — plaintext, not recommended)";
117
+ }
118
+ case "bun-secrets": {
119
+ return "TypeSafe (key: Bun.secrets)";
120
+ }
121
+ case "file": {
122
+ return "TypeSafe (key: stored file, mode 0600)";
123
+ }
124
+ default: {
125
+ return "TypeSafe (key: TYPESAFE_API_KEY)";
126
+ }
127
+ }
128
+ };
129
+
130
+ export const canClearCredential = (credentials: JevCredentials) =>
131
+ credentials.source === "bun-secrets" ||
132
+ credentials.source === "file" ||
133
+ credentials.source === "openai-bun-secrets" ||
134
+ credentials.source === "openai-file";
135
+
136
+ export const canReplaceWithEnteredKey = (credentials: JevCredentials) =>
137
+ credentials.transport === "typesafe" ||
138
+ (credentials.transport === "openai-decisions" &&
139
+ (credentials.source === "openai-bun-secrets" ||
140
+ credentials.source === "openai-file"));
141
+
142
+ export const defaultVerify = async (credentials: JevCredentials) => {
143
+ const client = jevClientFromCredentials(credentials);
144
+ try {
145
+ await client.ask(
146
+ { purpose: "pi-advisor setup verification" },
147
+ { verified: noul("Answer yes.") }
148
+ );
149
+ return { ok: true };
150
+ } catch (error) {
151
+ return {
152
+ message: error instanceof Error ? error.message : String(error),
153
+ ok: false,
154
+ };
155
+ }
156
+ };
157
+
158
+ export const defaultSetupDeps: Required<JevSetupDeps> = {
159
+ clearStoredKey: (transport) =>
160
+ transport === "openai-decisions"
161
+ ? clearOpenAIDecisionsKey()
162
+ : clearKeyTypeSafeKey(),
163
+ removePlaintextKey: removeTypeSafeKeyFromAdvisorJson,
164
+ resolveTransport: (transport) =>
165
+ transport ? resolveJevTransportFor(transport) : resolveJevTransport(),
166
+ verify: defaultVerify,
167
+ writeKey: (key, transport) =>
168
+ transport === "openai-decisions"
169
+ ? writeOpenAIDecisionsKey(key)
170
+ : writeKeyTypeSafeKey(key),
171
+ };
172
+
173
+ export { consumePlaintextKeyWarning as consumeSetupPlaintextWarning } from "../jev/key-store.ts";
@@ -33,6 +33,8 @@ import { FALLBACK_ADVISOR_MODEL_DISABLED } from "./types.ts";
33
33
  import type {
34
34
  AdvisorSettings,
35
35
  ContextPreset,
36
+ JevSetupDeps,
37
+ JevSetupSelection,
36
38
  RenderRequester,
37
39
  } from "./types.ts";
38
40
 
@@ -42,6 +44,9 @@ export interface SettingsItemsOptions {
42
44
  modelWhitelist: SettingItem;
43
45
  presets: ContextPreset[];
44
46
  settings: AdvisorSettings;
47
+ afterJevSetup?: () => void;
48
+ jevSetupDeps?: JevSetupDeps;
49
+ onJevSetup?: (selection: JevSetupSelection) => boolean;
45
50
  theme: Theme;
46
51
  tui: RenderRequester;
47
52
  }
@@ -78,16 +83,28 @@ const scoutTimeoutItem = (settings: AdvisorSettings): SettingItem => {
78
83
  const jevItems = (
79
84
  settings: AdvisorSettings,
80
85
  theme: Theme,
81
- tui: RenderRequester
86
+ tui: RenderRequester,
87
+ jevSetupDeps?: JevSetupDeps,
88
+ onJevSetup?: (selection: JevSetupSelection) => boolean,
89
+ afterJevSetup?: () => void
82
90
  ): SettingItem[] => [
83
91
  {
84
92
  currentValue: settingValue(settings.jevFilterEnabled, false),
85
93
  description:
86
- "Screen low-stakes ask_advisor consultations with Jev; guided setup verifies credentials.",
94
+ "Screen low-stakes ask_advisor consultations with Jev/Decisions; guided setup verifies credentials.",
87
95
  id: "jevFilter",
88
- label: "Jev consultation filter",
96
+ label: "Jev/Decisions consultation filter",
89
97
  submenu: (currentValue, done) =>
90
- new JevSetupSubmenu({ currentValue, done, theme, tui }),
98
+ new JevSetupSubmenu({
99
+ afterSelection: afterJevSetup,
100
+ currentTransport: settings.jevTransport ?? DEFAULT_JEV_TRANSPORT,
101
+ currentValue,
102
+ done,
103
+ onSelection: onJevSetup,
104
+ setupDeps: jevSetupDeps,
105
+ theme,
106
+ tui,
107
+ }),
91
108
  },
92
109
  {
93
110
  currentValue: String(
@@ -170,7 +187,7 @@ const jevItems = (
170
187
  settings.jevPricePerMtok ?? DEFAULT_JEV_PRICE_PER_MTOK
171
188
  ),
172
189
  description:
173
- "Assumed TypeSafe price per million input tokens for cost lines.",
190
+ "TypeSafe/OpenRouter estimated input price per million tokens; ignored for OpenAI Decisions, whose cost is unavailable.",
174
191
  id: "jevPricePerMtok",
175
192
  label: "Jev price/Mtok",
176
193
  values: numericValues(
@@ -187,7 +204,7 @@ const jevItems = (
187
204
  description:
188
205
  "Proactively consult the Advisor every N turns without a consultation (0 = off).",
189
206
  id: "jevTurnGateEveryTurns",
190
- label: "Jev turn gate",
207
+ label: "Jev/Decisions turn gate",
191
208
  values: [
192
209
  "Off",
193
210
  "every 3 turns",
@@ -203,24 +220,12 @@ const jevItems = (
203
220
  description:
204
221
  "Jev confidence required before the turn gate interrupts with advice.",
205
222
  id: "jevTurnGateNoulThreshold",
206
- label: "Jev turn-gate threshold",
223
+ label: "Jev/Decisions turn-gate threshold",
207
224
  values: numericValues(
208
225
  settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD,
209
226
  [0.6, 0.7, 0.8, 0.85, 0.9, 0.95]
210
227
  ),
211
228
  },
212
- {
213
- currentValue: settings.jevTransport ?? DEFAULT_JEV_TRANSPORT,
214
- description:
215
- "How Jev calls travel: auto reuses an OpenRouter login when no TypeSafe key is set.",
216
- id: "jevTransport",
217
- label: "Jev transport",
218
- values: withCurrentValue(settings.jevTransport ?? DEFAULT_JEV_TRANSPORT, [
219
- "auto",
220
- "typesafe",
221
- "openrouter",
222
- ]),
223
- },
224
229
  ];
225
230
 
226
231
  export const advisorFallbackModelItem = (
@@ -290,6 +295,9 @@ export const createSettingsItems = ({
290
295
  modelWhitelist,
291
296
  presets,
292
297
  settings,
298
+ afterJevSetup,
299
+ jevSetupDeps,
300
+ onJevSetup,
293
301
  theme,
294
302
  tui,
295
303
  }: SettingsItemsOptions): SettingItem[] => {
@@ -591,7 +599,7 @@ export const createSettingsItems = ({
591
599
  settings.outcomeLogging,
592
600
  false
593
601
  ),
594
- ...jevItems(settings, theme, tui)
602
+ ...jevItems(settings, theme, tui, jevSetupDeps, onJevSetup, afterJevSetup)
595
603
  );
596
604
  return items;
597
605
  };
@@ -183,11 +183,6 @@ const applyJevMutation = (
183
183
  settings.jevPricePerMtok = Number(value);
184
184
  return true;
185
185
  }
186
- case "jevTransport": {
187
- // SAFETY: value comes from the fixed jevTransport options of the settings list.
188
- settings.jevTransport = value as AdvisorSettings["jevTransport"];
189
- return true;
190
- }
191
186
  default: {
192
187
  return false;
193
188
  }
@@ -17,6 +17,7 @@ import type {
17
17
  AdvisorSettings,
18
18
  AdvisorSettingsSelectorOptions,
19
19
  ContextPreset,
20
+ JevSetupSelection,
20
21
  } from "./types.ts";
21
22
 
22
23
  export class AdvisorSettingsSelector implements Component, Focusable {
@@ -112,9 +113,15 @@ export class AdvisorSettingsSelector implements Component, Focusable {
112
113
  this.options.tui
113
114
  );
114
115
  const items = createSettingsItems({
116
+ afterJevSetup: () => {
117
+ this.settingsList = this.createSettingsList("jevFilter");
118
+ this.options.tui.requestRender();
119
+ },
115
120
  effortLevels: this.options.effortLevels,
116
121
  fallbackModel,
122
+ jevSetupDeps: this.options.jevSetupDeps,
117
123
  modelWhitelist,
124
+ onJevSetup: (selection) => this.applyJevSetup(selection),
118
125
  presets: this.presets,
119
126
  settings: this.settings,
120
127
  theme: this.options.theme,
@@ -135,6 +142,28 @@ export class AdvisorSettingsSelector implements Component, Focusable {
135
142
  return adapter;
136
143
  }
137
144
 
145
+ private applyJevSetup(selection: JevSetupSelection): boolean {
146
+ const updated: AdvisorSettings = {
147
+ ...this.settings,
148
+ jevFilterEnabled: selection.enabled,
149
+ jevTransport: selection.transport,
150
+ showUsageDetails: this.settings.showUsageDetails ?? true,
151
+ toolPolicies: { ...this.settings.toolPolicies },
152
+ };
153
+ try {
154
+ const result = this.options.onJevSetup
155
+ ? this.options.onJevSetup(selection, updated)
156
+ : (this.options.onChange ?? this.options.onSave)?.(updated);
157
+ if (result === false) {
158
+ return false;
159
+ }
160
+ } catch {
161
+ return false;
162
+ }
163
+ Object.assign(this.settings, updated);
164
+ return true;
165
+ }
166
+
138
167
  private startSimpleModeGradient(): void {
139
168
  this.stopSimpleModeGradient();
140
169
  this.simpleModeGradientStartedAt = Date.now();
package/src/ui/types.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import type { KeybindingsManager, TUI } from "@earendil-works/pi-tui";
3
3
 
4
- import type { GateFailureMode } from "../config/types.ts";
4
+ import type { GateFailureMode, JevTransport } from "../config/types.ts";
5
5
  import type { GitContextLevel } from "../git.ts";
6
+ import type { JevKeyStoreResult } from "../jev/key-store.ts";
7
+ import type { JevCredentials, JevTransportKind } from "../jev/transport.ts";
6
8
 
7
9
  export interface RenderRequester {
8
10
  requestRender: () => void;
@@ -93,7 +95,7 @@ export interface AdvisorSettings {
93
95
  jevModel?: string;
94
96
  jevPricePerMtok?: number;
95
97
  jevTimeoutMs?: number;
96
- jevTransport?: "auto" | "typesafe" | "openrouter";
98
+ jevTransport?: "auto" | "typesafe" | "openrouter" | "openai-decisions";
97
99
  jevTurnGateEveryTurns?: number;
98
100
  jevTurnGateNoulThreshold?: number;
99
101
  loopThreshold?: number;
@@ -115,6 +117,29 @@ export interface AdvisorSettings {
115
117
  untrackedContent?: boolean;
116
118
  }
117
119
 
120
+ export interface JevSetupSelection {
121
+ enabled: boolean;
122
+ transport: JevTransport;
123
+ }
124
+
125
+ export interface JevSetupResult {
126
+ message?: string;
127
+ ok: boolean;
128
+ }
129
+
130
+ export interface JevSetupDeps {
131
+ clearStoredKey?: (transport: JevTransportKind) => Promise<JevKeyStoreResult>;
132
+ removePlaintextKey?: () => JevKeyStoreResult;
133
+ resolveTransport?: (
134
+ transport?: JevTransportKind
135
+ ) => Promise<JevCredentials | undefined>;
136
+ verify?: (credentials: JevCredentials) => Promise<JevSetupResult>;
137
+ writeKey?: (
138
+ key: string,
139
+ transport: JevTransportKind
140
+ ) => Promise<JevKeyStoreResult>;
141
+ }
142
+
118
143
  export interface AdvisorSettingsSelectorOptions {
119
144
  effortLevels: string[];
120
145
  initial: AdvisorSettings;
@@ -125,6 +150,11 @@ export interface AdvisorSettingsSelectorOptions {
125
150
  /** @deprecated Use onChange; retained for extensions embedding this component. */
126
151
  onSave?: (settings: AdvisorSettings) => void;
127
152
  presets: ContextPreset[];
153
+ jevSetupDeps?: JevSetupDeps;
154
+ onJevSetup?: (
155
+ selection: JevSetupSelection,
156
+ settings: AdvisorSettings
157
+ ) => boolean;
128
158
  theme: Theme;
129
159
  tui: RenderRequester;
130
160
  }