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.
@@ -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: number;
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
- totals.cost += usage.cost;
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)} · $${gate.usage.cost.toFixed(4)}), ${gate.consultations} consultation${gate.consultations === 1 ? "" : "s"} ($${gateSpend.toFixed(4)})`;
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 · $${usage.cost.toFixed(4)} (input only; output free)`
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
+ };
@@ -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
- /** Where the TypeSafe key came from; present on the typesafe transport. */
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
- /** Resolves how Jev calls authenticate: a dedicated TypeSafe key first, then
36
- * the existing pi OpenRouter login (reuse per advisorJevTransport). */
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 !== "openrouter") {
43
- const resolveTypesafe = deps.resolveTypesafe ?? resolveTypeSafeKey;
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 openrouter = await openRouterKey(ctx, deps);
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
  };
@@ -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 Jev credentials resolved (no TypeSafe key and no OpenRouter login)."
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 Jev credentials resolved (no TypeSafe key and no OpenRouter login)."
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
+ };