@homer0/pi-config 0.0.0-stage → 1.0.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/README.md +329 -2
- package/dist/extensions/config.js +2 -0
- package/dist/extensions/config.js.map +1 -0
- package/dist/format-EJuykEtc.js +3 -0
- package/dist/format-EJuykEtc.js.map +1 -0
- package/dist/handlers-B2Dm1nBF.js +2 -0
- package/dist/handlers-B2Dm1nBF.js.map +1 -0
- package/dist/index.js +1 -0
- package/dist/utils-DYlB6FdR.js +2 -0
- package/dist/utils-DYlB6FdR.js.map +1 -0
- package/package.json +72 -4
- package/src/errors.ts +35 -0
- package/src/extensions/config.ts +238 -0
- package/src/format.ts +58 -0
- package/src/handlers.ts +225 -0
- package/src/index.ts +20 -0
- package/src/schemas.ts +39 -0
- package/src/types.ts +240 -0
- package/src/utils.ts +583 -0
- package/src/window.ts +593 -0
package/README.md
CHANGED
|
@@ -1,3 +1,330 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🎛️ Pi Config
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Config management building block for [Pi](https://pi.dev).
|
|
4
|
+
|
|
5
|
+
This package provides tools and utilities that can be used to make your Pi stuff easier to configure.
|
|
6
|
+
|
|
7
|
+
## 🍿 Usage
|
|
8
|
+
|
|
9
|
+
> - ⚠️ **This package is for Pi.** It reads Pi's config files and registers a Pi extension; it's not a general-purpose config loader.
|
|
10
|
+
> - If you are wondering why I built this, go to the [Motivation](#motivation) section.
|
|
11
|
+
|
|
12
|
+
- 📦 [Installation](#-installation)
|
|
13
|
+
- [As an extension](#as-an-extension)
|
|
14
|
+
- [As a library](#as-a-library)
|
|
15
|
+
- 🧙 [Configs](#-configs)
|
|
16
|
+
- [File locations](#file-locations)
|
|
17
|
+
- [Merging](#merging)
|
|
18
|
+
- [Namespaces](#namespaces)
|
|
19
|
+
- [Documenting your config](#documenting-your-config)
|
|
20
|
+
- [Extending configs](#extending-configs)
|
|
21
|
+
- [Session override](#session-override)
|
|
22
|
+
- ⚙️ [Examples](#%EF%B8%8F-examples)
|
|
23
|
+
- 🧰 [Tools and commands](#-tools-and-commands)
|
|
24
|
+
- [get_config](#get_config)
|
|
25
|
+
- [set_config](#set_config)
|
|
26
|
+
- [/config-reset](#config-reset)
|
|
27
|
+
- 🚀 [TUI](#-tui)
|
|
28
|
+
- 🤘 [Development](#-development)
|
|
29
|
+
- 📝 [Motivation](#-motivation)
|
|
30
|
+
|
|
31
|
+
### 📦 Installation
|
|
32
|
+
|
|
33
|
+
You can use this package as an extension, which provides tools and commands for the agent, or as a library to read config from your own extension, tool, or skill.
|
|
34
|
+
|
|
35
|
+
#### As an extension
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pi install npm:@homer0/pi-config
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This will expose the following tools and commands to your Pi agent:
|
|
42
|
+
|
|
43
|
+
| Name | What it does |
|
|
44
|
+
| --------------------------- | ----------------------------------------------------------------------------- |
|
|
45
|
+
| [`get_config`](#get_config) | Lets the agent read the resolved config (secret-looking values are redacted). |
|
|
46
|
+
| [`set_config`](#set_config) | Lets the agent override one config value for the current session. |
|
|
47
|
+
| `/config` | Opens a TUI window to see the resolved config and edit the session override. |
|
|
48
|
+
| `/config-reset` | Clears the session override. |
|
|
49
|
+
|
|
50
|
+
⚠️: When using `get_config`, secret-looking values will be redacted in the output. To access those, you'll have to get them using the library functions like `getConfigValue` or `queryConfig`.
|
|
51
|
+
|
|
52
|
+
#### As a library
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pnpm add @homer0/pi-config
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui` are peer dependencies: Pi provides them to your extension, so don't bundle them.
|
|
59
|
+
|
|
60
|
+
Installing it as a dependency will give you access to an ESM module with the following functions:
|
|
61
|
+
|
|
62
|
+
| Function | What it does |
|
|
63
|
+
| ---------------- | -------------------------------------------- |
|
|
64
|
+
| `getConfig` | Reads the full resolved config. |
|
|
65
|
+
| `getConfigValue` | Reads a single config value by its path. |
|
|
66
|
+
| `queryConfig` | Queries the config using a structured query. |
|
|
67
|
+
|
|
68
|
+
### 🧙 Configs
|
|
69
|
+
|
|
70
|
+
#### File locations
|
|
71
|
+
|
|
72
|
+
The config files can be written in JSON or YAML format, and the way the package will be read and merged follows a specific order, with later files overriding earlier ones. It will start in the global Pi directory and then look for project-specific config files (`<project>/.pi/`):
|
|
73
|
+
|
|
74
|
+
```txt
|
|
75
|
+
~/<agent>/config.{json,yml,yaml}
|
|
76
|
+
~/<agent>/config.local.{json,yml,yaml}
|
|
77
|
+
<project>/.pi/config.{json,yml,yaml}
|
|
78
|
+
<project>/.pi/config.local.{json,yml,yaml}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
> The `<agent>` directory is given by Pi's `getAgentDir()` function, so it doesn't matter what you name it or where it is located; Pi will resolve it correctly.
|
|
82
|
+
|
|
83
|
+
The idea with the `.local.{json,yml,yaml}` files is that they are meant for local overrides that should not be committed to version control.
|
|
84
|
+
|
|
85
|
+
#### Merging
|
|
86
|
+
|
|
87
|
+
Each file is merged on top of the previous ones. Objects are deep-merged, and primitive values replace the previous value.
|
|
88
|
+
|
|
89
|
+
Arrays are merged by index, not replaced: the items of a later file take the place of the ones in the same position, but the extra items of an earlier array stay. For example, merging `[1, 2]` with `[3]` gives `[3, 2]`, so a later file can't make an array shorter.
|
|
90
|
+
|
|
91
|
+
#### Namespaces
|
|
92
|
+
|
|
93
|
+
Pi's config is organized into namespaces, each corresponding to a different kind of integration. The main namespaces are:
|
|
94
|
+
|
|
95
|
+
| Name. | Description |
|
|
96
|
+
| ------------------- | ------------------------------------------------------- |
|
|
97
|
+
| `pi` | Something that would affect the overall behavior of Pi. |
|
|
98
|
+
| `skills.<name>` | Configuration for a specific skill. |
|
|
99
|
+
| `tools.<name>` | Configuration for a specific tool. |
|
|
100
|
+
| `extensions.<name>` | Configuration for a specific extension. |
|
|
101
|
+
|
|
102
|
+
For example:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
pi:
|
|
106
|
+
setting: value
|
|
107
|
+
skills:
|
|
108
|
+
commit:
|
|
109
|
+
runChecks: true
|
|
110
|
+
tools:
|
|
111
|
+
bash:
|
|
112
|
+
version: '5.1'
|
|
113
|
+
extensions:
|
|
114
|
+
myExtension:
|
|
115
|
+
enabled: true
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### Documenting your config
|
|
119
|
+
|
|
120
|
+
The schema of a config always belongs to whatever consumes it: a skill, tool, or extension that uses config should document its own keys. This package only defines the files, how they are merged, and how to read them.
|
|
121
|
+
|
|
122
|
+
When you document yours, include:
|
|
123
|
+
|
|
124
|
+
- The config path, for example `skills.commit.runChecks`.
|
|
125
|
+
- The type of the value and what happens when it's not set.
|
|
126
|
+
- Whether it's safe to set globally, it only makes sense per project, or it should only be local.
|
|
127
|
+
- An example.
|
|
128
|
+
- How invalid values are handled.
|
|
129
|
+
|
|
130
|
+
#### Extending configs
|
|
131
|
+
|
|
132
|
+
Config files can extend other config files using the `extends` key. This allows you to build on top of a base configuration and override specific values as needed. For example:
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
extends: ../base-config.yaml
|
|
136
|
+
pi:
|
|
137
|
+
setting: overriddenValue
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The path for `extends` can be...
|
|
141
|
+
|
|
142
|
+
1. A relative path: `../base-config.yaml`
|
|
143
|
+
2. An absolute path: `/path/to/base-config.yaml`
|
|
144
|
+
3. A prefixed path:
|
|
145
|
+
3.a. `pi:` refers to a config in the pi directory: `pi:base-config.yaml`
|
|
146
|
+
3.b. `agent:` refers to a config in the agent directory: `agent:./some/base-config.yaml`
|
|
147
|
+
3.c. `profile:<name>:` refers to a config inside a specific profile: `profile:dev:base-config.yaml`
|
|
148
|
+
|
|
149
|
+
> - `pi:` and `agent:` may map to the same directory or not, it depends on your Pi setup.
|
|
150
|
+
> - `profile:<name>:` is used when you have my `pi-profiles` extension.
|
|
151
|
+
|
|
152
|
+
A few more details:
|
|
153
|
+
|
|
154
|
+
- The base file is loaded first and the file that extends it is merged on top, so the current file always wins.
|
|
155
|
+
- The `extends` key is removed from the resolved config.
|
|
156
|
+
- Chains work: a base file can extend another one. Circular references are detected and logged as warnings, without breaking the loading of the config.
|
|
157
|
+
|
|
158
|
+
#### Session override
|
|
159
|
+
|
|
160
|
+
On top of the files, there's one more layer: the session override. It only lives in memory, for the current Pi process, and it has the highest precedence:
|
|
161
|
+
|
|
162
|
+
```txt
|
|
163
|
+
~/<agent>/config.{json,yml,yaml}
|
|
164
|
+
~/<agent>/config.local.{json,yml,yaml}
|
|
165
|
+
<project>/.pi/config.{json,yml,yaml}
|
|
166
|
+
<project>/.pi/config.local.{json,yml,yaml}
|
|
167
|
+
session override (in-memory, if set)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`getConfig`, `getConfigValue`, `queryConfig`, and the `get_config` tool apply it automatically, so your code doesn't need to know it exists. You set it with the [`set_config`](#set_config) tool or the [`/config`](#-tui) window, and you clear it with [`/config-reset`](#config-reset) or by emptying the editor in the window.
|
|
171
|
+
|
|
172
|
+
The override is stored on `globalThis` (as `__piConfigSessionOverride`) and not in a module variable. Pi loads each extension with its own copy of the modules it imports, so a module variable would only be visible to the code that shares the copy that wrote it. With `globalThis`, every extension in the process sees the same override, whichever copy of this package it uses.
|
|
173
|
+
|
|
174
|
+
Limitations:
|
|
175
|
+
|
|
176
|
+
- **Process-only, in-memory.** It's never written to a config file, and it's lost when the process exits (restart, new session, etc.).
|
|
177
|
+
- **Stale reads.** Only reads that happen after the override is set will reflect it. Anything that already read or cached the config earlier in the same process won't pick it up retroactively.
|
|
178
|
+
- **Whole-document override, not per-key unset.** Setting a path only replaces its value; there's no way to take a single path back to its file-based value. Clear the override entirely to go back to file-only config.
|
|
179
|
+
- **Not schema-validated when set.** Neither the `/config` editor nor `set_config` validate the shape of the override against a consumer's schema. A badly-shaped override only shows up later, as an error, if a library consumer calls `getConfigValue` with a Zod `schema` for the affected path. `queryConfig` and `get_config` never validate.
|
|
180
|
+
|
|
181
|
+
### ⚙️ Examples
|
|
182
|
+
|
|
183
|
+
This section will focus on the examples as a library and not the tools provides to the agent, since that will depend on how you would tell the agent to use them through AGENTS.md and/or SKILL.md files.
|
|
184
|
+
|
|
185
|
+
#### Reading config
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import { getConfig, getConfigValue, queryConfig } from '@homer0/pi-config';
|
|
189
|
+
|
|
190
|
+
const config = await getConfig(); // the full resolved config
|
|
191
|
+
const runChecks = await getConfigValue('skills.commit.runChecks'); // a single path
|
|
192
|
+
const bash = await queryConfig({ scope: 'tool', name: 'bash' }); // a structured query
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
By default, the project config is looked up from `process.cwd()`. If your code works on another directory, send it as `cwd`:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
const config = await getConfig({ cwd: '/path/to/project' });
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### Validating your config
|
|
202
|
+
|
|
203
|
+
`getConfig` and `getConfigValue` accept a [Zod](https://zod.dev) schema. If the config doesn't exist, you get the usual missing-config result; if it exists and the schema doesn't accept it, they throw a `ConfigValidationError`:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { getConfigValue } from '@homer0/pi-config';
|
|
207
|
+
import { z } from 'zod';
|
|
208
|
+
|
|
209
|
+
const commitConfigSchema = z.object({
|
|
210
|
+
runChecks: z.boolean().optional(),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const commitConfig = await getConfigValue('skills.commit', {
|
|
214
|
+
schema: commitConfigSchema,
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`queryConfig` doesn't accept a schema: it's the structured query the `get_config` tool is built on.
|
|
219
|
+
|
|
220
|
+
#### Typing your config
|
|
221
|
+
|
|
222
|
+
To get the type of a value from its path, extend the interfaces the package exports with [module augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation). There's one for each [namespace](#namespaces): `PiConfig`, `SkillsConfig`, `ToolsConfig`, and `ExtensionsConfig`:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
declare module '@homer0/pi-config' {
|
|
226
|
+
interface SkillsConfig {
|
|
227
|
+
commit?: {
|
|
228
|
+
runChecks?: boolean;
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
With that, `getConfigValue` infers the type of the value from the path:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
const commit = await getConfigValue('skills.commit'); // { runChecks?: boolean } | undefined
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The path itself is not checked: any string is accepted, and the type of the value is `unknown` when it can't be resolved. If you pass a [schema](#validating-your-config), its type is used instead.
|
|
241
|
+
|
|
242
|
+
### 🧰 Tools and commands
|
|
243
|
+
|
|
244
|
+
These are the tools and commands you get when you [install the package as an extension](#as-an-extension). The tools are for the agent: they are meant for when a tool, skill, or extension tells it (through its `AGENTS.md` or `SKILL.md`) to read or override config. `/config-reset` is a command for you, and the `/config` window has its own section: [TUI](#-tui).
|
|
245
|
+
|
|
246
|
+
#### get_config
|
|
247
|
+
|
|
248
|
+
Reads the resolved config: the files, plus the [session override](#session-override). Its parameters are:
|
|
249
|
+
|
|
250
|
+
| Parameter | Description |
|
|
251
|
+
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
252
|
+
| `path` | A dot-path to read, like `skills.commit.runChecks`. |
|
|
253
|
+
| `scope` | What to read: `all`, a whole namespace (`pi`, `skills`, `tools`, `extensions`), or a single integration (`skill`, `tool`, `extension`, which also need a `name`). |
|
|
254
|
+
| `name` | The name of the skill, tool, or extension, for the `skill`, `tool`, and `extension` scopes. |
|
|
255
|
+
|
|
256
|
+
The agent should use the narrowest query that answers its question:
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{ "path": "skills.commit.runChecks" }
|
|
260
|
+
{ "scope": "pi" }
|
|
261
|
+
{ "scope": "skill", "name": "commit" }
|
|
262
|
+
{ "scope": "tool", "name": "bash" }
|
|
263
|
+
{ "scope": "extension", "name": "myExtension" }
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
- Its prompt guidelines tell the agent to call it only when a tool, skill, or extension explicitly calls for config, and to treat missing config as no preference, not as an error.
|
|
267
|
+
- Secret-looking values are redacted in the result. Use the library functions if you need the raw values.
|
|
268
|
+
|
|
269
|
+
#### set_config
|
|
270
|
+
|
|
271
|
+
Overrides a single config value for the current session. Its parameters are:
|
|
272
|
+
|
|
273
|
+
| Parameter | Description |
|
|
274
|
+
| --------- | ------------------------------------------------------------------------ |
|
|
275
|
+
| `path` | The dot-path to override, like `extensions.myExtension.enabled`. |
|
|
276
|
+
| `value` | The value to set at that path. The rest of the override stays untouched. |
|
|
277
|
+
|
|
278
|
+
- It changes one path at a time, into the existing [session override](#session-override), and it replies with `Session override set: <path> = <value>`.
|
|
279
|
+
- Its prompt guidelines tell the agent to call it only when the user explicitly asks for an override, never speculatively or to work around an error.
|
|
280
|
+
- It can't clear the override or remove a path from it. Clearing is done with [`/config-reset`](#config-reset) or from the [`/config`](#-tui) window.
|
|
281
|
+
|
|
282
|
+
#### /config-reset
|
|
283
|
+
|
|
284
|
+
Clears the [session override](#session-override) right away, without opening a window. It's how you undo [`set_config`](#set_config), which can only set values, and it's also the quick alternative to emptying the editor in [`/config`](#-tui). It tells you if there was an override to clear: `Session config override cleared.` or `No session override was set.`
|
|
285
|
+
|
|
286
|
+
### 🚀 TUI
|
|
287
|
+
|
|
288
|
+
You open it with the `/config` command. It's a floating window titled "Config", with two tabs. Use `Tab` and `Shift+Tab` to switch between them, and `Esc` to close the window; closing it doesn't discard anything you already applied.
|
|
289
|
+
|
|
290
|
+
**Effective config** shows the resolved config as syntax-highlighted YAML. If it's larger than the window, you can scroll with `↑`, `↓`, `PageUp`, `PageDown`, `Home`, and `End`, and the footer shows which lines you are looking at. Press `t` to switch between the config coming only from the files and the effective config, which includes the session override. The status line always tells you what you are looking at:
|
|
291
|
+
|
|
292
|
+
| Status line | What it means |
|
|
293
|
+
| ------------------------------------------- | ------------------------------------------------------- |
|
|
294
|
+
| `No session override set` | There's no override. |
|
|
295
|
+
| `Session override set — hidden (t to show)` | There's an override, but you are looking at files only. |
|
|
296
|
+
| `Session override set — shown (t to hide)` | There's an override, and it's applied to what you see. |
|
|
297
|
+
|
|
298
|
+
**Override** is a YAML editor, prefilled with the current session override (empty if there's none). Press `Enter` to submit it:
|
|
299
|
+
|
|
300
|
+
- A non-empty YAML mapping becomes the new override, and the window says `Session override applied.`
|
|
301
|
+
- Empty content, or YAML that parses to `null`, clears the override: `Session override cleared.`
|
|
302
|
+
- Anything else (invalid YAML, or a value that is not a mapping, like a string or a number) shows an inline error, like `Invalid YAML: ...`, and the previous override stays as it was.
|
|
303
|
+
|
|
304
|
+
To add a new line without submitting, use `Shift+Enter` or `Ctrl+J`.
|
|
305
|
+
|
|
306
|
+
### 🤘 Development
|
|
307
|
+
|
|
308
|
+
As this project is part of a monorepo, some of the tooling, like `lint-staged` and `husky`, is installed in the root's `package.json`.
|
|
309
|
+
|
|
310
|
+
#### Tasks
|
|
311
|
+
|
|
312
|
+
| Task | Description |
|
|
313
|
+
| ------------- | ------------------------------- |
|
|
314
|
+
| `build` | Builds the package. |
|
|
315
|
+
| `lint` | Lints the package. |
|
|
316
|
+
| `lint:file` | Lints the files you send to it. |
|
|
317
|
+
| `test` | Runs the unit tests. |
|
|
318
|
+
| `types:check` | Validates the TypeScript types. |
|
|
319
|
+
|
|
320
|
+
## 📝 Motivation
|
|
321
|
+
|
|
322
|
+
When I started working with Pi, I saw a couple of things that I didn't like:
|
|
323
|
+
|
|
324
|
+
- Changing the model made a change in my `settings.json`
|
|
325
|
+
- I couldn't have a `settings.json` in the project I was working on for project-specific settings.
|
|
326
|
+
- I couldn't have a `settings.local.json` for local overrides, and I spent half of my day with a work laptop and the other half with a personal laptop.
|
|
327
|
+
|
|
328
|
+
Then I started building stuff for Pi, and realized that I wanted things to be configurable without having to mess with the `settings.json`, owned by the agent.
|
|
329
|
+
|
|
330
|
+
So I built this, and it's like one of my core building blocks for all the things I create for Pi.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{i as e,n as t,r as n}from"../format-EJuykEtc.js";import{z as r}from"zod";import{Text as i}from"@earendil-works/pi-tui";const a=r.object({path:r.string().optional().describe(`Dot-path to read, for example skills.commit.runChecks.`),scope:r.enum([`all`,`pi`,`skills`,`tools`,`extensions`,`skill`,`tool`,`extension`]).optional().describe(`Config scope to read. Use pi for general Pi settings, or skill/tool/extension with name.`),name:r.string().optional().describe(`Name for skill, tool, or extension scoped lookups.`)}),o=r.object({path:r.string().describe(`Dot-path to override for the rest of this session, for example extensions.notifyMe.priority.`),value:r.unknown().describe(`Value to set at path. Only this path changes; the rest of the config is unaffected.`)});let s;const c=()=>(s??=import(`../handlers-B2Dm1nBF.js`).catch(e=>{throw s=void 0,e}),s),l={name:`get_config`,label:`Get Config`,description:`Read resolved Pi Core config from config.json and config.local.json for local extensions, tools, and skills that explicitly use this convention.`,promptSnippet:`Use get_config only when a tool, skill, or extension explicitly calls for Pi Core config.`,promptGuidelines:[`Call get_config only when a tool, skill, or extension explicitly calls for Pi Core config.`,`Do not infer or guess config support from package names or behavior.`,`Prefer the narrowest lookup: path first, then pi or named skill/tool/extension, then namespace, and full config only when necessary.`,`Use scope: 'pi' for general Pi/session/agent settings, for example { scope: 'pi' }.`,`Use scope: 'skill' with name for local skill preferences, for example { scope: 'skill', name: 'commit' }.`,`Use scope: 'tool' with name for local tool defaults, for example { scope: 'tool', name: 'bash' }.`,`Use scope: 'extension' with name for local extension settings, for example { scope: 'extension', name: 'config' }.`,`Do not expose config values to the user unless necessary for the answer or requested by the user.`,`Treat missing config as absence of preference, not as an error.`,`Ask the user before relying on unclear or conflicting config.`],parameters:r.toJSONSchema(a),async execute(e,t,n,r,i){let{handleGetConfig:a}=await c();return a({params:t,signal:n,ctx:i})},renderCall(e,t){let r=e,a=n(r),o=`full config`;return r.path?o=`path lookup`:r.scope&&r.name?o=`${r.scope} scope`:r.scope&&(o=`${r.scope} namespace`),new i(`${t.fg(`toolTitle`,t.bold(`get_config `))}${t.fg(`accent`,a)}\n${t.fg(`dim`,` ${o}`)}`,0,0)},renderResult(n,r,a){let o=n.details,s=r.isPartial?`${a.fg(`muted`,`… `)} `:``;if(o.status===`found`){let n=e(o.sources),r=n===1?`1 source`:`${n} sources`;return new i(`${s}${a.fg(`success`,`✓`)} ${a.fg(`muted`,r)} ${t(o.value)}`,0,0)}return new i(`${s}${a.fg(o.status===`not_found`?`warning`:`error`,o.message??`Config lookup failed`)}`,0,0)}},u={name:`set_config`,label:`Set Config`,description:`Override one Pi Core config value for the rest of this session. In-memory only: never written to config.json/config.local.json/config.yaml/config.local.yaml, and lost when the session ends.`,promptSnippet:`Only call set_config when the user explicitly asks to override a config value for this session.`,promptGuidelines:[`Only call set_config when the user explicitly asks to override a specific config value for this session.`,`Never call set_config speculatively, to work around an error, or to "fix" a config problem the user did not ask you to change.`,`set_config only affects this session/process and is never persisted to any config file.`,`set_config can only set a value; it cannot remove a file-based value or restore file-only behavior for one path. Tell the user to use /config to clear the whole session override.`],parameters:r.toJSONSchema(o),async execute(e,t){let{handleSetConfig:n}=await c();return n({params:t})},renderCall(e,t){let n=e;return new i(`${t.fg(`toolTitle`,t.bold(`set_config `))}${t.fg(`accent`,n.path??``)}`,0,0)},renderResult(e,t,n){let r=e.details,a=t.isPartial?`${n.fg(`muted`,`… `)} `:``;return!r||r.status!==`applied`?new i(`${a}${n.fg(`error`,r?.message??`set_config failed`)}`,0,0):new i(`${a}${n.fg(`success`,`✓ `)}${n.fg(`accent`,r.path)} = ${JSON.stringify(r.value)}`,0,0)}},d=e=>{e.registerTool(l),e.registerTool(u),e.registerCommand(`config`,{description:`View the effective Pi Core config and edit the session override`,handler:async(e,t)=>{let{handleConfig:n}=await c();await n({ctx:t})}}),e.registerCommand(`config-reset`,{description:`Clear the session config override`,handler:async(e,t)=>{let{handleConfigReset:n}=await c();n({ctx:t})}})};export{d as default};
|
|
2
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","names":["getUsedSourceCount","formatValuePreview","getQueryLabel","getConfigParamsSchema","z","setConfigParamsSchema","handlersModule","loadHandlers","error","getConfigTool","handleGetConfig","rawParams","query","args","label","details","Text","theme","result","prefix","options","usedSourceCount","sourceText","setConfigTool","handleSetConfig","params","config","pi","handleConfig","handleConfigReset"],"sources":["../../src/schemas.ts","../../src/extensions/config.ts"],"sourcesContent":["import { z } from 'zod';\n\n/** Model-facing schema for the get_config tool. */\nexport const getConfigParamsSchema = z.object({\n path: z\n .string()\n .optional()\n .describe('Dot-path to read, for example skills.commit.runChecks.'),\n scope: z\n .enum(['all', 'pi', 'skills', 'tools', 'extensions', 'skill', 'tool', 'extension'])\n .optional()\n .describe(\n 'Config scope to read. Use pi for general Pi settings, or skill/tool/extension with name.',\n ),\n name: z\n .string()\n .optional()\n .describe('Name for skill, tool, or extension scoped lookups.'),\n});\n\n/** The parameters the model sends to the `get_config` tool. */\nexport type GetConfigParams = z.infer<typeof getConfigParamsSchema>;\n\n/** Model-facing schema for the set_config tool. */\nexport const setConfigParamsSchema = z.object({\n path: z\n .string()\n .describe(\n 'Dot-path to override for the rest of this session, for example extensions.notifyMe.priority.',\n ),\n value: z\n .unknown()\n .describe(\n 'Value to set at path. Only this path changes; the rest of the config is unaffected.',\n ),\n});\n\n/** The parameters the model sends to the `set_config` tool. */\nexport type SetConfigParams = z.infer<typeof setConfigParamsSchema>;\n","import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { Text } from '@earendil-works/pi-tui';\nimport { z } from 'zod';\nimport { formatValuePreview, getQueryLabel, getUsedSourceCount } from '../format.js';\nimport {\n getConfigParamsSchema,\n setConfigParamsSchema,\n type GetConfigParams,\n type SetConfigParams,\n} from '../schemas.js';\nimport type { ConfigQuery, SetConfigDetails } from '../types.js';\n\n/** Module shape of the lazily loaded handlers. */\ntype HandlersModule = typeof import('../handlers.js');\n\n/** The promise of the handlers module, shared by every call once it's been requested. */\nlet handlersModule: Promise<HandlersModule> | undefined;\n\n/**\n * Lazy-loads all the handlers through a single shared `import()`. Tools can be called in\n * parallel by the model, and jiti evaluates lazily loaded modules asynchronously, handing\n * partial exports to any `import()` of a module (or of a dependency shared by several\n * handlers) that is still evaluating. Loading everything through one memoized promise\n * makes overlapping calls wait for the same evaluation. A failed load is not cached, so\n * the next call retries.\n *\n * @returns The module with all the handlers.\n */\nconst loadHandlers = (): Promise<HandlersModule> => {\n handlersModule ??= import('../handlers.js').catch((error: unknown) => {\n handlersModule = undefined;\n throw error;\n });\n return handlersModule;\n};\n\n/** Tool definition for reading resolved Pi Core config. */\nconst getConfigTool: Parameters<ExtensionAPI['registerTool']>[0] = {\n name: 'get_config',\n label: 'Get Config',\n description:\n 'Read resolved Pi Core config from config.json and config.local.json for local extensions, tools, and skills that explicitly use this convention.',\n promptSnippet:\n 'Use get_config only when a tool, skill, or extension explicitly calls for Pi Core config.',\n promptGuidelines: [\n 'Call get_config only when a tool, skill, or extension explicitly calls for Pi Core config.',\n 'Do not infer or guess config support from package names or behavior.',\n 'Prefer the narrowest lookup: path first, then pi or named skill/tool/extension, then namespace, and full config only when necessary.',\n \"Use scope: 'pi' for general Pi/session/agent settings, for example { scope: 'pi' }.\",\n \"Use scope: 'skill' with name for local skill preferences, for example { scope: 'skill', name: 'commit' }.\",\n \"Use scope: 'tool' with name for local tool defaults, for example { scope: 'tool', name: 'bash' }.\",\n \"Use scope: 'extension' with name for local extension settings, for example { scope: 'extension', name: 'config' }.\",\n 'Do not expose config values to the user unless necessary for the answer or requested by the user.',\n 'Treat missing config as absence of preference, not as an error.',\n 'Ask the user before relying on unclear or conflicting config.',\n ],\n parameters: z.toJSONSchema(getConfigParamsSchema),\n\n /**\n * Runs the tool: it lazy-loads the handler and delegates the work to it.\n *\n * @param _toolCallId The id of the tool call. It's not used.\n * @param rawParams The parameters the model sent.\n * @param signal The signal to abort the lookup.\n * @param _onUpdate The callback for partial results. It's not used.\n * @param ctx The context of the session, to know the project directory.\n * @returns The result of the tool.\n */\n async execute(_toolCallId, rawParams, signal, _onUpdate, ctx) {\n const { handleGetConfig } = await loadHandlers();\n return handleGetConfig({ params: rawParams as GetConfigParams, signal, ctx });\n },\n\n /**\n * Renders the call of the tool in the TUI.\n *\n * @param args The parameters of the call.\n * @param theme The theme to color the text with.\n * @returns The text to show: the label of the query and what kind of lookup it is.\n */\n renderCall(args, theme) {\n const query = args as ConfigQuery;\n const label = getQueryLabel(query);\n let details = 'full config';\n if (query.path) details = 'path lookup';\n else if (query.scope && query.name) details = `${query.scope} scope`;\n else if (query.scope) details = `${query.scope} namespace`;\n return new Text(\n `${theme.fg('toolTitle', theme.bold('get_config '))}${theme.fg(\n 'accent',\n label,\n )}\\n${theme.fg('dim', ` ${details}`)}`,\n 0,\n 0,\n );\n },\n\n /**\n * Renders the result of the tool in the TUI.\n *\n * @param result The result of the tool.\n * @param options The render options, to know if the result is partial.\n * @param theme The theme to color the text with.\n * @returns The text to show: a preview of the value and how many files it came from, or\n * the reason why there's no value.\n */\n renderResult(result, options, theme) {\n const details = result.details as {\n status?: string;\n value?: unknown;\n message?: string;\n sources?: unknown;\n };\n const prefix = options.isPartial ? `${theme.fg('muted', '… ')} ` : '';\n if (details.status === 'found') {\n const usedSourceCount = getUsedSourceCount(details.sources);\n const sourceText =\n usedSourceCount === 1 ? '1 source' : `${usedSourceCount} sources`;\n return new Text(\n `${prefix}${theme.fg('success', '✓')} ${theme.fg(\n 'muted',\n sourceText,\n )} ${formatValuePreview(details.value)}`,\n 0,\n 0,\n );\n }\n return new Text(\n `${prefix}${theme.fg(\n details.status === 'not_found' ? 'warning' : 'error',\n details.message ?? 'Config lookup failed',\n )}`,\n 0,\n 0,\n );\n },\n};\n\n/** Tool definition for setting an in-memory session config override. */\nconst setConfigTool: Parameters<ExtensionAPI['registerTool']>[0] = {\n name: 'set_config',\n label: 'Set Config',\n description:\n 'Override one Pi Core config value for the rest of this session. In-memory only: never written to config.json/config.local.json/config.yaml/config.local.yaml, and lost when the session ends.',\n promptSnippet:\n 'Only call set_config when the user explicitly asks to override a config value for this session.',\n promptGuidelines: [\n 'Only call set_config when the user explicitly asks to override a specific config value for this session.',\n 'Never call set_config speculatively, to work around an error, or to \"fix\" a config problem the user did not ask you to change.',\n 'set_config only affects this session/process and is never persisted to any config file.',\n 'set_config can only set a value; it cannot remove a file-based value or restore file-only behavior for one path. Tell the user to use /config to clear the whole session override.',\n ],\n parameters: z.toJSONSchema(setConfigParamsSchema),\n\n /**\n * Runs the tool: it lazy-loads the handler and delegates the work to it.\n *\n * @param _toolCallId The id of the tool call. It's not used.\n * @param rawParams The parameters the model sent.\n * @returns The result of the tool.\n */\n async execute(_toolCallId, rawParams) {\n const { handleSetConfig } = await loadHandlers();\n return handleSetConfig({ params: rawParams as SetConfigParams });\n },\n\n /**\n * Renders the call of the tool in the TUI.\n *\n * @param args The parameters of the call.\n * @param theme The theme to color the text with.\n * @returns The text to show: the path that is being set.\n */\n renderCall(args, theme) {\n const params = args as Partial<SetConfigParams>;\n return new Text(\n `${theme.fg('toolTitle', theme.bold('set_config '))}${theme.fg('accent', params.path ?? '')}`,\n 0,\n 0,\n );\n },\n\n /**\n * Renders the result of the tool in the TUI.\n *\n * @param result The result of the tool.\n * @param options The render options, to know if the result is partial.\n * @param theme The theme to color the text with.\n * @returns The text to show: the path and the value that were set, or the reason why it\n * failed.\n */\n renderResult(result, options, theme) {\n const details = result.details as SetConfigDetails | undefined;\n const prefix = options.isPartial ? `${theme.fg('muted', '… ')} ` : '';\n if (!details || details.status !== 'applied') {\n return new Text(\n `${prefix}${theme.fg('error', details?.message ?? 'set_config failed')}`,\n 0,\n 0,\n );\n }\n return new Text(\n `${prefix}${theme.fg('success', '✓ ')}${theme.fg('accent', details.path)} = ${JSON.stringify(details.value)}`,\n 0,\n 0,\n );\n },\n};\n\n/**\n * Registers config extension tools and the `/config`/`/config-reset` commands with Pi.\n * The definitions are light; the tools' `execute` and the commands lazy-load their\n * implementation, through the shared handlers loader.\n *\n * @param pi The Pi extension API.\n */\nconst config = (pi: ExtensionAPI): void => {\n pi.registerTool(getConfigTool);\n pi.registerTool(setConfigTool);\n\n pi.registerCommand('config', {\n description: 'View the effective Pi Core config and edit the session override',\n handler: async (_args, ctx) => {\n const { handleConfig } = await loadHandlers();\n await handleConfig({ ctx });\n },\n });\n\n pi.registerCommand('config-reset', {\n description: 'Clear the session config override',\n handler: async (_args, ctx) => {\n const { handleConfigReset } = await loadHandlers();\n handleConfigReset({ ctx });\n },\n });\n};\n\nexport default config;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
const e=e=>typeof e==`string`?e:JSON.stringify(e,null,2),t=t=>{let n=e(t).replaceAll(`
|
|
2
|
+
`,` `);return n.length<=120?n:`${n.slice(0,119)}…`},n=e=>e.path?e.path:e.scope&&e.name?`${e.scope}:${e.name}`:e.scope?e.scope:`all`,r=e=>Array.isArray(e)?e.filter(e=>e&&typeof e==`object`&&`used`in e&&e.used===!0).length:0;export{r as i,t as n,n as r,e as t};
|
|
3
|
+
//# sourceMappingURL=format-EJuykEtc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-EJuykEtc.js","names":["formatValue","value","formatValuePreview","preview","getQueryLabel","query","getUsedSourceCount","sources","source"],"sources":["../src/format.ts"],"sourcesContent":["import type { ConfigQuery } from './types.js';\n\n/** The number of spaces used to indent the JSON output. */\nconst JSON_INDENT = 2;\n/** The max number of characters of a value preview. */\nconst SUMMARY_LIMIT = 120;\n\n/**\n * Formats config values for model-facing tool output.\n *\n * @param value The value to format.\n * @returns The value as it is if it's a string, or as indented JSON for anything else.\n */\nexport const formatValue = (value: unknown): string => {\n if (typeof value === 'string') return value;\n return JSON.stringify(value, null, JSON_INDENT);\n};\n\n/**\n * Formats a compact single-line value preview for TUI rendering.\n *\n * @param value The value to preview.\n * @returns The formatted value in a single line, cut with an ellipsis if it's too long.\n */\nexport const formatValuePreview = (value: unknown): string => {\n const preview = formatValue(value).replaceAll('\\n', ' ');\n if (preview.length <= SUMMARY_LIMIT) return preview;\n return `${preview.slice(0, SUMMARY_LIMIT - 1)}…`;\n};\n\n/**\n * Builds a compact label for displaying get_config calls.\n *\n * @param query The query of the call.\n * @returns The path if there's one; `scope:name` for a named integration; the scope for a\n * namespace; or `all` for the whole config.\n */\nexport const getQueryLabel = (query: ConfigQuery): string => {\n if (query.path) return query.path;\n if (query.scope && query.name) return `${query.scope}:${query.name}`;\n if (query.scope) return query.scope;\n return 'all';\n};\n\n/**\n * Counts config source files that exist and contributed to a lookup.\n *\n * @param sources The `sources` of a query result. It comes from the details of a tool\n * result, so it can't be trusted to have the right shape.\n * @returns The number of sources that were used, or `0` if `sources` is not a list.\n */\nexport const getUsedSourceCount = (sources: unknown): number => {\n if (!Array.isArray(sources)) return 0;\n return sources.filter(\n (source) =>\n source && typeof source === 'object' && 'used' in source && source.used === true,\n ).length;\n};\n"],"mappings":"AAGA,MAUaA,EAAe,GACtB,OAAOC,GAAU,SAAiBA,EAC/B,KAAK,UAAUA,EAAO,KAAM,CAAW,EASnCC,EAAsB,GAA2B,CAC5D,IAAMC,EAAUH,EAAYC,CAAK,CAAC,CAAC,WAAW;EAAM,GAAG,EAEvD,OADIE,EAAQ,QAAU,IAAsBA,EACrC,GAAGA,EAAQ,MAAM,EAAG,GAAiB,EAAE,EAChD,EASaC,EAAiB,GACxBC,EAAM,KAAaA,EAAM,KACzBA,EAAM,OAASA,EAAM,KAAa,GAAGA,EAAM,MAAM,GAAGA,EAAM,OAC1DA,EAAM,MAAcA,EAAM,MACvB,MAUIC,EAAsB,GAC5B,MAAM,QAAQC,CAAO,EACnBA,EAAQ,OACZ,GACCC,GAAU,OAAOA,GAAW,UAAY,SAAUA,GAAUA,EAAO,OAAS,EAChF,CAAC,CAAC,OAJkC,E"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{c as e,i as t,l as n,n as r,o as i,r as a,s as o,t as s,u as c}from"./utils-DYlB6FdR.js";import{r as l,t as u}from"./format-EJuykEtc.js";import{highlightCode as d}from"@earendil-works/pi-coding-agent";import{set as f}from"@homer0/object-utils";import{parse as p,stringify as m}from"yaml";import{Editor as h,Key as g,matchesKey as _,truncateToWidth as v,visibleWidth as y}from"@earendil-works/pi-tui";const b=[`Effective config`,`Override`],x=`yaml`,S=[`Loading…`],C=`borderAccent`,w=e=>({borderColor:t=>e.fg(`accent`,t),selectList:{selectedPrefix:t=>e.fg(`accent`,t),selectedText:t=>e.fg(`accent`,t),description:t=>e.fg(`muted`,t),scrollInfo:t=>e.fg(`dim`,t),noMatch:t=>e.fg(`warning`,t)}}),T=e=>i()?e?`Session override set — shown (t to hide)`:`Session override set — hidden (t to show)`:`No session override set`,E=e=>{let t=e.trim();if(!t)return{override:void 0,message:{kind:`success`,text:`Session override cleared.`}};let n;try{n=p(t)}catch(e){return{override:void 0,message:{kind:`error`,text:`Invalid YAML: ${a(e)}`}}}return n==null?{override:void 0,message:{kind:`success`,text:`Session override cleared.`}}:typeof n==`object`&&!Array.isArray(n)?{override:n,message:{kind:`success`,text:`Session override applied.`}}:{override:void 0,message:{kind:`error`,text:`Override must be a YAML mapping (object).`}}},D=async()=>{try{let{config:t}=await e();return d(m(t),x)}catch(e){return[`Failed to load config: ${a(e)}`]}},O=async()=>{try{return d(m(await t()),x)}catch(e){return[`Failed to load config: ${a(e)}`]}},k=e=>Math.max(5,Math.floor(e.terminal.rows*.85)-8),A=e=>(e.showOverride?e.effectiveConfigLines:e.fileConfigLines)??S,j=(e,t,n)=>Math.min(Math.max(0,e),Math.max(0,t-n)),M=(e,t)=>{let n=`┌─ Config `,r=Math.max(0,t-y(n)-1);return e.fg(C,`${n}${`─`.repeat(r)}┐`)},N=(e,t)=>e.fg(C,`└${`─`.repeat(Math.max(0,t-2))}┘`),P=(e,t,n)=>{let r=v(t,n),i=` `.repeat(Math.max(0,n-y(r)));return`${e.fg(C,`│`)} ${r}${i} ${e.fg(C,`│`)}`},F=(e,t)=>b.map((n,r)=>{let i=r===t;return e.fg(i?`accent`:`muted`,i?`[${n}]`:` ${n} `)}).join(` `),I=({theme:e,state:t,visibleContentLines:n})=>{let r=[];r.push(e.fg(`muted`,T(t.showOverride))),r.push(``);let i=A(t),a=j(t.scrollOffset,i.length,n);r.push(...i.slice(a,a+n)),r.push(``);let o=i.length>n?`(${a+1}-${Math.min(a+n,i.length)}/${i.length}) ↑↓/PgUp/PgDn scroll • `:``;return r.push(e.fg(`dim`,`${o}Tab switch tab • t toggle override • Esc close`)),r},L=({theme:e,state:t,editor:n,innerWidth:r})=>{let i=[];return i.push(e.fg(`muted`,`Session override (YAML). Empty clears it.`)),i.push(``),i.push(...n.render(r)),t.overrideMessage&&(i.push(``),i.push(e.fg(t.overrideMessage.kind===`error`?`warning`:`success`,t.overrideMessage.text))),i.push(``),i.push(e.fg(`dim`,`Enter apply/clear • Shift+Enter newline • Tab switch tab • Esc close`)),i},R=(e,t)=>{let{theme:n,state:r}=e,i=[F(n,r.activeTab),``,...r.activeTab===0?I(e):L(e)];return[M(n,t),...i.map(t=>P(n,t,e.innerWidth)),N(n,t)].map(e=>v(e,t))},z=({state:e,editor:t,tui:n,refresh:r,close:i})=>a=>{if(_(a,g.escape))i();else if(_(a,g.tab)||_(a,g.shift(`tab`)))e.activeTab=+(e.activeTab===0),r();else if(e.activeTab===0){if(a.toLowerCase()===`t`){e.showOverride=!e.showOverride,e.scrollOffset=0,r();return}let t=k(n),i=A(e).length,o;_(a,g.up)?o=e.scrollOffset-1:_(a,g.down)?o=e.scrollOffset+1:_(a,g.pageUp)?o=e.scrollOffset-t:_(a,g.pageDown)?o=e.scrollOffset+t:_(a,g.home)?o=0:_(a,g.end)&&(o=i),o!==void 0&&(e.scrollOffset=j(o,i,t),r())}else t.handleInput(a),r()},B=({tui:e,theme:t,done:r})=>{let a={activeTab:0,showOverride:!1,scrollOffset:0},o,s,c=new h(e,w(t)),l=i();l&&c.setText(m(l));let u=()=>{o=void 0,e.requestRender()};return D().then(e=>{a.fileConfigLines=e,u()}),O().then(e=>{a.effectiveConfigLines=e,u()}),c.onSubmit=e=>{let{override:t,message:r}=E(e);r.kind===`success`?(n(t),c.setText(t?m(t):``)):c.setText(e),a.overrideMessage=r,O().then(e=>{a.effectiveConfigLines=e,u()}),u()},{render:n=>{if(o&&s===n)return o;let r=Math.max(1,n-4);return o=R({theme:t,state:a,editor:c,innerWidth:r,visibleContentLines:k(e)},n),s=n,o},invalidate:()=>{o=void 0},handleInput:z({state:a,editor:c,tui:e,refresh:u,close:()=>r(void 0)})}},V=async e=>{e.hasUI?await e.ui.custom((e,t,n,r)=>B({tui:e,theme:t,done:r}),{overlay:!0,overlayOptions:{width:`90%`,maxHeight:`85%`,anchor:`center`}}):e.ui.notify(`/config requires interactive mode UI`,`warning`)},H=/(?:secret|password|passwd|token|apiKey|api[_-]?key|privateKey|private[_-]?key|accessKey|access[_-]?key|key$)/iu,U=e=>H.test(e),W=(e,t)=>{if(t.some(U))return{value:`[REDACTED]`,redacted:!0};if(Array.isArray(e)){let n=!1;return{value:e.map((e,r)=>{let i=W(e,t.concat(String(r)));return n||=i.redacted,i.value}),redacted:n}}if(!e||typeof e!=`object`)return{value:e,redacted:!1};let n=!1,r=Object.entries(e).map(([e,r])=>{let i=W(r,t.concat(e));return n||=i.redacted,[e,i.value]});return{value:Object.fromEntries(r),redacted:n}},G=e=>e.path?e.path.split(`.`).filter(Boolean):[],K=async({params:e,signal:t,ctx:n})=>{if(t?.aborted)return{content:[{type:`text`,text:`Config lookup cancelled.`}],details:{status:`error`,query:e,redacted:!1,sources:[],message:`Config lookup cancelled.`}};try{let t=await o(e,{cwd:n.cwd}),r=t.status===`found`?W(t.value,G(e)):{value:t.value,redacted:!1};return{content:[{type:`text`,text:t.status===`found`?`Config value for ${l(e)}:\n${u(r.value)}`:t.message??t.status}],details:{...t,value:r.value,redacted:r.redacted}}}catch(t){let n=t instanceof Error?t.message:String(t);return{content:[{type:`text`,text:n}],details:{status:`error`,query:e,redacted:!1,sources:[],message:n,filePath:t instanceof c?t.filePath:void 0}}}},q=({params:e})=>{let t=e.path.trim();if(!t){let e=`set_config requires a non-empty path.`;return{content:[{type:`text`,text:e}],details:{status:`error`,path:t,message:e}}}let r=structuredClone(i()??s),a=f({target:r,path:t,value:e.value});if(a===void 0){let e=`Could not set config path: ${t}`;return{content:[{type:`text`,text:e}],details:{status:`error`,path:t,message:e}}}return n(a),{content:[{type:`text`,text:`Session override set: ${t} = ${JSON.stringify(e.value)}`}],details:{status:`applied`,path:t,value:e.value}}},J=async({ctx:e})=>{await V(e)},Y=({ctx:e})=>{let t=r();e.ui.notify(t?`Session config override cleared.`:`No session override was set.`,`info`)};export{J as handleConfig,Y as handleConfigReset,K as handleGetConfig,q as handleSetConfig};
|
|
2
|
+
//# sourceMappingURL=handlers-B2Dm1nBF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handlers-B2Dm1nBF.js","names":["parseYaml","stringifyYaml","readConfigFiles","getConfig","setSessionConfigOverride","clearSessionConfigOverride","getSessionConfigOverride","ensureErrorMessage","queryConfig","EMPTY_PI_CORE_CONFIG","ConfigReadError","getQueryLabel","formatValue","TAB_LABELS","YAML_LANGUAGE","LOADING_LINES","BORDER_COLOR","createEditorTheme","theme","text","getStatusText","showOverride","parseOverrideInput","trimmed","value","parsed","parse","error","loadFileConfigLines","highlightCode","stringify","config","loadEffectiveConfigLines","getVisibleContentLineCount","tui","getEffectiveTabLines","state","clampScrollOffset","scrollOffset","lineCount","visibleContentLines","renderTopBorder","prefix","fillCount","width","visibleWidth","renderBottomBorder","wrapBodyLine","truncated","truncateToWidth","line","innerWidth","padding","renderTabBar","active","index","activeTab","label","renderEffectiveConfigTab","lines","allLines","offset","scrollHint","renderOverrideTab","editor","renderConfigWindow","context","bodyLines","createConfigWindowInputHandler","matchesKey","data","Key","close","refresh","createConfigWindow","cachedLines","cachedWidth","Editor","currentOverride","message","override","done","showConfigWindow","ctx","SENSITIVE_KEY_PATTERN","isSensitiveKey","key","redactValue","path","redacted","result","item","entries","childValue","getQueryPathSegments","query","handleGetConfig","signal","params","handleSetConfig","current","updated","set","handleConfig","handleConfigReset","hadOverride"],"sources":["../src/window.ts","../src/handlers.ts"],"sourcesContent":["import {\n highlightCode,\n type ExtensionCommandContext,\n type Theme,\n} from '@earendil-works/pi-coding-agent';\nimport {\n type Component,\n Editor,\n type EditorTheme,\n Key,\n matchesKey,\n truncateToWidth,\n type TUI,\n visibleWidth,\n} from '@earendil-works/pi-tui';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport type { PiCoreConfig } from './types.js';\nimport {\n ensureErrorMessage,\n getConfig,\n getSessionConfigOverride,\n readConfigFiles,\n setSessionConfigOverride,\n} from './utils.js';\n\n/** The title shown in the top border of the window. */\nconst WINDOW_TITLE = 'Config';\n/** The labels of the tabs, in order. */\nconst TAB_LABELS = ['Effective config', 'Override'] as const;\n/** The key that toggles the override on the effective config tab. */\nconst TOGGLE_OVERRIDE_KEY = 't';\n/** The language used to highlight the config. */\nconst YAML_LANGUAGE = 'yaml';\n/** The lines shown on the effective config tab while the config is loading. */\nconst LOADING_LINES = ['Loading…'];\n\n/** The theme color for the border of the window. */\nconst BORDER_COLOR = 'borderAccent';\n/** Columns spent on the left/right border chars and their padding: `│ ` + ` │`. */\nconst BORDER_OVERHEAD_WIDTH = 4;\n/** Columns spent on the top/bottom border corner chars: `┌`/`┐` or `└`/`┘`. */\nconst BORDER_CORNER_WIDTH = 2;\n\n/** The width of the window, relative to the terminal. */\nconst OVERLAY_WIDTH = '90%';\n/** The max height of the window, relative to the terminal. */\nconst OVERLAY_MAX_HEIGHT = '85%';\n/**\n * The same max height as {@link OVERLAY_MAX_HEIGHT}, as a ratio, to calculate how many\n * lines of content fit in the window.\n */\nconst OVERLAY_HEIGHT_RATIO = 0.85;\n/** The minimum number of content lines to show, even in very short terminals. */\nconst MIN_VISIBLE_CONTENT_LINES = 5;\n/**\n * Chrome lines around tab 1's scrollable YAML body: outer border/tab-bar/blanks (4) plus\n * tab 1's own status/blank/blank/hint (4).\n */\nconst EFFECTIVE_TAB_CHROME_LINES = 8;\n\n/** Which `/config` window tab is focused: 0 = effective config, 1 = override editor. */\ntype ConfigTab = 0 | 1;\n\n/** Feedback shown under the override editor after an Enter-submit attempt. */\ntype OverrideMessage = {\n /** Whether the submit worked, to pick the color of the message. */\n kind: 'success' | 'error';\n /** The text to show. */\n text: string;\n};\n\n/** Result of parsing tab 2's editor content into a new session override. */\ntype OverrideParseResult = {\n /** The new override. It's `undefined` when the override should be cleared, or on error. */\n override?: PiCoreConfig;\n /** The feedback for the user. */\n message: OverrideMessage;\n};\n\n/**\n * Mutable state for one open `/config` window. Held as a plain object (rather than\n * closure-local variables) so the render/input-handling functions can stay pure and be\n * composed instead of nesting all logic inside the window factory.\n */\ntype ConfigWindowState = {\n /** The tab that is focused. */\n activeTab: ConfigTab;\n /** Whether the effective config tab shows the session override applied. */\n showOverride: boolean;\n /** First visible line index into tab 1's current YAML text (manual scroll position). */\n scrollOffset: number;\n /** Syntax-highlighted lines for the file-based config, once loaded. */\n fileConfigLines?: string[];\n /** Syntax-highlighted lines for the override-inclusive effective config, once loaded. */\n effectiveConfigLines?: string[];\n /** The feedback of the last submit on the override tab, if there's one. */\n overrideMessage?: OverrideMessage;\n};\n\n/** Shared render context passed to the `/config` window's tab renderers. */\ntype ConfigWindowRenderContext = {\n /** The theme to color the window with. */\n theme: Theme;\n /** The state of the window. */\n state: ConfigWindowState;\n /** The editor of the override tab. */\n editor: Editor;\n /** Usable width inside the window's left/right border. */\n innerWidth: number;\n /** How many lines of the config fit in the effective config tab. */\n visibleContentLines: number;\n};\n\n/** Options for {@link createConfigWindowInputHandler}. */\ntype ConfigWindowInputHandlerOptions = {\n /** The state of the window, which the handler updates. */\n state: ConfigWindowState;\n /** The editor of the override tab, which receives the input of that tab. */\n editor: Editor;\n /** The TUI, to know the size of the terminal. */\n tui: TUI;\n /** Re-renders the window. */\n refresh: () => void;\n /** Closes the window. */\n close: () => void;\n};\n\n/** Options for {@link createConfigWindow}. */\ntype CreateConfigWindowOptions = {\n /** The TUI that will render the window. */\n tui: TUI;\n /** The theme to color the window with. */\n theme: Theme;\n /** Closes the window. */\n done: (result: unknown) => void;\n};\n\n/**\n * Adapts Pi's theme object to the embedded editor theme.\n *\n * @param theme Pi's theme.\n * @returns The theme for the editor.\n */\nconst createEditorTheme = (theme: Theme): EditorTheme => ({\n borderColor: (text) => theme.fg('accent', text),\n selectList: {\n selectedPrefix: (text) => theme.fg('accent', text),\n selectedText: (text) => theme.fg('accent', text),\n description: (text) => theme.fg('muted', text),\n scrollInfo: (text) => theme.fg('dim', text),\n noMatch: (text) => theme.fg('warning', text),\n },\n});\n\n/**\n * Renders the current session-override status line for tab 1.\n *\n * @param showOverride Whether the tab is showing the override applied.\n * @returns The text of the status line.\n */\nconst getStatusText = (showOverride: boolean): string => {\n const override = getSessionConfigOverride();\n if (!override) return 'No session override set';\n return showOverride\n ? 'Session override set — shown (t to hide)'\n : 'Session override set — hidden (t to show)';\n};\n\n/**\n * Parses tab 2's editor content into a new override, or reports why it was rejected.\n *\n * @param value The content of the editor.\n * @returns The new override (or none, if it should be cleared) and the feedback for the\n * user.\n */\nconst parseOverrideInput = (value: string): OverrideParseResult => {\n const trimmed = value.trim();\n if (!trimmed) {\n return {\n override: undefined,\n message: { kind: 'success', text: 'Session override cleared.' },\n };\n }\n\n let parsed: unknown;\n try {\n parsed = parseYaml(trimmed);\n } catch (error) {\n return {\n override: undefined,\n message: { kind: 'error', text: `Invalid YAML: ${ensureErrorMessage(error)}` },\n };\n }\n\n if (typeof parsed === 'undefined' || parsed === null) {\n return {\n override: undefined,\n message: { kind: 'success', text: 'Session override cleared.' },\n };\n }\n\n if (typeof parsed === 'object' && !Array.isArray(parsed)) {\n return {\n override: parsed as PiCoreConfig,\n message: { kind: 'success', text: 'Session override applied.' },\n };\n }\n\n return {\n override: undefined,\n message: { kind: 'error', text: 'Override must be a YAML mapping (object).' },\n };\n};\n\n/**\n * Loads the file-based config, syntax-highlighted, or an error message on failure.\n *\n * @returns The lines to show.\n */\nconst loadFileConfigLines = async (): Promise<string[]> => {\n try {\n const { config } = await readConfigFiles();\n return highlightCode(stringifyYaml(config), YAML_LANGUAGE);\n } catch (error) {\n return [`Failed to load config: ${ensureErrorMessage(error)}`];\n }\n};\n\n/**\n * Loads the override-inclusive effective config, syntax-highlighted, or an error message\n * on failure.\n *\n * @returns The lines to show.\n */\nconst loadEffectiveConfigLines = async (): Promise<string[]> => {\n try {\n return highlightCode(stringifyYaml(await getConfig()), YAML_LANGUAGE);\n } catch (error) {\n return [`Failed to load config: ${ensureErrorMessage(error)}`];\n }\n};\n\n/**\n * Gets how many YAML content lines fit in the window, given the terminal's height.\n *\n * @param tui The TUI, to know the size of the terminal.\n * @returns The number of lines.\n */\nconst getVisibleContentLineCount = (tui: TUI): number =>\n Math.max(\n MIN_VISIBLE_CONTENT_LINES,\n Math.floor(tui.terminal.rows * OVERLAY_HEIGHT_RATIO) - EFFECTIVE_TAB_CHROME_LINES,\n );\n\n/**\n * Gets tab 1's current syntax-highlighted YAML lines.\n *\n * @param state The state of the window.\n * @returns The lines, or a loading message if they are not ready yet.\n */\nconst getEffectiveTabLines = (state: ConfigWindowState): string[] =>\n (state.showOverride ? state.effectiveConfigLines : state.fileConfigLines) ??\n LOADING_LINES;\n\n/**\n * Clamps a scroll offset to the valid range for the given content/viewport sizes.\n *\n * @param scrollOffset The offset to clamp.\n * @param lineCount The total number of lines of the content.\n * @param visibleContentLines How many lines of the content fit in the window.\n * @returns The offset, within the valid range.\n */\nconst clampScrollOffset = (\n scrollOffset: number,\n lineCount: number,\n visibleContentLines: number,\n): number =>\n Math.min(Math.max(0, scrollOffset), Math.max(0, lineCount - visibleContentLines));\n\n/**\n * Renders the window's top border row, with the title embedded.\n *\n * @param theme The theme to color the border with.\n * @param width The width of the window.\n * @returns The line.\n */\nconst renderTopBorder = (theme: Theme, width: number): string => {\n const prefix = `┌─ ${WINDOW_TITLE} `;\n const fillCount = Math.max(0, width - visibleWidth(prefix) - 1);\n return theme.fg(BORDER_COLOR, `${prefix}${'─'.repeat(fillCount)}┐`);\n};\n\n/**\n * Renders the window's bottom border row.\n *\n * @param theme The theme to color the border with.\n * @param width The width of the window.\n * @returns The line.\n */\nconst renderBottomBorder = (theme: Theme, width: number): string =>\n theme.fg(BORDER_COLOR, `└${'─'.repeat(Math.max(0, width - BORDER_CORNER_WIDTH))}┘`);\n\n/**\n * Wraps one body line with the window's left/right border, padded to `innerWidth`.\n *\n * @param theme The theme to color the border with.\n * @param line The line to wrap.\n * @param innerWidth The usable width inside the borders.\n * @returns The wrapped line.\n */\nconst wrapBodyLine = (theme: Theme, line: string, innerWidth: number): string => {\n const truncated = truncateToWidth(line, innerWidth);\n const padding = ' '.repeat(Math.max(0, innerWidth - visibleWidth(truncated)));\n return `${theme.fg(BORDER_COLOR, '│')} ${truncated}${padding} ${theme.fg(BORDER_COLOR, '│')}`;\n};\n\n/**\n * Renders the tab bar row, highlighting the active tab.\n *\n * @param theme The theme to color the tabs with.\n * @param activeTab The tab that is focused.\n * @returns The line.\n */\nconst renderTabBar = (theme: Theme, activeTab: ConfigTab): string =>\n TAB_LABELS.map((label, index) => {\n const active = index === activeTab;\n return theme.fg(active ? 'accent' : 'muted', active ? `[${label}]` : ` ${label} `);\n }).join(' ');\n\n/**\n * Renders tab 1 (\"Effective config\") body lines, scrolled to the current offset.\n *\n * @param context The render context, with only the properties this tab uses.\n * @returns The lines of the tab.\n */\nconst renderEffectiveConfigTab = ({\n theme,\n state,\n visibleContentLines,\n}: Pick<\n ConfigWindowRenderContext,\n 'theme' | 'state' | 'visibleContentLines'\n>): string[] => {\n const lines: string[] = [];\n lines.push(theme.fg('muted', getStatusText(state.showOverride)));\n lines.push('');\n\n const allLines = getEffectiveTabLines(state);\n const offset = clampScrollOffset(\n state.scrollOffset,\n allLines.length,\n visibleContentLines,\n );\n lines.push(...allLines.slice(offset, offset + visibleContentLines));\n\n lines.push('');\n const scrollHint =\n allLines.length > visibleContentLines\n ? `(${offset + 1}-${Math.min(offset + visibleContentLines, allLines.length)}/${allLines.length}) ↑↓/PgUp/PgDn scroll • `\n : '';\n lines.push(\n theme.fg('dim', `${scrollHint}Tab switch tab • t toggle override • Esc close`),\n );\n return lines;\n};\n\n/**\n * Renders tab 2 (\"Override\") body lines.\n *\n * @param context The render context.\n * @returns The lines of the tab.\n */\nconst renderOverrideTab = ({\n theme,\n state,\n editor,\n innerWidth,\n}: ConfigWindowRenderContext): string[] => {\n const lines: string[] = [];\n lines.push(theme.fg('muted', 'Session override (YAML). Empty clears it.'));\n lines.push('');\n lines.push(...editor.render(innerWidth));\n if (state.overrideMessage) {\n lines.push('');\n lines.push(\n theme.fg(\n state.overrideMessage.kind === 'error' ? 'warning' : 'success',\n state.overrideMessage.text,\n ),\n );\n }\n lines.push('');\n lines.push(\n theme.fg(\n 'dim',\n 'Enter apply/clear • Shift+Enter newline • Tab switch tab • Esc close',\n ),\n );\n return lines;\n};\n\n/**\n * Renders the full `/config` window for the given terminal width.\n *\n * @param context The render context.\n * @param width The width of the window.\n * @returns The lines of the window.\n */\nconst renderConfigWindow = (\n context: ConfigWindowRenderContext,\n width: number,\n): string[] => {\n const { theme, state } = context;\n const bodyLines = [\n renderTabBar(theme, state.activeTab),\n '',\n ...(state.activeTab === 0\n ? renderEffectiveConfigTab(context)\n : renderOverrideTab(context)),\n ];\n const lines = [\n renderTopBorder(theme, width),\n ...bodyLines.map((line) => wrapBodyLine(theme, line, context.innerWidth)),\n renderBottomBorder(theme, width),\n ];\n return lines.map((line) => truncateToWidth(line, width));\n};\n\n/**\n * Builds the `handleInput` handler for an open `/config` window.\n *\n * @param options The state and the actions the handler needs.\n * @returns The handler, which receives the raw input of the terminal.\n */\nconst createConfigWindowInputHandler =\n ({\n state,\n editor,\n tui,\n refresh,\n close,\n }: ConfigWindowInputHandlerOptions): ((data: string) => void) =>\n (data) => {\n if (matchesKey(data, Key.escape)) {\n close();\n return;\n }\n if (matchesKey(data, Key.tab) || matchesKey(data, Key.shift('tab'))) {\n state.activeTab = state.activeTab === 0 ? 1 : 0;\n refresh();\n return;\n }\n if (state.activeTab === 0) {\n if (data.toLowerCase() === TOGGLE_OVERRIDE_KEY) {\n state.showOverride = !state.showOverride;\n state.scrollOffset = 0;\n refresh();\n return;\n }\n\n const visibleContentLines = getVisibleContentLineCount(tui);\n const lineCount = getEffectiveTabLines(state).length;\n let scrollOffset: number | undefined;\n if (matchesKey(data, Key.up)) scrollOffset = state.scrollOffset - 1;\n else if (matchesKey(data, Key.down)) scrollOffset = state.scrollOffset + 1;\n else if (matchesKey(data, Key.pageUp))\n scrollOffset = state.scrollOffset - visibleContentLines;\n else if (matchesKey(data, Key.pageDown))\n scrollOffset = state.scrollOffset + visibleContentLines;\n else if (matchesKey(data, Key.home)) scrollOffset = 0;\n else if (matchesKey(data, Key.end)) scrollOffset = lineCount;\n\n if (typeof scrollOffset !== 'undefined') {\n state.scrollOffset = clampScrollOffset(\n scrollOffset,\n lineCount,\n visibleContentLines,\n );\n refresh();\n }\n return;\n }\n editor.handleInput(data);\n refresh();\n };\n\n/**\n * Creates the `/config` window component: a 2-tab effective-config/override viewer.\n *\n * @param options The TUI, the theme, and the function to close the window.\n * @returns The component.\n */\nconst createConfigWindow = ({\n tui,\n theme,\n done,\n}: CreateConfigWindowOptions): Component => {\n const state: ConfigWindowState = { activeTab: 0, showOverride: false, scrollOffset: 0 };\n let cachedLines: string[] | undefined;\n let cachedWidth: number | undefined;\n\n const editor = new Editor(tui, createEditorTheme(theme));\n const currentOverride = getSessionConfigOverride();\n if (currentOverride) editor.setText(stringifyYaml(currentOverride));\n\n const refresh = (): void => {\n cachedLines = undefined;\n tui.requestRender();\n };\n\n loadFileConfigLines().then((lines) => {\n state.fileConfigLines = lines;\n refresh();\n });\n loadEffectiveConfigLines().then((lines) => {\n state.effectiveConfigLines = lines;\n refresh();\n });\n\n editor.onSubmit = (value) => {\n const { override, message } = parseOverrideInput(value);\n if (message.kind === 'success') {\n setSessionConfigOverride(override);\n editor.setText(override ? stringifyYaml(override) : '');\n } else {\n // submitValue() clears the editor before calling onSubmit; restore the rejected\n // text so the user can fix it instead of retyping from scratch.\n editor.setText(value);\n }\n state.overrideMessage = message;\n loadEffectiveConfigLines().then((lines) => {\n state.effectiveConfigLines = lines;\n refresh();\n });\n refresh();\n };\n\n const handleInput = createConfigWindowInputHandler({\n state,\n editor,\n tui,\n refresh,\n close: () => done(undefined),\n });\n\n return {\n render: (width) => {\n if (cachedLines && cachedWidth === width) return cachedLines;\n const innerWidth = Math.max(1, width - BORDER_OVERHEAD_WIDTH);\n cachedLines = renderConfigWindow(\n {\n theme,\n state,\n editor,\n innerWidth,\n visibleContentLines: getVisibleContentLineCount(tui),\n },\n width,\n );\n cachedWidth = width;\n return cachedLines;\n },\n invalidate: () => {\n cachedLines = undefined;\n },\n handleInput,\n };\n};\n\n/**\n * Opens the `/config` window, or reports that interactive UI is required.\n *\n * @param ctx The context of the command that opened the window.\n */\nconst showConfigWindow = async (ctx: ExtensionCommandContext): Promise<void> => {\n if (!ctx.hasUI) {\n ctx.ui.notify('/config requires interactive mode UI', 'warning');\n return;\n }\n await ctx.ui.custom<unknown>(\n (tui, theme, _keybindings, done) => createConfigWindow({ tui, theme, done }),\n {\n overlay: true,\n overlayOptions: {\n width: OVERLAY_WIDTH,\n maxHeight: OVERLAY_MAX_HEIGHT,\n anchor: 'center',\n },\n },\n );\n};\n\nexport { showConfigWindow };\n","import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';\nimport { set } from '@homer0/object-utils';\nimport { ConfigReadError } from './errors.js';\nimport { formatValue, getQueryLabel } from './format.js';\nimport type { GetConfigParams, SetConfigParams } from './schemas.js';\nimport type { ConfigQuery, PiCoreConfig, SetConfigDetails } from './types.js';\nimport {\n clearSessionConfigOverride,\n EMPTY_PI_CORE_CONFIG,\n getSessionConfigOverride,\n queryConfig,\n setSessionConfigOverride,\n} from './utils.js';\nimport { showConfigWindow } from './window.js';\n\n/** The text that replaces a value that looks like a secret. */\nconst REDACTED_VALUE = '[REDACTED]';\n/** The pattern of the keys that look like they would contain secrets. */\nconst SENSITIVE_KEY_PATTERN =\n /(?:secret|password|passwd|token|apiKey|api[_-]?key|privateKey|private[_-]?key|accessKey|access[_-]?key|key$)/iu;\n\n/**\n * Checks whether a config path segment is likely to contain secret material.\n *\n * @param key The segment of the path.\n * @returns Whether it looks like a secret.\n */\nconst isSensitiveKey = (key: string): boolean => SENSITIVE_KEY_PATTERN.test(key);\n\n/**\n * Redacts model-facing config values while preserving raw access through the Node API.\n *\n * @param value The value to redact.\n * @param path The segments of the path the value is at. If any of them looks like a\n * secret, the whole value is redacted.\n * @returns The value, and whether something was redacted.\n */\nconst redactValue = (\n value: unknown,\n path: string[],\n): { value: unknown; redacted: boolean } => {\n if (path.some(isSensitiveKey)) return { value: REDACTED_VALUE, redacted: true };\n\n if (Array.isArray(value)) {\n let redacted = false;\n const items = value.map((item, index) => {\n const result = redactValue(item, path.concat(String(index)));\n redacted ||= result.redacted;\n return result.value;\n });\n return { value: items, redacted };\n }\n\n if (!value || typeof value !== 'object') return { value, redacted: false };\n\n let redacted = false;\n const entries = Object.entries(value).map(([key, childValue]) => {\n const result = redactValue(childValue, path.concat(key));\n redacted ||= result.redacted;\n return [key, result.value] as const;\n });\n\n return { value: Object.fromEntries(entries), redacted };\n};\n\n/**\n * Gets the segments of the query path, when the lookup targets a specific path.\n *\n * @param query The query of the lookup.\n * @returns The segments, or an empty list if the query doesn't have a path.\n */\nconst getQueryPathSegments = (query: ConfigQuery): string[] =>\n query.path ? query.path.split('.').filter(Boolean) : [];\n\n/** Options for {@link handleGetConfig}. */\ntype HandleGetConfigOptions = {\n /** The parameters the model sent. */\n params: GetConfigParams;\n /** The signal to abort the lookup. */\n signal: AbortSignal | undefined;\n /** The context of the session, to know the project directory. */\n ctx: { cwd: string };\n};\n\n/**\n * Executes the `get_config` tool: reads, redacts and formats the requested config.\n *\n * @param options The parameters of the call and the context it was made in.\n * @returns The result of the tool, with the text for the model and the details for the\n * TUI. Failures are reported in the result, they don't throw.\n */\nexport const handleGetConfig = async ({\n params,\n signal,\n ctx,\n}: HandleGetConfigOptions) => {\n if (signal?.aborted) {\n return {\n content: [{ type: 'text' as const, text: 'Config lookup cancelled.' }],\n details: {\n status: 'error',\n query: params,\n redacted: false,\n sources: [],\n message: 'Config lookup cancelled.',\n },\n };\n }\n\n try {\n const result = await queryConfig(params, { cwd: ctx.cwd });\n const redacted =\n result.status === 'found'\n ? redactValue(result.value, getQueryPathSegments(params))\n : { value: result.value, redacted: false };\n const text =\n result.status === 'found'\n ? `Config value for ${getQueryLabel(params)}:\\n${formatValue(redacted.value)}`\n : (result.message ?? result.status);\n\n return {\n content: [{ type: 'text' as const, text }],\n details: {\n ...result,\n value: redacted.value,\n redacted: redacted.redacted,\n },\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n content: [{ type: 'text' as const, text: message }],\n details: {\n status: 'error',\n query: params,\n redacted: false,\n sources: [],\n message,\n filePath: error instanceof ConfigReadError ? error.filePath : undefined,\n },\n };\n }\n};\n\n/** Options for {@link handleSetConfig}. */\ntype HandleSetConfigOptions = {\n /** The parameters the model sent. */\n params: SetConfigParams;\n};\n\n/**\n * Executes the `set_config` tool: applies an in-memory session config override.\n *\n * @param options The parameters of the call.\n * @returns The result of the tool, with the text for the model and the details for the\n * TUI. Failures are reported in the result, they don't throw.\n */\nexport const handleSetConfig = ({ params }: HandleSetConfigOptions) => {\n const path = params.path.trim();\n\n if (!path) {\n const message = 'set_config requires a non-empty path.';\n return {\n content: [{ type: 'text' as const, text: message }],\n details: { status: 'error', path, message } satisfies SetConfigDetails,\n };\n }\n\n const current = structuredClone(getSessionConfigOverride() ?? EMPTY_PI_CORE_CONFIG);\n const updated = set<PiCoreConfig>({ target: current, path, value: params.value });\n\n if (typeof updated === 'undefined') {\n const message = `Could not set config path: ${path}`;\n return {\n content: [{ type: 'text' as const, text: message }],\n details: { status: 'error', path, message } satisfies SetConfigDetails,\n };\n }\n\n setSessionConfigOverride(updated);\n const text = `Session override set: ${path} = ${JSON.stringify(params.value)}`;\n return {\n content: [{ type: 'text' as const, text }],\n details: {\n status: 'applied',\n path,\n value: params.value,\n } satisfies SetConfigDetails,\n };\n};\n\n/** Options for {@link handleConfig}. */\ntype HandleConfigOptions = {\n /** The context of the command. */\n ctx: ExtensionCommandContext;\n};\n\n/**\n * Handles `/config`: opens the config window.\n *\n * @param options The context of the command.\n */\nexport const handleConfig = async ({ ctx }: HandleConfigOptions): Promise<void> => {\n await showConfigWindow(ctx);\n};\n\n/** Options for {@link handleConfigReset}. */\ntype HandleConfigResetOptions = {\n /** The context of the command. */\n ctx: ExtensionCommandContext;\n};\n\n/**\n * Handles `/config-reset`: clears the session config override, and tells the user if\n * there was one.\n *\n * @param options The context of the command.\n */\nexport const handleConfigReset = ({ ctx }: HandleConfigResetOptions): void => {\n const hadOverride = clearSessionConfigOverride();\n ctx.ui.notify(\n hadOverride ? 'Session config override cleared.' : 'No session override was set.',\n 'info',\n );\n};\n"],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{d as ConfigValidationError,i as getConfig,a as getConfigValue,s as queryConfig}from"./utils-DYlB6FdR.js";
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{CONFIG_DIR_NAME as e,getAgentDir as t}from"@earendil-works/pi-coding-agent";import{get as n,merge as r}from"@homer0/object-utils";import{readFile as i}from"node:fs/promises";import{homedir as a}from"node:os";import{dirname as o,extname as s,isAbsolute as c,join as l}from"node:path";import{parse as u}from"yaml";import{z as d}from"zod";var f=class extends Error{filePath;constructor(e,t,n){super(e,n),this.filePath=t,this.name=`ConfigReadError`}},p=class extends Error{issues;constructor(e,t){super(e),this.issues=t,this.name=`ConfigValidationError`}};const m=(...e)=>l(t(),...e),h=e=>e instanceof d.ZodError?e.issues.map(e=>e.message).join(`; `):e instanceof Error?e.message:String(e),g=l(a(),e),_=t(),v=m(`profiles`),y=/^profile:([^:]+):(.+)$/,b=(e,t)=>{if(e.startsWith(`pi:`))return l(g,e.slice(3));if(e.startsWith(`agent:`))return l(_,e.slice(6));if(c(e))return e;let n=y.exec(e);if(n){let[,e,t]=n;return l(v,e,t)}return l(t,e)},x={pi:{},skills:{},tools:{},extensions:{}},S=globalThis,C=e=>{S.__piConfigSessionOverride=e},w=()=>S.__piConfigSessionOverride,T=()=>{let e=S.__piConfigSessionOverride!==void 0;return S.__piConfigSessionOverride=void 0,e},E=(e,t,n)=>new f(e,t,{cause:n}),D=(e,t)=>s(t)===`.json`?JSON.parse(e):u(e),O=e=>s(e)===`.json`?`JSON`:`YAML`,k=e=>{console.warn(`[pi-core config] ${e.message}`)},A=async e=>{let t;try{t=await i(e,`utf8`)}catch(t){return t&&typeof t==`object`&&`code`in t&&t.code===`ENOENT`?{exists:!1}:{exists:!1,error:E(`Failed to read config file ${e}`,e,t)}}try{return{exists:!0,value:D(t,e)}}catch(t){let n=O(e);return{exists:!1,error:E(`Invalid ${n} in config file ${e}`,e,t)}}},j=async(e,t=/* @__PURE__ */ new Set)=>{if(t.has(e))return{exists:!1,error:E(`Circular extends detected for config file ${e}`,e,void 0)};let n=await A(e);if(!n.exists)return n;let i=n.value,a=i.extends;if(typeof a!=`string`||!a.trim())return n;let s=new Set(t);s.add(e);let c=b(a.trim(),o(e)),l=await j(c,s),{extends:u,...d}=i;return l.exists?{exists:!0,value:r({},l.value,d)}:(l.error&&k(l.error),{exists:!0,value:d})},M=[`.json`,`.yml`,`.yaml`],N=(e,t,n,r)=>M.map(i=>({path:l(e,`${t}${i}`),scope:n,local:r,exists:!1,used:!1})),P=t=>[...N(_,`config`,`global`,!1),...N(_,`config.local`,`global`,!0),...N(l(t,e),`config`,`project`,!1),...N(l(t,e),`config.local`,`project`,!0)],F=e=>{if(e.path)return e.path;if(e.scope&&e.scope!==`all`)return e.scope===`pi`||e.scope===`skills`||e.scope===`tools`||e.scope===`extensions`?e.scope:e.scope===`skill`?`skills.${e.name}`:e.scope===`tool`?`tools.${e.name}`:`extensions.${e.name}`},I=e=>{if(e.path&&(e.scope||e.name))return`Use either path or scope/name, not both.`;if((e.scope===`skill`||e.scope===`tool`||e.scope===`extension`)&&!e.name)return`scope '${e.scope}' requires name.`;if(e.name&&e.scope!==`skill`&&e.scope!==`tool`&&e.scope!==`extension`)return`name is only valid with skill, tool, or extension scope.`},L=(e,t)=>{let n=t.safeParse(e);if(n.success)return n.data;throw new p(`Config value failed schema validation.`,n.error.issues)},R=async(e=process.cwd())=>{let t=[],n=[];for(let r of P(e)){let e=await j(r.path);!e.exists&&e.error&&k(e.error),t.push({...r,exists:e.exists,used:e.exists}),e.exists&&n.push(e.value)}return{config:r(x,...n),sources:t}},z=e=>{let t=w();return t===void 0?e:r({},e,t)},B=async(e={})=>{let t=await R(e.cwd),n=z(t.config);return e.schema?L(n,e.schema):n},V=async(e,t={})=>{let r=await B({cwd:t.cwd}),i=n(r,e);if(i!==void 0)return t.schema?L(i,t.schema):i},H=async(e,t={})=>{let r=I(e);if(r)return{status:`error`,query:e,redacted:!1,sources:[],message:r};let{config:i,sources:a}=await R(t.cwd),o=z(i),s=F(e),c=s?n(o,s):o;return c===void 0?{status:`not_found`,query:e,redacted:!1,sources:a,message:`Config path not found: ${s}`}:{status:`found`,query:e,value:c,redacted:!1,sources:a}};export{V as a,R as c,p as d,B as i,C as l,T as n,w as o,h as r,H as s,x as t,f as u};
|
|
2
|
+
//# sourceMappingURL=utils-DYlB6FdR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils-DYlB6FdR.js","names":["parseYaml","ConfigReadError","message","options","filePath","ConfigValidationError","issues","getAgentPath","segments","join","getAgentDir","ensureErrorMessage","error","z","issue","PI_DIR","homedir","CONFIG_DIR_NAME","AGENT_DIR","PROFILES_DIR","PROFILE_PATTERN","resolvePiPath","value","isAbsolute","profileMatch","profileName","profilePath","baseDir","EMPTY_PI_CORE_CONFIG","sessionStore","setSessionConfigOverride","getSessionConfigOverride","clearSessionConfigOverride","hadOverride","toReadError","parseConfigFile","extname","content","parse","getConfigFileFormat","logConfigReadError","readConfigIfExists","readFile","format","readConfigResolvingExtends","seen","result","raw","extendsValue","\"extends\"","nextSeen","extendsPath","dirname","baseResult","_","currentWithoutExtends","merge","CONFIG_EXTENSIONS","getConfigFileVariants","directory","basename","extension","getConfigFilePaths","cwd","getConfigPath","query","validateConfigQuery","validateConfigValue","schema","readConfigFiles","sources","configs","source","applySessionConfigOverride","override","config","getConfig","getConfigValue","get","path","queryConfig","validationError","fileConfig"],"sources":["../src/errors.ts","../src/utils.ts"],"sourcesContent":["/* eslint-disable max-classes-per-file -- The config errors live together. */\nimport type { z } from 'zod';\n\n/** The error for the config files that couldn't be read or parsed. */\nexport class ConfigReadError extends Error {\n /** The path of the config file that failed. */\n readonly filePath: string;\n\n /**\n * @param message The description of what failed.\n * @param filePath The path of the config file that failed.\n * @param options The error options, to preserve the original `cause`.\n */\n constructor(message: string, filePath: string, options?: ErrorOptions) {\n super(message, options);\n this.filePath = filePath;\n this.name = 'ConfigReadError';\n }\n}\n\n/** The error thrown when the resolved config doesn't match the schema that was provided. */\nexport class ConfigValidationError extends Error {\n /** The Zod issues that explain why the value didn't match the schema. */\n readonly issues: z.core.$ZodIssue[];\n\n /**\n * @param message The description of what failed.\n * @param issues The Zod issues that explain why the value didn't match the schema.\n */\n constructor(message: string, issues: z.core.$ZodIssue[]) {\n super(message);\n this.issues = issues;\n this.name = 'ConfigValidationError';\n }\n}\n","import { CONFIG_DIR_NAME, getAgentDir } from '@earendil-works/pi-coding-agent';\nimport { get, merge } from '@homer0/object-utils';\nimport { readFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { dirname, extname, isAbsolute, join } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport { z } from 'zod';\nimport { ConfigReadError, ConfigValidationError } from './errors.js';\nimport type {\n ConfigPath,\n ConfigPathValue,\n ConfigQuery,\n ConfigSource,\n ConfigSchema,\n GetConfigOptions,\n PiCoreConfig,\n QueryConfigOptions,\n QueryConfigResult,\n ReadConfigResult,\n} from './types.js';\n\n/**\n * Resolves a path relative to the Pi agent directory.\n *\n * @param segments The segments of the path, relative to the agent directory.\n * @returns The absolute path.\n */\nconst getAgentPath = (...segments: string[]): string => join(getAgentDir(), ...segments);\n\n/**\n * Returns the error message string from any thrown value. Prettifies zod validation\n * errors instead of surfacing their raw JSON `.message`.\n *\n * @param error The thrown value.\n * @returns The message of the error, or the value as a string if it's not an error.\n */\nconst ensureErrorMessage = (error: unknown): string => {\n if (error instanceof z.ZodError) {\n return error.issues.map((issue) => issue.message).join('; ');\n }\n return error instanceof Error ? error.message : String(error);\n};\n\n/** The Pi directory, in the home of the user. */\nconst PI_DIR = join(homedir(), CONFIG_DIR_NAME);\n/** The absolute path of Pi's agent directory, where the global config files live. */\nconst AGENT_DIR = getAgentDir();\n/** The directory where the profiles are. */\nconst PROFILES_DIR = getAgentPath('profiles');\n\n/** The prefix of the paths relative to the Pi directory. */\nconst PI_PREFIX = 'pi:';\n/** The prefix of the paths relative to the agent directory. */\nconst AGENT_PREFIX = 'agent:';\n/** The pattern of the paths relative to a profile; it captures the name and the path. */\nconst PROFILE_PATTERN = /^profile:([^:]+):(.+)$/;\n\n/**\n * Resolves a pi-style path reference to an absolute path.\n *\n * Supported formats:\n *\n * - `pi:<path>` — relative to the Pi directory (`~/.pi/`)\n * - `agent:<path>` — relative to the agent directory (`~/.pi/agent/`)\n * - `profile:<name>:<path>` — relative to a profile directory\n * (`~/.pi/agent/profiles/<name>/`)\n * - Absolute path — used as-is\n * - Anything else — resolved relative to `baseDir`\n *\n * @example <caption>Relative to the agent directory</caption>\n * resolvePiPath('agent:shared/base.yaml', '/project');\n *\n * @example <caption>Relative to the file that references it</caption>\n * resolvePiPath('../base.yaml', '/project/.pi');\n *\n * @param value The path reference.\n * @param baseDir The directory to resolve the path from, when it doesn't have a prefix\n * and it's not absolute.\n * @returns The absolute path.\n */\nconst resolvePiPath = (value: string, baseDir: string): string => {\n if (value.startsWith(PI_PREFIX)) return join(PI_DIR, value.slice(PI_PREFIX.length));\n if (value.startsWith(AGENT_PREFIX))\n return join(AGENT_DIR, value.slice(AGENT_PREFIX.length));\n if (isAbsolute(value)) return value;\n\n const profileMatch = PROFILE_PATTERN.exec(value);\n if (profileMatch) {\n const [, profileName, profilePath] = profileMatch;\n return join(PROFILES_DIR, profileName!, profilePath!);\n }\n return join(baseDir, value);\n};\n\n/**\n * The base config shape with all the top-level namespaces present, so consumers never see\n * missing keys.\n */\nconst EMPTY_PI_CORE_CONFIG: PiCoreConfig = {\n pi: {},\n skills: {},\n tools: {},\n extensions: {},\n};\n\n/**\n * Session-scoped config override, held on `globalThis` rather than as module state or\n * passed through `ExtensionAPI`/`ctx`: Pi loads each extension with its own copy of the\n * modules it imports, so module state would not be shared between extensions. One pi\n * process is assumed to correspond to one session.\n *\n * This is `globalThis` typed with the property that holds the override. The assertion is\n * the only place that needs to know about it.\n */\nconst sessionStore = globalThis as typeof globalThis & {\n __piConfigSessionOverride?: PiCoreConfig;\n};\n\n/**\n * Replaces the session-scoped config override wholesale, or clears it when `undefined`.\n *\n * @param value The new override, or `undefined` to clear it.\n */\nconst setSessionConfigOverride = (value: PiCoreConfig | undefined): void => {\n sessionStore.__piConfigSessionOverride = value;\n};\n\n/**\n * Gets the current session-scoped config override.\n *\n * @returns The override, or `undefined` if there's none.\n */\nconst getSessionConfigOverride = (): PiCoreConfig | undefined =>\n sessionStore.__piConfigSessionOverride;\n\n/**\n * Clears the session-scoped config override.\n *\n * @returns Whether an override was actually set.\n */\nconst clearSessionConfigOverride = (): boolean => {\n const hadOverride = typeof sessionStore.__piConfigSessionOverride !== 'undefined';\n sessionStore.__piConfigSessionOverride = undefined;\n return hadOverride;\n};\n\n/**\n * The result of reading a config file: either its parsed value, or that it doesn't exist.\n * When the file couldn't be read or parsed, and it's not just missing, the result also\n * has the error.\n */\ntype ReadConfigFileResult =\n | { exists: true; value: unknown }\n | { exists: false; error?: ConfigReadError };\n\n/** The formats a config file can have, used to describe errors. */\ntype ConfigFileFormat = 'JSON' | 'YAML';\n\n/**\n * Creates a read error with the file path as context, preserving the original cause.\n *\n * @param message The description of what failed.\n * @param filePath The path of the config file that failed.\n * @param error The original error.\n * @returns The new error.\n */\nconst toReadError = (\n message: string,\n filePath: string,\n error: unknown,\n): ConfigReadError => new ConfigReadError(message, filePath, { cause: error });\n\n/**\n * Parses the content of a config file, based on the extension of the file.\n *\n * @param content The content of the file.\n * @param filePath The path of the file, to know its format.\n * @returns The parsed value.\n * @throws If the content is not valid for the format of the file.\n */\nconst parseConfigFile = (content: string, filePath: string): unknown => {\n const extension = extname(filePath);\n if (extension === '.json') return JSON.parse(content);\n return parseYaml(content);\n};\n\n/**\n * Gets the display label for the format of a config file.\n *\n * @param filePath The path of the config file.\n * @returns `JSON` for `.json` files, and `YAML` for everything else.\n */\nconst getConfigFileFormat = (filePath: string): ConfigFileFormat =>\n extname(filePath) === '.json' ? 'JSON' : 'YAML';\n\n/**\n * Logs a non-fatal config read issue, without interrupting the resolution chain.\n *\n * @param error The error to log.\n */\nconst logConfigReadError = (error: ConfigReadError): void => {\n // eslint-disable-next-line no-console -- Non-fatal config read failures should be visible without breaking the config chain.\n console.warn(`[pi-core config] ${error.message}`);\n};\n\n/**\n * Reads and parses a config file, returning the missing or invalid files as non-fatal\n * results.\n *\n * @param filePath The path of the config file.\n * @returns The parsed value, or that the file doesn't exist; if it failed for another\n * reason than being missing, the result has the error.\n */\nconst readConfigIfExists = async (filePath: string): Promise<ReadConfigFileResult> => {\n let content: string;\n\n try {\n content = await readFile(filePath, 'utf8');\n } catch (error) {\n if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')\n return { exists: false };\n return {\n exists: false,\n error: toReadError(`Failed to read config file ${filePath}`, filePath, error),\n };\n }\n\n try {\n return { exists: true, value: parseConfigFile(content, filePath) };\n } catch (error) {\n const format = getConfigFileFormat(filePath);\n return {\n exists: false,\n error: toReadError(`Invalid ${format} in config file ${filePath}`, filePath, error),\n };\n }\n};\n\n/**\n * Reads a config file and recursively resolves its `extends` chain.\n *\n * The `extends` key is stripped from the returned value; the base config is deep-merged\n * first and the current file's own keys win. Circular references are detected via\n * `seen`.\n *\n * @param filePath The path of the config file.\n * @param seen The files that are already part of the chain, to detect circular\n * references.\n * @returns The resolved value, or that the file doesn't exist. A base file that can't be\n * read doesn't make the file fail: the error is logged and the file is used on its\n * own.\n */\nconst readConfigResolvingExtends = async (\n filePath: string,\n seen = new Set<string>(),\n): Promise<ReadConfigFileResult> => {\n if (seen.has(filePath)) {\n return {\n exists: false,\n error: toReadError(\n `Circular extends detected for config file ${filePath}`,\n filePath,\n undefined,\n ),\n };\n }\n\n const result = await readConfigIfExists(filePath);\n if (!result.exists) return result;\n\n const raw = result.value as Record<string, unknown>;\n const extendsValue = raw['extends'];\n if (typeof extendsValue !== 'string' || !extendsValue.trim()) {\n return result;\n }\n\n const nextSeen = new Set(seen);\n nextSeen.add(filePath);\n const extendsPath = resolvePiPath(extendsValue.trim(), dirname(filePath));\n const baseResult = await readConfigResolvingExtends(extendsPath, nextSeen);\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- `extends` is stripped from the merged config; the value is intentionally discarded.\n const { extends: _, ...currentWithoutExtends } = raw;\n if (!baseResult.exists) {\n if (baseResult.error) logConfigReadError(baseResult.error);\n return { exists: true, value: currentWithoutExtends };\n }\n\n return {\n exists: true,\n value: merge({}, baseResult.value, currentWithoutExtends),\n };\n};\n\n/** The file extensions a config file can have. */\nconst CONFIG_EXTENSIONS = ['.json', '.yml', '.yaml'] as const;\n\n/**\n * Expands the basename of a config file into all the supported file extensions.\n *\n * @param directory The directory where the file would be.\n * @param basename The name of the file, without the extension.\n * @param scope Whether the directory is global or from a project.\n * @param local Whether it's a `config.local` file.\n * @returns One candidate for each supported extension.\n */\nconst getConfigFileVariants = (\n directory: string,\n basename: string,\n scope: ConfigSource['scope'],\n local: boolean,\n): ConfigSource[] =>\n CONFIG_EXTENSIONS.map((extension) => ({\n path: join(directory, `${basename}${extension}`),\n scope,\n local,\n exists: false,\n used: false,\n }));\n\n/**\n * Gets the config file candidates, in merge order: global files first, then the project\n * ones, with the `config.local` files after their `config` counterparts.\n *\n * @param cwd The project directory.\n * @returns The candidates.\n */\nconst getConfigFilePaths = (cwd: string): ConfigSource[] => [\n ...getConfigFileVariants(AGENT_DIR, 'config', 'global', false),\n ...getConfigFileVariants(AGENT_DIR, 'config.local', 'global', true),\n ...getConfigFileVariants(join(cwd, CONFIG_DIR_NAME), 'config', 'project', false),\n ...getConfigFileVariants(join(cwd, CONFIG_DIR_NAME), 'config.local', 'project', true),\n];\n\n/**\n * Converts a query into a dot-path.\n *\n * @param query The query to convert. It has to be valid (see\n * {@link validateConfigQuery}), which means that a `skill`, `tool`, or `extension`\n * scope always has a name.\n * @returns The dot-path, or `undefined` if the query is for the whole config.\n */\nconst getConfigPath = (query: ConfigQuery): string | undefined => {\n if (query.path) return query.path;\n if (!query.scope || query.scope === 'all') return undefined;\n if (\n query.scope === 'pi' ||\n query.scope === 'skills' ||\n query.scope === 'tools' ||\n query.scope === 'extensions'\n )\n return query.scope;\n if (query.scope === 'skill') return `skills.${query.name}`;\n if (query.scope === 'tool') return `tools.${query.name}`;\n return `extensions.${query.name}`;\n};\n\n/**\n * Validates the combinations a model-facing config query can have.\n *\n * Returns a human-readable phrase because callers surface validation failures as tool\n * result messages. There is no recovery branch needed beyond \"show this to the\n * model/user\", so an Error object or machine-code enum would add handling noise without\n * improving current behavior.\n *\n * @param query The query to validate.\n * @returns The description of the problem, or `undefined` if the query is valid.\n */\nconst validateConfigQuery = (query: ConfigQuery): string | undefined => {\n if (query.path && (query.scope || query.name))\n return 'Use either path or scope/name, not both.';\n\n if (\n (query.scope === 'skill' || query.scope === 'tool' || query.scope === 'extension') &&\n !query.name\n )\n return `scope '${query.scope}' requires name.`;\n\n if (\n query.name &&\n query.scope !== 'skill' &&\n query.scope !== 'tool' &&\n query.scope !== 'extension'\n )\n return 'name is only valid with skill, tool, or extension scope.';\n\n return undefined;\n};\n\n/**\n * Validates a resolved config value with a Zod schema provided by the caller.\n *\n * @template TValue The type of the value the schema produces.\n * @param value The value to validate.\n * @param schema The schema to validate and parse the value with.\n * @returns The value, as parsed by the schema.\n * @throws If the value doesn't match the schema; the error is a\n * {@link ConfigValidationError}.\n */\nconst validateConfigValue = <TValue>(\n value: unknown,\n schema: ConfigSchema<TValue>,\n): TValue => {\n const result = schema.safeParse(value);\n if (result.success) return result.data;\n\n throw new ConfigValidationError(\n 'Config value failed schema validation.',\n result.error.issues,\n );\n};\n\n/**\n * Reads the global and the project config files, and merges them in order.\n *\n * @param cwd The project directory.\n * @returns The resolved config, without the session override, and the status of each file\n * that was checked.\n */\nconst readConfigFiles = async (cwd = process.cwd()): Promise<ReadConfigResult> => {\n const sources = [];\n const configs: unknown[] = [];\n\n for (const source of getConfigFilePaths(cwd)) {\n const result = await readConfigResolvingExtends(source.path);\n if (!result.exists && result.error) logConfigReadError(result.error);\n sources.push({ ...source, exists: result.exists, used: result.exists });\n if (result.exists) configs.push(result.value);\n }\n\n return {\n config: merge<PiCoreConfig>(EMPTY_PI_CORE_CONFIG, ...configs),\n sources,\n };\n};\n\n/**\n * Deep-merges the session override (if there's one) on top of a config resolved from\n * files.\n *\n * @param config The config resolved from files.\n * @returns The config with the override applied, or the same config if there's no\n * override.\n */\nconst applySessionConfigOverride = (config: PiCoreConfig): PiCoreConfig => {\n const override = getSessionConfigOverride();\n return typeof override === 'undefined'\n ? config\n : merge<PiCoreConfig>({}, config, override);\n};\n\n/**\n * Loads and returns the full resolved config: the files, merged in order, plus the\n * session override.\n *\n * @example <caption>The full config</caption>\n * const config = await getConfig();\n *\n * @example <caption>The config of another project</caption>\n * const config = await getConfig({ cwd: '/path/to/project' });\n *\n * @template TValue The type of the value the schema produces; by default, the config\n * itself.\n * @param options The options to read the config.\n * @returns The config, or the value the schema produced from it.\n * @throws If a schema was provided and the config doesn't match; the error is a\n * {@link ConfigValidationError}.\n */\nconst getConfig = async <TValue = PiCoreConfig>(\n options: GetConfigOptions<TValue> = {},\n): Promise<TValue> => {\n const result = await readConfigFiles(options.cwd);\n const config = applySessionConfigOverride(result.config);\n return options.schema\n ? validateConfigValue(config, options.schema)\n : (config as TValue);\n};\n\n/**\n * Loads config and returns a single typed dot-path value.\n *\n * Use this from Node code when the caller already knows the exact config path it needs\n * and wants declaration-merging based return type inference.\n *\n * @example <caption>A single value</caption>\n * const runChecks = await getConfigValue('skills.commit.runChecks');\n *\n * @example <caption>Validating the value with a schema</caption>\n * const commit = await getConfigValue('skills.commit', {\n * schema: z.object({ runChecks: z.boolean().optional() }),\n * });\n *\n * @template TPath The dot-path of the value.\n * @template TValue The type of the value; by default, the one inferred from the path.\n * @param path The dot-path of the value, like `skills.commit.runChecks`.\n * @param options The options to read the config.\n * @returns The value, or `undefined` if the path doesn't exist.\n * @throws If a schema was provided and the value doesn't match; the error is a\n * {@link ConfigValidationError}.\n */\nconst getConfigValue = async <\n TPath extends ConfigPath | string,\n TValue = ConfigPathValue<TPath>,\n>(\n path: TPath,\n options: GetConfigOptions<TValue> = {},\n): Promise<TValue | undefined> => {\n const config = await getConfig({ cwd: options.cwd });\n const value = get<ConfigPathValue<TPath>>(config, path);\n if (value === undefined) return undefined;\n return options.schema ? validateConfigValue(value, options.schema) : (value as TValue);\n};\n\n/**\n * Loads config and applies the model-facing query format used by the get_config tool.\n *\n * This is intentionally less type-specific than getConfigValue because tool calls use\n * runtime scope/name/path parameters and need structured status, source metadata, and\n * validation messages.\n *\n * @example <caption>The config of a skill</caption>\n * const result = await queryConfig({ scope: 'skill', name: 'commit' });\n * if (result.status === 'found') {\n * console.log(result.value);\n * }\n *\n * @param query The query to run.\n * @param options The options to read the config.\n * @returns The result of the query. A query that is not valid, or a path that doesn't\n * exist, are reported in the `status`, they don't throw.\n */\nconst queryConfig = async (\n query: ConfigQuery,\n options: QueryConfigOptions = {},\n): Promise<QueryConfigResult> => {\n const validationError = validateConfigQuery(query);\n if (validationError) {\n return {\n status: 'error',\n query,\n redacted: false,\n sources: [],\n message: validationError,\n };\n }\n\n const { config: fileConfig, sources } = await readConfigFiles(options.cwd);\n const config = applySessionConfigOverride(fileConfig);\n const path = getConfigPath(query);\n const value = path ? get(config, path) : config;\n\n if (value === undefined) {\n return {\n status: 'not_found',\n query,\n redacted: false,\n sources,\n message: `Config path not found: ${path}`,\n };\n }\n\n return {\n status: 'found',\n query,\n value,\n redacted: false,\n sources,\n };\n};\n\nexport {\n clearSessionConfigOverride,\n EMPTY_PI_CORE_CONFIG,\n ensureErrorMessage,\n getAgentPath,\n getConfig,\n getConfigValue,\n getSessionConfigOverride,\n queryConfig,\n readConfigFiles,\n resolvePiPath,\n setSessionConfigOverride,\n};\n"],"mappings":"A,uVAIA,IAAaC,EAAb,cAAqC,KAAM,CAEzC,SAOA,YAAY,EAAiB,EAAkB,EAAwB,CACrE,MAAMC,EAASC,CAAO,EACtB,KAAK,SAAWC,EAChB,KAAK,KAAO,iBACd,CACF,EAGaC,EAAb,cAA2C,KAAM,CAE/C,OAMA,YAAY,EAAiB,EAA4B,CACvD,MAAMH,CAAO,EACb,KAAK,OAASI,EACd,KAAK,KAAO,uBACd,CACF,ECPA,MAAMC,GAAgB,GAAGC,IAA+BC,EAAKC,EAAY,EAAG,GAAGF,CAAQ,EASjFG,EAAsB,GACtBC,aAAiBC,EAAE,SACdD,EAAM,OAAO,IAAK,GAAUE,EAAM,OAAO,CAAC,CAAC,KAAK,IAAI,EAEtDF,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,EAIxDG,EAASN,EAAKO,EAAQ,EAAGC,CAAe,EAExCC,EAAYR,EAAY,EAExBS,EAAeZ,EAAa,UAAU,EAOtCa,EAAkB,yBAyBlBC,GAAiB,EAAe,IAA4B,CAChE,GAAIC,EAAM,WAAW,KAAS,EAAG,OAAOb,EAAKM,EAAQO,EAAM,MAAM,CAAgB,CAAC,EAClF,GAAIA,EAAM,WAAW,QAAY,EAC/B,OAAOb,EAAKS,EAAWI,EAAM,MAAM,CAAmB,CAAC,EACzD,GAAIC,EAAWD,CAAK,EAAG,OAAOA,EAE9B,IAAME,EAAeJ,EAAgB,KAAKE,CAAK,EAC/C,GAAIE,EAAc,CAChB,GAAM,EAAGC,EAAaC,GAAeF,EACrC,OAAOf,EAAKU,EAAcM,EAAcC,CAAY,CACtD,CACA,OAAOjB,EAAKkB,EAASL,CAAK,CAC5B,EAMMM,EAAqC,CACzC,GAAI,CAAC,EACL,OAAQ,CAAC,EACT,MAAO,CAAC,EACR,WAAY,CAAC,CACf,EAWMC,EAAe,WASfC,EAA4B,GAA0C,CAC1E,EAAa,0BAA4BR,CAC3C,EAOMS,MACJF,EAAa,0BAOTG,MAA4C,CAChD,IAAMC,EAAqBJ,EAAa,4BAA8B,OAEtE,MADA,GAAa,0BAA4B,IAAA,GAClCI,CACT,EAsBMC,GACJ,EACA,EACA,IACoB,IAAIjC,EAAgBC,EAASE,EAAU,CAAE,MAAOQ,CAAM,CAAC,EAUvEuB,GAAmB,EAAiB,IACtBC,EAAQhC,CACd,IAAM,QAAgB,KAAK,MAAMiC,CAAO,EAC7CrC,EAAUqC,CAAO,EASpBE,EAAuB,GAC3BH,EAAQhC,CAAQ,IAAM,QAAU,OAAS,OAOrCoC,EAAsB,GAAiC,CAE3D,QAAQ,KAAK,oBAAoB5B,EAAM,SAAS,CAClD,EAUM6B,EAAqB,KAAO,IAAoD,CACpF,IAAIJ,EAEJ,GAAI,CACF,EAAU,MAAMK,EAAStC,EAAU,MAAM,CAC3C,OAASQ,EAAO,CAGd,OAFIA,GAAS,OAAOA,GAAU,UAAY,SAAUA,GAASA,EAAM,OAAS,SACnE,CAAE,OAAQ,EAAM,EAClB,CACL,OAAQ,GACR,MAAOsB,EAAY,8BAA8B9B,IAAYA,EAAUQ,CAAK,CAC9E,CACF,CAEA,GAAI,CACF,MAAO,CAAE,OAAQ,GAAM,MAAOuB,EAAgBE,EAASjC,CAAQ,CAAE,CACnE,OAASQ,EAAO,CACd,IAAM+B,EAASJ,EAAoBnC,CAAQ,EAC3C,MAAO,CACL,OAAQ,GACR,MAAO8B,EAAY,WAAWS,EAAO,kBAAkBvC,IAAYA,EAAUQ,CAAK,CACpF,CACF,CACF,EAgBMgC,EAA6B,MACjC,EACA,kBAAO,IAAI,MACuB,CAClC,GAAIC,EAAK,IAAIzC,CAAQ,EACnB,MAAO,CACL,OAAQ,GACR,MAAO8B,EACL,6CAA6C9B,IAC7CA,EACA,IAAA,EACF,CACF,EAGF,IAAM0C,EAAS,MAAML,EAAmBrC,CAAQ,EAChD,GAAI,CAAC0C,EAAO,OAAQ,OAAOA,EAE3B,IAAMC,EAAMD,EAAO,MACbE,EAAeD,EAAIE,QACzB,GAAI,OAAOD,GAAiB,UAAY,CAACA,EAAa,KAAK,EACzD,OAAOF,EAGT,IAAMI,EAAW,IAAI,IAAIL,CAAI,EAC7BK,EAAS,IAAI9C,CAAQ,EACrB,IAAM+C,EAAc9B,EAAc2B,EAAa,KAAK,EAAGI,EAAQhD,CAAQ,CAAC,EAClEiD,EAAa,MAAMT,EAA2BO,EAAaD,CAAQ,EAGnE,CAAE,QAASI,EAAG,GAAGC,GAA0BR,EAMjD,OALKM,EAAW,OAKT,CACL,OAAQ,GACR,MAAOG,EAAM,CAAC,EAAGH,EAAW,MAAOE,CAAqB,CAC1D,GAPMF,EAAW,OAAOb,EAAmBa,EAAW,KAAK,EAClD,CAAE,OAAQ,GAAM,MAAOE,CAAsB,EAOxD,EAGME,EAAoB,CAAC,QAAS,OAAQ,OAAO,EAW7CC,GACJ,EACA,EACA,EACA,IAEAD,EAAkB,IAAK,IAAe,CACpC,KAAMhD,EAAKkD,EAAW,GAAGC,IAAWC,GAAW,EAC/C,QACA,QACA,OAAQ,GACR,KAAM,EACR,EAAE,EASEC,EAAsB,GAAgC,CAC1D,GAAGJ,EAAsBxC,EAAW,SAAU,SAAU,EAAK,EAC7D,GAAGwC,EAAsBxC,EAAW,eAAgB,SAAU,EAAI,EAClE,GAAGwC,EAAsBjD,EAAKsD,EAAK9C,CAAe,EAAG,SAAU,UAAW,EAAK,EAC/E,GAAGyC,EAAsBjD,EAAKsD,EAAK9C,CAAe,EAAG,eAAgB,UAAW,EAAI,CACtF,EAUM+C,EAAiB,GAA2C,CAChE,GAAIC,EAAM,KAAM,OAAOA,EAAM,KAC7B,GAAKA,EAAM,OAASA,EAAM,QAAU,MAUpC,OAREA,EAAM,QAAU,MAChBA,EAAM,QAAU,UAChBA,EAAM,QAAU,SAChBA,EAAM,QAAU,aAETA,EAAM,MACXA,EAAM,QAAU,QAAgB,UAAUA,EAAM,OAChDA,EAAM,QAAU,OAAe,SAASA,EAAM,OAC3C,cAAcA,EAAM,MAC7B,EAaMC,EAAuB,GAA2C,CACtE,GAAID,EAAM,OAASA,EAAM,OAASA,EAAM,MACtC,MAAO,2CAET,IACGA,EAAM,QAAU,SAAWA,EAAM,QAAU,QAAUA,EAAM,QAAU,cACtE,CAACA,EAAM,KAEP,MAAO,UAAUA,EAAM,MAAM,kBAE/B,GACEA,EAAM,MACNA,EAAM,QAAU,SAChBA,EAAM,QAAU,QAChBA,EAAM,QAAU,YAEhB,MAAO,0DAGX,EAYME,GACJ,EACA,IACW,CACX,IAAMrB,EAASsB,EAAO,UAAU9C,CAAK,EACrC,GAAIwB,EAAO,QAAS,OAAOA,EAAO,KAElC,MAAM,IAAIzC,EACR,yCACAyC,EAAO,MAAM,MACf,CACF,EASMuB,EAAkB,MAAO,EAAM,QAAQ,IAAI,IAAiC,CAChF,IAAMC,EAAU,CAAC,EACXC,EAAqB,CAAC,EAE5B,IAAK,IAAMC,KAAUV,EAAmBC,CAAG,EAAG,CAC5C,IAAMjB,EAAS,MAAMF,EAA2B4B,EAAO,IAAI,EACvD,CAAC1B,EAAO,QAAUA,EAAO,OAAON,EAAmBM,EAAO,KAAK,EACnEwB,EAAQ,KAAK,CAAE,GAAGE,EAAQ,OAAQ1B,EAAO,OAAQ,KAAMA,EAAO,MAAO,CAAC,EAClEA,EAAO,QAAQyB,EAAQ,KAAKzB,EAAO,KAAK,CAC9C,CAEA,MAAO,CACL,OAAQU,EAAoB5B,EAAsB,GAAG2C,CAAO,EAC5D,SACF,CACF,EAUME,EAA8B,GAAuC,CACzE,IAAMC,EAAW3C,EAAyB,EAC1C,OAAc2C,IAAa,OACvBC,EACAnB,EAAoB,CAAC,EAAGmB,EAAQD,CAAQ,CAC9C,EAmBME,EAAY,MAChB,EAAoC,CAAC,IACjB,CACpB,IAAM9B,EAAS,MAAMuB,EAAgBlE,EAAQ,GAAG,EAC1CwE,EAASF,EAA2B3B,EAAO,MAAM,EACvD,OAAO3C,EAAQ,OACXgE,EAAoBQ,EAAQxE,EAAQ,MAAM,EACzCwE,CACP,EAwBME,EAAiB,MAIrB,EACA,EAAoC,CAAC,IACL,CAChC,IAAMF,EAAS,MAAMC,EAAU,CAAE,IAAKzE,EAAQ,GAAI,CAAC,EAC7CmB,EAAQwD,EAA4BH,EAAQI,CAAI,EACtD,GAAIzD,IAAU,IAAA,GACd,OAAOnB,EAAQ,OAASgE,EAAoB7C,EAAOnB,EAAQ,MAAM,EAAKmB,CACxE,EAoBM0D,EAAc,MAClB,EACA,EAA8B,CAAC,IACA,CAC/B,IAAMC,EAAkBf,EAAoBD,CAAK,EACjD,GAAIgB,EACF,MAAO,CACL,OAAQ,QACR,QACA,SAAU,GACV,QAAS,CAAC,EACV,QAASA,CACX,EAGF,GAAM,CAAE,OAAQC,EAAY,WAAY,MAAMb,EAAgBlE,EAAQ,GAAG,EACnEwE,EAASF,EAA2BS,CAAU,EAC9CH,EAAOf,EAAcC,CAAK,EAC1B3C,EAAQyD,EAAOD,EAAIH,EAAQI,CAAI,EAAIJ,EAYzC,OAVIrD,IAAU,IAAA,GACL,CACL,OAAQ,YACR,QACA,SAAU,GACV,UACA,QAAS,0BAA0ByD,GACrC,EAGK,CACL,OAAQ,QACR,QACA,QACA,SAAU,GACV,SACF,CACF,E"}
|