decorated-pi 0.2.2 → 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.
package/README.md CHANGED
@@ -1,65 +1,64 @@
1
1
  # decorated-pi
2
2
 
3
- `decorated-pi` is a Pi extension that adds safety gates, LSP tools, image/compaction model helpers, smarter `@` file search, dynamic subdirectory `AGENTS.md` loading, and a few workflow quality-of-life improvements.
3
+ `decorated-pi` is a practical enhancement pack for [Pi](https://github.com/earendil-works/pi).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pi install npm:decorated-pi
9
+ pi install git:github.com/lcwecker/decorated-pi
10
+ pi install /path/to/decorated-pi
11
+ ```
4
12
 
5
13
  ## Features
6
14
 
7
- ### 1. Safety Layer
8
-
9
- - **Dangerous bash guard**
10
- - asks for confirmation on destructive commands such as:
11
- - `rm`
12
- - `sudo`
13
- - `svn commit/revert`
14
- - `git reset/restore/clean/push/revert`
15
- - `npm publish`
16
- - `>` / `1>` / `2>` / `&>` / `tee` overwrite existing files
17
- - Hints the agent to use `edit` instead of `write` on non-empty files
18
- - **Protected paths**
19
- - Blocks read/write access (via `write`/`edit`/`read` tools or `cat`/`head`/`tail`/`grep`/`rg` etc. bash commands) to sensitive locations such as `.env`, `.git/`, `.ssh/`, `*.pem`, `*.key`, etc.
20
- - **Secret redaction**
21
- - Dual-layer detection: 40+ known-format patterns (AWS, GitHub, OpenAI, etc.) + Adjusted Shannon Entropy analysis for unknown formats. Based on [opencode-secrets-protect](https://github.com/jscheel/opencode-secrets-protect) (MIT)
22
-
23
- ### 2. Smart `@` File Search
24
-
25
- Replaces Pi's default file search with a faster project-aware search strategy:
26
-
27
- - Uses `git ls-files` in git repos
28
- - Falls back to `fd` outside git repos
29
- - Caches results for 10 seconds
30
- - Scores primarily on **filename match quality**, not full-path fuzziness
31
- - Penalizes hidden/cache/build directories
32
- - Hides hidden paths from empty-query results
33
-
34
- ### 3. LSP Tool Suite
35
-
36
- Based on [@spences10/pi-lsp](https://github.com/spences10/my-pi/tree/main/packages/pi-lsp) by Scott Spence (MIT License), with additions:
37
-
38
- - C/C++ (clangd) and Lua support
39
- - `lsp_find_symbol`, `lsp_rename`, multi-file support merged into `lsp_diagnostics`
40
- - Force-sync on `didChange` (no stale diagnostics)
41
-
42
- Registered tools:
43
-
44
- - `lsp_diagnostics`
45
- - `lsp_find_symbol`
46
- - `lsp_hover`
47
- - `lsp_definition`
48
- - `lsp_references`
49
- - `lsp_document_symbols`
50
- - `lsp_rename`
51
-
52
- Supported languages:
53
-
54
- - c/cpp
55
- - go
56
- - java
57
- - lua
58
- - python
59
- - ruby
60
- - rust
61
- - svelte
62
- - typescript
15
+ ### 1. Patch Tool
16
+
17
+ Replaces Pi's built-in `edit` / `write` with a stronger `patch` tool:
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
22
+
23
+ ### 2. Secret redaction
24
+
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
28
+
29
+ Zero-config MCP client with two built-in servers:
30
+
31
+ | Server | Tool Prefix | Source |
32
+ | --- | --- | --- |
33
+ | Context7 | `context7_*` | `https://mcp.context7.com/mcp` |
34
+ | Exa | `exa_*` | `https://mcp.exa.ai/mcp` |
35
+
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
+ ```
48
+
49
+ **Global config** — persist custom servers in `~/.pi/agent/decorated-pi.json`:
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "my-server": { "url": "https://my-mcp.example.com/mcp" }
55
+ }
56
+ }
57
+ ```
58
+
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.
63
62
 
64
63
  ### 4. Auxiliary Models (Image + Compact)
65
64
 
@@ -68,15 +67,32 @@ Uses cheaper models for auxiliary tasks, configured via `/dp-model`:
68
67
  - **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)
69
68
  - **Compact model** — uses a configured model for context compaction (instead of the main model), auto-resumes after compaction.
70
69
 
71
- ### 5. Dynamic Subdirectory `AGENTS.md` / `CLAUDE.md`
70
+ ### 5. Smart `@` File Search
71
+
72
+ Replaces Pi's default file search with a faster, project-aware search strategy:
73
+
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
78
+
79
+ ### 6. LSP Tool Suite
80
+
81
+ A cleaned-up, minimal LSP toolset. The extension keeps only the two LSP tools that cover the most practical coding workflows: checking diagnostics after edits and inspecting file structure before focused changes.
82
+
83
+ - **`lsp_diagnostics`** — file diagnostics with severity filtering
84
+ - **`lsp_document_symbols`** — file symbol outline
85
+
86
+ Supported languages: c/cpp, go, java, lua, json, python, ruby, rust, svelte, typescript
87
+
88
+ ### 7. Dynamic Subdirectory `AGENTS.md` / `CLAUDE.md`
72
89
 
73
90
  When the agent reads or edits a file:
74
91
 
75
92
  - discovers `AGENTS.md` / `CLAUDE.md` in the file's directory and ancestor directories
76
93
  - injects newly discovered guidance into tool results
77
- - persists discovered files into the session so they are restored on resume
78
94
 
79
- ### 6. Extend Providers
95
+ ### 8. Extend Providers
80
96
 
81
97
  Extend providers are registered via `/login` → "Use a subscription":
82
98
 
@@ -86,16 +102,6 @@ Extend providers are registered via `/login` → "Use a subscription":
86
102
  | Baidu Qianfan | `qianfan.baidubce.com/v2/coding` |
87
103
  | ARK Coding | `ark.cn-beijing.volces.com/api/coding/v3` |
88
104
 
89
- ## Install
90
-
91
- ```bash
92
- pi install /path/to/decorated-pi #local
93
- pi install npm:decorated-pi #npm
94
- pi install git:github.com/lcwecker/decorated-pi #github
95
- ```
96
-
97
- Then reload Pi
98
-
99
105
  ## Configuration
100
106
 
101
107
  Runtime settings are stored in:
@@ -110,24 +116,26 @@ Modules can be toggled on/off. Changes take effect after `/reload`.
110
116
 
111
117
  | Module | Default | Effect when disabled |
112
118
  | -------- | --------- | --------------------- |
113
- | `safety` | `true` | No command guard, no protected path check, no secret redaction |
119
+ | `patch` | `true` | Reverts to Pi's built-in `edit` / `write` tools |
120
+ | `safety` | `true` | No secret redaction on `read` / `bash` output |
114
121
  | `lsp` | `true` | All `lsp_*` tools unregistered — no diagnostics, hover, etc. |
115
122
  | `smart-at` | `true` | Fallback to Pi's built-in `@` file completion |
123
+ | `mcp` | `true` | All `{server}_*` MCP tools unregistered |
116
124
 
117
125
  Use `/dp-settings` to toggle, or edit the config file directly:
118
126
 
119
127
  ```json
120
128
  {
121
129
  "modules": {
130
+ "patch": true,
122
131
  "safety": true,
123
132
  "lsp": false,
124
- "smart-at": true
133
+ "smart-at": true,
134
+ "mcp": true
125
135
  }
126
136
  }
127
137
  ```
128
138
 
129
- Omitted keys default to `true` (enabled).
130
-
131
139
  ## License
132
140
 
133
141
  MIT
@@ -0,0 +1,124 @@
1
+ /**
2
+ * File mtime tracking for stale-read protection.
3
+ *
4
+ * - `read` tool records mtime when the LLM reads a file
5
+ * - `patch` tool checks mtime before editing — rejects if file changed since last read
6
+ * - `patch` tool updates mtime after a successful write
7
+ * - Markers are persisted to session custom entries and restored from the
8
+ * current branch after the last compaction boundary.
9
+ */
10
+
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+
14
+ export const FILE_TIMES_CUSTOM_TYPE = "decorated-pi.file-times";
15
+
16
+ export interface FileTimeMarkerData {
17
+ path: string;
18
+ mtimeMs: number;
19
+ }
20
+
21
+ interface SessionLikeEntry {
22
+ type: string;
23
+ customType?: string;
24
+ data?: unknown;
25
+ }
26
+
27
+ /** Last-known mtime for each absolute file path (ms since epoch). */
28
+ const readMarkers = new Map<string, number>();
29
+
30
+ /** Get current file mtime in ms. Throws if file doesn't exist. */
31
+ function getFileMtime(absPath: string): number {
32
+ const stat = fs.statSync(absPath);
33
+ return stat.mtimeMs;
34
+ }
35
+
36
+ /** Record that the LLM has seen the current version of a file.
37
+ * Called after `read` tool completes and after `patch` writes. */
38
+ export function recordReadTime(absPath: string): void {
39
+ if (!fs.existsSync(absPath)) return;
40
+ readMarkers.set(absPath, getFileMtime(absPath));
41
+ }
42
+
43
+ /** Check if file has been modified since last read.
44
+ * Returns an error message if stale, or undefined if ok to edit. */
45
+ export function checkStaleFile(absPath: string, displayPath: string): string | undefined {
46
+ // If file doesn't exist on disk, always allow — creating a new file
47
+ // doesn't require reading it first, and recreating a deleted file is safe.
48
+ if (!fs.existsSync(absPath)) {
49
+ return undefined;
50
+ }
51
+
52
+ const lastRead = readMarkers.get(absPath);
53
+ if (lastRead === undefined) {
54
+ // File exists but never read — must read first to avoid blind edits
55
+ return (
56
+ `File not read yet: ${displayPath}. ` +
57
+ `Please read the file with the read tool before editing.`
58
+ );
59
+ }
60
+
61
+ const currentMtime = getFileMtime(absPath);
62
+ if (currentMtime > lastRead) {
63
+ return (
64
+ `File modified since last read: ${displayPath}. ` +
65
+ `Please re-read the file with the read tool before editing.`
66
+ );
67
+ }
68
+
69
+ return undefined;
70
+ }
71
+
72
+ function toStoredPath(cwd: string, absPath: string): string {
73
+ const normalizedCwd = path.normalize(cwd);
74
+ const normalizedAbs = path.normalize(absPath);
75
+ const rel = path.relative(normalizedCwd, normalizedAbs);
76
+ if (rel && !rel.startsWith("..") && !path.isAbsolute(rel)) return rel;
77
+ return normalizedAbs;
78
+ }
79
+
80
+ function lastCompactionIndex(entries: SessionLikeEntry[]): number {
81
+ for (let i = entries.length - 1; i >= 0; i--) {
82
+ if (entries[i]?.type === "compaction") return i;
83
+ }
84
+ return -1;
85
+ }
86
+
87
+ function isFileTimeMarkerData(value: unknown): value is FileTimeMarkerData {
88
+ return !!value
89
+ && typeof value === "object"
90
+ && typeof (value as any).path === "string"
91
+ && typeof (value as any).mtimeMs === "number";
92
+ }
93
+
94
+ /** Build a session-persisted marker payload for the current file version. */
95
+ export function createFileTimeMarkerData(cwd: string, absPath: string): FileTimeMarkerData | undefined {
96
+ if (!fs.existsSync(absPath)) return undefined;
97
+ return {
98
+ path: toStoredPath(cwd, absPath),
99
+ mtimeMs: getFileMtime(absPath),
100
+ };
101
+ }
102
+
103
+ /** Restore markers from the current branch, ignoring anything before the last compaction. */
104
+ export function restoreReadMarkersFromBranch(entries: SessionLikeEntry[], cwd: string): void {
105
+ clearReadMarkers();
106
+ const start = lastCompactionIndex(entries) + 1;
107
+ for (const entry of entries.slice(start)) {
108
+ if (entry.type !== "custom" || entry.customType !== FILE_TIMES_CUSTOM_TYPE) continue;
109
+ if (!isFileTimeMarkerData(entry.data)) continue;
110
+ const absPath = resolveAbsolutePath(cwd, entry.data.path);
111
+ readMarkers.set(absPath, entry.data.mtimeMs);
112
+ }
113
+ }
114
+
115
+ /** Clear all tracked file times (e.g., on session start or compaction). */
116
+ export function clearReadMarkers(): void {
117
+ readMarkers.clear();
118
+ }
119
+
120
+ /** Resolve a relative path to absolute (for consistent map keys). */
121
+ export function resolveAbsolutePath(cwd: string, filePath: string): string {
122
+ if (path.isAbsolute(filePath)) return path.normalize(filePath);
123
+ return path.normalize(path.resolve(cwd, filePath));
124
+ }
@@ -9,9 +9,11 @@ export function setupGuidance(pi: ExtensionAPI) {
9
9
  const guidance = [
10
10
  DECORATED_PI_GUIDANCE_MARKER,
11
11
  "",
12
- "Before taking any action on a user's prompt, briefly restate your understanding of what the user wants. If ambiguous, ask clarifying questions. Only proceed after intent is clear.",
13
- "",
14
- "For medium-to-large tasks (more than 3 steps or touching multiple files/systems), break the task into discrete steps. For small tasks (1-2 steps), do it directly.",
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.",
15
17
  ].join("\n");
16
18
 
17
19
  return {
@@ -4,27 +4,31 @@
4
4
 
5
5
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
6
  import { setupSafety } from "./safety/index.js";
7
- import { setupExtendModel } from "./extend-model";
7
+ import { setupModelIntegration } from "./model-integration";
8
8
  import { setupSlash } from "./slash";
9
9
  import { setupSubdirAgents } from "./subdir-agents";
10
10
  import { setupSessionTitle } from "./session-title";
11
+ import { setupIO } from "./io";
11
12
  import { setupGuidance } from "./guidance";
12
13
  import { setupLsp } from "./lsp/index";
13
14
  import { setupProviders } from "./providers/index";
14
15
  import { setupSmartAt } from "./smart-at";
16
+ import { setupMcp } from "./mcp/index.js";
15
17
  import { isModuleEnabled } from "./settings";
16
18
 
17
19
  export default function (pi: ExtensionAPI) {
18
20
  // Always loaded — core commands and providers
19
21
  setupSlash(pi);
20
22
  setupProviders(pi);
21
- setupExtendModel(pi);
23
+ setupModelIntegration(pi);
22
24
  setupSubdirAgents(pi);
23
25
  setupSessionTitle(pi);
24
26
  setupGuidance(pi);
25
27
 
26
28
  // Configurable modules
29
+ if (isModuleEnabled("patch")) setupIO(pi);
27
30
  if (isModuleEnabled("safety")) setupSafety(pi);
28
31
  if (isModuleEnabled("lsp")) setupLsp(pi);
29
32
  if (isModuleEnabled("smart-at")) setupSmartAt(pi);
33
+ if (isModuleEnabled("mcp")) setupMcp(pi);
30
34
  }