pi-advisor-flow 0.5.6 → 0.7.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -0
  3. package/dist/index.js +3160 -1255
  4. package/extensions/index.ts +9 -9
  5. package/package.json +12 -8
  6. package/src/{untracked.ts → attachments.ts} +2 -5
  7. package/src/commands/activation-preparation.ts +6 -6
  8. package/src/commands/activation.ts +7 -7
  9. package/src/commands/lifecycle.ts +5 -5
  10. package/src/commands/manual-command.ts +13 -9
  11. package/src/commands/manual-consultation.ts +24 -10
  12. package/src/commands/manual-progress.ts +3 -3
  13. package/src/commands/model-commands.ts +6 -6
  14. package/src/commands/model-options.ts +4 -4
  15. package/src/commands/model-picker.ts +2 -2
  16. package/src/commands/registration.ts +8 -8
  17. package/src/commands/renderers.ts +5 -5
  18. package/src/commands/runtime.ts +8 -8
  19. package/src/commands/settings-commands.ts +8 -8
  20. package/src/commands/settings-persistence.ts +52 -4
  21. package/src/commands/types.ts +6 -13
  22. package/src/commands.ts +2 -2
  23. package/src/config/args.ts +3 -3
  24. package/src/config/defaults.ts +78 -3
  25. package/src/config/schema.ts +434 -0
  26. package/src/config/state.ts +71 -2
  27. package/src/config/storage.ts +27 -119
  28. package/src/config/types.ts +30 -1
  29. package/src/config/validation.ts +61 -181
  30. package/src/config.ts +7 -7
  31. package/src/content-utils.ts +30 -0
  32. package/src/conversation.ts +13 -168
  33. package/src/git.ts +4 -18
  34. package/src/herdr.ts +6 -8
  35. package/src/jev/client.ts +348 -0
  36. package/src/jev/key-store.ts +252 -0
  37. package/src/jev/ledger.ts +234 -0
  38. package/src/jev/questions.ts +119 -0
  39. package/src/jev/state.ts +47 -0
  40. package/src/jev/transport.ts +58 -0
  41. package/src/model-stream.ts +5 -8
  42. package/src/outcomes.ts +3 -2
  43. package/src/pi-settings.ts +59 -0
  44. package/src/preferences.ts +2 -2
  45. package/src/redaction.ts +50 -0
  46. package/src/scout-context.ts +121 -485
  47. package/src/scout-curation.ts +84 -0
  48. package/src/scout-groups.ts +485 -0
  49. package/src/scout-protocol.ts +122 -0
  50. package/src/scout-reconstruct.ts +44 -0
  51. package/src/scout-types.ts +66 -0
  52. package/src/scout.ts +53 -63
  53. package/src/session-state.ts +215 -78
  54. package/src/tool-result-cap.ts +88 -0
  55. package/src/tools/consult-context.ts +155 -0
  56. package/src/tools/consultation.ts +71 -220
  57. package/src/tools/gate-policy.ts +5 -5
  58. package/src/tools/gate-protocol.ts +2 -2
  59. package/src/tools/jev-filter.ts +202 -0
  60. package/src/tools/jev-turn-gate.ts +183 -0
  61. package/src/tools/loop-gate.ts +71 -61
  62. package/src/tools/outage-notifier.ts +30 -0
  63. package/src/tools/prompts.ts +4 -7
  64. package/src/tools/register-ask-advisor.ts +46 -15
  65. package/src/tools/register-lifecycle.ts +7 -7
  66. package/src/tools/register-outcome.ts +5 -5
  67. package/src/tools/register-renderers.ts +63 -5
  68. package/src/tools/registration.ts +27 -10
  69. package/src/tools/render-advisor-result.ts +54 -22
  70. package/src/tools/render-common.ts +25 -13
  71. package/src/tools/scout-status.ts +5 -5
  72. package/src/tools/session.ts +1 -1
  73. package/src/tools/types.ts +17 -7
  74. package/src/tools.ts +9 -9
  75. package/src/ui/jev-setup-submenu.ts +344 -0
  76. package/src/ui/manual-dialog-render.ts +2 -2
  77. package/src/ui/manual-dialog.ts +4 -9
  78. package/src/ui/masked-input.ts +68 -0
  79. package/src/ui/model-selector.ts +1 -1
  80. package/src/ui/settings-formatting.ts +2 -2
  81. package/src/ui/settings-items.ts +166 -5
  82. package/src/ui/settings-mutations.ts +37 -2
  83. package/src/ui/settings-selector.ts +5 -5
  84. package/src/ui/text-setting-submenu.ts +1 -1
  85. package/src/ui/types.ts +13 -2
  86. package/src/ui.ts +4 -4
  87. package/src/usage.ts +4 -7
@@ -0,0 +1,344 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ type Component,
4
+ type Focusable,
5
+ Key,
6
+ matchesKey,
7
+ truncateToWidth,
8
+ } from "@earendil-works/pi-tui";
9
+ import { noul } from "@typesafe-ai/sdk";
10
+ import { jevClientFromCredentials } from "../jev/client.ts";
11
+ import {
12
+ clearKeyTypeSafeKey,
13
+ consumePlaintextKeyWarning,
14
+ type JevKeyStoreResult,
15
+ removeTypeSafeKeyFromAdvisorJson,
16
+ writeKeyTypeSafeKey,
17
+ } from "../jev/key-store.ts";
18
+ import { type JevCredentials, resolveJevTransport } from "../jev/transport.ts";
19
+ import { MaskedInput } from "./masked-input.ts";
20
+ import type { RenderRequester } from "./types.ts";
21
+
22
+ export interface JevSetupSubmenuOptions {
23
+ currentValue: string;
24
+ done: (selectedValue?: string) => void;
25
+ theme: Theme;
26
+ tui: RenderRequester;
27
+ }
28
+
29
+ export interface JevSetupDeps {
30
+ clearStoredKey?: () => Promise<JevKeyStoreResult>;
31
+ removePlaintextKey?: () => JevKeyStoreResult;
32
+ resolveTransport?: () => Promise<JevCredentials | undefined>;
33
+ verify?: (credentials: JevCredentials) => Promise<JevFailureLike>;
34
+ writeKey?: (key: string) => Promise<JevKeyStoreResult>;
35
+ }
36
+
37
+ interface JevFailureLike {
38
+ message?: string;
39
+ ok: boolean;
40
+ }
41
+
42
+ type SetupMode = "menu" | "key-entry" | "verifying";
43
+ type SetupAction =
44
+ | "verify-enable"
45
+ | "verify-again"
46
+ | "disable"
47
+ | "disable-clear"
48
+ | "enter-key"
49
+ | "done";
50
+
51
+ const transportLabel = (credentials: JevCredentials): string => {
52
+ if (credentials.transport === "openrouter") {
53
+ return "OpenRouter (reusing pi login)";
54
+ }
55
+ switch (credentials.source) {
56
+ case "advisor-json":
57
+ return "TypeSafe (key: advisor.json — plaintext, not recommended)";
58
+ case "bun-secrets":
59
+ return "TypeSafe (key: Bun.secrets)";
60
+ case "file":
61
+ return "TypeSafe (key: stored file, mode 0600)";
62
+ default:
63
+ return "TypeSafe (key: TYPESAFE_API_KEY)";
64
+ }
65
+ };
66
+
67
+ const defaultVerify = async (credentials: JevCredentials) => {
68
+ const client = jevClientFromCredentials(credentials);
69
+ try {
70
+ await client.ask(
71
+ { purpose: "pi-advisor setup verification" },
72
+ { verified: noul("Answer yes.") }
73
+ );
74
+ return { ok: true };
75
+ } catch (error) {
76
+ return {
77
+ message: error instanceof Error ? error.message : String(error),
78
+ ok: false,
79
+ };
80
+ }
81
+ };
82
+
83
+ /** Guided Jev credentials setup: detects a reusable login, verifies live
84
+ * before enabling, and never echoes an entered key back. */
85
+ export class JevSetupSubmenu implements Component, Focusable {
86
+ private readonly options: JevSetupSubmenuOptions;
87
+ private readonly deps: JevSetupDeps;
88
+ private readonly maskedInput: MaskedInput;
89
+ private credentials: JevCredentials | undefined;
90
+ private mode: SetupMode = "menu";
91
+ private notice: string | undefined;
92
+ private selectedIndex = 0;
93
+ private _focused = true;
94
+
95
+ constructor(options: JevSetupSubmenuOptions, deps: JevSetupDeps = {}) {
96
+ this.options = options;
97
+ this.deps = {
98
+ clearStoredKey: deps.clearStoredKey ?? clearKeyTypeSafeKey,
99
+ removePlaintextKey:
100
+ deps.removePlaintextKey ?? removeTypeSafeKeyFromAdvisorJson,
101
+ resolveTransport: deps.resolveTransport ?? (() => resolveJevTransport()),
102
+ verify: deps.verify ?? defaultVerify,
103
+ writeKey: deps.writeKey ?? writeKeyTypeSafeKey,
104
+ };
105
+ this.maskedInput = new MaskedInput({
106
+ onEscape: () => this.options.done(),
107
+ onSubmit: (value) => this.submitEnteredKey(value),
108
+ placeholder: "Paste a TypeSafe API key",
109
+ });
110
+ this.refresh().catch(() => undefined);
111
+ }
112
+
113
+ get focused(): boolean {
114
+ return this._focused;
115
+ }
116
+
117
+ set focused(value: boolean) {
118
+ this._focused = value;
119
+ this.maskedInput.focused = value;
120
+ }
121
+
122
+ invalidate(): void {
123
+ this.maskedInput.invalidate();
124
+ }
125
+
126
+ handleInput(keyData: string): void {
127
+ if (this.mode === "key-entry") {
128
+ this.maskedInput.handleInput(keyData);
129
+ this.options.tui.requestRender();
130
+ return;
131
+ }
132
+ if (this.mode !== "menu") {
133
+ return;
134
+ }
135
+ const actions = this.actions();
136
+ if (
137
+ matchesKey(keyData, Key.down) ||
138
+ keyData === "\u001b[B" ||
139
+ keyData === "\u001bOB"
140
+ ) {
141
+ this.selectedIndex = (this.selectedIndex + 1) % actions.length;
142
+ } else if (
143
+ matchesKey(keyData, Key.up) ||
144
+ keyData === "\u001b[A" ||
145
+ keyData === "\u001bOA"
146
+ ) {
147
+ this.selectedIndex =
148
+ (this.selectedIndex - 1 + actions.length) % actions.length;
149
+ } else if (matchesKey(keyData, Key.enter) || keyData === "\r") {
150
+ this.activate(actions[this.selectedIndex]);
151
+ } else {
152
+ return;
153
+ }
154
+ this.options.tui.requestRender();
155
+ }
156
+
157
+ render(width: number): string[] {
158
+ const { theme } = this.options;
159
+ const enabled = this.options.currentValue === "On";
160
+ const lines = [
161
+ theme.fg("accent", theme.bold(" Jev consultation filter")),
162
+ "",
163
+ ];
164
+ if (this.credentials) {
165
+ lines.push(` Transport: ${transportLabel(this.credentials)}`);
166
+ lines.push(` Filter: ${enabled ? "On" : "Off"}`);
167
+ } else if (this.mode === "verifying") {
168
+ lines.push(" Checking available Jev credentials…");
169
+ } else {
170
+ lines.push(
171
+ " No Jev credentials found. Enter a TypeSafe API key below, or add"
172
+ );
173
+ lines.push(" an OpenRouter login in pi; it is reused automatically.");
174
+ }
175
+ if (this.notice) {
176
+ lines.push("");
177
+ lines.push(theme.fg("warning", ` ${this.notice}`));
178
+ }
179
+ lines.push("");
180
+ if (this.mode === "verifying") {
181
+ lines.push(" Verifying with a live Jev call…");
182
+ } else if (this.mode === "key-entry") {
183
+ lines.push(
184
+ ` ${this.maskedInput.render(Math.max(10, width - 4))[0] ?? ""}`
185
+ );
186
+ lines.push(theme.fg("dim", " Enter: verify · Esc: cancel"));
187
+ } else {
188
+ for (const [index, label] of this.labels().entries()) {
189
+ const prefix = index === this.selectedIndex ? "→ " : " ";
190
+ lines.push(`${prefix}${label}`);
191
+ }
192
+ }
193
+ return lines.map((line) => truncateToWidth(line, width));
194
+ }
195
+
196
+ private actions(): SetupAction[] {
197
+ if (!this.credentials) {
198
+ return ["enter-key", "done"];
199
+ }
200
+ const enabled = this.options.currentValue === "On";
201
+ const actions: SetupAction[] = [enabled ? "verify-again" : "verify-enable"];
202
+ actions.push("disable");
203
+ if (
204
+ this.credentials.source === "bun-secrets" ||
205
+ this.credentials.source === "file"
206
+ ) {
207
+ actions.push("disable-clear");
208
+ }
209
+ actions.push("done");
210
+ return actions;
211
+ }
212
+
213
+ private actionLabels(): Record<SetupAction, string> {
214
+ return {
215
+ disable: "Disable",
216
+ "disable-clear": "Disable and clear stored key",
217
+ done: "Done",
218
+ "enter-key": "Enter a TypeSafe API key",
219
+ "verify-again": "Verify again",
220
+ "verify-enable": "Verify and enable",
221
+ };
222
+ }
223
+
224
+ private labels(): string[] {
225
+ const labels = this.actionLabels();
226
+ return this.actions().map((action) => labels[action]);
227
+ }
228
+
229
+ private async refresh(): Promise<void> {
230
+ const wasVerifying = this.mode === "verifying";
231
+ this.mode = "verifying";
232
+ if (!wasVerifying) {
233
+ this.options.tui.requestRender();
234
+ }
235
+ this.credentials = await this.deps.resolveTransport?.();
236
+ this.mode = "menu";
237
+ this.selectedIndex = 0;
238
+ if (this.credentials?.source === "advisor-json") {
239
+ this.notice ??= consumePlaintextKeyWarning();
240
+ }
241
+ this.options.tui.requestRender();
242
+ }
243
+
244
+ private activate(action: SetupAction): void {
245
+ switch (action) {
246
+ case "done":
247
+ this.options.done();
248
+ return;
249
+ case "enter-key":
250
+ this.mode = "key-entry";
251
+ return;
252
+ case "disable":
253
+ this.notice = undefined;
254
+ this.options.done("Off");
255
+ return;
256
+ case "disable-clear":
257
+ this.disableAndClear().catch(() => undefined);
258
+ return;
259
+ case "verify-again":
260
+ case "verify-enable":
261
+ this.verifyAndEnable().catch(() => undefined);
262
+ return;
263
+ default:
264
+ return;
265
+ }
266
+ }
267
+
268
+ private async disableAndClear(): Promise<void> {
269
+ const clear = this.deps.clearStoredKey;
270
+ if (!clear) {
271
+ return;
272
+ }
273
+ const result = await clear();
274
+ this.credentials = undefined;
275
+ this.notice = result.message;
276
+ this.options.done("Off");
277
+ }
278
+
279
+ private async verifyAndEnable(): Promise<void> {
280
+ const { credentials } = this;
281
+ if (!(credentials && this.deps.verify)) {
282
+ return;
283
+ }
284
+ this.mode = "verifying";
285
+ this.notice = undefined;
286
+ this.options.tui.requestRender();
287
+ const outcome = await this.deps.verify(credentials);
288
+ this.mode = "menu";
289
+ if (!outcome.ok) {
290
+ this.notice = `Verification failed: ${outcome.message ?? "unknown error"}`;
291
+ this.options.tui.requestRender();
292
+ return;
293
+ }
294
+ if (
295
+ credentials.transport === "typesafe" &&
296
+ credentials.source === "advisor-json"
297
+ ) {
298
+ const stored = await this.deps.writeKey?.(credentials.apiKey);
299
+ if (!stored?.ok) {
300
+ this.notice =
301
+ stored?.message ??
302
+ "Storing the key failed; the plaintext advisor.json key keeps working.";
303
+ this.options.tui.requestRender();
304
+ return;
305
+ }
306
+ const removed = this.deps.removePlaintextKey?.();
307
+ this.notice = removed?.ok
308
+ ? "Key moved from advisor.json into the secure store."
309
+ : `Stored securely, but ${removed?.message ?? "removing the plaintext copy failed; remove it yourself."}`;
310
+ }
311
+ this.options.done("On");
312
+ }
313
+
314
+ private async submitEnteredKey(value: string): Promise<void> {
315
+ const key = value.trim();
316
+ if (!key) {
317
+ return;
318
+ }
319
+ if (!this.deps.verify) {
320
+ return;
321
+ }
322
+ this.mode = "verifying";
323
+ this.options.tui.requestRender();
324
+ const outcome = await this.deps.verify({
325
+ apiKey: key,
326
+ transport: "typesafe",
327
+ });
328
+ this.mode = "menu";
329
+ if (!outcome.ok) {
330
+ this.notice = `Verification failed: ${outcome.message ?? "unknown error"}`;
331
+ this.options.tui.requestRender();
332
+ return;
333
+ }
334
+ const stored = await this.deps.writeKey?.(key);
335
+ if (!stored?.ok) {
336
+ this.notice = stored?.message ?? "Storing the key failed.";
337
+ this.options.tui.requestRender();
338
+ return;
339
+ }
340
+ this.notice = `${stored.message} Verification succeeded.`;
341
+ await this.refresh();
342
+ this.options.done("On");
343
+ }
344
+ }
@@ -3,8 +3,8 @@ import {
3
3
  visibleWidth,
4
4
  wrapTextWithAnsi,
5
5
  } from "@earendil-works/pi-tui";
6
- import type { GitContextLevel } from "../git.js";
7
- import type { ManualAdvisorDialogView } from "./types.js";
6
+ import type { GitContextLevel } from "../git.ts";
7
+ import type { ManualAdvisorDialogView } from "./types.ts";
8
8
 
9
9
  const MANUAL_GIT_CONTEXT_COPY: Record<
10
10
  GitContextLevel,
@@ -12,24 +12,19 @@ import {
12
12
  clampGitContextLevel,
13
13
  GIT_CONTEXT_LEVELS,
14
14
  type GitContextLevel,
15
- } from "../git.js";
16
- import { renderManualAdvisorDialog } from "./manual-dialog-render.js";
15
+ } from "../git.ts";
16
+ import { renderManualAdvisorDialog } from "./manual-dialog-render.ts";
17
17
  import type {
18
18
  ManualAdvisorDialogOptions,
19
19
  ManualAdvisorFocus,
20
- } from "./types.js";
20
+ } from "./types.ts";
21
21
 
22
22
  // Keep the keybinding components separate from Socket's URL-string heuristic.
23
23
  const TUI_INPUT_TAB: keyof Keybindings = ["tui", "input", "tab"].join(
24
24
  "."
25
25
  ) as keyof Keybindings;
26
26
 
27
- /**
28
- * The small, deliberately non-persistent form used by `/advisor-manual`.
29
- *
30
- * The dialog owns only local input state. Callers decide when to consume a
31
- * budget or start a consultation after the submitted request is returned.
32
- */
27
+ /** The small, non-persistent /advisor-manual form; callers own budget and consultation decisions. */
33
28
  export class ManualAdvisorDialog implements Component, Focusable {
34
29
  private readonly options: ManualAdvisorDialogOptions;
35
30
  private readonly editor: Editor;
@@ -0,0 +1,68 @@
1
+ import {
2
+ type Component,
3
+ type Focusable,
4
+ Input,
5
+ truncateToWidth,
6
+ } from "@earendil-works/pi-tui";
7
+
8
+ export interface MaskedInputOptions {
9
+ onEscape?: () => void;
10
+ onSubmit?: (value: string) => void;
11
+ placeholder?: string;
12
+ }
13
+
14
+ interface InputPrivateFields {
15
+ cursor: number;
16
+ }
17
+
18
+ /** Single-line secret input rendering bullets; the value never reaches the screen. */
19
+ export class MaskedInput implements Component, Focusable {
20
+ private readonly input: Input;
21
+ private readonly options: MaskedInputOptions;
22
+ private _focused = true;
23
+
24
+ constructor(options: MaskedInputOptions = {}) {
25
+ this.options = options;
26
+ this.input = new Input({ placeholder: options.placeholder });
27
+ this.input.focused = true;
28
+ this.input.onSubmit = (value) => options.onSubmit?.(value);
29
+ this.input.onEscape = () => options.onEscape?.();
30
+ }
31
+
32
+ get focused(): boolean {
33
+ return this._focused;
34
+ }
35
+
36
+ set focused(value: boolean) {
37
+ this._focused = value;
38
+ this.input.focused = value;
39
+ }
40
+
41
+ getValue(): string {
42
+ return this.input.getValue();
43
+ }
44
+
45
+ setValue(value: string): void {
46
+ this.input.setValue(value);
47
+ }
48
+
49
+ handleInput(keyData: string): void {
50
+ this.input.handleInput(keyData);
51
+ }
52
+
53
+ invalidate(): void {
54
+ this.input.invalidate();
55
+ }
56
+
57
+ render(width: number): string[] {
58
+ const value = this.input.getValue();
59
+ // Compatibility-only read for the caret position; Input does not expose it.
60
+ const { cursor } = this.input as unknown as InputPrivateFields;
61
+ const masked = `${"•".repeat(Math.min(cursor, value.length))}█${"•".repeat(Math.max(0, value.length - cursor))}`;
62
+ const placeholder =
63
+ value.length === 0 && this.options.placeholder
64
+ ? this.options.placeholder
65
+ : masked;
66
+ return [truncateToWidth(placeholder, Math.max(1, width))];
67
+ }
68
+ }
@@ -8,7 +8,7 @@ import {
8
8
  type KeybindingsManager,
9
9
  truncateToWidth,
10
10
  } from "@earendil-works/pi-tui";
11
- import type { SearchableModelSelectorOptions } from "./types.js";
11
+ import type { SearchableModelSelectorOptions } from "./types.ts";
12
12
 
13
13
  export class SearchableModelSelector implements Component, Focusable {
14
14
  private readonly tui: SearchableModelSelectorOptions["tui"];
@@ -1,7 +1,7 @@
1
1
  import { visibleWidth } from "@earendil-works/pi-tui";
2
- import type { ContextPreset } from "./types.js";
2
+ import { DEFAULT_EFFORT_LEVEL } from "../commands/model-options.ts";
3
+ import type { ContextPreset } from "./types.ts";
3
4
 
4
- export const DEFAULT_EFFORT_LEVEL = "Default (Model Default)";
5
5
  export const TOGGLE_VALUES = ["On", "Off"];
6
6
  export const SIMPLE_MODE_GRADIENT_INTERVAL_MS = 100;
7
7
  // Purple steps with a moving light highlight, retained from the original UI.
@@ -1,6 +1,18 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import type { SettingItem } from "@earendil-works/pi-tui";
3
- import { isValidAdvisorToolPolicies } from "../config/validation.js";
3
+ import {
4
+ DEFAULT_JEV_DIGEST_MAX_CHARS,
5
+ DEFAULT_JEV_FILTER_NOUL_MARGIN,
6
+ DEFAULT_JEV_FILTER_OVERRIDE_WINDOW,
7
+ DEFAULT_JEV_FILTER_SKIP_CONFIDENCE,
8
+ DEFAULT_JEV_MODEL,
9
+ DEFAULT_JEV_PRICE_PER_MTOK,
10
+ DEFAULT_JEV_TIMEOUT_MS,
11
+ DEFAULT_JEV_TRANSPORT,
12
+ DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD,
13
+ } from "../config/types.ts";
14
+ import { isValidAdvisorToolPolicies } from "../config/validation.ts";
15
+ import { JevSetupSubmenu } from "./jev-setup-submenu.ts";
4
16
  import {
5
17
  contextDescription,
6
18
  currentContextLabel,
@@ -10,13 +22,13 @@ import {
10
22
  settingValue,
11
23
  TOGGLE_VALUES,
12
24
  withCurrentValue,
13
- } from "./settings-formatting.js";
14
- import { TextSettingSubmenu } from "./text-setting-submenu.js";
25
+ } from "./settings-formatting.ts";
26
+ import { TextSettingSubmenu } from "./text-setting-submenu.ts";
15
27
  import type {
16
28
  AdvisorSettings,
17
29
  ContextPreset,
18
30
  RenderRequester,
19
- } from "./types.js";
31
+ } from "./types.ts";
20
32
 
21
33
  export interface SettingsItemsOptions {
22
34
  effortLevels: string[];
@@ -40,6 +52,154 @@ const toggle = (
40
52
  values: TOGGLE_VALUES,
41
53
  });
42
54
 
55
+ const jevItems = (
56
+ settings: AdvisorSettings,
57
+ theme: Theme,
58
+ tui: RenderRequester
59
+ ): SettingItem[] => [
60
+ {
61
+ currentValue: settingValue(settings.jevFilterEnabled, false),
62
+ description:
63
+ "Screen low-stakes ask_advisor consultations with Jev; guided setup verifies credentials.",
64
+ id: "jevFilter",
65
+ label: "Jev consultation filter",
66
+ submenu: (currentValue, done) =>
67
+ new JevSetupSubmenu({ currentValue, done, theme, tui }),
68
+ },
69
+ {
70
+ currentValue: String(
71
+ settings.jevFilterSkipConfidence ?? DEFAULT_JEV_FILTER_SKIP_CONFIDENCE
72
+ ),
73
+ description:
74
+ "Required probability on negligible stakes before a consultation is skipped.",
75
+ id: "jevFilterSkipConfidence",
76
+ label: "Jev skip confidence",
77
+ values: numericValues(
78
+ settings.jevFilterSkipConfidence ?? DEFAULT_JEV_FILTER_SKIP_CONFIDENCE,
79
+ [0.6, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95]
80
+ ),
81
+ },
82
+ {
83
+ currentValue: String(
84
+ settings.jevFilterNoulMargin ?? DEFAULT_JEV_FILTER_NOUL_MARGIN
85
+ ),
86
+ description:
87
+ "Extra margin over a coin flip required on self-answerability before skipping.",
88
+ id: "jevFilterNoulMargin",
89
+ label: "Jev Noul margin",
90
+ values: numericValues(
91
+ settings.jevFilterNoulMargin ?? DEFAULT_JEV_FILTER_NOUL_MARGIN,
92
+ [0.1, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45]
93
+ ),
94
+ },
95
+ {
96
+ currentValue: `${settings.jevFilterOverrideWindow ?? DEFAULT_JEV_FILTER_OVERRIDE_WINDOW} turns`,
97
+ description:
98
+ "Turns after a skip during which the same question passes automatically.",
99
+ id: "jevFilterOverrideWindow",
100
+ label: "Jev override window",
101
+ values: numericValues(
102
+ settings.jevFilterOverrideWindow ?? DEFAULT_JEV_FILTER_OVERRIDE_WINDOW,
103
+ [0, 3, 5, 10, 20, 50]
104
+ ).map((value) => `${value} turns`),
105
+ },
106
+ {
107
+ currentValue: settings.jevModel ?? DEFAULT_JEV_MODEL,
108
+ description: "TypeSafe Jev model used for screening and turn-gate checks.",
109
+ id: "jevModel",
110
+ label: "Jev model",
111
+ submenu: (_currentValue, done) =>
112
+ new TextSettingSubmenu({
113
+ description:
114
+ "Enter a TypeSafe model name (for example jev-latest or jev-1.13.0).",
115
+ initial: settings.jevModel ?? DEFAULT_JEV_MODEL,
116
+ onCancel: done,
117
+ onSubmit: (value) => ({ value: value.trim() || DEFAULT_JEV_MODEL }),
118
+ theme,
119
+ title: "Jev model",
120
+ tui,
121
+ }),
122
+ },
123
+ {
124
+ currentValue: String(settings.jevTimeoutMs ?? DEFAULT_JEV_TIMEOUT_MS),
125
+ description: "Total wall-time budget for one Jev call, including retries.",
126
+ id: "jevTimeoutMs",
127
+ label: "Jev timeout ms",
128
+ values: numericValues(
129
+ settings.jevTimeoutMs ?? DEFAULT_JEV_TIMEOUT_MS,
130
+ [1000, 2000, 5000, 8000, 15_000, 30_000]
131
+ ),
132
+ },
133
+ {
134
+ currentValue: String(
135
+ settings.jevDigestMaxChars ?? DEFAULT_JEV_DIGEST_MAX_CHARS
136
+ ),
137
+ description: "Conversation characters sent to Jev as screening evidence.",
138
+ id: "jevDigestMaxChars",
139
+ label: "Jev digest chars",
140
+ values: numericValues(
141
+ settings.jevDigestMaxChars ?? DEFAULT_JEV_DIGEST_MAX_CHARS,
142
+ [0, 1000, 2000, 4000, 8000, 15_000]
143
+ ),
144
+ },
145
+ {
146
+ currentValue: String(
147
+ settings.jevPricePerMtok ?? DEFAULT_JEV_PRICE_PER_MTOK
148
+ ),
149
+ description:
150
+ "Assumed TypeSafe price per million input tokens for cost lines.",
151
+ id: "jevPricePerMtok",
152
+ label: "Jev price/Mtok",
153
+ values: numericValues(
154
+ settings.jevPricePerMtok ?? DEFAULT_JEV_PRICE_PER_MTOK,
155
+ [0.01, 0.02, 0.042, 0.05, 0.1]
156
+ ),
157
+ },
158
+ {
159
+ currentValue:
160
+ settings.jevTurnGateEveryTurns === 0 ||
161
+ settings.jevTurnGateEveryTurns === undefined
162
+ ? "Off"
163
+ : `every ${settings.jevTurnGateEveryTurns} turns`,
164
+ description:
165
+ "Proactively consult the Advisor every N turns without a consultation (0 = off).",
166
+ id: "jevTurnGateEveryTurns",
167
+ label: "Jev turn gate",
168
+ values: [
169
+ "Off",
170
+ "every 3 turns",
171
+ "every 5 turns",
172
+ "every 10 turns",
173
+ "every 20 turns",
174
+ ],
175
+ },
176
+ {
177
+ currentValue: String(
178
+ settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD
179
+ ),
180
+ description:
181
+ "Jev confidence required before the turn gate interrupts with advice.",
182
+ id: "jevTurnGateNoulThreshold",
183
+ label: "Jev turn-gate threshold",
184
+ values: numericValues(
185
+ settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD,
186
+ [0.6, 0.7, 0.8, 0.85, 0.9, 0.95]
187
+ ),
188
+ },
189
+ {
190
+ currentValue: settings.jevTransport ?? DEFAULT_JEV_TRANSPORT,
191
+ description:
192
+ "How Jev calls travel: auto reuses an OpenRouter login when no TypeSafe key is set.",
193
+ id: "jevTransport",
194
+ label: "Jev transport",
195
+ values: withCurrentValue(settings.jevTransport ?? DEFAULT_JEV_TRANSPORT, [
196
+ "auto",
197
+ "typesafe",
198
+ "openrouter",
199
+ ]),
200
+ },
201
+ ];
202
+
43
203
  export const createSettingsItems = ({
44
204
  effortLevels,
45
205
  presets,
@@ -317,7 +477,8 @@ export const createSettingsItems = ({
317
477
  "Allow anonymized Advisor outcomes to be logged globally.",
318
478
  settings.outcomeLogging,
319
479
  false
320
- )
480
+ ),
481
+ ...jevItems(settings, theme, tui)
321
482
  );
322
483
  return items;
323
484
  };