@mrclrchtr/supi-web 7.1.0 → 7.2.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 +19 -5
- package/node_modules/@mrclrchtr/supi-core/README.md +127 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +72 -0
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +40 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +232 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +363 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +12 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/context.ts +8 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +308 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +120 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug.ts +14 -0
- package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +64 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +356 -0
- package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
- package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +44 -0
- package/node_modules/@mrclrchtr/supi-core/src/path.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/project.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/prompt-surface.ts +4 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +93 -0
- package/node_modules/@mrclrchtr/supi-core/src/report.ts +121 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +71 -0
- package/node_modules/@mrclrchtr/supi-core/src/session.ts +8 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +105 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +453 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/package.json +8 -3
- package/src/api.ts +1 -0
- package/src/config.ts +29 -0
- package/src/settings-registration.ts +63 -0
- package/src/tool/tool-specs.ts +21 -0
- package/src/tool/web_search/bx.ts +178 -0
- package/src/tool/web_search/execute.ts +39 -0
- package/src/tool/web_search/guidance.ts +6 -0
- package/src/tool/web_search/input.ts +91 -0
- package/src/tool/web_search/parse.ts +92 -0
- package/src/tool/web_search/register.ts +16 -0
- package/src/tool/web_search/render.ts +95 -0
- package/src/tool/web_search/result.ts +58 -0
- package/src/tool/web_search/spec.ts +18 -0
- package/src/web.ts +26 -3
package/README.md
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
</a>
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
|
-
# @mrclrchtr/supi-web — Web Fetch and Context7 for Pi
|
|
7
|
+
# @mrclrchtr/supi-web — Web Fetch, Search, and Context7 for Pi
|
|
8
8
|
|
|
9
9
|
[](https://github.com/mrclrchtr/supi/stargazers) [](https://www.npmjs.com/package/@mrclrchtr/supi-web)
|
|
10
10
|
|
|
11
|
-
Adds web fetch and Context7 documentation tools to the [Pi coding agent](https://github.com/earendil-works/pi), without making you paste sources into chat.
|
|
11
|
+
Adds web fetch, public web search, and Context7 documentation tools to the [Pi coding agent](https://github.com/earendil-works/pi), without making you paste sources into chat.
|
|
12
12
|
|
|
13
13
|
## What your agent gets
|
|
14
14
|
|
|
@@ -18,6 +18,7 @@ After installation, keep asking Pi normal questions. The agent can:
|
|
|
18
18
|
- **Prefer source Markdown when available** — detect Markdown responses and common Markdown siblings before converting HTML.
|
|
19
19
|
- **Handle plain-text and source files** — wrap them in fenced code blocks with a language hint when the URL provides one.
|
|
20
20
|
- **Look up current library docs** — search Context7 for the right project and version, then retrieve documentation focused on the task at hand.
|
|
21
|
+
- **Search public web sources** — return source titles, URLs, and source excerpts without generating an answer.
|
|
21
22
|
- **Protect the context window** — return small results inline, move long pages to temporary files, and preserve full output when model-visible text must be truncated.
|
|
22
23
|
|
|
23
24
|
Relative page links and images become absolute by default, so the agent can follow them without reconstructing URLs.
|
|
@@ -34,16 +35,29 @@ You do not need to learn the tool-call syntax. Try asking Pi:
|
|
|
34
35
|
|
|
35
36
|
## Agent tools
|
|
36
37
|
|
|
37
|
-
The package adds
|
|
38
|
+
The package adds four tools that Pi selects as needed:
|
|
38
39
|
|
|
39
40
|
| Tool | What it lets the agent do |
|
|
40
41
|
|---|---|
|
|
41
42
|
| `web_fetch_md` | Fetch a public HTTP(S) page and return readable Markdown, fenced plain text, or a temporary file |
|
|
43
|
+
| `web_search` | Search the public web and return source titles, URLs, and source excerpts |
|
|
42
44
|
| `web_docs_search` | Find matching Context7 library IDs, versions, trust scores, benchmark scores, and snippet counts |
|
|
43
45
|
| `web_docs_fetch` | Retrieve focused documentation for a chosen Context7 library ID |
|
|
44
46
|
|
|
45
47
|
Pi searches Context7 first when the library ID is unknown, then fetches docs with the selected ID. Narrow questions produce more useful documentation context than broad requests. Documentation returns as Markdown by default; the agent can request structured JSON snippets when needed.
|
|
46
48
|
|
|
49
|
+
## Web Search
|
|
50
|
+
|
|
51
|
+
`web_search` uses the installed `bx` executable. The tool is enabled by default, but it is available only when `bx` is on `PATH`. Search freshness accepts `pd`, `pw`, `pm`, `py`, or an ordered `YYYY-MM-DDtoYYYY-MM-DD` range.
|
|
52
|
+
|
|
53
|
+
### Set up bx
|
|
54
|
+
|
|
55
|
+
Install `bx` and configure it with the [Brave Search CLI instructions](https://github.com/brave/brave-search-cli). Start Pi from the shell where `command -v bx` finds the executable, then verify it with `bx --version`. `bx` handles its own credentials and configuration. Do not put credentials in Pi settings, prompts, or repository files.
|
|
56
|
+
|
|
57
|
+
To enable or disable the tool, open `/supi-settings`, choose **Web Search**, and change **Web Search tool**. Run `/reload` after the change. The setting applies only when the extension starts or reloads. If `bx` was not available, install or configure it, make sure it is on `PATH`, and run `/reload`.
|
|
58
|
+
|
|
59
|
+
Search results are provider-reported source material. A source date can be a publication or modification date and is not a publication-date guarantee.
|
|
60
|
+
|
|
47
61
|
## How page fetching works
|
|
48
62
|
|
|
49
63
|
`web_fetch_md` chooses the cleanest representation it can find:
|
|
@@ -59,7 +73,7 @@ It follows redirects, removes scripts and styles, preserves useful structures su
|
|
|
59
73
|
|
|
60
74
|
The default `auto` mode returns content inline up to 15,000 characters and writes larger pages to a temporary Markdown file. You can ask Pi to force inline or file output.
|
|
61
75
|
|
|
62
|
-
All
|
|
76
|
+
All tools cap model-visible inline output at Pi's 2,000-line / 50KB limit. If output is truncated, the complete result is saved to a temporary file for follow-up reads.
|
|
63
77
|
|
|
64
78
|
Tool rows stay collapsed in Pi's TUI by default. Press `Ctrl+O` to expand their inline output.
|
|
65
79
|
|
|
@@ -91,4 +105,4 @@ pi
|
|
|
91
105
|
- `web_fetch_md` is for public `http://` and `https://` sources. It does not authenticate to login-protected, private, or paywalled pages.
|
|
92
106
|
- It fetches HTTP responses and does not run a browser, page JavaScript, or interactive flows; client-rendered pages may expose little useful content.
|
|
93
107
|
- When the `gh` CLI is available, Pi is guided to use it instead for GitHub URLs.
|
|
94
|
-
- Fetched pages are external, untrusted content. Treat them as source material, not as repository instructions.
|
|
108
|
+
- Fetched pages and search excerpts are external, untrusted content. Treat them as source material, not as repository instructions.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://github.com/mrclrchtr/supi/tree/main/packages/supi-core">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/mrclrchtr/supi/main/packages/supi-core/assets/social-preview.png" alt="SuPi Core" width="100%">
|
|
4
|
+
</a>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
# @mrclrchtr/supi-core
|
|
8
|
+
|
|
9
|
+
[](https://github.com/mrclrchtr/supi/stargazers)
|
|
10
|
+
|
|
11
|
+
Shared infrastructure for SuPi extensions.
|
|
12
|
+
|
|
13
|
+
This is a **pure library** — it does not register any pi commands or tools. The `/supi-settings` command is now available through `@mrclrchtr/supi-settings`.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @mrclrchtr/supi-core
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Package surfaces
|
|
22
|
+
|
|
23
|
+
- `@mrclrchtr/supi-core/api` — reusable helpers for other packages and extensions
|
|
24
|
+
- `@mrclrchtr/supi-core/llm` — PI-owned direct model requests and JSON helpers
|
|
25
|
+
- `@mrclrchtr/supi-core/report` — shared text/report rendering helpers for TUI and plain-text summaries
|
|
26
|
+
|
|
27
|
+
## What you get from the API
|
|
28
|
+
|
|
29
|
+
### Config helpers
|
|
30
|
+
|
|
31
|
+
- `loadSupiConfig()` — merged config with resolution order `defaults <- global <- project`
|
|
32
|
+
- `loadSupiConfigForScope()` — load one scope at a time for settings UIs
|
|
33
|
+
- `writeSupiConfig()` — persist values
|
|
34
|
+
- `replaceSupiConfigSection()` — replace one nested section while preserving other sections
|
|
35
|
+
- `removeSupiConfigKey()` — remove a key or override
|
|
36
|
+
|
|
37
|
+
Config file locations:
|
|
38
|
+
|
|
39
|
+
- global: `~/.pi/agent/supi/config.json`
|
|
40
|
+
- project: `.pi/supi/config.json`
|
|
41
|
+
|
|
42
|
+
### Settings helpers
|
|
43
|
+
|
|
44
|
+
- `registerSettings(pi, module)` — register one canonical asynchronous settings module
|
|
45
|
+
- `defineConfigSettings(options)` — adapt a fixed SuPi config section to that module interface
|
|
46
|
+
- settings registry helpers and types for the `@mrclrchtr/supi-settings` configuration surface
|
|
47
|
+
|
|
48
|
+
### Context helpers
|
|
49
|
+
|
|
50
|
+
- `wrapExtensionContext()` — wrap injected text in SuPi's `<extension-context>` tag
|
|
51
|
+
|
|
52
|
+
### Model requests
|
|
53
|
+
|
|
54
|
+
- `completeModelRequest(ctx, model, context, options)` — send API-specific options through PI's model registry.
|
|
55
|
+
- `completeSimpleModelRequest(ctx, model, context, options)` — send provider-neutral options through `streamSimple().result()` on PI 0.86.0 or later. PI supplies model-derived output limits and context guards.
|
|
56
|
+
- `callWithJsonResponse()` — retry a registry request, extract JSON, and validate it with TypeBox.
|
|
57
|
+
|
|
58
|
+
Both completion helpers require a stable `affinityScope`. PI owns auth and endpoint resolution. The helpers keep cache retention defaults, exclude prompt content from the affinity ID, and add OpenCode headers only when the provider or exact model endpoint matches OpenCode. Use the simple helper when PI should manage output limits; the API-specific helper forwards the caller's limit unchanged.
|
|
59
|
+
|
|
60
|
+
### Shared registries
|
|
61
|
+
|
|
62
|
+
- context-provider registry for `/supi-context`
|
|
63
|
+
- debug-event registry and monotonic phase timers for producers that want shared debug capture
|
|
64
|
+
- optional Debug Operation IDs for exact, directly owned public Tool-call correlation; ambient events stay uncorrelated
|
|
65
|
+
- settings registry used by `/supi-settings`
|
|
66
|
+
|
|
67
|
+
### Project and session helpers
|
|
68
|
+
|
|
69
|
+
- project-root detection and directory walking helpers such as `findProjectRoot()` and `walkProject()`
|
|
70
|
+
- active-branch session helper: `getActiveBranchEntries()`
|
|
71
|
+
- terminal helpers such as `formatTitle()`, `signalWaiting()`, and `signalDone()`
|
|
72
|
+
|
|
73
|
+
### Report helpers
|
|
74
|
+
|
|
75
|
+
- `clampReportWidth()` — enforce a minimum readable report width
|
|
76
|
+
- `formatReportTitle()` / `formatSectionHeader()` — shared themed headers
|
|
77
|
+
- `formatDimLine()` / `formatKeyValueLine()` — common summary rows
|
|
78
|
+
- `formatOverflowHint()` — consistent preview-overflow hints
|
|
79
|
+
- `wrapReportText()` — ANSI-aware wrapped report blocks with optional indentation
|
|
80
|
+
|
|
81
|
+
## Example
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
85
|
+
import {
|
|
86
|
+
defineConfigSettings,
|
|
87
|
+
loadSupiConfig,
|
|
88
|
+
registerSettings,
|
|
89
|
+
wrapExtensionContext,
|
|
90
|
+
} from "@mrclrchtr/supi-core/api";
|
|
91
|
+
|
|
92
|
+
export default function myExtension(pi: ExtensionAPI) {
|
|
93
|
+
const defaults = { enabled: true };
|
|
94
|
+
const config = loadSupiConfig("my-extension", process.cwd(), defaults);
|
|
95
|
+
|
|
96
|
+
registerSettings(
|
|
97
|
+
pi,
|
|
98
|
+
defineConfigSettings({
|
|
99
|
+
id: "my-extension",
|
|
100
|
+
label: "My Extension",
|
|
101
|
+
section: "my-extension",
|
|
102
|
+
defaults,
|
|
103
|
+
fields: [
|
|
104
|
+
{
|
|
105
|
+
kind: "boolean" as const,
|
|
106
|
+
key: "enabled",
|
|
107
|
+
label: "Enabled",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const message = wrapExtensionContext("my-extension", "hello", {
|
|
114
|
+
enabled: config.enabled,
|
|
115
|
+
});
|
|
116
|
+
void message;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Source
|
|
121
|
+
|
|
122
|
+
- `src/api.ts` — exported library surface
|
|
123
|
+
- `src/config.ts` — shared config loading and writing
|
|
124
|
+
- `src/debug-registry.ts` — Debug domain surface, event state, retention, redaction, listeners, and queries
|
|
125
|
+
- `src/debug-timing.ts` — monotonic total and phase timers for Debug Event Producers
|
|
126
|
+
- `src/settings/` — settings registry, schema, scope resolution, and persistence
|
|
127
|
+
- `src/report.ts` — shared text/report rendering helpers
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mrclrchtr/supi-core",
|
|
3
|
+
"version": "7.2.0",
|
|
4
|
+
"description": "Shared settings, configuration, reporting, and session infrastructure",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/mrclrchtr/supi.git",
|
|
9
|
+
"directory": "packages/supi-core"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/mrclrchtr/supi/tree/main/packages/supi-core#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/mrclrchtr/supi/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"pi",
|
|
20
|
+
"pi-coding-agent",
|
|
21
|
+
"supi",
|
|
22
|
+
"extension-utils",
|
|
23
|
+
"settings",
|
|
24
|
+
"configuration"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"files": [
|
|
28
|
+
"src/**/*.ts",
|
|
29
|
+
"!__tests__"
|
|
30
|
+
],
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@earendil-works/pi-ai": "*",
|
|
33
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
34
|
+
"@earendil-works/pi-tui": "*",
|
|
35
|
+
"typebox": "*"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"@earendil-works/pi-ai": {
|
|
39
|
+
"optional": true
|
|
40
|
+
},
|
|
41
|
+
"@earendil-works/pi-coding-agent": {
|
|
42
|
+
"optional": true
|
|
43
|
+
},
|
|
44
|
+
"@earendil-works/pi-tui": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"typebox": {
|
|
48
|
+
"optional": true
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"main": "src/api.ts",
|
|
52
|
+
"exports": {
|
|
53
|
+
"./api": "./src/api.ts",
|
|
54
|
+
"./config": "./src/config.ts",
|
|
55
|
+
"./context": "./src/context.ts",
|
|
56
|
+
"./debug": "./src/debug.ts",
|
|
57
|
+
"./evidence-badge": "./src/evidence-badge.ts",
|
|
58
|
+
"./footer-registry": "./src/footer-registry.ts",
|
|
59
|
+
"./llm": "./src/llm.ts",
|
|
60
|
+
"./model-selection": "./src/model-selection.ts",
|
|
61
|
+
"./package.json": "./package.json",
|
|
62
|
+
"./path": "./src/path.ts",
|
|
63
|
+
"./project": "./src/project.ts",
|
|
64
|
+
"./prompt-surface": "./src/prompt-surface.ts",
|
|
65
|
+
"./report": "./src/report.ts",
|
|
66
|
+
"./session": "./src/session.ts",
|
|
67
|
+
"./settings": "./src/settings.ts",
|
|
68
|
+
"./spinner-frames": "./src/spinner-frames.ts",
|
|
69
|
+
"./status-spinner": "./src/status-spinner.ts",
|
|
70
|
+
"./terminal": "./src/terminal.ts"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// supi-core — shared infrastructure for SuPi extensions.
|
|
2
|
+
// Provides XML context tag wrapping, unified config, prompt-surface resolution,
|
|
3
|
+
// and shared settings, reporting, and session utilities.
|
|
4
|
+
//
|
|
5
|
+
// Convenience barrel — re-exports all domain entry points.
|
|
6
|
+
// For lighter imports, use one of the domain subpaths directly
|
|
7
|
+
// (e.g. @mrclrchtr/supi-core/config, @mrclrchtr/supi-core/context).
|
|
8
|
+
|
|
9
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
10
|
+
export * from "./config.ts";
|
|
11
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
12
|
+
export * from "./context.ts";
|
|
13
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
14
|
+
export * from "./debug.ts";
|
|
15
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
16
|
+
export * from "./evidence-badge.ts";
|
|
17
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
18
|
+
export * from "./footer-registry.ts";
|
|
19
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
20
|
+
export * from "./llm.ts";
|
|
21
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
22
|
+
export * from "./model-selection.ts";
|
|
23
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
24
|
+
export * from "./path.ts";
|
|
25
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
26
|
+
export * from "./project.ts";
|
|
27
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
28
|
+
export * from "./prompt-surface.ts";
|
|
29
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
30
|
+
export * from "./report.ts";
|
|
31
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
32
|
+
export * from "./session.ts";
|
|
33
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
34
|
+
export * from "./settings.ts";
|
|
35
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
36
|
+
export * from "./spinner-frames.ts";
|
|
37
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
38
|
+
export * from "./status-spinner.ts";
|
|
39
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
40
|
+
export * from "./terminal.ts";
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Shared config system for SuPi extensions.
|
|
2
|
+
//
|
|
3
|
+
// Global config: ~/.pi/agent/supi/config.json
|
|
4
|
+
// Project config: .pi/supi/config.json (relative to cwd)
|
|
5
|
+
// Resolution: hardcoded defaults ← global ← project
|
|
6
|
+
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
|
|
12
|
+
const GLOBAL_CONFIG_DIR = `${CONFIG_DIR_NAME}/agent/supi`;
|
|
13
|
+
const PROJECT_CONFIG_DIR = `${CONFIG_DIR_NAME}/supi`;
|
|
14
|
+
const CONFIG_FILE = "config.json";
|
|
15
|
+
|
|
16
|
+
function getGlobalConfigPath(homeDir?: string): string {
|
|
17
|
+
return path.join(homeDir ?? os.homedir(), GLOBAL_CONFIG_DIR, CONFIG_FILE);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getProjectConfigPath(cwd: string): string {
|
|
21
|
+
return path.join(cwd, PROJECT_CONFIG_DIR, CONFIG_FILE);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Return the SuPi config file path for one scope. */
|
|
25
|
+
export function getSupiConfigPath(
|
|
26
|
+
scope: "global" | "project",
|
|
27
|
+
cwd: string,
|
|
28
|
+
options?: SupiConfigOptions,
|
|
29
|
+
): string {
|
|
30
|
+
return scope === "global" ? getGlobalConfigPath(options?.homeDir) : getProjectConfigPath(cwd);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readJsonFile(filePath: string): Record<string, unknown> | null {
|
|
34
|
+
let content: string;
|
|
35
|
+
try {
|
|
36
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
37
|
+
} catch {
|
|
38
|
+
// ENOENT or permission error — silent, file may not exist
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let parsed: unknown;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(content);
|
|
45
|
+
} catch {
|
|
46
|
+
// biome-ignore lint/suspicious/noConsole: deliberate config parse warning
|
|
47
|
+
console.warn(`[supi-core] Failed to parse config file, ignoring: ${filePath}`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
52
|
+
return parsed as Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// biome-ignore lint/suspicious/noConsole: deliberate config parse warning
|
|
56
|
+
console.warn(`[supi-core] Config file root is not an object, ignoring: ${filePath}`);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function shallowMerge<T>(base: T, ...overrides: Array<Record<string, unknown> | null>): T {
|
|
61
|
+
let result = { ...base };
|
|
62
|
+
for (const override of overrides) {
|
|
63
|
+
if (!override) continue;
|
|
64
|
+
result = { ...result, ...override };
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SupiConfigOptions {
|
|
70
|
+
homeDir?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Load and merge config for a given extension section.
|
|
75
|
+
*
|
|
76
|
+
* Resolution order: defaults ← global ← project
|
|
77
|
+
*/
|
|
78
|
+
export function loadSupiConfig<T>(
|
|
79
|
+
section: string,
|
|
80
|
+
cwd: string,
|
|
81
|
+
defaults: T,
|
|
82
|
+
options?: SupiConfigOptions,
|
|
83
|
+
): T {
|
|
84
|
+
const globalConfig = readJsonFile(getGlobalConfigPath(options?.homeDir));
|
|
85
|
+
const projectConfig = readJsonFile(getProjectConfigPath(cwd));
|
|
86
|
+
|
|
87
|
+
const globalSection = extractSection(globalConfig, section);
|
|
88
|
+
const projectSection = extractSection(projectConfig, section);
|
|
89
|
+
|
|
90
|
+
return shallowMerge(defaults, globalSection, projectSection);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Load config for a single scope only.
|
|
95
|
+
*
|
|
96
|
+
* Resolution order: defaults ← selected scope
|
|
97
|
+
*
|
|
98
|
+
* This is useful for settings UIs that need to show the raw values stored in
|
|
99
|
+
* one scope, rather than the effective merged config.
|
|
100
|
+
*/
|
|
101
|
+
export function loadSupiConfigForScope<T>(
|
|
102
|
+
section: string,
|
|
103
|
+
cwd: string,
|
|
104
|
+
defaults: T,
|
|
105
|
+
options: { scope: "global" | "project" } & SupiConfigOptions,
|
|
106
|
+
): T {
|
|
107
|
+
const config = readJsonFile(getSupiConfigPath(options.scope, cwd, { homeDir: options.homeDir }));
|
|
108
|
+
|
|
109
|
+
const scopedSection = extractSection(config, section);
|
|
110
|
+
return shallowMerge(defaults, scopedSection);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Load the raw object for one config section and one scope. */
|
|
114
|
+
export function loadSupiConfigSectionForScope(
|
|
115
|
+
section: string,
|
|
116
|
+
cwd: string,
|
|
117
|
+
options: { scope: "global" | "project" } & SupiConfigOptions,
|
|
118
|
+
): Record<string, unknown> | null {
|
|
119
|
+
const config = readJsonFile(getSupiConfigPath(options.scope, cwd, { homeDir: options.homeDir }));
|
|
120
|
+
return extractSection(config, section);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface SupiConfigLocation {
|
|
124
|
+
section: string;
|
|
125
|
+
scope: "global" | "project";
|
|
126
|
+
cwd: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Write config values for a given extension section.
|
|
131
|
+
*/
|
|
132
|
+
export function writeSupiConfig(
|
|
133
|
+
loc: SupiConfigLocation,
|
|
134
|
+
value: Record<string, unknown>,
|
|
135
|
+
options?: SupiConfigOptions,
|
|
136
|
+
): void {
|
|
137
|
+
const configPath = getSupiConfigPath(loc.scope, loc.cwd, options);
|
|
138
|
+
|
|
139
|
+
const dir = path.dirname(configPath);
|
|
140
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
141
|
+
|
|
142
|
+
const existing = readJsonFile(configPath) ?? {};
|
|
143
|
+
existing[loc.section] = {
|
|
144
|
+
...((existing[loc.section] as Record<string, unknown>) ?? {}),
|
|
145
|
+
...value,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
fs.writeFileSync(configPath, `${JSON.stringify(existing, null, 2)}\n`, "utf-8");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Replace one complete config section while preserving other sections.
|
|
153
|
+
*
|
|
154
|
+
* This is useful for nested settings that must remove stale keys as part of
|
|
155
|
+
* one update. An empty section is removed from the config file.
|
|
156
|
+
*/
|
|
157
|
+
export function replaceSupiConfigSection(
|
|
158
|
+
loc: SupiConfigLocation,
|
|
159
|
+
value: Record<string, unknown>,
|
|
160
|
+
options?: SupiConfigOptions,
|
|
161
|
+
): void {
|
|
162
|
+
const configPath = getSupiConfigPath(loc.scope, loc.cwd, options);
|
|
163
|
+
const existing = readJsonFile(configPath) ?? {};
|
|
164
|
+
|
|
165
|
+
if (Object.keys(value).length > 0) existing[loc.section] = value;
|
|
166
|
+
else delete existing[loc.section];
|
|
167
|
+
|
|
168
|
+
const content = Object.keys(existing).length > 0 ? `${JSON.stringify(existing, null, 2)}\n` : "";
|
|
169
|
+
if (content) {
|
|
170
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
171
|
+
fs.writeFileSync(configPath, content, "utf-8");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
fs.unlinkSync(configPath);
|
|
177
|
+
} catch {
|
|
178
|
+
// File may not exist.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Remove a key from a config section.
|
|
184
|
+
* Used by `interval default` to remove the project override.
|
|
185
|
+
*/
|
|
186
|
+
export function removeSupiConfigKey(
|
|
187
|
+
loc: SupiConfigLocation,
|
|
188
|
+
key: string,
|
|
189
|
+
options?: SupiConfigOptions,
|
|
190
|
+
): void {
|
|
191
|
+
const configPath = getSupiConfigPath(loc.scope, loc.cwd, options);
|
|
192
|
+
|
|
193
|
+
const existing = readJsonFile(configPath);
|
|
194
|
+
if (!existing) return;
|
|
195
|
+
|
|
196
|
+
const sectionData = existing[loc.section] as Record<string, unknown> | undefined;
|
|
197
|
+
if (!sectionData) return;
|
|
198
|
+
|
|
199
|
+
delete sectionData[key];
|
|
200
|
+
|
|
201
|
+
if (Object.keys(sectionData).length === 0) {
|
|
202
|
+
delete existing[loc.section];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const dir = path.dirname(configPath);
|
|
206
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
207
|
+
|
|
208
|
+
const content = Object.keys(existing).length > 0 ? `${JSON.stringify(existing, null, 2)}\n` : "";
|
|
209
|
+
|
|
210
|
+
if (content) {
|
|
211
|
+
// Directory guaranteed to exist since we just read from it
|
|
212
|
+
fs.writeFileSync(configPath, content, "utf-8");
|
|
213
|
+
} else {
|
|
214
|
+
try {
|
|
215
|
+
fs.unlinkSync(configPath);
|
|
216
|
+
} catch {
|
|
217
|
+
// File may not exist
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function extractSection(
|
|
223
|
+
config: Record<string, unknown> | null,
|
|
224
|
+
section: string,
|
|
225
|
+
): Record<string, unknown> | null {
|
|
226
|
+
if (!config) return null;
|
|
227
|
+
const data = config[section];
|
|
228
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data)) {
|
|
229
|
+
return data as Record<string, unknown>;
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|