decorated-pi 0.4.1 → 0.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # decorated-pi
2
2
 
3
- `decorated-pi` is a practical enhancement pack for [Pi](https://github.com/earendil-works/pi).
3
+ `decorated-pi` is a practical enhancement pack for [Pi](https://github.com/earendil-works/pi) — smarter tools that are token efficient and cache friendly.
4
4
 
5
5
  ## Install
6
6
 
@@ -14,67 +14,60 @@ pi install /path/to/decorated-pi
14
14
 
15
15
  ### 1. Patch Tool
16
16
 
17
- Replaces Pi's built-in `edit` / `write` with a stronger `patch` tool:
17
+ Replaces Pi's built-in `edit` / `write` with a stronger `patch` tool that adds unique safety and usability improvements on top of the native tools.
18
18
 
19
- - **anchor mechanism** narrows the search range by specifying a unique string that appears before `old_str`, preventing mismatches in files with repeated patterns
20
- - **mtime tracking** records file modification time on `read`, rejects `patch` if the file changed since last read, preventing blind or stale edits
21
- - **explicit overwrite** offer atomic `overwrite: true` mode for overwrite files or full-file creation to prevent unintened overwrite
19
+ | Capability | Pi native `edit` | `patch` |
20
+ | ------ | :---: | :---: |
21
+ | Exact string replacement | `oldText` | `old_str` |
22
+ | Atomic overwrite | ✅ `write` | ✅ `overwrite` |
23
+ | Syntax‑highlighted overwrite | ✅ streaming | ✅ incremental |
24
+ | **Anchor‑based search** | ❌ extending `oldText` for uniqueness | ✅ `anchor` bounds scope for precise matching |
25
+ | **Fuzzy whitespace match** | ❌ only reports "not found" | ✅ auto‑corrects tab↔space / trailing whitespace mismatches |
26
+ | **Edit fault diagnostics** | ❌ only reports "not found" | ✅ pinpoint faults for LLM comprehension |
27
+ | **Stale‑read protection** | ❌ Blind to external changes | ✅ `read` captures mtime, `patch` rejects stale targets |
22
28
 
23
- ### 2. Secret redaction
29
+ ### 2. Smarter `@` File Search
24
30
 
25
- Three-layer detection: high-confidence known-format patterns (AWS, GitHub, OpenAI, etc.), config-key regex matching, and adjusted Shannon entropy heuristics for unknown secret-like values.
26
-
27
- ### 3. Built-in MCP Client
31
+ Replaces Pi's built-in `@` file completion with smarter matching and noise filtering:
28
32
 
29
- Zero-config MCP client with two built-in servers:
33
+ | Aspect | Pi native `@` | `decorated-pi` |
34
+ | ------ | :---: | :---: |
35
+ | **Speed** | ❌ re‑scans filesystem on every trigger | ✅ caches once per `@` trigger |
36
+ | **Noise filtering** | ❌ no penalty system, shows hidden files | ✅ tiered penalty auto‑filters clutter |
37
+ | **Default suggestions** | ❌ all files visible on empty query | ✅ only visible project files |
38
+ | **Match precision** | ❌ case‑insensitive simple scoring | ✅ multi‑level case‑sensitive scoring |
30
39
 
31
- | Server | Tool Prefix | Source |
32
- | --- | --- | --- |
33
- | Context7 | `context7_*` | `https://mcp.context7.com/mcp` |
34
- | Exa | `exa_*` | `https://mcp.exa.ai/mcp` |
40
+ ### 3. Secret redaction
35
41
 
36
- **Project-level config** add custom MCP servers by placing an `mcp.json` or `.mcp.json` (also `.pi/mcp.json`, `.agents/mcp.json`, `.claude/mcp.json` and their `.`-prefixed variants) anywhere in your project tree. Servers found closer to `cwd` take precedence over ancestor directories.
37
-
38
- ```json
39
- {
40
- "mcpServers": {
41
- "my-server": {
42
- "url": "https://my-mcp.example.com/mcp",
43
- "enabled": true
44
- }
45
- }
46
- }
47
- ```
42
+ Three-layer detection: high-confidence known-format patterns (AWS, GitHub, OpenAI, etc.), config-key regex matching, and adjusted Shannon entropy heuristics for unknown secret-like values.
48
43
 
49
- **Global config** persist custom servers in `~/.pi/agent/decorated-pi.json`:
44
+ Example redaction on a `read` / `bash` output:
50
45
 
51
46
  ```json
52
47
  {
53
- "mcpServers": {
54
- "my-server": { "url": "https://my-mcp.example.com/mcp" }
55
- }
48
+ "aws_access_key_id": "AKI**************PLE",
49
+ "github_token": "ghp***************def",
50
+ "database_password": "Sup#######t99",
51
+ "api_key": "sk_**************f5a",
52
+ "random_secret": "a1b??????5f5"
56
53
  }
57
54
  ```
58
55
 
59
- **Priority**: project > global > builtin. A server with the same name from a higher-priority source overrides lower ones.
60
-
61
- Use `/mcp` to view connection status and registered tools.
56
+ > `*` = known pattern, `#` = config key regex, `?` = entropy heuristic.
62
57
 
63
58
  ### 4. Auxiliary Models (Image + Compact)
64
59
 
65
- Uses cheaper models for auxiliary tasks, configured via `/dp-model`:
60
+ Offloads auxiliary ops to cheaper models, reducing cost on every session. Configured via `/dp-model`:
66
61
 
67
62
  - **Image read fallback** — when the model reads an image file, detects type via magic bytes, calls a configured vision-capable model, and replaces the read result with image analysis text (jpeg, png, gif, webp)
68
63
  - **Compact model** — uses a configured model for context compaction (instead of the main model), auto-resumes after compaction.
69
64
 
70
- ### 5. Smart `@` File Search
65
+ ### 5. Progressive Context from `AGENTS.md` / `CLAUDE.md`
71
66
 
72
- Replaces Pi's default file search with a faster, project-aware search strategy:
67
+ Extension capability: context is disclosed progressively as the agent works across different parts of the project.
73
68
 
74
- - Uses project-aware file discovery
75
- - Prioritizes filename-based matches for more intuitive results
76
- - Reduces clutter from hidden, cache, and build directories
77
- - Keeps default suggestions focused on visible project files
69
+ - When reading or editing a file, discovers `AGENTS.md` / `CLAUDE.md` in that file's directory and ancestor directories
70
+ - Newly discovered guidance is injected into tool results, scoped to the current context
78
71
 
79
72
  ### 6. LSP Tool Suite
80
73
 
@@ -85,12 +78,39 @@ A cleaned-up, minimal LSP toolset. The extension keeps only the two LSP tools th
85
78
 
86
79
  Supported languages: c/cpp, go, java, lua, json, python, ruby, rust, svelte, typescript
87
80
 
88
- ### 7. Dynamic Subdirectory `AGENTS.md` / `CLAUDE.md`
81
+ ### 7. Built-in MCP Client
89
82
 
90
- When the agent reads or edits a file:
83
+ Zero-config MCP client with built-in servers:
91
84
 
92
- - discovers `AGENTS.md` / `CLAUDE.md` in the file's directory and ancestor directories
93
- - injects newly discovered guidance into tool results
85
+ | Server | Tool Prefix | Source |
86
+ | --- | --- | --- |
87
+ | Context7 | `context7_*` | `https://mcp.context7.com/mcp` |
88
+ | Exa | `exa_*` | `https://mcp.exa.ai/mcp` |
89
+
90
+ **Custom servers** in `.pi/agent/mcp.json` (project) or `~/.pi/agent/decorated-pi.json` (global). Project overrides global.
91
+ Tool prompts and schemas are cached locally so MCP tools are available immediately on startup, even before servers connect.
92
+
93
+ ```json
94
+ {
95
+ "mcpServers": {
96
+ "my-server": {
97
+ "url": "https://my-mcp.example.com/mcp",
98
+ "enabled": true
99
+ },
100
+ "my-sse": {
101
+ "url": "https://my-mcp.example.com/sse",
102
+ "enabled": false
103
+ },
104
+ "my-stdio": {
105
+ "command": "npx",
106
+ "args": ["-y", "my-mcp-server"],
107
+ "env": { "DEBUG": "1" }
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ Use `/mcp` to view connection status and registered tools.
94
114
 
95
115
  ### 8. Extend Providers
96
116
 
@@ -102,6 +122,11 @@ Extend providers are registered via `/login` → "Use a subscription":
102
122
  | Baidu Qianfan | `qianfan.baidubce.com/v2/coding` |
103
123
  | ARK Coding | `ark.cn-beijing.volces.com/api/coding/v3` |
104
124
 
125
+ ### 9. Other
126
+
127
+ - **RTK** — integrates [RTK](https://github.com/rtk-ai/rtk) for token-efficient command output.
128
+ - **WakaTime** — tracks coding activity via [WakaTime](https://wakatime.com).
129
+
105
130
  ## Configuration
106
131
 
107
132
  Runtime settings are stored in:
@@ -112,26 +137,18 @@ Runtime settings are stored in:
112
137
 
113
138
  ### Module Loading
114
139
 
115
- Modules can be toggled on/off. Changes take effect after `/reload`.
116
-
117
- | Module | Default | Effect when disabled |
118
- | -------- | --------- | --------------------- |
119
- | `patch` | `true` | Reverts to Pi's built-in `edit` / `write` tools |
120
- | `safety` | `true` | No secret redaction on `read` / `bash` output |
121
- | `lsp` | `true` | All `lsp_*` tools unregistered — no diagnostics, hover, etc. |
122
- | `smart-at` | `true` | Fallback to Pi's built-in `@` file completion |
123
- | `mcp` | `true` | All `{server}_*` MCP tools unregistered |
124
-
125
- Use `/dp-settings` to toggle, or edit the config file directly:
140
+ Modules can be toggled on/off by `/dp-settings`. Changes take effect after `/reload`.
126
141
 
127
142
  ```json
128
143
  {
129
144
  "modules": {
130
145
  "patch": true,
131
146
  "safety": true,
132
- "lsp": false,
147
+ "rtk": true,
148
+ "lsp": true,
133
149
  "smart-at": true,
134
- "mcp": true
150
+ "mcp": true,
151
+ "wakatime": true
135
152
  }
136
153
  }
137
154
  ```
@@ -4,20 +4,27 @@ const DECORATED_PI_GUIDANCE_MARKER = "## Decorated Pi Guidance";
4
4
 
5
5
  export function setupGuidance(pi: ExtensionAPI) {
6
6
  pi.on("before_agent_start", async (event) => {
7
- if (event.systemPrompt.includes(DECORATED_PI_GUIDANCE_MARKER)) return;
7
+ // Remove "Current date: YYYY-MM-DD" from system prompt to improve cache stability
8
+ let prompt = event.systemPrompt.replace(/\nCurrent date: \d{4}-\d{2}-\d{2}/, "");
8
9
 
9
- const guidance = [
10
- DECORATED_PI_GUIDANCE_MARKER,
11
- "",
12
- "- Before acting on a user's prompt, ensure you fully understand their needs. If the intent is ambiguous, ask clarifying questions. Proceed only when the intent is clear.",
13
- "- Look before you leap! Ensure you have conducted thorough research before taking any action.",
14
- "- Exercise caution when performing any **write** operations, especially when you are in a research or exploration phase.",
15
- "- You don't need to read **AGENTS.md** or **CLAUDE.md** files unless you're explicitly asked to, these files will loaded automatically if neccessary.",
16
- "- CAUTION: Do not perform write operations in the following directories unless explicitly instructed: `node_modules`, `venv`, `env`, `__pycache__`, `.git` or any other hidden directories.",
17
- ].join("\n");
10
+ if (!prompt.includes(DECORATED_PI_GUIDANCE_MARKER)) {
11
+ const guidance = [
12
+ DECORATED_PI_GUIDANCE_MARKER,
13
+ "",
14
+ "- Before acting on a user's prompt, ensure you fully understand their needs. If the intent is ambiguous, ask clarifying questions. Proceed only when the intent is clear.",
15
+ "- Look before you leap! Ensure you have conducted thorough research before taking any action.",
16
+ "- Exercise caution when performing any **write** operations, especially when you are in a research or exploration phase.",
17
+ "- You don't need to read **AGENTS.md** or **CLAUDE.md** files unless you're explicitly asked to, these files will loaded automatically if neccessary.",
18
+ "- CAUTION: Do not perform write operations in the following directories unless explicitly instructed: `node_modules`, `venv`, `env`, `__pycache__`, `.git` or any other hidden directories.",
19
+ "",
20
+ "### Secret Redaction",
21
+ "",
22
+ "- When you see masked secret values (e.g. `sk-***...***` where `*`, `#`, or `?` are mask characters), the real value has been redacted by the system. Do not attempt to read or guess it. If you need the secret, use tools like `jq` or `grep` to extract it from the original source file.",
23
+ ].join("\n");
18
24
 
19
- return {
20
- systemPrompt: `${event.systemPrompt}\n\n${guidance}`,
21
- };
25
+ prompt = `${prompt}\n\n${guidance}`;
26
+ }
27
+
28
+ return { systemPrompt: prompt };
22
29
  });
23
30
  }
@@ -11,11 +11,74 @@ import { setupSessionTitle } from "./session-title";
11
11
  import { setupIO } from "./io";
12
12
  import { setupGuidance } from "./guidance";
13
13
  import { setupLsp } from "./lsp/index";
14
+ import { collectLspDependencyStatuses } from "./lsp/servers";
14
15
  import { setupProviders } from "./providers/index";
15
- import { setupSmartAt } from "./smart-at";
16
+ import { getSmartAtDependencyStatuses, setupSmartAt } from "./smart-at";
16
17
  import { setupMcp } from "./mcp/index.js";
18
+ import { collectMcpDependencyStatuses } from "./mcp/builtin";
19
+ import { setupWakatime } from "./wakatime";
20
+ import { findSystemRtk, getRtkDependencyStatuses, setupRtkIntegration, type DependencyStatus } from "./rtk";
17
21
  import { isModuleEnabled } from "./settings";
18
22
 
23
+ function collectDependencyStatuses(cwd: string): DependencyStatus[] {
24
+ const statuses: DependencyStatus[] = [];
25
+ if (isModuleEnabled("rtk")) statuses.push(...getRtkDependencyStatuses());
26
+ if (isModuleEnabled("smart-at")) statuses.push(...getSmartAtDependencyStatuses(cwd));
27
+ if (isModuleEnabled("lsp")) statuses.push(...collectLspDependencyStatuses(cwd));
28
+ if (isModuleEnabled("mcp")) statuses.push(...collectMcpDependencyStatuses(cwd));
29
+ return statuses;
30
+ }
31
+
32
+ function formatDependencyLines(statuses: DependencyStatus[]): string[] {
33
+ const missing = statuses.filter((item) => item.state === "missing");
34
+ const grouped = new Map<string, string[]>();
35
+
36
+ for (const item of missing) {
37
+ const labels = grouped.get(item.module) ?? [];
38
+ labels.push(item.label);
39
+ grouped.set(item.module, labels);
40
+ }
41
+
42
+ const lines = ["[decorated-pi] missing dependencies:"];
43
+ for (const [module, labels] of grouped) {
44
+ lines.push(` [${module}] ${labels.join(", ")}`);
45
+ }
46
+ return lines;
47
+ }
48
+
49
+ function setupDependencyReminders(pi: ExtensionAPI) {
50
+ let notifyTimer: ReturnType<typeof setTimeout> | undefined;
51
+
52
+ pi.on("session_start", async (event, ctx) => {
53
+ if (!ctx.hasUI) return;
54
+ if (event.reason !== "startup" && event.reason !== "reload") return;
55
+
56
+ const statuses = collectDependencyStatuses(ctx.cwd);
57
+ const missing = statuses.filter((item) => item.state === "missing");
58
+ if (missing.length === 0) return;
59
+
60
+ if (notifyTimer) clearTimeout(notifyTimer);
61
+ const message = formatDependencyLines(statuses).join("\n");
62
+
63
+ // Defer until after pi finishes startup/reload UI rebuild, otherwise
64
+ // notify() is appended to the chat and then wiped by rebuildChatFromMessages().
65
+ notifyTimer = setTimeout(() => {
66
+ notifyTimer = undefined;
67
+ try {
68
+ ctx.ui.notify(message, "info");
69
+ } catch {
70
+ // Extension context may be stale if another reload/session switch happened.
71
+ }
72
+ }, 0);
73
+ });
74
+
75
+ pi.on("session_shutdown", async () => {
76
+ if (!notifyTimer) return;
77
+ clearTimeout(notifyTimer);
78
+ notifyTimer = undefined;
79
+ });
80
+ }
81
+
19
82
  export default function (pi: ExtensionAPI) {
20
83
  // Always loaded — core commands and providers
21
84
  setupSlash(pi);
@@ -24,11 +87,14 @@ export default function (pi: ExtensionAPI) {
24
87
  setupSubdirAgents(pi);
25
88
  setupSessionTitle(pi);
26
89
  setupGuidance(pi);
90
+ setupDependencyReminders(pi);
27
91
 
28
92
  // Configurable modules
29
- if (isModuleEnabled("patch")) setupIO(pi);
30
- if (isModuleEnabled("safety")) setupSafety(pi);
31
- if (isModuleEnabled("lsp")) setupLsp(pi);
32
- if (isModuleEnabled("smart-at")) setupSmartAt(pi);
33
- if (isModuleEnabled("mcp")) setupMcp(pi);
93
+ if (isModuleEnabled("patch")) setupIO(pi);
94
+ if (isModuleEnabled("safety")) setupSafety(pi);
95
+ if (isModuleEnabled("lsp")) setupLsp(pi);
96
+ if (isModuleEnabled("smart-at")) setupSmartAt(pi);
97
+ if (isModuleEnabled("mcp")) setupMcp(pi);
98
+ if (isModuleEnabled("wakatime")) setupWakatime(pi);
99
+ if (isModuleEnabled("rtk") && findSystemRtk()) setupRtkIntegration(pi);
34
100
  }
package/extensions/io.ts CHANGED
@@ -193,7 +193,7 @@ function normalizeDisplayText(text: string): string {
193
193
  }
194
194
 
195
195
  function replaceTabs(text: string): string {
196
- return text.replace(/\t/g, " ");
196
+ return text.replace(/\t/g, " ");
197
197
  }
198
198
 
199
199
  function highlightSingleLine(line: string, lang: string | undefined): string {
@@ -302,7 +302,7 @@ function appendPatchDiffChildren(parent: Box, body: string, theme: any): void {
302
302
 
303
303
  const flush = () => {
304
304
  if (buffer.length === 0) return;
305
- parent.addChild(new Text(renderDiff(buffer.join("\n")), 0, 0));
305
+ parent.addChild(new Text(renderDiff(replaceTabs(buffer.join("\n"))), 0, 0));
306
306
  buffer = [];
307
307
  };
308
308
 
@@ -336,7 +336,7 @@ export function buildPatchCallComponent(component: PatchCallComponent, args: any
336
336
  if (args?.path) {
337
337
  label = theme.fg("accent", args.path);
338
338
  if (args.overwrite) {
339
- label += theme.fg("warning", " [overwrite]");
339
+ label += theme.fg("error", " [overwrite]");
340
340
  } else if (args.edits?.length > 0) {
341
341
  label += theme.fg("dim", ` (${args.edits.length} edit${args.edits.length > 1 ? "s" : ""})`);
342
342
  }
@@ -447,6 +447,7 @@ export function setupIO(pi: ExtensionAPI) {
447
447
  promptGuidelines: [
448
448
  "Always prefer modifying files with PATCH tool over bash commands or python scripts.",
449
449
  "For full-file replacement, always use patch tool to prevent unintended edits or data loss.",
450
+ "To prevent hallucinations: 1. Keep each edit batch ≤ 5 changes; 2. Process remaining revisions in sequential steps",
450
451
  ],
451
452
  parameters: PatchSchema,
452
453
  renderShell: "self",
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * LSP Server Config — language detection, server commands, workspace roots.
3
3
  */
4
- import { existsSync } from "node:fs";
4
+ import { existsSync, readdirSync, type Dirent } from "node:fs";
5
+ import { spawnSync } from "node:child_process";
5
6
  import { dirname, extname, isAbsolute, join, resolve } from "node:path";
7
+ import type { DependencyStatus } from "../rtk";
6
8
 
7
9
  // ─── File extension → language mapping ────────────────────────────────────
8
10
 
@@ -39,8 +41,8 @@ const LANGUAGE_SERVERS: Record<string, Omit<LanguageConfig, "is_project_local">>
39
41
  install_hint: "Install clangd and ensure the clangd binary is on PATH.",
40
42
  },
41
43
  python: {
42
- language: "python", command: "pylsp", args: [],
43
- install_hint: "Install Python LSP with: pip install python-lsp-server",
44
+ language: "python", command: "pyright-langserver", args: ["--stdio"],
45
+ install_hint: "Install a Python LSP and ensure the pyright-langserver binary is on PATH.",
44
46
  },
45
47
  rust: {
46
48
  language: "rust", command: "rust-analyzer", args: [],
@@ -110,6 +112,54 @@ export function languageIdForFile(filePath: string): string | undefined {
110
112
  return detectLanguage(filePath);
111
113
  }
112
114
 
115
+ export function detectProjectLanguages(cwd: string, limit = 1500): Set<string> {
116
+ const found = new Set<string>();
117
+ let seen = 0;
118
+ const skipDirs = new Set([".git", "node_modules", ".pi", "dist", "build", "coverage"]);
119
+
120
+ function walk(dir: string): void {
121
+ if (seen >= limit) return;
122
+ let entries: Dirent[] = [];
123
+ try {
124
+ entries = readdirSync(dir, { withFileTypes: true });
125
+ } catch {
126
+ return;
127
+ }
128
+ for (const entry of entries) {
129
+ if (seen >= limit) return;
130
+ const full = join(dir, entry.name);
131
+ if (entry.isDirectory()) {
132
+ if (skipDirs.has(entry.name) || entry.name.startsWith('.')) continue;
133
+ walk(full);
134
+ } else if (entry.isFile()) {
135
+ seen++;
136
+ const language = detectLanguage(full);
137
+ if (language) found.add(language);
138
+ }
139
+ }
140
+ }
141
+
142
+ walk(cwd);
143
+ return found;
144
+ }
145
+
146
+ export function collectLspDependencyStatuses(cwd: string): DependencyStatus[] {
147
+ const statuses: DependencyStatus[] = [];
148
+ const seen = new Set<string>();
149
+ for (const language of listSupportedLanguages()) {
150
+ const cfg = getServerConfig(language, cwd);
151
+ if (!cfg || seen.has(cfg.command)) continue;
152
+ seen.add(cfg.command);
153
+ statuses.push({
154
+ module: "lsp",
155
+ label: cfg.command,
156
+ state: commandExists(cfg.command) ? "ok" : "missing",
157
+ detail: cfg.install_hint,
158
+ });
159
+ }
160
+ return statuses;
161
+ }
162
+
113
163
  export function findWorkspaceRoot(
114
164
  filePath: string,
115
165
  fallback = process.cwd(),
@@ -124,6 +174,16 @@ export function findWorkspaceRoot(
124
174
 
125
175
  // ─── Internal helpers ─────────────────────────────────────────────────────
126
176
 
177
+ function commandExists(command: string): boolean {
178
+ if (isAbsolute(command) || command.includes("/") || command.includes("\\")) {
179
+ return existsSync(command);
180
+ }
181
+ const result = process.platform === "win32"
182
+ ? spawnSync("where", [command], { encoding: "utf-8" })
183
+ : spawnSync(process.env.SHELL || "sh", ["-lc", `command -v '${command.replace(/'/g, `'"'"'`)}'`], { encoding: "utf-8" });
184
+ return result.status === 0;
185
+ }
186
+
127
187
  function resolveLocalBinary(
128
188
  command: string,
129
189
  cwd: string,