decorated-pi 0.7.1 → 0.7.2

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/index.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
+ import { fileURLToPath } from "node:url";
19
20
 
20
21
  import { createSkeleton } from "./hooks/skeleton.js";
21
22
 
@@ -36,8 +37,9 @@ import { setupRtk } from "./hooks/rtk.js";
36
37
  import { registerPatchTool } from "./tools/patch/index.js";
37
38
  import { registerLspTools } from "./tools/lsp/tools.js";
38
39
  import { LspServerManager } from "./tools/lsp/manager.js";
40
+ import { collectLspDependencyStatuses } from "./tools/lsp/servers.js";
39
41
  import { registerAskTool } from "./tools/ask/index.js";
40
- import { resolveMcpConfigs, migrateLegacyGlobalMcpConfig } from "./tools/mcp/config.js";
42
+ import { resolveMcpConfigs, migrateLegacyGlobalMcpConfig, collectMcpDependencyStatuses } from "./tools/mcp/config.js";
41
43
  import { ensureMcpServerReady } from "./hooks/mcp.js";
42
44
 
43
45
  import { registerDpModelCommand } from "./commands/dp-model.js";
@@ -65,6 +67,71 @@ const BASE_GUIDANCE = [
65
67
  "- CAUTION: Do not perform write operations in the following directories unless explicitly instructed: `node_modules`, `venv`, `env`, `__pycache__`, `.git` or any other hidden directories.",
66
68
  ].join("\n");
67
69
 
70
+ /** Remove the injected Pi documentation block from the base system prompt.
71
+ * Matches a line containing "Pi documentation" and deletes it plus all
72
+ * following non-empty lines, stopping at the first blank line. */
73
+ export function stripPiDocsBlock(prompt: string): string {
74
+ const lines = prompt.split("\n");
75
+ const out: string[] = [];
76
+ let i = 0;
77
+ while (i < lines.length) {
78
+ const line = lines[i];
79
+ if (line.includes("Pi documentation")) {
80
+ i++;
81
+ while (i < lines.length && lines[i].trim() !== "") i++;
82
+ // Drop the terminating blank line as well so we don't leave orphan whitespace.
83
+ if (i < lines.length && lines[i].trim() === "") i++;
84
+ continue;
85
+ }
86
+ out.push(line);
87
+ i++;
88
+ }
89
+ return out.join("\n");
90
+ }
91
+
92
+ /** Sort the <available_skills> block in the system prompt by skill name.
93
+ * Pi core appends extension-provided skills after user/project skills and does
94
+ * not sort the XML; this makes the final prompt stable and cache-friendly. */
95
+ export function sortSkillsInSystemPrompt(prompt: string): string {
96
+ const startMarker = "\n<available_skills>";
97
+ const endMarker = "</available_skills>";
98
+ const startIdx = prompt.indexOf(startMarker);
99
+ if (startIdx === -1) return prompt;
100
+ const endIdx = prompt.indexOf(endMarker, startIdx);
101
+ if (endIdx === -1) return prompt;
102
+
103
+ const before = prompt.slice(0, startIdx + startMarker.length);
104
+ const after = prompt.slice(endIdx);
105
+ const inner = prompt.slice(startIdx + startMarker.length, endIdx);
106
+
107
+ const chunks: string[][] = [];
108
+ let current: string[] = [];
109
+ for (const line of inner.split("\n")) {
110
+ const trimmed = line.trim();
111
+ if (trimmed === "<skill>") {
112
+ current = [line];
113
+ } else if (trimmed === "</skill>") {
114
+ current.push(line);
115
+ chunks.push(current);
116
+ current = [];
117
+ } else if (current.length > 0) {
118
+ current.push(line);
119
+ }
120
+ }
121
+
122
+ const nameOf = (chunk: string[]) => {
123
+ const line = chunk.find((l) => l.trim().startsWith("<name>"));
124
+ if (!line) return "";
125
+ const t = line.trim();
126
+ return t.slice(6, t.indexOf("</name>"));
127
+ };
128
+
129
+ chunks.sort((a, b) => nameOf(a).localeCompare(nameOf(b)));
130
+
131
+ const sortedInner = "\n" + chunks.map((chunk) => chunk.join("\n")).join("\n") + "\n";
132
+ return before + sortedInner + after;
133
+ }
134
+
68
135
  /** Build the list of guideline strings to inject, in prompt order.
69
136
  * Always-on rules first, then per-module guidelines. REDACT_GUIDANCE is
70
137
  * gated by the secretRedaction module; CodeGraph guidance follows the MCP
@@ -80,6 +147,26 @@ function buildGuidelines(): string[] {
80
147
  return out;
81
148
  }
82
149
 
150
+ function canRegisterMcpServer(config: { name: string; command?: string }, deps: Array<{ module: string; state: string }>): boolean {
151
+ if (!config.command) return true;
152
+ const dep = deps.find((d) => d.module === `mcp:${config.name}`);
153
+ return dep ? dep.state === "ok" : true;
154
+ }
155
+
156
+ /** Absolute path to the plugin's builtin skills directory.
157
+ * Used by `resources_discover` so the skill travels with the plugin
158
+ * regardless of which project pi is running in. */
159
+ export function getBuiltinSkillPaths(): string[] {
160
+ return [fileURLToPath(new URL("./skills", import.meta.url))];
161
+ }
162
+
163
+ /** Register the plugin's builtin skill paths with Pi core. */
164
+ function installBuiltinSkills(pi: ExtensionAPI): void {
165
+ pi.on("resources_discover", async (_event: any) => ({
166
+ skillPaths: getBuiltinSkillPaths(),
167
+ }));
168
+ }
169
+
83
170
  /** Install a single before_agent_start handler that appends every
84
171
  * guideline in order, stripping the volatile "Current date: …" line
85
172
  * for cache stability. Idempotent — re-injection is a no-op via marker. */
@@ -90,7 +177,9 @@ function installGuidelines(pi: ExtensionAPI): void {
90
177
 
91
178
  pi.on("before_agent_start", async (event: any) => {
92
179
  if (!event.systemPrompt) return undefined;
93
- let prompt: string = event.systemPrompt.replace(/\nCurrent date: \d{4}-\d{2}-\d{2}/, "");
180
+ let prompt: string = stripPiDocsBlock(event.systemPrompt);
181
+ prompt = sortSkillsInSystemPrompt(prompt);
182
+ prompt = prompt.replace(/\nCurrent date: \d{4}-\d{2}-\d{2}/, "");
94
183
  if (prompt.includes(marker)) return undefined; // already injected this turn
95
184
  return { systemPrompt: `${prompt}\n\n${joined}` };
96
185
  });
@@ -130,7 +219,19 @@ export default async function (pi: ExtensionAPI) {
130
219
 
131
220
  // ── Tools (conditional on module switches) ────────────────────────────
132
221
  if (isModuleEnabled("patchOverrideEdit")) registerPatchTool(pi);
133
- if (isModuleEnabled("lsp")) registerLspTools(pi, new LspServerManager());
222
+ if (isModuleEnabled("lsp")) {
223
+ const lspDeps = collectLspDependencyStatuses(process.cwd());
224
+ if (lspDeps.some((d) => d.state === "ok")) {
225
+ registerLspTools(pi, new LspServerManager());
226
+ }
227
+ for (const dep of lspDeps) {
228
+ sk.declareDependency({
229
+ label: `lsp:${dep.label}`,
230
+ module: `lsp:${dep.label}`,
231
+ check: () => collectLspDependencyStatuses(process.cwd()).some((s) => s.label === dep.label && s.state === "ok"),
232
+ });
233
+ }
234
+ }
134
235
  if (isModuleEnabled("ask")) registerAskTool(pi);
135
236
 
136
237
  // MCP: hook, tools, and /mcp command are gated together. Disabling the
@@ -142,16 +243,29 @@ export default async function (pi: ExtensionAPI) {
142
243
  // explicitly here so `loadGlobalMcpConfigs` stays pure.
143
244
  migrateLegacyGlobalMcpConfig();
144
245
  sk.register(mcpModule);
246
+ const mcpDeps = collectMcpDependencyStatuses(process.cwd());
247
+ for (const dep of mcpDeps) {
248
+ sk.declareDependency({
249
+ label: dep.module,
250
+ module: dep.module,
251
+ check: () => collectMcpDependencyStatuses(process.cwd()).some((s) => s.module === dep.module && s.state === "ok"),
252
+ });
253
+ }
145
254
  const configs = resolveMcpConfigs(process.cwd()).filter(s => s.enabled);
146
255
  // Per-server readiness: cache hit → register from cache (fast).
147
256
  // Cache miss → connect synchronously, write cache, then register
148
257
  // live tools. This blocks startup only for cache-miss servers.
258
+ // Skip servers whose binary is missing (dependency not met).
149
259
  for (const config of configs) {
260
+ if (!canRegisterMcpServer(config, mcpDeps)) continue;
150
261
  await ensureMcpServerReady(pi, config, process.cwd());
151
262
  }
152
263
  registerMcpStatusCommand(pi);
153
264
  }
154
265
 
266
+ // ── Builtin skills (travel with the plugin in every project) ─────────────
267
+ installBuiltinSkills(pi);
268
+
155
269
  // ── System-prompt guidelines (single handler, array order = prompt order) ──
156
270
  installGuidelines(pi);
157
271
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decorated-pi",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
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",
@@ -14,9 +14,7 @@
14
14
  "secret-redaction",
15
15
  "codegraph",
16
16
  "fff",
17
- "smart-at",
18
- "fuzzy-search",
19
- "file-finder"
17
+ "fuzzy-search"
20
18
  ],
21
19
  "license": "MIT",
22
20
  "repository": {
package/settings.ts CHANGED
@@ -82,9 +82,11 @@ export function loadConfig(): DecoratedPiConfig {
82
82
  try {
83
83
  if (fs.existsSync(CONFIG_FILE)) {
84
84
  const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")) as DecoratedPiConfig;
85
- if (migrateModuleSettings(config)) {
86
- saveConfig(config);
87
- }
85
+ // Mutate in place. Do NOT call saveConfig here — saveConfig calls
86
+ // loadConfig first, which would re-trigger the migration (and the
87
+ // file on disk hasn't been written yet), causing deep recursion.
88
+ // The migrated shape is persisted on the next explicit saveConfig.
89
+ migrateModuleSettings(config);
88
90
  return config;
89
91
  }
90
92
  } catch {}
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: pi-docs
3
+ description: pi docs resources
4
+ ---
5
+
6
+ Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):
7
+ - Main documentation: /home/liuchang/.local/share/fnm/node-versions/v24.14.0/installation/lib/node_modules/@earendil-works/pi-coding-agent/README.md
8
+ - Additional docs: /home/liuchang/.local/share/fnm/node-versions/v24.14.0/installation/lib/node_modules/@earendil-works/pi-coding-agent/docs
9
+ - Examples: /home/liuchang/.local/share/fnm/node-versions/v24.14.0/installation/lib/node_modules/@earendil-works/pi-coding-agent/examples (extensions, custom tools, SDK)
10
+ - When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
11
+ - When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md)
12
+ - When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
13
+ - Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)
@@ -69,7 +69,6 @@ function severityLabel(s: number): string {
69
69
  // ─── Register tools ───────────────────────────────────────────────────────
70
70
 
71
71
  export function registerLspTools(pi: ExtensionAPI, manager: LspServerManager) {
72
-
73
72
  // ── lsp_diagnostics ────────────────────────────────────────────────────
74
73
  pi.registerTool({
75
74
  name: "lsp_diagnostics",
@@ -14,7 +14,7 @@ export const CODEGRAPH_BUILTIN: Omit<McpServerConfig, "source"> = {
14
14
  name: "codegraph",
15
15
  command: "codegraph",
16
16
  args: ["serve", "--mcp"],
17
- enabled: false,
17
+ enabled: true,
18
18
  description:
19
19
  "Local code knowledge graph (colbymchenry/codegraph). Enable via /mcp.",
20
20
  canUseInProject: (cwd: string) => fs.existsSync(path.join(cwd, ".codegraph")),
@@ -16,7 +16,7 @@ export class McpConnection {
16
16
  client: Client;
17
17
  transport: StreamableHTTPClientTransport | SSEClientTransport | StdioClientTransport | undefined;
18
18
  tools: McpToolSpec[] = [];
19
- private connected = false;
19
+ connected = false;
20
20
 
21
21
  source: string = "unknown";
22
22
 
@@ -1521,3 +1521,13 @@ function truncate(s: string, maxLen = 60): string {
1521
1521
  if (firstLine.length <= maxLen) return firstLine;
1522
1522
  return firstLine.slice(0, maxLen - 3) + "...";
1523
1523
  }
1524
+
1525
+ // Test exports
1526
+ export const __patchCoreTest = {
1527
+ charOffsetToLine,
1528
+ detectTabWidth,
1529
+ normalizeIndentForFuzzy,
1530
+ truncate,
1531
+ collapseSequentialReplacements,
1532
+ generateReplacementDiff,
1533
+ };