@mrclrchtr/supi-prompt-suggestions 1.16.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.
Files changed (48) hide show
  1. package/README.md +62 -0
  2. package/node_modules/@mrclrchtr/supi-core/README.md +113 -0
  3. package/node_modules/@mrclrchtr/supi-core/package.json +79 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/api.ts +44 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +188 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +207 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/config.ts +12 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +119 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/context.ts +16 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  14. package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +57 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/index.ts +40 -0
  16. package/node_modules/@mrclrchtr/supi-core/src/llm.ts +211 -0
  17. package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
  18. package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +40 -0
  19. package/node_modules/@mrclrchtr/supi-core/src/path.ts +2 -0
  20. package/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +189 -0
  21. package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  22. package/node_modules/@mrclrchtr/supi-core/src/project.ts +15 -0
  23. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +93 -0
  24. package/node_modules/@mrclrchtr/supi-core/src/report.ts +121 -0
  25. package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  26. package/node_modules/@mrclrchtr/supi-core/src/session.ts +4 -0
  27. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +15 -0
  28. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +48 -0
  29. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +331 -0
  30. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +6 -0
  31. package/node_modules/@mrclrchtr/supi-core/src/settings.ts +9 -0
  32. package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
  33. package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
  34. package/node_modules/@mrclrchtr/supi-core/src/substrate-types.ts +11 -0
  35. package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  36. package/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +182 -0
  37. package/node_modules/@mrclrchtr/supi-core/src/types.ts +2 -0
  38. package/package.json +39 -0
  39. package/src/api.ts +8 -0
  40. package/src/config/config.ts +16 -0
  41. package/src/config/settings.ts +44 -0
  42. package/src/editor/editor.ts +138 -0
  43. package/src/extension.ts +26 -0
  44. package/src/generation/client.ts +122 -0
  45. package/src/generation/generator.ts +225 -0
  46. package/src/generation/model-resolution.ts +72 -0
  47. package/src/generation/normalize.ts +50 -0
  48. package/src/session.ts +168 -0
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ <div align="center">
2
+ <h1>@mrclrchtr/supi-prompt-suggestions</h1>
3
+ </div>
4
+
5
+ # @mrclrchtr/supi-prompt-suggestions
6
+
7
+ Advisory ghost-text prompt suggestions for the [pi coding agent](https://github.com/earendil-works/pi).
8
+
9
+ **Experimental.** Disabled by default — no surprise background model calls.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pi install npm:@mrclrchtr/supi-prompt-suggestions
15
+ ```
16
+
17
+ For local development:
18
+
19
+ ```bash
20
+ pi install ./packages/supi-prompt-suggestions
21
+ ```
22
+
23
+ ## What you get
24
+
25
+ After install and configuration, suggested next user prompts appear as dim **ghost text** in the editor after each assistant response.
26
+
27
+ - **Right Arrow** — accept the suggestion without submitting
28
+ - **Escape** or **any keystroke** — dismiss the suggestion
29
+
30
+ Suggestions are generated by a cheap, fast model you choose. Only the last assistant text is sent — no transcript, files, or project data.
31
+
32
+ ## Configuration
33
+
34
+ Settings live under `/supi-settings` → **Prompt suggestions**:
35
+
36
+ - **Suggestion model** — `disabled` (default) or a model from PI's scoped `enabledModels`
37
+ - Uses SuPi's normal scoped config: set a global default, override per project
38
+
39
+ ## Privacy
40
+
41
+ - Only the last 8,000 characters of the final assistant message are sent
42
+ - The system prompt does not mention PI, SuPi, the project, or the conversation
43
+ - No tool outputs, file contents, or session metadata are included
44
+
45
+ ## Package surfaces
46
+
47
+ - `@mrclrchtr/supi-prompt-suggestions/extension` — pi extension entrypoint
48
+ - `@mrclrchtr/supi-prompt-suggestions/api` — public config type: `PromptSuggestionsConfig`
49
+
50
+ ## Source layout
51
+
52
+ - `src/extension.ts` — pi extension entrypoint: registers settings, wires events to `SessionLifecycle`
53
+ - `src/session.ts` — `SessionLifecycle` class: owns ghost editor, status spinner, and generation orchestration
54
+ - `src/api.ts` — package API surface (`./api` export)
55
+ - `src/index.ts` — package root re-export
56
+ - `src/config/config.ts` — config types and defaults
57
+ - `src/config/settings.ts` — settings registration + model picker submenu
58
+ - `src/editor/editor.ts` — ghost-text editor wrapper component
59
+ - `src/generation/generator.ts` — `SuggestionGenerator` class: async orchestration with concurrency control
60
+ - `src/generation/client.ts` — low-level model API client (pure functions)
61
+ - `src/generation/model-resolution.ts` — model + API key resolution from scoped config
62
+ - `src/generation/normalize.ts` — suggestion text normalization
@@ -0,0 +1,113 @@
1
+ <div align="center">
2
+ <a href="https://github.com/mrclrchtr/supi/tree/main/packages/supi-core">
3
+ <picture>
4
+ <img src="https://raw.githubusercontent.com/mrclrchtr/supi/main/packages/supi-core/assets/logo.png" alt="SuPi" width="50%">
5
+ </picture>
6
+ </a>
7
+ </div>
8
+
9
+ # @mrclrchtr/supi-core
10
+
11
+ Shared infrastructure for SuPi extensions.
12
+
13
+ This is a **pure library** — it does not register any pi commands or tools. The `/supi-settings` command is now available through `@mrclrchtr/supi-settings`.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pnpm add @mrclrchtr/supi-core
19
+ ```
20
+
21
+ ## Package surfaces
22
+
23
+ - `@mrclrchtr/supi-core/api` — reusable helpers for other packages and extensions
24
+ - `@mrclrchtr/supi-core/report` — shared text/report rendering helpers for TUI and plain-text summaries
25
+
26
+ ## What you get from the API
27
+
28
+ ### Config helpers
29
+
30
+ - `loadSupiConfig()` — merged config with resolution order `defaults <- global <- project`
31
+ - `loadSupiConfigForScope()` — load one scope at a time for settings UIs
32
+ - `writeSupiConfig()` — persist values
33
+ - `removeSupiConfigKey()` — remove a key or override
34
+
35
+ Config file locations:
36
+
37
+ - global: `~/.pi/agent/supi/config.json`
38
+ - project: `.pi/supi/config.json`
39
+
40
+ ### Settings helpers
41
+
42
+ - `registerSettings()` — register an arbitrary settings section
43
+ - `registerConfigSettings()` — register a config-backed settings section with scoped persistence helpers
44
+ - `registerSettingsCommand()` — register `/supi-settings`
45
+ - `openSettingsOverlay()` — open the shared settings UI directly
46
+ - `createInputSubmenu()` — helper for simple text-entry submenus
47
+
48
+ The built-in settings UI supports:
49
+
50
+ - project/global scope toggle
51
+ - grouped extension sections
52
+ - searchable setting lists
53
+
54
+ ### Context helpers
55
+
56
+ - `wrapExtensionContext()` — wrap injected text in SuPi's `<extension-context>` tag
57
+ - `findLastUserMessageIndex()`
58
+ - `getContextToken()`
59
+ - `getPromptContent()`
60
+ - `pruneAndReorderContextMessages()`
61
+ - `restorePromptContent()`
62
+
63
+ ### Shared registries
64
+
65
+ - context-provider registry for `/supi-context`
66
+ - debug-event registry for producers that want shared debug capture
67
+ - settings registry used by `/supi-settings`
68
+
69
+ ### Project and session helpers
70
+
71
+ - project-root detection and directory walking helpers such as `findProjectRoot()` and `walkProject()`
72
+ - active-branch session helper: `getActiveBranchEntries()`
73
+ - terminal helpers such as `formatTitle()`, `signalWaiting()`, and `signalDone()`
74
+
75
+ ### Report helpers
76
+
77
+ - `clampReportWidth()` — enforce a minimum readable report width
78
+ - `formatReportTitle()` / `formatSectionHeader()` — shared themed headers
79
+ - `formatDimLine()` / `formatKeyValueLine()` — common summary rows
80
+ - `formatOverflowHint()` — consistent preview-overflow hints
81
+ - `wrapReportText()` — ANSI-aware wrapped report blocks with optional indentation
82
+
83
+ ## Example
84
+
85
+ ```ts
86
+ import { loadSupiConfig, registerConfigSettings, wrapExtensionContext } from "@mrclrchtr/supi-core/api";
87
+
88
+ const config = loadSupiConfig("my-extension", process.cwd(), {
89
+ enabled: true,
90
+ });
91
+
92
+ registerConfigSettings({
93
+ id: "my-extension",
94
+ label: "My Extension",
95
+ section: "my-extension",
96
+ defaults: { enabled: true },
97
+ buildItems: () => [],
98
+ persistChange: () => {},
99
+ });
100
+
101
+ const message = wrapExtensionContext("my-extension", "hello", {
102
+ file: "CLAUDE.md",
103
+ turn: 1,
104
+ });
105
+ ```
106
+
107
+ ## Source
108
+
109
+ - `src/api.ts` — exported library surface
110
+ - `src/config.ts` — shared config loading and writing
111
+ - `src/config-settings.ts` — config-backed settings registration helper
112
+ - `src/settings-ui.ts` — shared settings overlay
113
+ - `src/report.ts` — shared text/report rendering helpers
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@mrclrchtr/supi-core",
3
+ "version": "1.16.1",
4
+ "description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/mrclrchtr/supi.git",
9
+ "directory": "packages/supi-core"
10
+ },
11
+ "homepage": "https://github.com/mrclrchtr/supi/tree/main/packages/supi-core#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/mrclrchtr/supi/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "keywords": [
19
+ "pi",
20
+ "pi-coding-agent",
21
+ "supi",
22
+ "extension-utils",
23
+ "settings",
24
+ "configuration"
25
+ ],
26
+ "type": "module",
27
+ "files": [
28
+ "src/**/*.ts",
29
+ "!__tests__"
30
+ ],
31
+ "peerDependencies": {
32
+ "@earendil-works/pi-ai": "*",
33
+ "@earendil-works/pi-coding-agent": "*",
34
+ "@earendil-works/pi-tui": "*",
35
+ "typebox": "*"
36
+ },
37
+ "peerDependenciesMeta": {
38
+ "@earendil-works/pi-ai": {
39
+ "optional": true
40
+ },
41
+ "@earendil-works/pi-coding-agent": {
42
+ "optional": true
43
+ },
44
+ "@earendil-works/pi-tui": {
45
+ "optional": true
46
+ },
47
+ "typebox": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "25.9.4",
53
+ "vitest": "4.1.9"
54
+ },
55
+ "main": "src/api.ts",
56
+ "exports": {
57
+ "./abort-utils": "./src/abort-utils.ts",
58
+ "./api": "./src/api.ts",
59
+ "./config": "./src/config.ts",
60
+ "./context": "./src/context.ts",
61
+ "./debug": "./src/debug-registry.ts",
62
+ "./footer-registry": "./src/footer-registry.ts",
63
+ "./llm": "./src/llm.ts",
64
+ "./model-selection": "./src/model-selection.ts",
65
+ "./package.json": "./package.json",
66
+ "./path": "./src/path.ts",
67
+ "./report": "./src/report.ts",
68
+ "./progress-widget": "./src/progress-widget.ts",
69
+ "./project": "./src/project.ts",
70
+ "./session": "./src/session.ts",
71
+ "./settings": "./src/settings.ts",
72
+ "./settings-ui": "./src/settings-ui.ts",
73
+ "./spinner-frames": "./src/spinner-frames.ts",
74
+ "./status-spinner": "./src/status-spinner.ts",
75
+ "./terminal": "./src/terminal.ts",
76
+ "./tool-framework": "./src/tool-framework.ts",
77
+ "./types": "./src/types.ts"
78
+ }
79
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Abort controller/signal utility helpers.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ /**
8
+ * Combine a caller-provided abort controller with a generation timeout signal.
9
+ *
10
+ * Returns the combined signal and a cleanup function that removes both
11
+ * event listeners. Callers must invoke cleanup in a `finally` block
12
+ * to avoid listener leaks.
13
+ */
14
+ export function combineAbortSignals(
15
+ abort: AbortController,
16
+ timeoutMs: number,
17
+ ): { signal: AbortSignal; cleanup: () => void } {
18
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
19
+ const combinedController = new AbortController();
20
+ const onAbort = () => combinedController.abort();
21
+ abort.signal.addEventListener("abort", onAbort, { once: true });
22
+ timeoutSignal.addEventListener("abort", onAbort, { once: true });
23
+
24
+ return {
25
+ signal: combinedController.signal,
26
+ cleanup: () => {
27
+ abort.signal.removeEventListener("abort", onAbort);
28
+ timeoutSignal.removeEventListener("abort", onAbort);
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,44 @@
1
+ // supi-core — shared infrastructure for SuPi extensions.
2
+ // Provides XML context tag wrapping, unified config system, context-message utilities,
3
+ // settings registry for supi-wide TUI settings, and a shared tool-spec/registration framework.
4
+ //
5
+ // Convenience barrel — re-exports all domain entry points.
6
+ // For lighter imports, use one of the domain subpaths directly
7
+ // (e.g. @mrclrchtr/supi-core/config, @mrclrchtr/supi-core/context).
8
+
9
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
10
+ export * from "./abort-utils.ts";
11
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
12
+ export * from "./config.ts";
13
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
14
+ export * from "./context.ts";
15
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
16
+ export * from "./debug-registry.ts";
17
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
18
+ export * from "./footer-registry.ts";
19
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
20
+ export * from "./llm.ts";
21
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
22
+ export * from "./model-selection.ts";
23
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
24
+ export * from "./path.ts";
25
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
26
+ export * from "./project.ts";
27
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
28
+ export * from "./report.ts";
29
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
30
+ export * from "./session.ts";
31
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
32
+ export * from "./settings.ts";
33
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
34
+ export * from "./settings-ui.ts";
35
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
36
+ export * from "./spinner-frames.ts";
37
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
38
+ export * from "./status-spinner.ts";
39
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
40
+ export * from "./terminal.ts";
41
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
42
+ export * from "./tool-framework.ts";
43
+ // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
44
+ export * from "./types.ts";
@@ -0,0 +1,188 @@
1
+ // Config-aware settings helper for SuPi config-backed settings sections.
2
+ // Wraps registerSettings() and centralizes selected-scope loading + scoped persistence.
3
+ //
4
+ // Setting items can declare a `configType` ("boolean" | "number" | "stringList")
5
+ // to enable auto-generated persistChange. When all items have a configType,
6
+ // the persistChange callback can be omitted.
7
+
8
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
9
+ import type { SettingItem } from "@earendil-works/pi-tui";
10
+ import type { SettingsScope } from "../settings/settings-registry.ts";
11
+ import { registerSettings } from "../settings/settings-registry.ts";
12
+ import { loadSupiConfigForScope, removeSupiConfigKey, writeSupiConfig } from "./config.ts";
13
+
14
+ // ── Types ──────────────────────────────────────────────────────────────────
15
+
16
+ /**
17
+ * Supported config value types for declarative persistChange.
18
+ *
19
+ * - `"boolean"`: maps "on" → true, "off" → false
20
+ * - `"number"`: parses integer via Number.parseInt, falls back to unset on invalid
21
+ * - `"stringList"`: splits on comma, trims whitespace, unsets on empty
22
+ */
23
+ export type ConfigSettingType = "boolean" | "number" | "stringList";
24
+
25
+ /**
26
+ * Extended setting item that can declare its config type for auto-generated
27
+ * persistence handling.
28
+ */
29
+ export interface ConfigSettingItem extends SettingItem {
30
+ /**
31
+ * When set, persistChange for this item is auto-generated.
32
+ * All items must declare a configType for auto-generation to activate.
33
+ */
34
+ configType?: ConfigSettingType;
35
+ }
36
+
37
+ /**
38
+ * Helpers provided to the persistChange callback for writing or removing
39
+ * scoped config values.
40
+ */
41
+ export interface ConfigSettingsHelpers {
42
+ /** Write a key to the selected scope's config section. */
43
+ set(key: string, value: unknown): void;
44
+ /** Remove a key from the selected scope's config section. */
45
+ unset(key: string): void;
46
+ }
47
+
48
+ export interface ConfigSettingsOptions<T> {
49
+ /** Extension identifier — e.g. "lsp", "claude-md" */
50
+ id: string;
51
+ /** Human-readable label shown in the UI */
52
+ label: string;
53
+ /** SuPi config section name — e.g. "lsp", "claude-md" */
54
+ section: string;
55
+ /** Default config values */
56
+ defaults: T;
57
+ /**
58
+ * Build SettingItem[] from scoped config. Called by loadValues.
59
+ *
60
+ * Items can include a `configType` property for auto-generated
61
+ * persistChange handling. When ALL items declare a configType,
62
+ * the `persistChange` callback can be omitted.
63
+ */
64
+ buildItems: (
65
+ settings: T,
66
+ scope: SettingsScope,
67
+ cwd: string,
68
+ ctx?: ExtensionContext,
69
+ ) => ConfigSettingItem[];
70
+ /**
71
+ * Handle a settings change with scoped persistence helpers.
72
+ *
73
+ * Optional when all items returned by `buildItems` declare a `configType`.
74
+ * Required when any item lacks a `configType`.
75
+ */
76
+ persistChange?: (
77
+ scope: SettingsScope,
78
+ cwd: string,
79
+ settingId: string,
80
+ value: string,
81
+ helpers: ConfigSettingsHelpers,
82
+ ) => void;
83
+ /** Optional home directory for config resolution (testing). */
84
+ homeDir?: string;
85
+ }
86
+
87
+ // ── Auto-generated persistChange ───────────────────────────────────────────
88
+
89
+ function autoPersistChange(
90
+ settingId: string,
91
+ value: string,
92
+ helpers: ConfigSettingsHelpers,
93
+ items: ConfigSettingItem[],
94
+ ): void {
95
+ const item = items.find((i) => i.id === settingId);
96
+ if (!item?.configType) return;
97
+
98
+ switch (item.configType) {
99
+ case "boolean": {
100
+ helpers.set(settingId, value === "on");
101
+ break;
102
+ }
103
+ case "number": {
104
+ const num = Number.parseInt(value, 10);
105
+ if (Number.isFinite(num) && num > 0) {
106
+ helpers.set(settingId, num);
107
+ } else {
108
+ helpers.unset(settingId);
109
+ }
110
+ break;
111
+ }
112
+ case "stringList": {
113
+ const names = value
114
+ .split(",")
115
+ .map((s) => s.trim())
116
+ .filter((s) => s.length > 0);
117
+ if (names.length > 0) {
118
+ helpers.set(settingId, names);
119
+ } else {
120
+ helpers.unset(settingId);
121
+ }
122
+ break;
123
+ }
124
+ }
125
+ }
126
+
127
+ function areAllItemsDeclarative(items: ConfigSettingItem[]): boolean {
128
+ return items.length > 0 && items.every((i) => i.configType !== undefined);
129
+ }
130
+
131
+ // ── Registration ───────────────────────────────────────────────────────────
132
+
133
+ /**
134
+ * Register a config-backed settings section.
135
+ *
136
+ * Loads display values from the selected scope only (`defaults <- selected scope`)
137
+ * instead of merged effective runtime config. Provides scoped `set` / `unset`
138
+ * persistence helpers so extensions don't need to wire `writeSupiConfig` /
139
+ * `removeSupiConfigKey` by hand.
140
+ *
141
+ * When every item returned by `buildItems` declares a `configType`, the
142
+ * `persistChange` callback is optional and will be auto-generated.
143
+ */
144
+ export function registerConfigSettings<T>(options: ConfigSettingsOptions<T>): void {
145
+ let cachedItems: ConfigSettingItem[] | undefined;
146
+
147
+ registerSettings({
148
+ id: options.id,
149
+ label: options.label,
150
+ loadValues: (scope, cwd, ctx) => {
151
+ const settings = loadSupiConfigForScope(options.section, cwd, options.defaults, {
152
+ scope,
153
+ homeDir: options.homeDir,
154
+ });
155
+ const items = options.buildItems(settings, scope, cwd, ctx);
156
+ cachedItems = items;
157
+ return items;
158
+ },
159
+ persistChange: (scope, cwd, settingId, value) => {
160
+ const helpers: ConfigSettingsHelpers = {
161
+ set: (key, val) => {
162
+ writeSupiConfig(
163
+ { section: options.section, scope, cwd },
164
+ { [key]: val },
165
+ { homeDir: options.homeDir },
166
+ );
167
+ },
168
+ unset: (key) => {
169
+ removeSupiConfigKey({ section: options.section, scope, cwd }, key, {
170
+ homeDir: options.homeDir,
171
+ });
172
+ },
173
+ };
174
+
175
+ // Use manual persistChange when provided
176
+ if (options.persistChange) {
177
+ options.persistChange(scope, cwd, settingId, value, helpers);
178
+ return;
179
+ }
180
+
181
+ // Auto-generate when all items are declarative
182
+ const items = cachedItems ?? options.buildItems(options.defaults, scope, cwd, undefined);
183
+ if (areAllItemsDeclarative(items)) {
184
+ autoPersistChange(settingId, value, helpers, items);
185
+ }
186
+ },
187
+ });
188
+ }