decorated-pi 0.6.0 → 0.7.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 -10
- package/commands/dp-settings.ts +6 -1
- package/hooks/mcp.ts +52 -0
- package/hooks/session-title.ts +25 -1
- package/index.ts +33 -22
- package/package.json +4 -3
- package/settings.ts +170 -31
- package/tools/ask/index.ts +93 -0
- package/tools/mcp/builtin/codegraph.ts +10 -34
- package/tools/mcp/builtin/index.ts +2 -2
- package/tools/mcp/config.ts +93 -21
- package/ui/ask.ts +443 -0
- package/ui/module-settings.ts +131 -26
- package/ui/usage.ts +4 -4
- package/.codegraph/daemon.pid +0 -6
- package/AGENTS.md +0 -92
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ pi install /path/to/decorated-pi
|
|
|
14
14
|
|
|
15
15
|
### 1. Token Efficiency
|
|
16
16
|
|
|
17
|
-
Multiple layers of token savings that compound across every session.
|
|
17
|
+
Multiple layers of token savings that compound across every session. **All integrated CLI tools only require installing their respective CLIs — zero config**.
|
|
18
18
|
|
|
19
19
|
**RTK** — integrates [RTK](https://github.com/rtk-ai/rtk) to compress bash output into structured summaries, so the LLM never sees raw noise.
|
|
20
20
|
|
|
@@ -73,7 +73,7 @@ Zero-config MCP client with built-in servers:
|
|
|
73
73
|
| Exa | `exa_*` | `https://mcp.exa.ai/mcp` |
|
|
74
74
|
| codegraph | `codegraph_*` | bundled binary |
|
|
75
75
|
|
|
76
|
-
**Custom servers** in `.pi/agent/mcp.json` (project) or `~/.pi/agent/
|
|
76
|
+
**Custom servers** in `.pi/agent/mcp.json` (project) or `~/.pi/agent/mcp.json` (global). Project overrides global. Tool prompts and schemas are cached locally so MCP tools are available immediately on startup.
|
|
77
77
|
|
|
78
78
|
```json
|
|
79
79
|
{
|
|
@@ -118,8 +118,8 @@ Example redaction on a `read` / `bash` output:
|
|
|
118
118
|
### 5. Other
|
|
119
119
|
|
|
120
120
|
- `/usage` — token stats with cache‑hit rate, per‑model breakdown (Session / Today / This Week / This Month / All Time)
|
|
121
|
-
- Progressive context — supports subdirectory `AGENTS.md` / `CLAUDE.md` discovery and injection
|
|
122
121
|
- `/retry` — continue after interruption
|
|
122
|
+
- Progressive context — supports subdirectory `AGENTS.md` / `CLAUDE.md` discovery and injection
|
|
123
123
|
- **WakaTime** — coding activity tracking via [WakaTime](https://wakatime.com)
|
|
124
124
|
|
|
125
125
|
## Configuration
|
|
@@ -129,13 +129,22 @@ Runtime settings in `~/.pi/agent/decorated-pi.json`. Modules can be toggled via
|
|
|
129
129
|
```json
|
|
130
130
|
{
|
|
131
131
|
"modules": {
|
|
132
|
-
"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"
|
|
132
|
+
"tools": {
|
|
133
|
+
"patchOverrideEdit": true,
|
|
134
|
+
"ask": true,
|
|
135
|
+
"lsp": true,
|
|
136
|
+
"mcp": true
|
|
137
|
+
},
|
|
138
|
+
"hooks": {
|
|
139
|
+
"secretRedaction": true,
|
|
140
|
+
"rtk": true,
|
|
141
|
+
"wakatime": true
|
|
142
|
+
},
|
|
143
|
+
"commands": {
|
|
144
|
+
"atOverride": true,
|
|
145
|
+
"retry": true,
|
|
146
|
+
"usage": true
|
|
147
|
+
}
|
|
139
148
|
}
|
|
140
149
|
}
|
|
141
150
|
```
|
package/commands/dp-settings.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { moduleSnapshotChanged } from "../settings.js";
|
|
6
7
|
import { ModuleSettingsComponent } from "../ui/module-settings.js";
|
|
7
8
|
|
|
8
9
|
export function registerDpSettingsCommand(pi: ExtensionAPI): void {
|
|
@@ -14,7 +15,11 @@ export function registerDpSettingsCommand(pi: ExtensionAPI): void {
|
|
|
14
15
|
(tui, theme, _kb, done) =>
|
|
15
16
|
new ModuleSettingsComponent(tui, theme, () => done(undefined))
|
|
16
17
|
);
|
|
17
|
-
|
|
18
|
+
// Only prompt for reload when the effective settings differ from
|
|
19
|
+
// the snapshot taken when pi loaded the extension.
|
|
20
|
+
if (moduleSnapshotChanged()) {
|
|
21
|
+
ctx.ui.notify("Module settings updated. /reload to apply.", "warning");
|
|
22
|
+
}
|
|
18
23
|
return;
|
|
19
24
|
}
|
|
20
25
|
ctx.ui.notify("dp-settings requires interactive mode.", "warning");
|
package/hooks/mcp.ts
CHANGED
|
@@ -31,6 +31,11 @@ function cacheScopeForSource(source: string): "global" | "project" {
|
|
|
31
31
|
return source === "project" ? "project" : "global";
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function canUseServer(config: McpServerConfig, cwd?: string): boolean {
|
|
35
|
+
if (!config.canUseInProject) return true;
|
|
36
|
+
return config.canUseInProject(cwd ?? process.cwd());
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
async function connectAll(
|
|
35
40
|
configs: McpServerConfig[],
|
|
36
41
|
registry: any,
|
|
@@ -81,6 +86,15 @@ async function connectAll(
|
|
|
81
86
|
for (const server of configs) {
|
|
82
87
|
if (watchdogFired) break; // remaining servers are already failed
|
|
83
88
|
|
|
89
|
+
if (!canUseServer(server, cachedCwd || undefined)) {
|
|
90
|
+
allServers.set(server.name, {
|
|
91
|
+
name: server.name, url: server.url ?? server.command ?? "(unknown)", source: server.source,
|
|
92
|
+
state: "failed", toolCount: 0, tools: [],
|
|
93
|
+
error: "Project is missing required artefacts for this server",
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
84
98
|
const conn = new McpConnection(server.name, server);
|
|
85
99
|
conn.source = server.source;
|
|
86
100
|
try {
|
|
@@ -89,6 +103,18 @@ async function connectAll(
|
|
|
89
103
|
try { await conn.disconnect(); } catch { /* ignore */ }
|
|
90
104
|
return { schemaChanges, hasNewServer };
|
|
91
105
|
}
|
|
106
|
+
if (conn.tools.length === 0) {
|
|
107
|
+
// Server connected but advertises no tools. Treat this as a
|
|
108
|
+
// failed state and do NOT write an empty cache, otherwise a
|
|
109
|
+
// subsequent /reload will keep using the empty cache forever.
|
|
110
|
+
try { await conn.disconnect(); } catch { /* ignore */ }
|
|
111
|
+
allServers.set(server.name, {
|
|
112
|
+
name: server.name, url: server.url ?? server.command ?? "(unknown)", source: server.source,
|
|
113
|
+
state: "failed", toolCount: 0, tools: [],
|
|
114
|
+
error: "Server connected but returned no tools (e.g. codegraph without a .codegraph index)",
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
92
118
|
activeConnections.push(conn);
|
|
93
119
|
const actualTools = conn.tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }));
|
|
94
120
|
const cachedEntry = cache?.servers[server.name];
|
|
@@ -195,6 +221,9 @@ export async function updateConfigEnabled(serverName: string, enabled: boolean):
|
|
|
195
221
|
export async function refreshServerCache(serverName: string, registry: any): Promise<{ ok: boolean; error?: string }> {
|
|
196
222
|
const config = resolveMcpConfigs(cachedCwd).find(s => s.name === serverName);
|
|
197
223
|
if (!config) return { ok: false, error: `Server "${serverName}" not found in config.` };
|
|
224
|
+
if (!canUseServer(config, cachedCwd || undefined)) {
|
|
225
|
+
return { ok: false, error: "Project is missing required artefacts for this server" };
|
|
226
|
+
}
|
|
198
227
|
const existing = activeConnections.find(c => c.serverName === serverName);
|
|
199
228
|
if (existing) {
|
|
200
229
|
try { await existing.disconnect(); } catch { /* ignore */ }
|
|
@@ -204,6 +233,12 @@ export async function refreshServerCache(serverName: string, registry: any): Pro
|
|
|
204
233
|
conn.source = config.source;
|
|
205
234
|
try {
|
|
206
235
|
await conn.connect(30_000);
|
|
236
|
+
if (conn.tools.length === 0) {
|
|
237
|
+
try { await conn.disconnect(); } catch { /* ignore */ }
|
|
238
|
+
const error = "Server connected but returned no tools (e.g. codegraph without a .codegraph index)";
|
|
239
|
+
allServers.set(config.name, { name: config.name, url: config.url ?? config.command ?? "(unknown)", source: config.source, state: "failed", toolCount: 0, tools: [], error });
|
|
240
|
+
return { ok: false, error };
|
|
241
|
+
}
|
|
207
242
|
activeConnections.push(conn);
|
|
208
243
|
allServers.set(config.name, {
|
|
209
244
|
name: config.name, url: config.url ?? config.command ?? "(unknown)", source: config.source,
|
|
@@ -242,6 +277,8 @@ export function getCachedMcpConfigs(): McpServerConfig[] {
|
|
|
242
277
|
* can find it.
|
|
243
278
|
*/
|
|
244
279
|
export async function ensureMcpServerReady(pi: ExtensionAPI, config: McpServerConfig, cwd?: string): Promise<void> {
|
|
280
|
+
if (!canUseServer(config, cwd)) return;
|
|
281
|
+
|
|
245
282
|
const scope = cacheScopeForSource(config.source);
|
|
246
283
|
const cache = loadScopedMcpCache(scope, cwd);
|
|
247
284
|
const entry = cache?.servers[config.name];
|
|
@@ -262,6 +299,21 @@ export async function ensureMcpServerReady(pi: ExtensionAPI, config: McpServerCo
|
|
|
262
299
|
conn.source = config.source;
|
|
263
300
|
try {
|
|
264
301
|
await conn.connect(30_000);
|
|
302
|
+
if (conn.tools.length === 0) {
|
|
303
|
+
// Empty tool list means the server is not useful. Disconnect and
|
|
304
|
+
// leave cache untouched so the next /reload retries the connection.
|
|
305
|
+
try { await conn.disconnect(); } catch { /* ignore */ }
|
|
306
|
+
allServers.set(config.name, {
|
|
307
|
+
name: config.name,
|
|
308
|
+
url: config.url ?? config.command ?? "(unknown)",
|
|
309
|
+
source: config.source,
|
|
310
|
+
state: "failed",
|
|
311
|
+
toolCount: 0,
|
|
312
|
+
tools: [],
|
|
313
|
+
error: "Server connected but returned no tools (e.g. codegraph without a .codegraph index)",
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
265
317
|
activeConnections.push(conn);
|
|
266
318
|
const tools: McpToolCache[] = conn.tools.map(t => ({
|
|
267
319
|
name: t.name,
|
package/hooks/session-title.ts
CHANGED
|
@@ -40,13 +40,37 @@ export function extractFirstMessage(entries: SessionEntryLike[]): string | undef
|
|
|
40
40
|
return undefined;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function normalizeTitle(text: string): string | undefined {
|
|
44
|
+
const nl = text.indexOf("\n");
|
|
45
|
+
const oneLine = (nl === -1 ? text : text.slice(0, nl)).trim();
|
|
46
|
+
if (!oneLine) return undefined;
|
|
47
|
+
if (oneLine.length <= MAX_SESSION_TITLE_LENGTH) return oneLine;
|
|
48
|
+
return oneLine.slice(0, MAX_SESSION_TITLE_LENGTH - 1) + "…";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function trySetSessionName(ctx: any, pi: ExtensionAPI): void {
|
|
52
|
+
if (ctx.sessionManager.getSessionName()) return;
|
|
53
|
+
const title = extractFirstMessage(ctx.sessionManager.getBranch());
|
|
54
|
+
if (title) (pi as any).setSessionName(title);
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
export const sessionTitleModule: Module = {
|
|
44
58
|
name: "session-title",
|
|
45
59
|
hooks: {
|
|
46
60
|
session_start: [
|
|
47
61
|
(_event, ctx, pi) => {
|
|
62
|
+
trySetSessionName(ctx, pi);
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
input: [
|
|
66
|
+
(event, ctx, pi) => {
|
|
67
|
+
// Skip if already named, not a fresh interactive message,
|
|
68
|
+
// or stream steering/follow-up messages.
|
|
48
69
|
if (ctx.sessionManager.getSessionName()) return;
|
|
49
|
-
|
|
70
|
+
if (event.source === "extension") return;
|
|
71
|
+
if (event.streamingBehavior) return;
|
|
72
|
+
const text = typeof event.text === "string" ? event.text.trim() : "";
|
|
73
|
+
const title = normalizeTitle(text);
|
|
50
74
|
if (title) (pi as any).setSessionName(title);
|
|
51
75
|
},
|
|
52
76
|
],
|
package/index.ts
CHANGED
|
@@ -36,9 +36,9 @@ import { setupRtk } from "./hooks/rtk.js";
|
|
|
36
36
|
import { registerPatchTool } from "./tools/patch/index.js";
|
|
37
37
|
import { registerLspTools } from "./tools/lsp/tools.js";
|
|
38
38
|
import { LspServerManager } from "./tools/lsp/manager.js";
|
|
39
|
-
import {
|
|
39
|
+
import { registerAskTool } from "./tools/ask/index.js";
|
|
40
|
+
import { resolveMcpConfigs, migrateLegacyGlobalMcpConfig } from "./tools/mcp/config.js";
|
|
40
41
|
import { ensureMcpServerReady } from "./hooks/mcp.js";
|
|
41
|
-
import { CODEGRAPH_GUIDANCE } from "./tools/mcp/builtin/codegraph.js";
|
|
42
42
|
|
|
43
43
|
import { registerDpModelCommand } from "./commands/dp-model.js";
|
|
44
44
|
import { registerDpSettingsCommand } from "./commands/dp-settings.js";
|
|
@@ -46,7 +46,7 @@ import { registerMcpStatusCommand } from "./commands/mcp-status.js";
|
|
|
46
46
|
import { registerRetryCommand } from "./commands/retry.js";
|
|
47
47
|
import { registerUsageCommand } from "./commands/usage.js";
|
|
48
48
|
|
|
49
|
-
import { isModuleEnabled } from "./settings.js";
|
|
49
|
+
import { captureModuleSnapshot, isModuleEnabled } from "./settings.js";
|
|
50
50
|
|
|
51
51
|
// ─── System-prompt guidelines (hard-coded base, per-module imports) ────────
|
|
52
52
|
//
|
|
@@ -57,7 +57,7 @@ const BASE_GUIDANCE = [
|
|
|
57
57
|
"## Decorated Pi Guidance",
|
|
58
58
|
"",
|
|
59
59
|
"### Workflow, how to approach tasks",
|
|
60
|
-
"- Before acting on a prompt
|
|
60
|
+
"- Before acting on a prompt, do sufficient research on the existing state — read files, search, investigate — and only proceed once you have a clear picture.",
|
|
61
61
|
"- Exercise caution when performing any **write** operations, especially when you are in a research or exploration phase.",
|
|
62
62
|
"- Before modifying code, match the user's existing code style (naming, formatting, patterns). Do not re-modify lines the user has manually edited since your last change.",
|
|
63
63
|
"",
|
|
@@ -66,16 +66,17 @@ const BASE_GUIDANCE = [
|
|
|
66
66
|
].join("\n");
|
|
67
67
|
|
|
68
68
|
/** Build the list of guideline strings to inject, in prompt order.
|
|
69
|
-
* Always-on rules first, then per-module guidelines
|
|
70
|
-
*
|
|
71
|
-
*
|
|
69
|
+
* Always-on rules first, then per-module guidelines. REDACT_GUIDANCE is
|
|
70
|
+
* gated by the secretRedaction module; CodeGraph guidance follows the MCP
|
|
71
|
+
* server switch chain (mcp module on → codegraph server on → guidance
|
|
72
|
+
* injected). The mcp module check lives in `resolveMcpConfigs`, so this
|
|
73
|
+
* code only needs to look at the server's own `enabled` flag. */
|
|
72
74
|
function buildGuidelines(): string[] {
|
|
73
75
|
const out: string[] = [
|
|
74
76
|
BASE_GUIDANCE,
|
|
75
|
-
|
|
76
|
-
INJECT_AGENTS_MD_GUIDANCE, // from hooks/inject-agents-md.ts — always on (smart-at module)
|
|
77
|
+
INJECT_AGENTS_MD_GUIDANCE, // from hooks/inject-agents-md.ts — always on
|
|
77
78
|
];
|
|
78
|
-
if (isModuleEnabled("
|
|
79
|
+
if (isModuleEnabled("secretRedaction")) out.push(REDACT_GUIDANCE);
|
|
79
80
|
return out;
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -96,13 +97,18 @@ function installGuidelines(pi: ExtensionAPI): void {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
export default async function (pi: ExtensionAPI) {
|
|
100
|
+
// Snapshot the module settings that pi is about to load. /dp-settings
|
|
101
|
+
// compares against this to avoid prompting for reload when the user
|
|
102
|
+
// has only returned the settings to the currently-loaded state.
|
|
103
|
+
captureModuleSnapshot();
|
|
104
|
+
|
|
99
105
|
// ── Skeleton (hooks) ───────────────────────────────────────────────────
|
|
100
106
|
const sk = createSkeleton();
|
|
101
107
|
|
|
102
108
|
// Order matters for tool_result compose chain:
|
|
103
109
|
// 1. redact → normalize-codeblocks → externalize → track-mtime → inject-agents-md → image-vision → wakatime
|
|
104
110
|
// The first module registered for a given event runs first (compose chain).
|
|
105
|
-
setupRedact(sk);
|
|
111
|
+
if (isModuleEnabled("secretRedaction")) setupRedact(sk);
|
|
106
112
|
sk.register(normalizeCodeblocksModule);
|
|
107
113
|
sk.register(externalizeModule);
|
|
108
114
|
sk.register(trackMtimeModule);
|
|
@@ -114,22 +120,27 @@ export default async function (pi: ExtensionAPI) {
|
|
|
114
120
|
// anything else inspects the tool list.
|
|
115
121
|
sk.register(piToolFilterModule);
|
|
116
122
|
sk.register(sessionTitleModule);
|
|
117
|
-
sk.register(smartAtModule);
|
|
118
|
-
sk.register(wakatimeModule);
|
|
123
|
+
if (isModuleEnabled("atOverride")) sk.register(smartAtModule);
|
|
124
|
+
if (isModuleEnabled("wakatime")) sk.register(wakatimeModule);
|
|
119
125
|
|
|
120
126
|
// Compaction + RTK (these also install their own pi.on via setup<>()).
|
|
121
127
|
setupCompaction(sk);
|
|
122
|
-
setupRtk(sk, pi);
|
|
123
|
-
setupWakatime(sk, pi);
|
|
128
|
+
if (isModuleEnabled("rtk")) setupRtk(sk, pi);
|
|
129
|
+
if (isModuleEnabled("wakatime")) setupWakatime(sk, pi);
|
|
124
130
|
|
|
125
131
|
// ── Tools (conditional on module switches) ────────────────────────────
|
|
126
|
-
if (isModuleEnabled("
|
|
132
|
+
if (isModuleEnabled("patchOverrideEdit")) registerPatchTool(pi);
|
|
127
133
|
if (isModuleEnabled("lsp")) registerLspTools(pi, new LspServerManager());
|
|
134
|
+
if (isModuleEnabled("ask")) registerAskTool(pi);
|
|
128
135
|
|
|
129
|
-
// MCP: hook
|
|
130
|
-
// means no session_start handler runs, no tools register,
|
|
131
|
-
// background connections are attempted.
|
|
136
|
+
// MCP: hook, tools, and /mcp command are gated together. Disabling the
|
|
137
|
+
// module means no session_start handler runs, no tools register, no
|
|
138
|
+
// /mcp command is available, and no background connections are attempted.
|
|
132
139
|
if (isModuleEnabled("mcp")) {
|
|
140
|
+
// One-time migration: legacy global MCP configs in
|
|
141
|
+
// ~/.pi/agent/decorated-pi.json move to ~/.pi/agent/mcp.json. Run
|
|
142
|
+
// explicitly here so `loadGlobalMcpConfigs` stays pure.
|
|
143
|
+
migrateLegacyGlobalMcpConfig();
|
|
133
144
|
sk.register(mcpModule);
|
|
134
145
|
const configs = resolveMcpConfigs(process.cwd()).filter(s => s.enabled);
|
|
135
146
|
// Per-server readiness: cache hit → register from cache (fast).
|
|
@@ -138,6 +149,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
138
149
|
for (const config of configs) {
|
|
139
150
|
await ensureMcpServerReady(pi, config, process.cwd());
|
|
140
151
|
}
|
|
152
|
+
registerMcpStatusCommand(pi);
|
|
141
153
|
}
|
|
142
154
|
|
|
143
155
|
// ── System-prompt guidelines (single handler, array order = prompt order) ──
|
|
@@ -146,9 +158,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
146
158
|
// ── Commands ──────────────────────────────────────────────────────────
|
|
147
159
|
registerDpModelCommand(pi);
|
|
148
160
|
registerDpSettingsCommand(pi);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
registerUsageCommand(pi);
|
|
161
|
+
if (isModuleEnabled("retry")) registerRetryCommand(pi);
|
|
162
|
+
if (isModuleEnabled("usage")) registerUsageCommand(pi);
|
|
152
163
|
|
|
153
164
|
// ── Install skeleton (last) ────────────────────────────────────────────
|
|
154
165
|
sk.install(pi);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decorated-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "decorated-pi is a practical enhancement pack for pi coding agent — token-efficient workflow, cache-friendly design, and smarter tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
"mcp",
|
|
11
11
|
"security",
|
|
12
12
|
"lsp",
|
|
13
|
-
"
|
|
14
|
-
"secret-
|
|
13
|
+
"lsp-client",
|
|
14
|
+
"secret-redaction",
|
|
15
|
+
"codegraph"
|
|
15
16
|
],
|
|
16
17
|
"license": "MIT",
|
|
17
18
|
"repository": {
|
package/settings.ts
CHANGED
|
@@ -35,14 +35,32 @@ export interface McpServerEntry {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export interface ModuleSettings {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
tools?: {
|
|
39
|
+
/** Replace Pi native edit/write with the patch tool. */
|
|
40
|
+
patchOverrideEdit?: boolean;
|
|
41
|
+
/** Interactive ask tool for user clarification (blocks loop until answered). */
|
|
42
|
+
ask?: boolean;
|
|
43
|
+
/** Language server diagnostics, hover, definition, references, symbols, rename. */
|
|
44
|
+
lsp?: boolean;
|
|
45
|
+
/** MCP client with builtin servers (context7, exa, codegraph). */
|
|
46
|
+
mcp?: boolean;
|
|
47
|
+
};
|
|
48
|
+
hooks?: {
|
|
49
|
+
/** Redact secrets from read/bash output before model context. */
|
|
50
|
+
secretRedaction?: boolean;
|
|
51
|
+
/** Rewrite bash through system RTK when available. */
|
|
52
|
+
"rtk"?: boolean;
|
|
53
|
+
/** Send coding activity heartbeats to WakaTime. */
|
|
54
|
+
wakatime?: boolean;
|
|
55
|
+
};
|
|
56
|
+
commands?: {
|
|
57
|
+
/** Project-aware file search replacing default autocomplete. */
|
|
58
|
+
atOverride?: boolean;
|
|
59
|
+
/** /retry command to continue after interruption. */
|
|
60
|
+
retry?: boolean;
|
|
61
|
+
/** /usage command for token stats. */
|
|
62
|
+
usage?: boolean;
|
|
63
|
+
};
|
|
46
64
|
}
|
|
47
65
|
|
|
48
66
|
export interface UsageIndexEntry {
|
|
@@ -62,7 +80,13 @@ export interface DecoratedPiConfig {
|
|
|
62
80
|
|
|
63
81
|
export function loadConfig(): DecoratedPiConfig {
|
|
64
82
|
try {
|
|
65
|
-
if (fs.existsSync(CONFIG_FILE))
|
|
83
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
84
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")) as DecoratedPiConfig;
|
|
85
|
+
if (migrateModuleSettings(config)) {
|
|
86
|
+
saveConfig(config);
|
|
87
|
+
}
|
|
88
|
+
return config;
|
|
89
|
+
}
|
|
66
90
|
} catch {}
|
|
67
91
|
return {};
|
|
68
92
|
}
|
|
@@ -152,42 +176,157 @@ export function setCompactModelKey(key: string | null) {
|
|
|
152
176
|
// ─── Module Switches ──────────────────────────────────────────────────────────
|
|
153
177
|
|
|
154
178
|
const DEFAULT_MODULES: Required<ModuleSettings> = {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
179
|
+
tools: {
|
|
180
|
+
patchOverrideEdit: true,
|
|
181
|
+
ask: true,
|
|
182
|
+
lsp: true,
|
|
183
|
+
mcp: true,
|
|
184
|
+
},
|
|
185
|
+
hooks: {
|
|
186
|
+
secretRedaction: true,
|
|
187
|
+
"rtk": true,
|
|
188
|
+
wakatime: true,
|
|
189
|
+
},
|
|
190
|
+
commands: {
|
|
191
|
+
atOverride: true,
|
|
192
|
+
retry: true,
|
|
193
|
+
usage: true,
|
|
194
|
+
},
|
|
163
195
|
};
|
|
164
196
|
|
|
165
|
-
|
|
197
|
+
/** Maps every module name to its category. Used by isModuleEnabled/setModuleEnabled. */
|
|
198
|
+
const MODULE_TO_CATEGORY: Record<string, keyof ModuleSettings> = {
|
|
199
|
+
patchOverrideEdit: "tools",
|
|
200
|
+
ask: "tools",
|
|
201
|
+
lsp: "tools",
|
|
202
|
+
mcp: "tools",
|
|
203
|
+
secretRedaction: "hooks",
|
|
204
|
+
"rtk": "hooks",
|
|
205
|
+
wakatime: "hooks",
|
|
206
|
+
atOverride: "commands",
|
|
207
|
+
retry: "commands",
|
|
208
|
+
usage: "commands",
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/** Legacy flat module keys that were renamed. Applied before flat→nested migration. */
|
|
212
|
+
const LEGACY_MODULE_KEYS: Record<string, string> = {
|
|
213
|
+
patch: "patchOverrideEdit",
|
|
214
|
+
safety: "secretRedaction",
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Migrate module settings to the nested tools/hooks/commands layout.
|
|
219
|
+
* Handles three legacy shapes:
|
|
220
|
+
* 1. Flat config with current names (patchOverrideEdit, secretRedaction, ...)
|
|
221
|
+
* 2. Flat config with old names (patch, safety)
|
|
222
|
+
* 3. Nested config with old inner names (tools.patch, hooks.safety)
|
|
223
|
+
* Already-correct nested configs are left untouched.
|
|
224
|
+
*/
|
|
225
|
+
function migrateModuleSettings(config: DecoratedPiConfig): boolean {
|
|
226
|
+
if (!config.modules) return false;
|
|
227
|
+
|
|
228
|
+
let migrated = false;
|
|
229
|
+
const result: ModuleSettings = {};
|
|
230
|
+
|
|
231
|
+
// Start from any already-nested values.
|
|
232
|
+
if (config.modules.tools) result.tools = { ...config.modules.tools };
|
|
233
|
+
if (config.modules.hooks) result.hooks = { ...config.modules.hooks };
|
|
234
|
+
if (config.modules.commands) result.commands = { ...config.modules.commands };
|
|
235
|
+
|
|
236
|
+
// Rename legacy keys inside already-nested categories.
|
|
237
|
+
function renameInCategory(
|
|
238
|
+
category: keyof ModuleSettings,
|
|
239
|
+
oldKey: string,
|
|
240
|
+
newKey: string,
|
|
241
|
+
) {
|
|
242
|
+
const cat = result[category] as Record<string, boolean> | undefined;
|
|
243
|
+
if (cat && oldKey in cat && !(newKey in cat)) {
|
|
244
|
+
cat[newKey] = cat[oldKey];
|
|
245
|
+
delete cat[oldKey];
|
|
246
|
+
migrated = true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
renameInCategory("tools", "patch", "patchOverrideEdit");
|
|
250
|
+
renameInCategory("hooks", "safety", "secretRedaction");
|
|
251
|
+
renameInCategory("commands", "smart-at", "atOverride");
|
|
252
|
+
|
|
253
|
+
// Migrate flat keys into nested categories.
|
|
254
|
+
const flatMapping: Record<string, [keyof ModuleSettings, string]> = {
|
|
255
|
+
patchOverrideEdit: ["tools", "patchOverrideEdit"],
|
|
256
|
+
ask: ["tools", "ask"],
|
|
257
|
+
lsp: ["tools", "lsp"],
|
|
258
|
+
mcp: ["tools", "mcp"],
|
|
259
|
+
secretRedaction: ["hooks", "secretRedaction"],
|
|
260
|
+
"rtk": ["hooks", "rtk"],
|
|
261
|
+
wakatime: ["hooks", "wakatime"],
|
|
262
|
+
atOverride: ["commands", "atOverride"],
|
|
263
|
+
retry: ["commands", "retry"],
|
|
264
|
+
usage: ["commands", "usage"],
|
|
265
|
+
patch: ["tools", "patchOverrideEdit"],
|
|
266
|
+
safety: ["hooks", "secretRedaction"],
|
|
267
|
+
"smart-at": ["commands", "atOverride"],
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
for (const [key, value] of Object.entries(config.modules)) {
|
|
271
|
+
if (key === "tools" || key === "hooks" || key === "commands") continue;
|
|
272
|
+
const mapping = flatMapping[key];
|
|
273
|
+
if (!mapping) continue;
|
|
274
|
+
const [category, newKey] = mapping;
|
|
275
|
+
if (!result[category]) result[category] = {} as any;
|
|
276
|
+
const cat = result[category] as Record<string, boolean>;
|
|
277
|
+
if (!(newKey in cat)) {
|
|
278
|
+
cat[newKey] = value as boolean;
|
|
279
|
+
}
|
|
280
|
+
migrated = true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!migrated) return false;
|
|
284
|
+
config.modules = result;
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function isModuleEnabled(name: string): boolean {
|
|
289
|
+
const category = MODULE_TO_CATEGORY[name];
|
|
290
|
+
if (!category) return true;
|
|
166
291
|
const modules = loadConfig().modules ?? {};
|
|
167
|
-
|
|
292
|
+
const cat = modules[category] as Record<string, boolean> | undefined;
|
|
293
|
+
const defaults = DEFAULT_MODULES[category] as Record<string, boolean>;
|
|
294
|
+
if (cat && name in cat) return cat[name];
|
|
295
|
+
return defaults[name] ?? true;
|
|
168
296
|
}
|
|
169
297
|
|
|
170
|
-
export function setModuleEnabled(name:
|
|
171
|
-
const
|
|
172
|
-
|
|
298
|
+
export function setModuleEnabled(name: string, enabled: boolean) {
|
|
299
|
+
const category = MODULE_TO_CATEGORY[name];
|
|
300
|
+
if (!category) return;
|
|
301
|
+
const modules: ModuleSettings = { ...loadConfig().modules };
|
|
302
|
+
modules[category] = { ...(modules[category] ?? {}), [name]: enabled } as any;
|
|
173
303
|
saveConfig({ modules });
|
|
174
304
|
}
|
|
175
305
|
|
|
176
306
|
export function getAllModuleSettings(): Required<ModuleSettings> {
|
|
177
307
|
const modules = loadConfig().modules ?? {};
|
|
178
|
-
return {
|
|
308
|
+
return {
|
|
309
|
+
tools: { ...DEFAULT_MODULES.tools, ...modules.tools },
|
|
310
|
+
hooks: { ...DEFAULT_MODULES.hooks, ...modules.hooks },
|
|
311
|
+
commands: { ...DEFAULT_MODULES.commands, ...modules.commands },
|
|
312
|
+
};
|
|
179
313
|
}
|
|
180
314
|
|
|
181
|
-
// ─── Codegraph (drives the builtin MCP server's auto-enable) ───────────────
|
|
182
|
-
|
|
183
315
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
316
|
+
* Snapshot of the module settings that pi is currently running with.
|
|
317
|
+
* Captured once when the extension loads; /dp-settings compares the
|
|
318
|
+
* current effective settings against this snapshot to decide whether a
|
|
319
|
+
* reload is actually necessary.
|
|
188
320
|
*/
|
|
189
|
-
|
|
190
|
-
|
|
321
|
+
let loadedModuleSnapshot: Required<ModuleSettings> | null = null;
|
|
322
|
+
|
|
323
|
+
export function captureModuleSnapshot(): void {
|
|
324
|
+
loadedModuleSnapshot = getAllModuleSettings();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function moduleSnapshotChanged(): boolean {
|
|
328
|
+
if (!loadedModuleSnapshot) return true;
|
|
329
|
+
return JSON.stringify(loadedModuleSnapshot) !== JSON.stringify(getAllModuleSettings());
|
|
191
330
|
}
|
|
192
331
|
|
|
193
332
|
// ─── Usage index (增量同步元数据) ─────────────────────────────────────────────
|