@mrclrchtr/supi-tree-sitter 1.3.1 → 1.5.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 +58 -39
- package/node_modules/@mrclrchtr/supi-core/README.md +107 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +44 -0
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +85 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +76 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +186 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +119 -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/debug-registry.ts +255 -0
- package/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +85 -0
- package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +40 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +86 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +226 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/package.json +8 -3
- package/src/api.ts +6 -2
- package/src/index.ts +6 -2
- package/src/{runtime.ts → session/runtime.ts} +6 -5
- package/src/session/service-registry.ts +30 -0
- package/src/{session.ts → session/session.ts} +20 -12
- package/src/tool/action-specs.ts +92 -0
- package/src/{callees.ts → tool/callees.ts} +3 -3
- package/src/{exports.ts → tool/exports.ts} +4 -4
- package/src/{formatting.ts → tool/formatting.ts} +1 -1
- package/src/tool/guidance.ts +31 -0
- package/src/{imports.ts → tool/imports.ts} +4 -4
- package/src/{node-at.ts → tool/node-at.ts} +3 -3
- package/src/{outline.ts → tool/outline.ts} +3 -3
- package/src/tree-sitter.ts +118 -91
- package/src/types.ts +13 -2
- /package/src/{structure.ts → tool/structure.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,72 +1,91 @@
|
|
|
1
1
|
# @mrclrchtr/supi-tree-sitter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Adds a `tree_sitter` tool to the [pi coding agent](https://github.com/earendil-works/pi) for parser-based structural code analysis.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:@mrclrchtr/supi-tree-sitter
|
|
9
|
+
```
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
For local development:
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
```bash
|
|
14
|
+
pi install ./packages/supi-tree-sitter
|
|
15
|
+
```
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
After editing the source, run `/reload`.
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
## What you get
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
After install, pi gets one tool:
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
- `tree_sitter` — inspect code structure through Tree-sitter parsers instead of plain text search
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
## `tree_sitter` actions
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
| Action | What it is for | Current language coverage |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `outline` | List structural declarations such as functions, classes, interfaces, and methods | JavaScript / TypeScript only |
|
|
30
|
+
| `imports` | List import statements | JavaScript / TypeScript only |
|
|
31
|
+
| `exports` | List export declarations, re-exports, and export assignments | JavaScript / TypeScript only |
|
|
32
|
+
| `node_at` | Show the syntax node at a position, including ancestry | Any supported grammar |
|
|
33
|
+
| `query` | Run a custom Tree-sitter query against a file | Any supported grammar |
|
|
34
|
+
| `callees` | Find outgoing calls from the enclosing function or method at a position | Supported for most grammars, but not all |
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
pi install npm:@mrclrchtr/supi-tree-sitter
|
|
27
|
-
```
|
|
36
|
+
Coordinates use **1-based** line and character columns. Character positions use UTF-16 code units. Relative paths resolve from the session cwd, and a leading `@` on file paths is stripped.
|
|
28
37
|
|
|
29
|
-
##
|
|
38
|
+
## Supported file families
|
|
30
39
|
|
|
31
|
-
The
|
|
40
|
+
The current tool description covers:
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
- JavaScript / TypeScript (`.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`, `.mts`, `.cts`)
|
|
43
|
+
- Python (`.py`, `.pyi`)
|
|
44
|
+
- Rust (`.rs`)
|
|
45
|
+
- Go (`.go`, `.mod`)
|
|
46
|
+
- C / C++ (`.c`, `.h`, `.cpp`, `.hpp`, `.cc`, `.cxx`, `.hxx`, `.c++`, `.h++`)
|
|
47
|
+
- Java (`.java`)
|
|
48
|
+
- Kotlin (`.kt`, `.kts`)
|
|
49
|
+
- Ruby (`.rb`)
|
|
50
|
+
- Bash / shell (`.sh`, `.bash`, `.zsh`)
|
|
51
|
+
- HTML (`.html`, `.htm`, `.xhtml`)
|
|
52
|
+
- R (`.r`)
|
|
53
|
+
- SQL (`.sql`)
|
|
42
54
|
|
|
43
55
|
## Package surfaces
|
|
44
56
|
|
|
45
|
-
- `@mrclrchtr/supi-tree-sitter/api` — reusable parsing
|
|
57
|
+
- `@mrclrchtr/supi-tree-sitter/api` — reusable parsing session factory, shared session-scoped structural service access, and shared result types
|
|
46
58
|
- `@mrclrchtr/supi-tree-sitter/extension` — pi extension entrypoint
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
## For extension developers
|
|
51
|
-
|
|
52
|
-
This package exports a reusable session-scoped parsing service:
|
|
60
|
+
Owned session example:
|
|
53
61
|
|
|
54
62
|
```ts
|
|
55
63
|
import { createTreeSitterSession } from "@mrclrchtr/supi-tree-sitter/api";
|
|
56
64
|
|
|
57
65
|
const session = createTreeSitterSession("/project");
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
const result = await session.canParse("src/index.ts");
|
|
61
|
-
|
|
62
|
-
// Get structural outline
|
|
67
|
+
const parseable = await session.canParse("src/index.ts");
|
|
63
68
|
const outline = await session.outline("src/index.ts");
|
|
64
|
-
|
|
65
|
-
// Trace outgoing calls from a position
|
|
66
69
|
const callees = await session.calleesAt("src/index.ts", 42, 10);
|
|
67
70
|
|
|
68
|
-
// Always clean up
|
|
69
71
|
session.dispose();
|
|
70
72
|
```
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
Shared session-scoped service example:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { getSessionTreeSitterService } from "@mrclrchtr/supi-tree-sitter/api";
|
|
78
|
+
|
|
79
|
+
const state = getSessionTreeSitterService("/project");
|
|
80
|
+
if (state.kind === "ready") {
|
|
81
|
+
const outline = await state.service.outline("src/index.ts");
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Source
|
|
86
|
+
|
|
87
|
+
- `src/tree-sitter.ts` — tool registration and action handling
|
|
88
|
+
- `src/session/runtime.ts` — parser and query runtime
|
|
89
|
+
- `src/session/session.ts` — runtime-backed service helpers and owned session API
|
|
90
|
+
- `src/session/service-registry.ts` — shared session-scoped structural service registry
|
|
91
|
+
- `src/tool/outline.ts`, `src/tool/imports.ts`, `src/tool/exports.ts`, `src/tool/node-at.ts`, `src/tool/callees.ts` — structural analyses
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @mrclrchtr/supi-core
|
|
2
|
+
|
|
3
|
+
Shared infrastructure for SuPi extensions.
|
|
4
|
+
|
|
5
|
+
This package is mainly for extension authors. It gives you a common config system, settings plumbing, context helpers, registries, and a small extension surface that registers `/supi-settings`.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
### As a dependency for another extension
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @mrclrchtr/supi-core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### As a pi package
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pi install npm:@mrclrchtr/supi-core
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Installing it as a pi package adds the minimal `/supi-settings` extension surface.
|
|
22
|
+
|
|
23
|
+
## Package surfaces
|
|
24
|
+
|
|
25
|
+
- `@mrclrchtr/supi-core/api` — reusable helpers for other packages and extensions
|
|
26
|
+
- `@mrclrchtr/supi-core/extension` — minimal pi extension that registers `/supi-settings`
|
|
27
|
+
|
|
28
|
+
## What you get from the API
|
|
29
|
+
|
|
30
|
+
### Config helpers
|
|
31
|
+
|
|
32
|
+
- `loadSupiConfig()` — merged config with resolution order `defaults <- global <- project`
|
|
33
|
+
- `loadSupiConfigForScope()` — load one scope at a time for settings UIs
|
|
34
|
+
- `writeSupiConfig()` — persist values
|
|
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()` — register an arbitrary settings section
|
|
45
|
+
- `registerConfigSettings()` — register a config-backed settings section with scoped persistence helpers
|
|
46
|
+
- `registerSettingsCommand()` — register `/supi-settings`
|
|
47
|
+
- `openSettingsOverlay()` — open the shared settings UI directly
|
|
48
|
+
- `createInputSubmenu()` — helper for simple text-entry submenus
|
|
49
|
+
|
|
50
|
+
The built-in settings UI supports:
|
|
51
|
+
|
|
52
|
+
- project/global scope toggle
|
|
53
|
+
- grouped extension sections
|
|
54
|
+
- searchable setting lists
|
|
55
|
+
|
|
56
|
+
### Context helpers
|
|
57
|
+
|
|
58
|
+
- `wrapExtensionContext()` — wrap injected text in SuPi's `<extension-context>` tag
|
|
59
|
+
- `findLastUserMessageIndex()`
|
|
60
|
+
- `getContextToken()`
|
|
61
|
+
- `getPromptContent()`
|
|
62
|
+
- `pruneAndReorderContextMessages()`
|
|
63
|
+
- `restorePromptContent()`
|
|
64
|
+
|
|
65
|
+
### Shared registries
|
|
66
|
+
|
|
67
|
+
- context-provider registry for `/supi-context`
|
|
68
|
+
- debug-event registry for producers that want shared debug capture
|
|
69
|
+
- settings registry used by `/supi-settings`
|
|
70
|
+
|
|
71
|
+
### Project and session helpers
|
|
72
|
+
|
|
73
|
+
- project-root detection and directory walking helpers such as `findProjectRoot()` and `walkProject()`
|
|
74
|
+
- active-branch session helper: `getActiveBranchEntries()`
|
|
75
|
+
- terminal helpers such as `formatTitle()`, `signalWaiting()`, and `signalDone()`
|
|
76
|
+
|
|
77
|
+
## Example
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { loadSupiConfig, registerConfigSettings, wrapExtensionContext } from "@mrclrchtr/supi-core/api";
|
|
81
|
+
|
|
82
|
+
const config = loadSupiConfig("my-extension", process.cwd(), {
|
|
83
|
+
enabled: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
registerConfigSettings({
|
|
87
|
+
id: "my-extension",
|
|
88
|
+
label: "My Extension",
|
|
89
|
+
section: "my-extension",
|
|
90
|
+
defaults: { enabled: true },
|
|
91
|
+
buildItems: () => [],
|
|
92
|
+
persistChange: () => {},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const message = wrapExtensionContext("my-extension", "hello", {
|
|
96
|
+
file: "CLAUDE.md",
|
|
97
|
+
turn: 1,
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Source
|
|
102
|
+
|
|
103
|
+
- `src/api.ts` — exported library surface
|
|
104
|
+
- `src/extension.ts` — minimal `/supi-settings` entrypoint
|
|
105
|
+
- `src/config.ts` — shared config loading and writing
|
|
106
|
+
- `src/config-settings.ts` — config-backed settings registration helper
|
|
107
|
+
- `src/settings-ui.ts` — shared settings overlay
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mrclrchtr/supi-core",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/mrclrchtr/supi.git"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pi",
|
|
15
|
+
"pi-coding-agent"
|
|
16
|
+
],
|
|
17
|
+
"files": [
|
|
18
|
+
"src/**/*.ts",
|
|
19
|
+
"!__tests__"
|
|
20
|
+
],
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
23
|
+
"@earendil-works/pi-tui": "*"
|
|
24
|
+
},
|
|
25
|
+
"peerDependenciesMeta": {
|
|
26
|
+
"@earendil-works/pi-coding-agent": {
|
|
27
|
+
"optional": true
|
|
28
|
+
},
|
|
29
|
+
"@earendil-works/pi-tui": {
|
|
30
|
+
"optional": true
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"main": "src/api.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
"./api": "./src/api.ts",
|
|
36
|
+
"./extension": "./src/extension.ts",
|
|
37
|
+
"./package.json": "./package.json"
|
|
38
|
+
},
|
|
39
|
+
"pi": {
|
|
40
|
+
"extensions": [
|
|
41
|
+
"./src/extension.ts"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// supi-core — shared infrastructure for SuPi extensions.
|
|
2
|
+
// Provides XML context tag wrapping, unified config system, context-message utilities,
|
|
3
|
+
// and settings registry for supi-wide TUI settings.
|
|
4
|
+
|
|
5
|
+
export type { SupiConfigLocation, SupiConfigOptions } from "./config/config.ts";
|
|
6
|
+
export {
|
|
7
|
+
loadSupiConfig,
|
|
8
|
+
loadSupiConfigForScope,
|
|
9
|
+
removeSupiConfigKey,
|
|
10
|
+
writeSupiConfig,
|
|
11
|
+
} from "./config/config.ts";
|
|
12
|
+
export type { ConfigSettingsHelpers, ConfigSettingsOptions } from "./config/config-settings.ts";
|
|
13
|
+
export { registerConfigSettings } from "./config/config-settings.ts";
|
|
14
|
+
export type { ContextMessageLike } from "./context/context-messages.ts";
|
|
15
|
+
export {
|
|
16
|
+
findLastUserMessageIndex,
|
|
17
|
+
getContextToken,
|
|
18
|
+
getPromptContent,
|
|
19
|
+
pruneAndReorderContextMessages,
|
|
20
|
+
restorePromptContent,
|
|
21
|
+
} from "./context/context-messages.ts";
|
|
22
|
+
export type { ContextProvider } from "./context/context-provider-registry.ts";
|
|
23
|
+
export {
|
|
24
|
+
clearRegisteredContextProviders,
|
|
25
|
+
getRegisteredContextProviders,
|
|
26
|
+
registerContextProvider,
|
|
27
|
+
} from "./context/context-provider-registry.ts";
|
|
28
|
+
export { wrapExtensionContext } from "./context/context-tag.ts";
|
|
29
|
+
export type {
|
|
30
|
+
DebugAgentAccess,
|
|
31
|
+
DebugEvent,
|
|
32
|
+
DebugEventInput,
|
|
33
|
+
DebugEventQuery,
|
|
34
|
+
DebugEventQueryResult,
|
|
35
|
+
DebugEventView,
|
|
36
|
+
DebugLevel,
|
|
37
|
+
DebugNotifyLevel,
|
|
38
|
+
DebugRegistryConfig,
|
|
39
|
+
DebugSummary,
|
|
40
|
+
} from "./debug-registry.ts";
|
|
41
|
+
export {
|
|
42
|
+
clearDebugEvents,
|
|
43
|
+
configureDebugRegistry,
|
|
44
|
+
DEBUG_REGISTRY_DEFAULTS,
|
|
45
|
+
getDebugEvents,
|
|
46
|
+
getDebugRegistryConfig,
|
|
47
|
+
getDebugSummary,
|
|
48
|
+
recordDebugEvent,
|
|
49
|
+
redactDebugData,
|
|
50
|
+
resetDebugRegistry,
|
|
51
|
+
} from "./debug-registry.ts";
|
|
52
|
+
export { fileToUri, resolveToolPath, stripToolPathPrefix, uriToFile } from "./path-utils.ts";
|
|
53
|
+
export type { KnownRootEntry } from "./project-roots.ts";
|
|
54
|
+
export {
|
|
55
|
+
buildKnownRootsMap,
|
|
56
|
+
byPathDepth,
|
|
57
|
+
dedupeTopmostRoots,
|
|
58
|
+
findProjectRoot,
|
|
59
|
+
isWithin,
|
|
60
|
+
isWithinOrEqual,
|
|
61
|
+
mergeKnownRoots,
|
|
62
|
+
resolveKnownRoot,
|
|
63
|
+
segmentCount,
|
|
64
|
+
sortRootsBySpecificity,
|
|
65
|
+
walkProject,
|
|
66
|
+
} from "./project-roots.ts";
|
|
67
|
+
export { createRegistry, createSessionStateRegistry } from "./registry-utils.ts";
|
|
68
|
+
export { getActiveBranchEntries } from "./session-utils.ts";
|
|
69
|
+
export { registerSettingsCommand } from "./settings/settings-command.ts";
|
|
70
|
+
export type { SettingsScope, SettingsSection } from "./settings/settings-registry.ts";
|
|
71
|
+
export {
|
|
72
|
+
clearRegisteredSettings,
|
|
73
|
+
getRegisteredSettings,
|
|
74
|
+
registerSettings,
|
|
75
|
+
} from "./settings/settings-registry.ts";
|
|
76
|
+
export { createInputSubmenu, openSettingsOverlay } from "./settings/settings-ui.ts";
|
|
77
|
+
export type { TitleTarget } from "./terminal.ts";
|
|
78
|
+
export {
|
|
79
|
+
DONE_SYMBOL,
|
|
80
|
+
formatTitle,
|
|
81
|
+
signalBell,
|
|
82
|
+
signalDone,
|
|
83
|
+
signalWaiting,
|
|
84
|
+
WAITING_SYMBOL,
|
|
85
|
+
} from "./terminal.ts";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Config-aware settings helper for SuPi config-backed settings sections.
|
|
2
|
+
// Wraps registerSettings() and centralizes selected-scope loading + scoped persistence.
|
|
3
|
+
|
|
4
|
+
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
5
|
+
import type { SettingsScope } from "../settings/settings-registry.ts";
|
|
6
|
+
import { registerSettings } from "../settings/settings-registry.ts";
|
|
7
|
+
import { loadSupiConfigForScope, removeSupiConfigKey, writeSupiConfig } from "./config.ts";
|
|
8
|
+
|
|
9
|
+
export interface ConfigSettingsHelpers {
|
|
10
|
+
/** Write a key to the selected scope's config section. */
|
|
11
|
+
set(key: string, value: unknown): void;
|
|
12
|
+
/** Remove a key from the selected scope's config section. */
|
|
13
|
+
unset(key: string): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ConfigSettingsOptions<T> {
|
|
17
|
+
/** Extension identifier — e.g. "lsp", "claude-md" */
|
|
18
|
+
id: string;
|
|
19
|
+
/** Human-readable label shown in the UI */
|
|
20
|
+
label: string;
|
|
21
|
+
/** SuPi config section name — e.g. "lsp", "claude-md" */
|
|
22
|
+
section: string;
|
|
23
|
+
/** Default config values */
|
|
24
|
+
defaults: T;
|
|
25
|
+
/** Build SettingItem[] from scoped config. Called by loadValues. */
|
|
26
|
+
buildItems: (settings: T, scope: SettingsScope, cwd: string) => SettingItem[];
|
|
27
|
+
/** Handle a settings change with scoped persistence helpers. */
|
|
28
|
+
persistChange: (
|
|
29
|
+
scope: SettingsScope,
|
|
30
|
+
cwd: string,
|
|
31
|
+
settingId: string,
|
|
32
|
+
value: string,
|
|
33
|
+
helpers: ConfigSettingsHelpers,
|
|
34
|
+
) => void;
|
|
35
|
+
/** Optional home directory for config resolution (testing). */
|
|
36
|
+
homeDir?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Register a config-backed settings section.
|
|
41
|
+
*
|
|
42
|
+
* Loads display values from the selected scope only (`defaults <- selected scope`)
|
|
43
|
+
* instead of merged effective runtime config. Provides scoped `set` / `unset`
|
|
44
|
+
* persistence helpers so extensions don't need to wire `writeSupiConfig` /
|
|
45
|
+
* `removeSupiConfigKey` by hand.
|
|
46
|
+
*/
|
|
47
|
+
export function registerConfigSettings<T>(options: ConfigSettingsOptions<T>): void {
|
|
48
|
+
registerSettings({
|
|
49
|
+
id: options.id,
|
|
50
|
+
label: options.label,
|
|
51
|
+
loadValues: (scope, cwd) => {
|
|
52
|
+
const settings = loadSupiConfigForScope(options.section, cwd, options.defaults, {
|
|
53
|
+
scope,
|
|
54
|
+
homeDir: options.homeDir,
|
|
55
|
+
});
|
|
56
|
+
return options.buildItems(settings, scope, cwd);
|
|
57
|
+
},
|
|
58
|
+
persistChange: (scope, cwd, settingId, value) => {
|
|
59
|
+
const helpers: ConfigSettingsHelpers = {
|
|
60
|
+
set: (key, val) => {
|
|
61
|
+
writeSupiConfig(
|
|
62
|
+
{ section: options.section, scope, cwd },
|
|
63
|
+
{ [key]: val },
|
|
64
|
+
{ homeDir: options.homeDir },
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
unset: (key) => {
|
|
68
|
+
removeSupiConfigKey({ section: options.section, scope, cwd }, key, {
|
|
69
|
+
homeDir: options.homeDir,
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
options.persistChange(scope, cwd, settingId, value, helpers);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
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
|
+
|
|
11
|
+
const GLOBAL_CONFIG_DIR = ".pi/agent/supi";
|
|
12
|
+
const PROJECT_CONFIG_DIR = ".pi/supi";
|
|
13
|
+
const CONFIG_FILE = "config.json";
|
|
14
|
+
|
|
15
|
+
function getGlobalConfigPath(homeDir?: string): string {
|
|
16
|
+
return path.join(homeDir ?? os.homedir(), GLOBAL_CONFIG_DIR, CONFIG_FILE);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getProjectConfigPath(cwd: string): string {
|
|
20
|
+
return path.join(cwd, PROJECT_CONFIG_DIR, CONFIG_FILE);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readJsonFile(filePath: string): Record<string, unknown> | null {
|
|
24
|
+
let content: string;
|
|
25
|
+
try {
|
|
26
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
27
|
+
} catch {
|
|
28
|
+
// ENOENT or permission error — silent, file may not exist
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let parsed: unknown;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(content);
|
|
35
|
+
} catch {
|
|
36
|
+
// biome-ignore lint/suspicious/noConsole: deliberate config parse warning
|
|
37
|
+
console.warn(`[supi-core] Failed to parse config file, ignoring: ${filePath}`);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
42
|
+
return parsed as Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// biome-ignore lint/suspicious/noConsole: deliberate config parse warning
|
|
46
|
+
console.warn(`[supi-core] Config file root is not an object, ignoring: ${filePath}`);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function shallowMerge<T>(base: T, ...overrides: Array<Record<string, unknown> | null>): T {
|
|
51
|
+
let result = { ...base };
|
|
52
|
+
for (const override of overrides) {
|
|
53
|
+
if (!override) continue;
|
|
54
|
+
result = { ...result, ...override };
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SupiConfigOptions {
|
|
60
|
+
homeDir?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Load and merge config for a given extension section.
|
|
65
|
+
*
|
|
66
|
+
* Resolution order: defaults ← global ← project
|
|
67
|
+
*/
|
|
68
|
+
export function loadSupiConfig<T>(
|
|
69
|
+
section: string,
|
|
70
|
+
cwd: string,
|
|
71
|
+
defaults: T,
|
|
72
|
+
options?: SupiConfigOptions,
|
|
73
|
+
): T {
|
|
74
|
+
const globalConfig = readJsonFile(getGlobalConfigPath(options?.homeDir));
|
|
75
|
+
const projectConfig = readJsonFile(getProjectConfigPath(cwd));
|
|
76
|
+
|
|
77
|
+
const globalSection = extractSection(globalConfig, section);
|
|
78
|
+
const projectSection = extractSection(projectConfig, section);
|
|
79
|
+
|
|
80
|
+
return shallowMerge(defaults, globalSection, projectSection);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Load config for a single scope only.
|
|
85
|
+
*
|
|
86
|
+
* Resolution order: defaults ← selected scope
|
|
87
|
+
*
|
|
88
|
+
* This is useful for settings UIs that need to show the raw values stored in
|
|
89
|
+
* one scope, rather than the effective merged config.
|
|
90
|
+
*/
|
|
91
|
+
export function loadSupiConfigForScope<T>(
|
|
92
|
+
section: string,
|
|
93
|
+
cwd: string,
|
|
94
|
+
defaults: T,
|
|
95
|
+
options: { scope: "global" | "project" } & SupiConfigOptions,
|
|
96
|
+
): T {
|
|
97
|
+
const config =
|
|
98
|
+
options.scope === "global"
|
|
99
|
+
? readJsonFile(getGlobalConfigPath(options.homeDir))
|
|
100
|
+
: readJsonFile(getProjectConfigPath(cwd));
|
|
101
|
+
|
|
102
|
+
const scopedSection = extractSection(config, section);
|
|
103
|
+
return shallowMerge(defaults, scopedSection);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface SupiConfigLocation {
|
|
107
|
+
section: string;
|
|
108
|
+
scope: "global" | "project";
|
|
109
|
+
cwd: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Write config values for a given extension section.
|
|
114
|
+
*/
|
|
115
|
+
export function writeSupiConfig(
|
|
116
|
+
loc: SupiConfigLocation,
|
|
117
|
+
value: Record<string, unknown>,
|
|
118
|
+
options?: SupiConfigOptions,
|
|
119
|
+
): void {
|
|
120
|
+
const configPath =
|
|
121
|
+
loc.scope === "global" ? getGlobalConfigPath(options?.homeDir) : getProjectConfigPath(loc.cwd);
|
|
122
|
+
|
|
123
|
+
const dir = path.dirname(configPath);
|
|
124
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
125
|
+
|
|
126
|
+
const existing = readJsonFile(configPath) ?? {};
|
|
127
|
+
existing[loc.section] = {
|
|
128
|
+
...((existing[loc.section] as Record<string, unknown>) ?? {}),
|
|
129
|
+
...value,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
fs.writeFileSync(configPath, `${JSON.stringify(existing, null, 2)}\n`, "utf-8");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Remove a key from a config section.
|
|
137
|
+
* Used by `interval default` to remove the project override.
|
|
138
|
+
*/
|
|
139
|
+
export function removeSupiConfigKey(
|
|
140
|
+
loc: SupiConfigLocation,
|
|
141
|
+
key: string,
|
|
142
|
+
options?: SupiConfigOptions,
|
|
143
|
+
): void {
|
|
144
|
+
const configPath =
|
|
145
|
+
loc.scope === "global" ? getGlobalConfigPath(options?.homeDir) : getProjectConfigPath(loc.cwd);
|
|
146
|
+
|
|
147
|
+
const existing = readJsonFile(configPath);
|
|
148
|
+
if (!existing) return;
|
|
149
|
+
|
|
150
|
+
const sectionData = existing[loc.section] as Record<string, unknown> | undefined;
|
|
151
|
+
if (!sectionData) return;
|
|
152
|
+
|
|
153
|
+
delete sectionData[key];
|
|
154
|
+
|
|
155
|
+
if (Object.keys(sectionData).length === 0) {
|
|
156
|
+
delete existing[loc.section];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const dir = path.dirname(configPath);
|
|
160
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
161
|
+
|
|
162
|
+
const content = Object.keys(existing).length > 0 ? `${JSON.stringify(existing, null, 2)}\n` : "";
|
|
163
|
+
|
|
164
|
+
if (content) {
|
|
165
|
+
// Directory guaranteed to exist since we just read from it
|
|
166
|
+
fs.writeFileSync(configPath, content, "utf-8");
|
|
167
|
+
} else {
|
|
168
|
+
try {
|
|
169
|
+
fs.unlinkSync(configPath);
|
|
170
|
+
} catch {
|
|
171
|
+
// File may not exist
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function extractSection(
|
|
177
|
+
config: Record<string, unknown> | null,
|
|
178
|
+
section: string,
|
|
179
|
+
): Record<string, unknown> | null {
|
|
180
|
+
if (!config) return null;
|
|
181
|
+
const data = config[section];
|
|
182
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data)) {
|
|
183
|
+
return data as Record<string, unknown>;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|