decorated-pi 0.7.2 → 0.8.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/index.ts CHANGED
@@ -23,8 +23,12 @@ import { createSkeleton } from "./hooks/skeleton.js";
23
23
  import { setupRedact, REDACT_GUIDANCE } from "./hooks/redact.js";
24
24
  import { externalizeModule } from "./hooks/externalize.js";
25
25
  import { normalizeCodeblocksModule } from "./hooks/normalize-codeblocks.js";
26
+ import { thinkingLabelStripModule } from "./hooks/thinking-label-strip.js";
26
27
  import { trackMtimeModule } from "./hooks/track-mtime.js";
27
- import { injectAgentsMdModule, INJECT_AGENTS_MD_GUIDANCE } from "./hooks/inject-agents-md.js";
28
+ import {
29
+ injectAgentsMdModule,
30
+ INJECT_AGENTS_MD_GUIDANCE,
31
+ } from "./hooks/inject-agents-md.js";
28
32
  import { imageVisionModule } from "./hooks/image-vision.js";
29
33
  import { smartAtModule } from "./hooks/smart-at.js";
30
34
  import { sessionTitleModule } from "./hooks/session-title.js";
@@ -39,7 +43,11 @@ import { registerLspTools } from "./tools/lsp/tools.js";
39
43
  import { LspServerManager } from "./tools/lsp/manager.js";
40
44
  import { collectLspDependencyStatuses } from "./tools/lsp/servers.js";
41
45
  import { registerAskTool } from "./tools/ask/index.js";
42
- import { resolveMcpConfigs, migrateLegacyGlobalMcpConfig, collectMcpDependencyStatuses } from "./tools/mcp/config.js";
46
+ import {
47
+ resolveMcpConfigs,
48
+ migrateLegacyGlobalMcpConfig,
49
+ collectMcpDependencyStatuses,
50
+ } from "./tools/mcp/config.js";
43
51
  import { ensureMcpServerReady } from "./hooks/mcp.js";
44
52
 
45
53
  import { registerDpModelCommand } from "./commands/dp-model.js";
@@ -56,80 +64,98 @@ import { captureModuleSnapshot, isModuleEnabled } from "./settings.js";
56
64
  // its constant and pushing it here. No priority / sort logic — just push.
57
65
 
58
66
  const BASE_GUIDANCE = [
59
- "## Decorated Pi Guidance",
60
- "",
61
- "### Workflow, how to approach tasks",
62
- "- 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.",
63
- "- Exercise caution when performing any **write** operations, especially when you are in a research or exploration phase.",
64
- "- 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.",
65
- "",
66
- "### Filesystem Safety, where NOT to write",
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.",
67
+ "## Decorated Pi Guidance",
68
+ "",
69
+ "### Workflow, how to approach tasks",
70
+ "- 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.",
71
+ "- Exercise caution when performing any **write** operations, especially when you are in a research or exploration phase.",
72
+ "- 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.",
73
+ "",
74
+ "### Filesystem Safety, where NOT to write",
75
+ "- CAUTION: Do not perform write operations in the following directories unless explicitly instructed: `node_modules`, `venv`, `env`, `__pycache__`, `.git` or any other hidden directories.",
76
+ ].join("\n");
77
+
78
+ // Adapted from hexiecs/talk-normal (MIT), prompt-chatgpt.md v0.6.2.
79
+ const TALK_NORMAL_GUIDANCE = [
80
+ "## Your talking style",
81
+ "",
82
+ "- Be direct and informative. No filler, no fluff, but give enough to be useful.",
83
+ "- Lead with the answer; add context only if it helps.",
84
+ "- Prefer direct positive claims. Avoid negation-based contrastive phrasing like `不是X,而是Y` / `it's not X, it's Y`; state the positive claim directly.",
85
+ "- Kill filler: `I'd be happy to`, `Great question`, `It's worth noting`, `Certainly`, `Of course`, `首先`, `值得注意的是`, `综上所述`.",
86
+ "- Never restate the question.",
87
+ "- Yes/no questions: answer first, then give one sentence of reasoning.",
88
+ "- Comparisons: give a recommendation with brief reasoning, not a balanced essay.",
89
+ "- Code: give the code plus a usage example when non-trivial. Skip preambles like `Certainly! Here is...`.",
90
+ "- Use bullets or numbered lists only when the content has real parallel or sequential structure.",
91
+ "- Do not end with conditional follow-up offers like `If you want, I can...` / `如果你愿意,我还可以...`; take the real next step or name it directly.",
92
+ "- Do not use summary-stamp closings like `In summary`, `Hope this helps`, `一句话总结`, `总结一下`, `简而言之`; state the final claim directly.",
68
93
  ].join("\n");
69
94
 
70
95
  /** Remove the injected Pi documentation block from the base system prompt.
71
96
  * Matches a line containing "Pi documentation" and deletes it plus all
72
97
  * following non-empty lines, stopping at the first blank line. */
73
98
  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;
99
+ const lines = prompt.split("\n");
100
+ const out: string[] = [];
101
+ let i = 0;
102
+ while (i < lines.length) {
103
+ const line = lines[i];
104
+ if (line.includes("Pi documentation")) {
105
+ i++;
106
+ while (i < lines.length && lines[i].trim() !== "") i++;
107
+ // Drop the terminating blank line as well so we don't leave orphan whitespace.
108
+ if (i < lines.length && lines[i].trim() === "") i++;
109
+ continue;
110
+ }
111
+ out.push(line);
112
+ i++;
85
113
  }
86
- out.push(line);
87
- i++;
88
- }
89
- return out.join("\n");
114
+ return out.join("\n");
90
115
  }
91
116
 
92
117
  /** Sort the <available_skills> block in the system prompt by skill name.
93
118
  * Pi core appends extension-provided skills after user/project skills and does
94
119
  * not sort the XML; this makes the final prompt stable and cache-friendly. */
95
120
  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);
121
+ const startMarker = "\n<available_skills>";
122
+ const endMarker = "</available_skills>";
123
+ const startIdx = prompt.indexOf(startMarker);
124
+ if (startIdx === -1) return prompt;
125
+ const endIdx = prompt.indexOf(endMarker, startIdx);
126
+ if (endIdx === -1) return prompt;
127
+
128
+ const before = prompt.slice(0, startIdx + startMarker.length);
129
+ const after = prompt.slice(endIdx);
130
+ const inner = prompt.slice(startIdx + startMarker.length, endIdx);
131
+
132
+ const chunks: string[][] = [];
133
+ let current: string[] = [];
134
+ for (const line of inner.split("\n")) {
135
+ const trimmed = line.trim();
136
+ if (trimmed === "<skill>") {
137
+ current = [line];
138
+ } else if (trimmed === "</skill>") {
139
+ current.push(line);
140
+ chunks.push(current);
141
+ current = [];
142
+ } else if (current.length > 0) {
143
+ current.push(line);
144
+ }
119
145
  }
120
- }
121
146
 
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
- };
147
+ const nameOf = (chunk: string[]) => {
148
+ const line = chunk.find((l) => l.trim().startsWith("<name>"));
149
+ if (!line) return "";
150
+ const t = line.trim();
151
+ return t.slice(6, t.indexOf("</name>"));
152
+ };
128
153
 
129
- chunks.sort((a, b) => nameOf(a).localeCompare(nameOf(b)));
154
+ chunks.sort((a, b) => nameOf(a).localeCompare(nameOf(b)));
130
155
 
131
- const sortedInner = "\n" + chunks.map((chunk) => chunk.join("\n")).join("\n") + "\n";
132
- return before + sortedInner + after;
156
+ const sortedInner =
157
+ "\n" + chunks.map((chunk) => chunk.join("\n")).join("\n") + "\n";
158
+ return before + sortedInner + after;
133
159
  }
134
160
 
135
161
  /** Build the list of guideline strings to inject, in prompt order.
@@ -139,142 +165,153 @@ export function sortSkillsInSystemPrompt(prompt: string): string {
139
165
  * injected). The mcp module check lives in `resolveMcpConfigs`, so this
140
166
  * code only needs to look at the server's own `enabled` flag. */
141
167
  function buildGuidelines(): string[] {
142
- const out: string[] = [
143
- BASE_GUIDANCE,
144
- INJECT_AGENTS_MD_GUIDANCE, // from hooks/inject-agents-md.ts — always on
145
- ];
146
- if (isModuleEnabled("secretRedaction")) out.push(REDACT_GUIDANCE);
147
- return out;
168
+ const out: string[] = [
169
+ BASE_GUIDANCE,
170
+ TALK_NORMAL_GUIDANCE,
171
+ INJECT_AGENTS_MD_GUIDANCE, // from hooks/inject-agents-md.ts — always on
172
+ ];
173
+ if (isModuleEnabled("secretRedaction")) out.push(REDACT_GUIDANCE);
174
+ return out;
148
175
  }
149
176
 
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;
177
+ function canRegisterMcpServer(
178
+ config: { name: string; command?: string },
179
+ deps: Array<{ module: string; state: string }>,
180
+ ): boolean {
181
+ if (!config.command) return true;
182
+ const dep = deps.find((d) => d.module === `mcp:${config.name}`);
183
+ return dep ? dep.state === "ok" : true;
154
184
  }
155
185
 
156
186
  /** Absolute path to the plugin's builtin skills directory.
157
187
  * Used by `resources_discover` so the skill travels with the plugin
158
188
  * regardless of which project pi is running in. */
159
189
  export function getBuiltinSkillPaths(): string[] {
160
- return [fileURLToPath(new URL("./skills", import.meta.url))];
190
+ return [fileURLToPath(new URL("./skills", import.meta.url))];
161
191
  }
162
192
 
163
193
  /** Register the plugin's builtin skill paths with Pi core. */
164
194
  function installBuiltinSkills(pi: ExtensionAPI): void {
165
- pi.on("resources_discover", async (_event: any) => ({
166
- skillPaths: getBuiltinSkillPaths(),
167
- }));
195
+ pi.on("resources_discover", async (_event: any) => ({
196
+ skillPaths: getBuiltinSkillPaths(),
197
+ }));
168
198
  }
169
199
 
170
200
  /** Install a single before_agent_start handler that appends every
171
201
  * guideline in order, stripping the volatile "Current date: …" line
172
202
  * for cache stability. Idempotent — re-injection is a no-op via marker. */
173
203
  function installGuidelines(pi: ExtensionAPI): void {
174
- const blocks = buildGuidelines();
175
- const joined = blocks.join("\n\n");
176
- const marker = "## Decorated Pi Guidance";
177
-
178
- pi.on("before_agent_start", async (event: any) => {
179
- if (!event.systemPrompt) return undefined;
180
- let prompt: string = stripPiDocsBlock(event.systemPrompt);
181
- prompt = sortSkillsInSystemPrompt(prompt);
182
- prompt = prompt.replace(/\nCurrent date: \d{4}-\d{2}-\d{2}/, "");
183
- if (prompt.includes(marker)) return undefined; // already injected this turn
184
- return { systemPrompt: `${prompt}\n\n${joined}` };
185
- });
204
+ const blocks = buildGuidelines();
205
+ const joined = blocks.join("\n\n");
206
+ const marker = "## Decorated Pi Guidance";
207
+
208
+ pi.on("before_agent_start", async (event: any) => {
209
+ if (!event.systemPrompt) return undefined;
210
+ let prompt: string = stripPiDocsBlock(event.systemPrompt);
211
+ prompt = sortSkillsInSystemPrompt(prompt);
212
+ prompt = prompt.replace(/\nCurrent date: \d{4}-\d{2}-\d{2}/, "");
213
+ if (prompt.includes(marker)) return undefined; // already injected this turn
214
+ return { systemPrompt: `${prompt}\n\n${joined}` };
215
+ });
186
216
  }
187
217
 
188
218
  export default async function (pi: ExtensionAPI) {
189
- // Snapshot the module settings that pi is about to load. /dp-settings
190
- // compares against this to avoid prompting for reload when the user
191
- // has only returned the settings to the currently-loaded state.
192
- captureModuleSnapshot();
193
-
194
- // ── Skeleton (hooks) ───────────────────────────────────────────────────
195
- const sk = createSkeleton();
196
-
197
- // Order matters for tool_result compose chain:
198
- // 1. redact → normalize-codeblocks → externalize → track-mtime → inject-agents-md → image-vision → wakatime
199
- // The first module registered for a given event runs first (compose chain).
200
- if (isModuleEnabled("secretRedaction")) setupRedact(sk);
201
- sk.register(normalizeCodeblocksModule);
202
- sk.register(externalizeModule);
203
- sk.register(trackMtimeModule);
204
- sk.register(injectAgentsMdModule);
205
- sk.register(imageVisionModule);
206
-
207
- // session_start handlers (parallel)
208
- // pi-tool-filter must register first so native tools are dropped before
209
- // anything else inspects the tool list.
210
- sk.register(piToolFilterModule);
211
- sk.register(sessionTitleModule);
212
- if (isModuleEnabled("atOverride")) sk.register(smartAtModule);
213
- if (isModuleEnabled("wakatime")) sk.register(wakatimeModule);
214
-
215
- // Compaction + RTK (these also install their own pi.on via setup<>()).
216
- setupCompaction(sk);
217
- if (isModuleEnabled("rtk")) setupRtk(sk, pi);
218
- if (isModuleEnabled("wakatime")) setupWakatime(sk, pi);
219
-
220
- // ── Tools (conditional on module switches) ────────────────────────────
221
- if (isModuleEnabled("patchOverrideEdit")) registerPatchTool(pi);
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
- }
235
- if (isModuleEnabled("ask")) registerAskTool(pi);
236
-
237
- // MCP: hook, tools, and /mcp command are gated together. Disabling the
238
- // module means no session_start handler runs, no tools register, no
239
- // /mcp command is available, and no background connections are attempted.
240
- if (isModuleEnabled("mcp")) {
241
- // One-time migration: legacy global MCP configs in
242
- // ~/.pi/agent/decorated-pi.json move to ~/.pi/agent/mcp.json. Run
243
- // explicitly here so `loadGlobalMcpConfigs` stays pure.
244
- migrateLegacyGlobalMcpConfig();
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
- });
219
+ // Snapshot the module settings that pi is about to load. /dp-settings
220
+ // compares against this to avoid prompting for reload when the user
221
+ // has only returned the settings to the currently-loaded state.
222
+ captureModuleSnapshot();
223
+
224
+ // ── Skeleton (hooks) ───────────────────────────────────────────────────
225
+ const sk = createSkeleton();
226
+
227
+ // Order matters for tool_result compose chain:
228
+ // 1. redact → normalize-codeblocks → externalize → track-mtime → inject-agents-md → image-vision → wakatime
229
+ // The first module registered for a given event runs first (compose chain).
230
+ if (isModuleEnabled("secretRedaction")) setupRedact(sk);
231
+ sk.register(normalizeCodeblocksModule);
232
+ sk.register(thinkingLabelStripModule);
233
+ sk.register(externalizeModule);
234
+ sk.register(trackMtimeModule);
235
+ sk.register(injectAgentsMdModule);
236
+ sk.register(imageVisionModule);
237
+
238
+ // session_start handlers (parallel)
239
+ // pi-tool-filter must register first so native tools are dropped before
240
+ // anything else inspects the tool list.
241
+ sk.register(piToolFilterModule);
242
+ sk.register(sessionTitleModule);
243
+ if (isModuleEnabled("atOverride")) sk.register(smartAtModule);
244
+ if (isModuleEnabled("wakatime")) sk.register(wakatimeModule);
245
+
246
+ // Compaction + RTK (these also install their own pi.on via setup<>()).
247
+ setupCompaction(sk);
248
+ if (isModuleEnabled("rtk")) setupRtk(sk);
249
+ if (isModuleEnabled("wakatime")) setupWakatime(sk, pi);
250
+
251
+ // ── Tools (conditional on module switches) ────────────────────────────
252
+ if (isModuleEnabled("patchOverrideEdit")) registerPatchTool(pi);
253
+ if (isModuleEnabled("lsp")) {
254
+ const lspDeps = collectLspDependencyStatuses(process.cwd());
255
+ if (lspDeps.some((d) => d.state === "ok")) {
256
+ registerLspTools(pi, new LspServerManager());
257
+ }
258
+ for (const dep of lspDeps) {
259
+ if (dep.state !== "ok") {
260
+ sk.declareMissing({
261
+ name: dep.label,
262
+ module: "lsp",
263
+ hint: dep.detail,
264
+ });
265
+ }
266
+ }
253
267
  }
254
- const configs = resolveMcpConfigs(process.cwd()).filter(s => s.enabled);
255
- // Per-server readiness: cache hit → register from cache (fast).
256
- // Cache miss connect synchronously, write cache, then register
257
- // live tools. This blocks startup only for cache-miss servers.
258
- // Skip servers whose binary is missing (dependency not met).
259
- for (const config of configs) {
260
- if (!canRegisterMcpServer(config, mcpDeps)) continue;
261
- await ensureMcpServerReady(pi, config, process.cwd());
268
+ if (isModuleEnabled("ask")) registerAskTool(pi);
269
+
270
+ // MCP: hook, tools, and /mcp command are gated together. Disabling the
271
+ // module means no session_start handler runs, no tools register, no
272
+ // /mcp command is available, and no background connections are attempted.
273
+ if (isModuleEnabled("mcp")) {
274
+ // One-time migration: legacy global MCP configs in
275
+ // ~/.pi/agent/decorated-pi.json move to ~/.pi/agent/mcp.json. Run
276
+ // explicitly here so `loadGlobalMcpConfigs` stays pure.
277
+ migrateLegacyGlobalMcpConfig();
278
+ sk.register(mcpModule);
279
+ const mcpDeps = collectMcpDependencyStatuses(process.cwd());
280
+ for (const dep of mcpDeps) {
281
+ if (dep.state !== "ok") {
282
+ sk.declareMissing({
283
+ name: dep.label, // binary name (e.g. "codegraph")
284
+ module: "mcp",
285
+ hint: dep.detail,
286
+ });
287
+ }
288
+ }
289
+ const configs = resolveMcpConfigs(process.cwd()).filter(
290
+ (s) => s.enabled,
291
+ );
292
+ // Per-server readiness: cache hit → register from cache (fast).
293
+ // Cache miss → connect synchronously, write cache, then register
294
+ // live tools. This blocks startup only for cache-miss servers.
295
+ // Skip servers whose binary is missing (dependency not met).
296
+ for (const config of configs) {
297
+ if (!canRegisterMcpServer(config, mcpDeps)) continue;
298
+ await ensureMcpServerReady(pi, config, process.cwd());
299
+ }
300
+ registerMcpStatusCommand(pi);
262
301
  }
263
- registerMcpStatusCommand(pi);
264
- }
265
302
 
266
- // ── Builtin skills (travel with the plugin in every project) ─────────────
267
- installBuiltinSkills(pi);
303
+ // ── Builtin skills (travel with the plugin in every project) ─────────────
304
+ installBuiltinSkills(pi);
268
305
 
269
- // ── System-prompt guidelines (single handler, array order = prompt order) ──
270
- installGuidelines(pi);
306
+ // ── System-prompt guidelines (single handler, array order = prompt order) ──
307
+ installGuidelines(pi);
271
308
 
272
- // ── Commands ──────────────────────────────────────────────────────────
273
- registerDpModelCommand(pi);
274
- registerDpSettingsCommand(pi);
275
- if (isModuleEnabled("retry")) registerRetryCommand(pi);
276
- if (isModuleEnabled("usage")) registerUsageCommand(pi);
309
+ // ── Commands ──────────────────────────────────────────────────────────
310
+ registerDpModelCommand(pi);
311
+ registerDpSettingsCommand(pi);
312
+ if (isModuleEnabled("retry")) registerRetryCommand(pi);
313
+ if (isModuleEnabled("usage")) registerUsageCommand(pi);
277
314
 
278
- // ── Install skeleton (last) ────────────────────────────────────────────
279
- sk.install(pi);
315
+ // ── Install skeleton (last) ────────────────────────────────────────────
316
+ sk.install(pi);
280
317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decorated-pi",
3
- "version": "0.7.2",
3
+ "version": "0.8.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",
@@ -26,7 +26,9 @@
26
26
  "dependencies": {
27
27
  "@ff-labs/fff-node": "^0.9.4",
28
28
  "@modelcontextprotocol/sdk": "^1.29.0",
29
+ "chardet": "^2.2.0",
29
30
  "file-type": "^21.3.4",
31
+ "iconv-lite": "^0.7.2",
30
32
  "openai": "^6.37.0"
31
33
  },
32
34
  "peerDependencies": {