decorated-pi 0.5.4 → 0.6.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.
Files changed (68) hide show
  1. package/.codegraph/daemon.pid +6 -0
  2. package/AGENTS.md +92 -0
  3. package/README.md +53 -64
  4. package/commands/dp-model.ts +23 -0
  5. package/commands/dp-settings.ts +23 -0
  6. package/commands/mcp-status.ts +62 -0
  7. package/commands/retry.ts +19 -0
  8. package/commands/usage.ts +544 -0
  9. package/hooks/compaction.ts +204 -0
  10. package/hooks/externalize.ts +70 -0
  11. package/hooks/image-vision.ts +132 -0
  12. package/hooks/inject-agents-md.ts +164 -0
  13. package/hooks/mcp.ts +340 -0
  14. package/hooks/normalize-codeblocks.ts +88 -0
  15. package/hooks/pi-tool-filter.ts +28 -0
  16. package/{extensions/safety → hooks/redact}/types.ts +1 -8
  17. package/hooks/redact.ts +104 -0
  18. package/{extensions → hooks}/rtk.ts +92 -115
  19. package/hooks/session-title.ts +54 -0
  20. package/hooks/skeleton.ts +212 -0
  21. package/hooks/smart-at.ts +318 -0
  22. package/{extensions/file-times.ts → hooks/track-mtime.ts} +40 -38
  23. package/{extensions → hooks}/wakatime.ts +120 -122
  24. package/index.ts +155 -1
  25. package/package.json +5 -4
  26. package/{extensions/settings.ts → settings.ts} +32 -0
  27. package/{extensions → tools}/lsp/client.ts +0 -25
  28. package/{extensions → tools}/lsp/format.ts +2 -99
  29. package/{extensions → tools}/lsp/index.ts +1 -1
  30. package/{extensions → tools}/lsp/servers.ts +1 -1
  31. package/{extensions → tools}/lsp/tools.ts +1 -66
  32. package/{extensions → tools}/lsp/types.ts +0 -11
  33. package/tools/mcp/builtin/codegraph.ts +45 -0
  34. package/tools/mcp/builtin/context7.ts +10 -0
  35. package/tools/mcp/builtin/exa.ts +10 -0
  36. package/tools/mcp/builtin/index.ts +19 -0
  37. package/tools/mcp/cache.ts +124 -0
  38. package/{extensions → tools}/mcp/client.ts +2 -2
  39. package/{extensions/mcp/builtin.ts → tools/mcp/config.ts} +21 -181
  40. package/tools/mcp/index.ts +44 -0
  41. package/tools/mcp/tool-definition.ts +112 -0
  42. package/{extensions/patch.ts → tools/patch/core.ts} +336 -65
  43. package/{extensions/io.ts → tools/patch/index.ts} +29 -205
  44. package/tsconfig.json +10 -1
  45. package/ui/mcp-status.ts +202 -0
  46. package/ui/model-picker.ts +162 -0
  47. package/ui/module-settings.ts +83 -0
  48. package/ui/usage.ts +396 -0
  49. package/extensions/index.ts +0 -154
  50. package/extensions/io-tool-output.ts +0 -33
  51. package/extensions/mcp/index.ts +0 -435
  52. package/extensions/model-integration.ts +0 -531
  53. package/extensions/providers/ark-coding.ts +0 -75
  54. package/extensions/providers/index.ts +0 -9
  55. package/extensions/providers/ollama-cloud.ts +0 -101
  56. package/extensions/providers/qianfan-coding.ts +0 -71
  57. package/extensions/safety/index.ts +0 -102
  58. package/extensions/session-title.ts +0 -40
  59. package/extensions/slash.ts +0 -458
  60. package/extensions/smart-at.ts +0 -481
  61. package/extensions/subdir-agents.ts +0 -151
  62. /package/{extensions/safety → hooks/redact}/detect.ts +0 -0
  63. /package/{extensions/safety → hooks/redact}/entropy.ts +0 -0
  64. /package/{extensions/safety → hooks/redact}/patterns.ts +0 -0
  65. /package/{extensions → tools}/lsp/env.ts +0 -0
  66. /package/{extensions → tools}/lsp/manager.ts +0 -0
  67. /package/{extensions → tools}/lsp/prompt.ts +0 -0
  68. /package/{extensions → tools}/lsp/protocol.ts +0 -0
@@ -1,458 +0,0 @@
1
- /**
2
- * Slash — 所有扩展命令
3
- *
4
- * /dp-model → 模型选择器 (TAB 切换 Image/Compact)
5
- * /dp-settings → 模块开关 (patch / safety / lsp / smart-at)
6
- * /retry → 中断后继续
7
- */
8
-
9
- import type { ExtensionAPI, ExtensionContext, Theme as PiTheme } from "@earendil-works/pi-coding-agent";
10
- import { ModelPickerComponent } from "./model-integration.js";
11
- import { getAllModuleSettings, setModuleEnabled, type ModuleSettings } from "./settings.js";
12
- import { getMcpStatus, refreshServerCache, updateConfigEnabled } from "./mcp/index.js";
13
- import { toggleMcpServerEnabled } from "./mcp/builtin.js";
14
- import { Container, SettingsList, Spacer, Text, type TUI, type SettingsListTheme, type Component, getKeybindings } from "@earendil-works/pi-tui";
15
-
16
- // ─── Border component (matches native DynamicBorder) ────────────────────────
17
-
18
- class DynamicBorder implements Component {
19
- private colorFn: (str: string) => string;
20
-
21
- constructor(theme: PiTheme) {
22
- this.colorFn = (str: string) => theme.fg("border", str);
23
- }
24
-
25
- invalidate() {}
26
-
27
- render(width: number): string[] {
28
- return [this.colorFn("─".repeat(Math.max(1, width)))];
29
- }
30
- }
31
-
32
- // ─── SettingsList Theme (matches native getSettingsListTheme) ───────────────
33
-
34
- function getSettingsListTheme(theme: PiTheme): SettingsListTheme {
35
- return {
36
- label: (text: string, selected: boolean) => selected ? theme.fg("accent", text) : text,
37
- value: (text: string, selected: boolean) => selected ? theme.fg("accent", text) : theme.fg("muted", text),
38
- description: (text: string) => theme.fg("dim", text),
39
- cursor: theme.fg("accent", "→ "),
40
- hint: (text: string) => theme.fg("dim", text),
41
- };
42
- }
43
-
44
- // ─── /dp-model ─────────────────────────────────────────────────────────────
45
-
46
- function setupDpModelCommand(pi: ExtensionAPI) {
47
- pi.registerCommand("dp-model", {
48
- description: "Configure image and compact models",
49
- handler: async (_args, ctx) => {
50
- if (ctx.hasUI) {
51
- await ctx.ui.custom<void>(
52
- (tui, theme, _kb, done) =>
53
- new ModelPickerComponent(tui, theme, ctx.modelRegistry, () => done(undefined))
54
- );
55
- return;
56
- }
57
- ctx.ui.notify("dp-model requires interactive mode.", "warning");
58
- },
59
- });
60
- }
61
-
62
- // ─── /dp-settings ──────────────────────────────────────────────────────────
63
-
64
- const MODULE_LABELS: Record<keyof ModuleSettings, string> = {
65
- patch: "patch",
66
- safety: "Secret Redaction",
67
- lsp: "LSP",
68
- "smart-at": "@ overload",
69
- mcp: "MCP",
70
- wakatime: "WakaTime",
71
- "rtk": "RTK",
72
- };
73
-
74
- const MODULE_DESCS: Record<keyof ModuleSettings, string> = {
75
- patch: "Replace edit/write with patch tool (old_str/new_str replacement + overwrite)",
76
- safety: "Redact secrets from read / bash output before they enter model context",
77
- lsp: "Language server diagnostics, hover, definition, references, symbols, rename",
78
- "smart-at": "Project-aware file search replacing default autocomplete",
79
- mcp: "MCP client for context7 and exa (zero-config)",
80
- wakatime: "Send coding activity heartbeats to WakaTime",
81
- "rtk": "Rewrite bash through system RTK when available",
82
- };
83
-
84
- class ModuleSettingsComponent extends Container {
85
- private settingsList: SettingsList;
86
-
87
- constructor(tui: TUI, theme: PiTheme, onDone: () => void) {
88
- super();
89
- const modules = getAllModuleSettings();
90
- const keys = Object.keys(MODULE_LABELS) as (keyof ModuleSettings)[];
91
-
92
- const items = keys.map(k => ({
93
- id: k,
94
- label: MODULE_LABELS[k],
95
- description: MODULE_DESCS[k],
96
- currentValue: modules[k] ? "on" : "off",
97
- values: ["on", "off"],
98
- }));
99
-
100
- this.addChild(new DynamicBorder(theme));
101
-
102
- this.settingsList = new SettingsList(
103
- items, 10, getSettingsListTheme(theme),
104
- (id: string, newValue: string) => {
105
- setModuleEnabled(id as keyof ModuleSettings, newValue === "on");
106
- tui.requestRender();
107
- },
108
- () => onDone(),
109
- { enableSearch: true },
110
- );
111
-
112
- this.addChild(this.settingsList);
113
- this.addChild(new DynamicBorder(theme));
114
- }
115
-
116
- handleInput(data: string) {
117
- this.settingsList.handleInput(data);
118
- }
119
- }
120
-
121
- function setupDpSettingsCommand(pi: ExtensionAPI) {
122
- pi.registerCommand("dp-settings", {
123
- description: "Toggle decorated-pi modules on/off",
124
- handler: async (_args, ctx) => {
125
- if (ctx.hasUI) {
126
- await ctx.ui.custom<void>(
127
- (tui, theme, _kb, done) =>
128
- new ModuleSettingsComponent(tui, theme, () => done(undefined))
129
- );
130
- ctx.ui.notify("Module settings updated. /reload to apply.", "warning");
131
- return;
132
- }
133
- ctx.ui.notify("dp-settings requires interactive mode.", "warning");
134
- },
135
- });
136
- }
137
-
138
- // ─── /mcp ──────────────────────────────────────────────────────────────────
139
-
140
- class McpStatusComponent extends Container {
141
- private linesComponent: Text;
142
- private hintComponent: Text;
143
- private notifyComponent: Text;
144
- private tui: TUI;
145
- private theme: PiTheme;
146
- private done: () => void;
147
- private registry: any;
148
- private selected = 0;
149
- private servers: ReturnType<typeof getMcpStatus> = [];
150
- private notifyText = "";
151
- private notifyTimer: ReturnType<typeof setTimeout> | null = null;
152
- private refreshing = new Set<string>();
153
- private autoRefreshTimer: ReturnType<typeof setInterval> | null = null;
154
-
155
- private cwd: string;
156
-
157
- constructor(tui: TUI, theme: PiTheme, registry: any, onDone: () => void, cwd: string) {
158
- super();
159
- this.tui = tui;
160
- this.theme = theme;
161
- this.done = onDone;
162
- this.registry = registry;
163
- this.cwd = cwd;
164
-
165
- this.addChild(new DynamicBorder(theme));
166
- this.addChild(new Spacer(1));
167
-
168
- this.linesComponent = new Text("", 1, 0);
169
- this.addChild(this.linesComponent);
170
-
171
- this.addChild(new Spacer(1));
172
-
173
- this.notifyComponent = new Text("", 1, 0);
174
- this.addChild(this.notifyComponent);
175
-
176
- this.hintComponent = new Text("", 1, 0);
177
- this.addChild(this.hintComponent);
178
-
179
- this.addChild(new Spacer(1));
180
- this.addChild(new DynamicBorder(theme));
181
-
182
- this.updateServers();
183
- this.renderView();
184
-
185
- // Auto-refresh while any server is still connecting
186
- this.autoRefreshTimer = setInterval(() => {
187
- this.updateServers();
188
- this.renderView();
189
- const allSettled = this.servers.every((s) => s.state !== "connecting");
190
- if (allSettled && this.autoRefreshTimer) {
191
- clearInterval(this.autoRefreshTimer);
192
- this.autoRefreshTimer = null;
193
- }
194
- }, 500);
195
- }
196
-
197
- private updateServers() {
198
- this.servers = getMcpStatus();
199
- if (this.selected >= this.servers.length) {
200
- this.selected = Math.max(0, this.servers.length - 1);
201
- }
202
- }
203
-
204
- private renderView() {
205
- if (this.servers.length === 0) {
206
- this.linesComponent.setText("No MCP servers configured.");
207
- this.hintComponent.setText(this.theme.fg("dim", "q close"));
208
- this.tui.requestRender();
209
- return;
210
- }
211
-
212
- const lines: string[] = [`MCP servers (${this.servers.length}):`, ""];
213
-
214
- const namePad = Math.max(...this.servers.map((s) => s.name.length), 12);
215
-
216
- for (let i = 0; i < this.servers.length; i++) {
217
- const s = this.servers[i];
218
- const isSelected = i === this.selected;
219
- const isDisabled = s.state === "disabled";
220
- const cursor = isSelected ? this.theme.fg("accent", "→ ") : " ";
221
-
222
- let statusIcon: string;
223
- let statusColor: (s: string) => string;
224
- if (s.state === "connected") {
225
- statusIcon = "🟢";
226
- statusColor = (str: string) => this.theme.fg("accent", str);
227
- } else if (s.state === "connecting") {
228
- statusIcon = "🟡";
229
- statusColor = (str: string) => this.theme.fg("warning", str);
230
- } else if (s.state === "disabled") {
231
- statusIcon = "⚪";
232
- statusColor = (str: string) => this.theme.fg("dim", str);
233
- } else {
234
- statusIcon = "🔴";
235
- statusColor = (str: string) => this.theme.fg("error", str);
236
- }
237
-
238
- const name = isDisabled
239
- ? this.theme.fg("dim", s.name)
240
- : isSelected
241
- ? this.theme.fg("accent", s.name)
242
- : s.name;
243
- const namePadding = " ".repeat(Math.max(0, namePad - s.name.length));
244
- const desc = s.description ? ` — ${s.description.slice(0, 50)}` : "";
245
- const descDim = isDisabled ? this.theme.fg("dim", desc) : desc;
246
- lines.push(
247
- `${cursor}${name}${namePadding} ${statusIcon} ${statusColor(s.state)}${descDim}`
248
- );
249
-
250
- if (isSelected) {
251
- lines.push(` ${this.theme.fg("dim", s.url)}`);
252
- if (s.error) {
253
- lines.push(` ${this.theme.fg("error", `Error: ${s.error}`)}`);
254
- }
255
- if (s.tools.length > 0) {
256
- lines.push(` ${s.toolCount} tool${s.toolCount === 1 ? "" : "s"}:`);
257
- for (const tool of s.tools.slice(0, 6)) {
258
- const flat = (tool.description ?? "").replace(/\s+/g, " ").trim();
259
- const td = flat
260
- ? flat.length > 55
261
- ? ` — ${flat.slice(0, 55)}…`
262
- : ` — ${flat}`
263
- : "";
264
- lines.push(` ${tool.name}${td}`);
265
- }
266
- if (s.tools.length > 6) {
267
- lines.push(` ... and ${s.tools.length - 6} more`);
268
- }
269
- }
270
- lines.push("");
271
- }
272
- }
273
-
274
- this.linesComponent.setText(lines.join("\n"));
275
-
276
- const s = this.servers[this.selected];
277
- const toggleHint = s?.state === "disabled" ? "space enable" : "space disable";
278
- const hintParts = ["↑↓ navigate", toggleHint, "r refresh", "q close"];
279
- this.hintComponent.setText(this.theme.fg("dim", hintParts.join(" | ")));
280
- this.notifyComponent.setText(
281
- this.notifyText ? this.theme.fg("warning", this.notifyText) : ""
282
- );
283
-
284
- this.tui.requestRender();
285
- }
286
-
287
- private showNotify(text: string) {
288
- this.notifyText = text;
289
- this.renderView();
290
- if (this.notifyTimer) clearTimeout(this.notifyTimer);
291
- this.notifyTimer = setTimeout(() => {
292
- this.notifyText = "";
293
- this.renderView();
294
- }, 3000);
295
- }
296
-
297
- private clearNotify() {
298
- if (this.notifyTimer) {
299
- clearTimeout(this.notifyTimer);
300
- this.notifyTimer = null;
301
- }
302
- this.notifyText = "";
303
- }
304
-
305
- handleInput(data: string) {
306
- const kb = getKeybindings();
307
-
308
- // Navigation
309
- if (kb.matches(data, "tui.select.up")) {
310
- this.selected = Math.max(0, this.selected - 1);
311
- this.clearNotify();
312
- this.renderView();
313
- return;
314
- }
315
- if (kb.matches(data, "tui.select.down")) {
316
- this.selected = Math.min(this.servers.length - 1, this.selected + 1);
317
- this.clearNotify();
318
- this.renderView();
319
- return;
320
- }
321
-
322
- // Quit
323
- if (
324
- data === "q" ||
325
- data === "\r" ||
326
- data === "\n" ||
327
- kb.matches(data, "tui.select.cancel")
328
- ) {
329
- if (this.autoRefreshTimer) { clearInterval(this.autoRefreshTimer); this.autoRefreshTimer = null; }
330
- if (this.notifyTimer) { clearTimeout(this.notifyTimer); this.notifyTimer = null; }
331
- this.done();
332
- return;
333
- }
334
-
335
- if (this.servers.length === 0) return;
336
- const s = this.servers[this.selected];
337
-
338
- // Toggle enable/disable
339
- if (data === " ") {
340
- const scope = s.source === "project" ? "project" : "global";
341
- const newEnabled = s.state === "disabled";
342
- const ok = toggleMcpServerEnabled(s.name, newEnabled, scope, this.cwd || undefined);
343
- if (ok) {
344
- updateConfigEnabled(s.name, newEnabled);
345
- }
346
- this.showNotify(
347
- ok
348
- ? `${newEnabled ? "Enabled" : "Disabled"} "${s.name}". Use /reload to apply.`
349
- : `Failed to toggle "${s.name}".`
350
- );
351
- this.updateServers();
352
- this.renderView();
353
- return;
354
- }
355
-
356
- // Refresh cache (reconnect + update)
357
- if (data === "r" || data === "r") {
358
- if (this.refreshing.has(s.name)) return;
359
- this.refreshing.add(s.name);
360
- const targetName = s.name;
361
- const targetIndex = this.selected;
362
- this.showNotify(`Refreshing "${targetName}"...`);
363
- (async () => {
364
- const result = await refreshServerCache(targetName, this.registry);
365
- this.refreshing.delete(targetName);
366
- // Only update UI if user hasn't navigated away
367
- if (this.selected === targetIndex) {
368
- this.updateServers();
369
- this.renderView();
370
- this.showNotify(result.ok ? `Refreshed "${targetName}".` : `Refresh failed: ${result.error}`);
371
- }
372
- })();
373
- return;
374
- }
375
-
376
-
377
- }
378
-
379
- dispose() {
380
- if (this.autoRefreshTimer) { clearInterval(this.autoRefreshTimer); this.autoRefreshTimer = null; }
381
- if (this.notifyTimer) { clearTimeout(this.notifyTimer); this.notifyTimer = null; }
382
- }
383
- }
384
-
385
- function setupMcpCommand(pi: ExtensionAPI) {
386
- pi.registerCommand("mcp", {
387
- description: "Show active MCP servers and their tools",
388
- handler: async (_args, ctx) => {
389
- if (ctx.hasUI) {
390
- await ctx.ui.custom<void>(
391
- (tui, theme, _kb, done) => new McpStatusComponent(tui, theme, ctx.modelRegistry, () => done(undefined), ctx.cwd)
392
- );
393
- return;
394
- }
395
-
396
- // Fallback for non-interactive (print / RPC) mode.
397
- const servers = getMcpStatus();
398
- if (servers.length === 0) {
399
- ctx.ui.notify("No MCP servers configured.", "info");
400
- return;
401
- }
402
-
403
- const lines: string[] = [`MCP servers (${servers.length}):`, ""];
404
- for (const s of servers) {
405
- lines.push(`• ${s.name} (${s.source})`);
406
- lines.push(` URL: ${s.url}`);
407
- if (s.state === "connecting") {
408
- lines.push(` Status: connecting...`);
409
- } else if (s.state === "failed") {
410
- lines.push(` Status: failed — ${s.error ?? "unknown error"}`);
411
- } else {
412
- lines.push(` Tools: ${s.toolCount}`);
413
- for (const tool of s.tools) {
414
- const desc = tool.description ? ` — ${tool.description.slice(0, 60)}` : "";
415
- lines.push(` - ${tool.name}${desc}`);
416
- }
417
- }
418
- lines.push("");
419
- }
420
-
421
- pi.sendMessage({ customType: "mcp-status", content: lines.join("\n"), display: true }, { triggerTurn: false });
422
- },
423
- });
424
- }
425
-
426
- // ─── /retry ────────────────────────────────────────────────────────────────
427
-
428
- function setupRetryCommand(pi: ExtensionAPI) {
429
- let retryInProgress = false;
430
-
431
- pi.registerCommand("retry", {
432
- description: "Continue after interruption",
433
- handler: async (_args, ctx) => {
434
- if (retryInProgress) {
435
- ctx.ui.notify("Retry is already in progress", "warning");
436
- return;
437
- }
438
- if (!ctx.isIdle()) ctx.abort();
439
-
440
- retryInProgress = true;
441
- pi.sendMessage(
442
- { customType: "retry-trigger", content: "Continue.", display: false },
443
- { triggerTurn: true }
444
- );
445
- },
446
- });
447
-
448
- pi.on("agent_start", () => { retryInProgress = false; });
449
- }
450
-
451
- // ─── 入口 ───────────────────────────────────────────────────────────────────
452
-
453
- export function setupSlash(pi: ExtensionAPI) {
454
- setupDpModelCommand(pi);
455
- setupDpSettingsCommand(pi);
456
- setupMcpCommand(pi);
457
- setupRetryCommand(pi);
458
- }