decorated-pi 0.3.0 → 0.4.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.
@@ -25,11 +25,17 @@ export interface ProviderCache {
25
25
  models: ProviderModelEntry[];
26
26
  }
27
27
 
28
+ export interface McpServerEntry {
29
+ url: string;
30
+ enabled?: boolean;
31
+ }
32
+
28
33
  export interface ModuleSettings {
29
34
  safety?: boolean;
30
35
  lsp?: boolean;
31
36
  "smart-at"?: boolean;
32
37
  patch?: boolean;
38
+ mcp?: boolean;
33
39
  }
34
40
 
35
41
  export interface DecoratedPiConfig {
@@ -37,6 +43,7 @@ export interface DecoratedPiConfig {
37
43
  compactModelKey?: string | null;
38
44
  providers?: Record<string, ProviderCache>;
39
45
  modules?: ModuleSettings;
46
+ mcpServers?: Record<string, McpServerEntry>;
40
47
  }
41
48
 
42
49
  export function loadConfig(): DecoratedPiConfig {
@@ -113,6 +120,7 @@ const DEFAULT_MODULES: Required<ModuleSettings> = {
113
120
  lsp: true,
114
121
  "smart-at": true,
115
122
  patch: true,
123
+ mcp: true,
116
124
  };
117
125
 
118
126
  export function isModuleEnabled(name: keyof ModuleSettings): boolean {
@@ -9,7 +9,8 @@
9
9
  import type { ExtensionAPI, ExtensionContext, Theme as PiTheme } from "@earendil-works/pi-coding-agent";
10
10
  import { ModelPickerComponent } from "./model-integration.js";
11
11
  import { getAllModuleSettings, setModuleEnabled, type ModuleSettings } from "./settings.js";
12
- import { Container, SettingsList, type TUI, type SettingsListTheme, type Component } from "@earendil-works/pi-tui";
12
+ import { getMcpStatus } from "./mcp/index.js";
13
+ import { Container, SettingsList, Spacer, Text, type TUI, type SettingsListTheme, type Component, getKeybindings } from "@earendil-works/pi-tui";
13
14
 
14
15
  // ─── Border component (matches native DynamicBorder) ────────────────────────
15
16
 
@@ -60,17 +61,19 @@ function setupDpModelCommand(pi: ExtensionAPI) {
60
61
  // ─── /dp-settings ──────────────────────────────────────────────────────────
61
62
 
62
63
  const MODULE_LABELS: Record<keyof ModuleSettings, string> = {
63
- patch: "Patch Tool",
64
- safety: "Safety Layer",
65
- lsp: "LSP Tools",
66
- "smart-at": "Smart @ Search",
64
+ patch: "patch",
65
+ safety: "Secret Redaction",
66
+ lsp: "LSP",
67
+ "smart-at": "@ overload",
68
+ mcp: "MCP",
67
69
  };
68
70
 
69
71
  const MODULE_DESCS: Record<keyof ModuleSettings, string> = {
70
72
  patch: "Replace edit/write with patch tool (old_str/new_str replacement + overwrite)",
71
- safety: "Command guard, protected paths, read guard, secret redaction",
73
+ safety: "Redact secrets from read / bash output before they enter model context",
72
74
  lsp: "Language server diagnostics, hover, definition, references, symbols, rename",
73
75
  "smart-at": "Project-aware file search replacing default autocomplete",
76
+ mcp: "MCP client for context7 and exa (zero-config)",
74
77
  };
75
78
 
76
79
  class ModuleSettingsComponent extends Container {
@@ -119,7 +122,7 @@ function setupDpSettingsCommand(pi: ExtensionAPI) {
119
122
  (tui, theme, _kb, done) =>
120
123
  new ModuleSettingsComponent(tui, theme, () => done(undefined))
121
124
  );
122
- ctx.ui.notify("Module settings updated. /reload to apply.", "info");
125
+ ctx.ui.notify("Module settings updated. /reload to apply.", "warning");
123
126
  return;
124
127
  }
125
128
  ctx.ui.notify("dp-settings requires interactive mode.", "warning");
@@ -127,6 +130,156 @@ function setupDpSettingsCommand(pi: ExtensionAPI) {
127
130
  });
128
131
  }
129
132
 
133
+ // ─── /mcp ──────────────────────────────────────────────────────────────────
134
+
135
+ class McpStatusComponent extends Container {
136
+ private textComponent: Text;
137
+ private tui: TUI;
138
+ private theme: PiTheme;
139
+ private done: () => void;
140
+ private timer: ReturnType<typeof setInterval> | null = null;
141
+
142
+ constructor(tui: TUI, theme: PiTheme, onDone: () => void) {
143
+ super();
144
+ this.tui = tui;
145
+ this.theme = theme;
146
+ this.done = onDone;
147
+
148
+ this.addChild(new DynamicBorder(theme));
149
+ this.addChild(new Spacer(1));
150
+
151
+ this.textComponent = new Text("", 1, 0);
152
+ this.addChild(this.textComponent);
153
+
154
+ this.addChild(new Spacer(1));
155
+ this.addChild(new Text(this.theme.fg("dim", "Press q to close."), 1, 0));
156
+ this.addChild(new Spacer(1));
157
+ this.addChild(new DynamicBorder(theme));
158
+
159
+ this.refresh();
160
+
161
+ this.timer = setInterval(() => {
162
+ this.refresh();
163
+ const allSettled = getMcpStatus().every((s) => s.state !== "connecting");
164
+ if (allSettled && this.timer) {
165
+ clearInterval(this.timer);
166
+ this.timer = null;
167
+ }
168
+ }, 500);
169
+ }
170
+
171
+ private refresh() {
172
+ const servers = getMcpStatus();
173
+
174
+ if (servers.length === 0) {
175
+ this.textComponent.setText("No MCP servers configured.");
176
+ this.tui.requestRender();
177
+ return;
178
+ }
179
+
180
+ const connected = servers.filter((s) => s.state === "connected");
181
+ const connecting = servers.filter((s) => s.state === "connecting");
182
+ const failed = servers.filter((s) => s.state === "failed");
183
+
184
+ const lines: string[] = [
185
+ `MCP servers (${servers.length}):`,
186
+ "",
187
+ ];
188
+
189
+ for (const s of connected) {
190
+ lines.push(this.theme.fg("accent", `• ${s.name}`) + ` (${s.source})`);
191
+ lines.push(` URL: ${s.url}`);
192
+ lines.push(` Tools: ${s.toolCount}`);
193
+ for (const tool of s.tools) {
194
+ const desc = tool.description ? ` — ${tool.description.slice(0, 60)}` : "";
195
+ lines.push(` - ${tool.name}${desc}`);
196
+ }
197
+ lines.push("");
198
+ }
199
+
200
+ for (const s of connecting) {
201
+ lines.push(this.theme.fg("accent", `• ${s.name}`) + ` (${s.source})`);
202
+ lines.push(` URL: ${s.url}`);
203
+ lines.push(` Status: ${this.theme.fg("warning", "connecting...")}`);
204
+ lines.push("");
205
+ }
206
+
207
+ for (const s of failed) {
208
+ lines.push(this.theme.fg("accent", `• ${s.name}`) + ` (${s.source})`);
209
+ lines.push(` URL: ${s.url}`);
210
+ lines.push(` Status: ${this.theme.fg("error", "failed")} — ${s.error ?? "unknown error"}`);
211
+ lines.push("");
212
+ }
213
+
214
+ this.textComponent.setText(lines.join("\n"));
215
+ this.tui.requestRender();
216
+ }
217
+
218
+ handleInput(data: string) {
219
+ const kb = getKeybindings();
220
+ if (
221
+ data === "q" ||
222
+ data === "\r" ||
223
+ data === "\n" ||
224
+ kb.matches(data, "tui.select.cancel")
225
+ ) {
226
+ if (this.timer) {
227
+ clearInterval(this.timer);
228
+ this.timer = null;
229
+ }
230
+ this.done();
231
+ }
232
+ }
233
+
234
+ dispose() {
235
+ if (this.timer) {
236
+ clearInterval(this.timer);
237
+ this.timer = null;
238
+ }
239
+ }
240
+ }
241
+
242
+ function setupMcpCommand(pi: ExtensionAPI) {
243
+ pi.registerCommand("mcp", {
244
+ description: "Show active MCP servers and their tools",
245
+ handler: async (_args, ctx) => {
246
+ if (ctx.hasUI) {
247
+ await ctx.ui.custom<void>(
248
+ (tui, theme, _kb, done) => new McpStatusComponent(tui, theme, () => done(undefined))
249
+ );
250
+ return;
251
+ }
252
+
253
+ // Fallback for non-interactive (print / RPC) mode.
254
+ const servers = getMcpStatus();
255
+ if (servers.length === 0) {
256
+ ctx.ui.notify("No MCP servers configured.", "info");
257
+ return;
258
+ }
259
+
260
+ const lines: string[] = [`MCP servers (${servers.length}):`, ""];
261
+ for (const s of servers) {
262
+ lines.push(`• ${s.name} (${s.source})`);
263
+ lines.push(` URL: ${s.url}`);
264
+ if (s.state === "connecting") {
265
+ lines.push(` Status: connecting...`);
266
+ } else if (s.state === "failed") {
267
+ lines.push(` Status: failed — ${s.error ?? "unknown error"}`);
268
+ } else {
269
+ lines.push(` Tools: ${s.toolCount}`);
270
+ for (const tool of s.tools) {
271
+ const desc = tool.description ? ` — ${tool.description.slice(0, 60)}` : "";
272
+ lines.push(` - ${tool.name}${desc}`);
273
+ }
274
+ }
275
+ lines.push("");
276
+ }
277
+
278
+ pi.sendMessage({ customType: "mcp-status", content: lines.join("\n"), display: true }, { triggerTurn: false });
279
+ },
280
+ });
281
+ }
282
+
130
283
  // ─── /retry ────────────────────────────────────────────────────────────────
131
284
 
132
285
  function setupRetryCommand(pi: ExtensionAPI) {
@@ -165,5 +318,6 @@ function setupRetryCommand(pi: ExtensionAPI) {
165
318
  export function setupSlash(pi: ExtensionAPI) {
166
319
  setupDpModelCommand(pi);
167
320
  setupDpSettingsCommand(pi);
321
+ setupMcpCommand(pi);
168
322
  setupRetryCommand(pi);
169
323
  }
@@ -20,7 +20,7 @@
20
20
  * Tier 2 (-300): 在 .* 或 __* 目录下
21
21
  * Tier 3 (-200): 在已知噪音目录下(build/dist/coverage 等)
22
22
  * Tier 4 (-150~-80): 坏扩展名(二进制/编译产物/媒体文件)
23
- * Base (always): -depth*2 - name.length
23
+ * Base (always): -depth*30 - name.length
24
24
  *
25
25
  * 匹配:
26
26
  * - 大小写敏感
@@ -104,7 +104,7 @@ function computePenaltyMeta(filePath: string, isDir: boolean, gitIgnored: boolea
104
104
  tierPenalty = EXT_PENALTY[ext]!;
105
105
  }
106
106
 
107
- const basePenalty = -(depth * 20) - name.length;
107
+ const basePenalty = -(depth * 30) - name.length;
108
108
  return { tier, penalty: tierPenalty + basePenalty };
109
109
  }
110
110
 
@@ -306,7 +306,7 @@ function computeMatchScore(candidate: FileCandidate, query: string): number {
306
306
  let s = 0;
307
307
 
308
308
  // 大小写敏感匹配
309
- if (stem === query) s = isDir ? 1500 : 1200;
309
+ if (stem === query) s = isDir ? 1500 : 1050;
310
310
  else if (name.startsWith(query + ".") || name.startsWith(query + "_")) s = 1000;
311
311
  else if (name.startsWith(query)) s = 900;
312
312
  else if (name.includes(query)) s = 600;
@@ -316,10 +316,10 @@ function computeMatchScore(candidate: FileCandidate, query: string): number {
316
316
  if (!s) return 0;
317
317
 
318
318
  // 目录轻微加成(tiebreaker,不碾压深度)
319
- if (isDir) s += 50;
319
+ if (isDir) s += 100;
320
320
 
321
321
  // 父目录命中加成
322
- if (inDir) s += 250;
322
+ if (inDir) s += 50;
323
323
 
324
324
  return s;
325
325
  }
@@ -4,29 +4,49 @@
4
4
  * 当 agent 读取或编辑子目录中的文件时,自动发现该目录及父目录中的 AGENTS.md/CLAUDE.md,
5
5
  * 将其内容注入到 tool result 中。
6
6
  *
7
- * 状态通过 pi.appendEntry() 持久化到 session JSONL 文件中,resume 时自动恢复。
7
+ * 状态通过 pi.appendEntry() 持久化到 session JSONL 文件中,resume / reload 时恢复。
8
+ * compaction 视为上下文边界:仅恢复当前 branch 上最后一个 compaction 之后的已加载状态。
8
9
  */
9
10
 
10
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
- import { dirname, resolve, relative, join } from "node:path";
12
+ import { dirname, resolve, relative, join, normalize } from "node:path";
12
13
  import { existsSync, readFileSync } from "node:fs";
13
14
 
14
15
  const CUSTOM_TYPE = "decorated-pi.subdir-agents";
15
16
  const AGENTS_NAMES = ["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"];
16
17
 
18
+ interface SessionLikeEntry {
19
+ type: string;
20
+ customType?: string;
21
+ data?: unknown;
22
+ }
23
+
17
24
  const discovered = new Set<string>();
18
25
  const pendingPaths = new Map<string, string>();
19
26
  let sessionCwd = process.cwd();
20
27
 
21
- function restoreFromSession(ctx: { cwd: string; sessionManager: { getEntries: () => Array<{ type: string; customType?: string; data?: unknown }> } }) {
28
+ function normalizeAbsPath(cwd: string, p: string): string {
29
+ return normalize(resolve(cwd, p));
30
+ }
31
+
32
+ function lastCompactionIndex(entries: SessionLikeEntry[]): number {
33
+ for (let i = entries.length - 1; i >= 0; i--) {
34
+ if (entries[i]?.type === "compaction") return i;
35
+ }
36
+ return -1;
37
+ }
38
+
39
+ function restoreFromBranch(ctx: { cwd: string; sessionManager: { getBranch: () => Array<SessionLikeEntry> } }) {
22
40
  discovered.clear();
23
- for (const entry of ctx.sessionManager.getEntries()) {
24
- if (entry.type === "custom" && entry.customType === CUSTOM_TYPE) {
25
- const paths = entry.data as string[] | undefined;
26
- if (paths) {
27
- for (const p of paths) {
28
- discovered.add(resolve(ctx.cwd, p));
29
- }
41
+ const branch = ctx.sessionManager.getBranch();
42
+ const start = lastCompactionIndex(branch) + 1;
43
+ for (const entry of branch.slice(start)) {
44
+ if (entry.type !== "custom" || entry.customType !== CUSTOM_TYPE) continue;
45
+ const paths = entry.data as string[] | undefined;
46
+ if (!Array.isArray(paths)) continue;
47
+ for (const p of paths) {
48
+ if (typeof p === "string" && p.trim()) {
49
+ discovered.add(normalizeAbsPath(ctx.cwd, p));
30
50
  }
31
51
  }
32
52
  }
@@ -34,7 +54,7 @@ function restoreFromSession(ctx: { cwd: string; sessionManager: { getEntries: ()
34
54
 
35
55
  function findNewAgents(filePath: string, cwd: string): Array<{ path: string; content: string }> {
36
56
  const resolvedCwd = resolve(cwd);
37
- let dir = dirname(resolve(filePath));
57
+ let dir = dirname(resolve(cwd, filePath));
38
58
  const results: Array<{ path: string; content: string }> = [];
39
59
 
40
60
  while (true) {
@@ -42,7 +62,7 @@ function findNewAgents(filePath: string, cwd: string): Array<{ path: string; con
42
62
  if (rel === "" || rel.startsWith("..")) break;
43
63
 
44
64
  for (const name of AGENTS_NAMES) {
45
- const agentsPath = join(dir, name);
65
+ const agentsPath = normalize(join(dir, name));
46
66
  if (existsSync(agentsPath) && !discovered.has(agentsPath)) {
47
67
  try {
48
68
  const content = readFileSync(agentsPath, "utf-8");
@@ -65,10 +85,20 @@ function findNewAgents(filePath: string, cwd: string): Array<{ path: string; con
65
85
  return results.reverse();
66
86
  }
67
87
 
88
+ export const __subdirAgentsTest = {
89
+ restoreFromBranch,
90
+ findNewAgents,
91
+ };
92
+
68
93
  export function setupSubdirAgents(pi: ExtensionAPI) {
69
94
  pi.on("session_start", (_event, ctx) => {
70
95
  sessionCwd = ctx.cwd;
71
- restoreFromSession(ctx);
96
+ restoreFromBranch(ctx);
97
+ });
98
+
99
+ pi.on("session_compact", () => {
100
+ discovered.clear();
101
+ pendingPaths.clear();
72
102
  });
73
103
 
74
104
  pi.on("tool_call", (event) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decorated-pi",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "A pi extension with better work-flow: patch tool, safety gates, secret redaction, smart @ completion, dynamic AGENTS loading, image fallback, and LSP tools",
5
5
  "keywords": [
6
6
  "pi",
@@ -22,8 +22,7 @@
22
22
  "homepage": "https://github.com/lcwecker/decorated-pi#readme",
23
23
  "bugs": "https://github.com/lcwecker/decorated-pi/issues",
24
24
  "dependencies": {
25
- "@spences10/pi-child-env": "0.1.4",
26
- "@spences10/pi-project-trust": "0.0.6",
25
+ "@modelcontextprotocol/sdk": "^1.29.0",
27
26
  "openai": "^6.37.0"
28
27
  },
29
28
  "peerDependencies": {
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "types": ["node"],
10
+ "outDir": "dist",
11
+ "rootDir": ".",
12
+ "noEmit": true
13
+ },
14
+ "include": ["extensions/**/*.ts", "test/**/*.ts"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }