decorated-pi 0.5.5 → 0.7.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 (70) hide show
  1. package/README.md +69 -71
  2. package/commands/dp-model.ts +23 -0
  3. package/commands/dp-settings.ts +28 -0
  4. package/commands/mcp-status.ts +62 -0
  5. package/commands/retry.ts +19 -0
  6. package/commands/usage.ts +544 -0
  7. package/hooks/compaction.ts +204 -0
  8. package/hooks/externalize.ts +70 -0
  9. package/hooks/image-vision.ts +132 -0
  10. package/hooks/inject-agents-md.ts +164 -0
  11. package/hooks/mcp.ts +392 -0
  12. package/hooks/normalize-codeblocks.ts +88 -0
  13. package/hooks/pi-tool-filter.ts +28 -0
  14. package/{extensions/safety → hooks/redact}/types.ts +1 -8
  15. package/hooks/redact.ts +104 -0
  16. package/{extensions → hooks}/rtk.ts +92 -115
  17. package/hooks/session-title.ts +78 -0
  18. package/hooks/skeleton.ts +212 -0
  19. package/hooks/smart-at.ts +318 -0
  20. package/{extensions/file-times.ts → hooks/track-mtime.ts} +40 -38
  21. package/{extensions → hooks}/wakatime.ts +120 -122
  22. package/index.ts +166 -1
  23. package/package.json +8 -6
  24. package/settings.ts +341 -0
  25. package/tools/ask/index.ts +93 -0
  26. package/{extensions → tools}/lsp/client.ts +0 -25
  27. package/{extensions → tools}/lsp/format.ts +2 -99
  28. package/{extensions → tools}/lsp/index.ts +1 -1
  29. package/{extensions → tools}/lsp/servers.ts +1 -1
  30. package/{extensions → tools}/lsp/tools.ts +1 -66
  31. package/{extensions → tools}/lsp/types.ts +0 -11
  32. package/tools/mcp/builtin/codegraph.ts +21 -0
  33. package/tools/mcp/builtin/context7.ts +10 -0
  34. package/tools/mcp/builtin/exa.ts +10 -0
  35. package/tools/mcp/builtin/index.ts +19 -0
  36. package/tools/mcp/cache.ts +124 -0
  37. package/{extensions → tools}/mcp/client.ts +2 -2
  38. package/tools/mcp/config.ts +288 -0
  39. package/tools/mcp/index.ts +44 -0
  40. package/tools/mcp/tool-definition.ts +112 -0
  41. package/{extensions/patch.ts → tools/patch/core.ts} +135 -43
  42. package/{extensions/io.ts → tools/patch/index.ts} +24 -206
  43. package/tsconfig.json +10 -1
  44. package/ui/ask.ts +443 -0
  45. package/ui/mcp-status.ts +202 -0
  46. package/ui/model-picker.ts +162 -0
  47. package/ui/module-settings.ts +188 -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/builtin.ts +0 -376
  52. package/extensions/mcp/index.ts +0 -435
  53. package/extensions/model-integration.ts +0 -531
  54. package/extensions/providers/ark-coding.ts +0 -75
  55. package/extensions/providers/index.ts +0 -9
  56. package/extensions/providers/ollama-cloud.ts +0 -101
  57. package/extensions/providers/qianfan-coding.ts +0 -71
  58. package/extensions/safety/index.ts +0 -102
  59. package/extensions/session-title.ts +0 -65
  60. package/extensions/settings.ts +0 -170
  61. package/extensions/slash.ts +0 -458
  62. package/extensions/smart-at.ts +0 -481
  63. package/extensions/subdir-agents.ts +0 -151
  64. /package/{extensions/safety → hooks/redact}/detect.ts +0 -0
  65. /package/{extensions/safety → hooks/redact}/entropy.ts +0 -0
  66. /package/{extensions/safety → hooks/redact}/patterns.ts +0 -0
  67. /package/{extensions → tools}/lsp/env.ts +0 -0
  68. /package/{extensions → tools}/lsp/manager.ts +0 -0
  69. /package/{extensions → tools}/lsp/prompt.ts +0 -0
  70. /package/{extensions → tools}/lsp/protocol.ts +0 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Model Picker — used by /dp-model command.
3
+ * Tab between Image and Compact model selection.
4
+ */
5
+
6
+ import { DynamicBorder, keyHint, rawKeyHint, type Theme } from "@earendil-works/pi-coding-agent";
7
+ import { Container, fuzzyFilter, getKeybindings, Input, Spacer, Text, type TUI } from "@earendil-works/pi-tui";
8
+ import type { Model } from "@earendil-works/pi-ai";
9
+ import { getImageModelKey, getCompactModelKey, setImageModelKey, setCompactModelKey, formatModelKey, parseModelKey } from "../settings.js";
10
+
11
+ const TAB_IMAGE = 0;
12
+ const TAB_COMPACT = 1;
13
+
14
+ export class ModelPickerComponent extends Container {
15
+ private searchInput: Input;
16
+ private tui: TUI;
17
+ private theme: Theme;
18
+ private registry: any;
19
+ private onDone: () => void;
20
+ private activeTab = TAB_IMAGE;
21
+ private imageKey: string | null;
22
+ private compactKey: string | null;
23
+ private allItems: { label: string; desc: string; model: Model<any> | null; modelName?: string }[] = [];
24
+ private filtered: typeof this.allItems = [];
25
+ private selectedIndex = 0;
26
+ private tabTitle = new Text("", 1, 0);
27
+ private subtitleText: Text;
28
+ private listContainer: Container;
29
+
30
+ constructor(tui: TUI, theme: unknown, registry: any, onDone: () => void) {
31
+ super();
32
+ this.tui = tui;
33
+ this.theme = theme as Theme;
34
+ this.registry = registry;
35
+ this.onDone = onDone;
36
+ this.imageKey = getImageModelKey();
37
+ this.compactKey = getCompactModelKey();
38
+
39
+ this.addChild(new DynamicBorder());
40
+ this.addChild(new Spacer(1));
41
+ this.addChild(this.tabTitle);
42
+ this.subtitleText = new Text("", 1, 0);
43
+ this.addChild(this.subtitleText);
44
+ this.addChild(new Spacer(1));
45
+
46
+ this.searchInput = new Input();
47
+ this.searchInput.onSubmit = () => { const s = this.filtered[this.selectedIndex]; if (s) this.selectModel(s.model); };
48
+ this.addChild(this.searchInput);
49
+ this.addChild(new Spacer(1));
50
+
51
+ this.listContainer = new Container();
52
+ this.addChild(this.listContainer);
53
+ this.addChild(new Spacer(1));
54
+
55
+ this.addChild(new Text(
56
+ rawKeyHint("↑↓", "navigate") + " " + keyHint("tui.input.tab", "switch") + " " +
57
+ keyHint("tui.select.confirm", "select") + " " + keyHint("tui.select.cancel", "cancel"), 1, 0));
58
+ this.addChild(new Spacer(1));
59
+ this.addChild(new DynamicBorder());
60
+
61
+ this.loadModels().then(() => { this.switchTab(TAB_IMAGE); this.tui.requestRender(); });
62
+ }
63
+
64
+ private async loadModels() {
65
+ this.registry.refresh();
66
+ const available = this.registry.getAvailable() as Model<any>[];
67
+ this.allItems = [{ label: "clear", desc: "(unset)", model: null }];
68
+ for (const m of available) {
69
+ this.allItems.push({ label: m.id, desc: `[${m.provider}]`, model: m as Model<any>, modelName: m.name });
70
+ }
71
+ }
72
+
73
+ private currentKey() { return this.activeTab === TAB_IMAGE ? this.imageKey : this.compactKey; }
74
+ private currentKind() { return this.activeTab === TAB_IMAGE ? "image" : "compact"; }
75
+
76
+ private switchTab(tab: number) {
77
+ this.activeTab = tab;
78
+ const key = this.currentKey();
79
+ const [clearItem, ...rest] = this.allItems;
80
+ const items = rest.map(it => {
81
+ const isCurrent = it.model && formatModelKey(it.model) === key;
82
+ return { ...it, desc: `${it.desc}${isCurrent ? " ✓" : ""}` };
83
+ });
84
+ items.sort((a, b) => {
85
+ const aCur = a.model && formatModelKey(a.model) === key;
86
+ const bCur = b.model && formatModelKey(b.model) === key;
87
+ if (aCur && !bCur) return -1; if (!aCur && bCur) return 1; return 0;
88
+ });
89
+ this.filtered = [clearItem, ...items];
90
+ this.selectedIndex = 0;
91
+ if (key) { const ix = this.filtered.findIndex(m => m.model && formatModelKey(m.model) === key); if (ix >= 0) this.selectedIndex = ix; }
92
+ this.searchInput.setValue("");
93
+ this.updateHeader();
94
+ this.updateList();
95
+ }
96
+
97
+ private updateHeader() {
98
+ const t = this.theme;
99
+ const im = this.activeTab === TAB_IMAGE ? t.fg("accent", "●") : "○";
100
+ const cm = this.activeTab === TAB_COMPACT ? t.fg("accent", "●") : "○";
101
+ const il = this.activeTab === TAB_IMAGE ? t.bold("Image") : t.fg("dim", "Image");
102
+ const cl = this.activeTab === TAB_COMPACT ? t.bold("Compact") : t.fg("dim", "Compact");
103
+ this.tabTitle.setText(`${im} ${il} | ${cm} ${cl}`);
104
+ const key = this.currentKey();
105
+ this.subtitleText.setText(key ? t.fg("warning", `Current ${this.currentKind()} model: ${key}`) : t.fg("warning", `No ${this.currentKind()} model set`));
106
+ }
107
+
108
+ handleInput(keyData: string) {
109
+ const kb = getKeybindings();
110
+ if (kb.matches(keyData, "tui.input.tab")) {
111
+ const next = this.activeTab === TAB_IMAGE ? TAB_COMPACT : TAB_IMAGE;
112
+ this.switchTab(next); this.tui.requestRender(); return;
113
+ }
114
+ if (kb.matches(keyData, "tui.select.up")) { this.selectedIndex = this.selectedIndex === 0 ? this.filtered.length - 1 : this.selectedIndex - 1; this.updateList(); return; }
115
+ if (kb.matches(keyData, "tui.select.down")) { this.selectedIndex = this.selectedIndex === this.filtered.length - 1 ? 0 : this.selectedIndex + 1; this.updateList(); return; }
116
+ if (kb.matches(keyData, "tui.select.confirm")) { const s = this.filtered[this.selectedIndex]; if (s) this.selectModel(s.model); return; }
117
+ if (kb.matches(keyData, "tui.select.cancel")) { this.onDone(); return; }
118
+ this.searchInput.handleInput(keyData); this.applyFilter();
119
+ }
120
+
121
+ private applyFilter() {
122
+ const raw = this.searchInput.getValue();
123
+ if (!raw) { this.switchTab(this.activeTab); return; }
124
+ const [clearItem, ...rest] = this.filtered;
125
+ this.filtered = [clearItem, ...fuzzyFilter(rest, raw, ({ label, desc }) => `${label} ${desc}`)];
126
+ this.selectedIndex = 0; this.updateList();
127
+ }
128
+
129
+ private selectModel(model: Model<any> | null) {
130
+ const kind = this.currentKind();
131
+ if (model) {
132
+ if (kind === "image") setImageModelKey(formatModelKey(model));
133
+ else setCompactModelKey(formatModelKey(model));
134
+ } else {
135
+ if (kind === "image") setImageModelKey(null);
136
+ else setCompactModelKey(null);
137
+ }
138
+ if (kind === "image") this.imageKey = model ? formatModelKey(model) : null;
139
+ else this.compactKey = model ? formatModelKey(model) : null;
140
+ this.switchTab(this.activeTab); this.tui.requestRender();
141
+ }
142
+
143
+ private updateList() {
144
+ this.listContainer.clear();
145
+ const t = this.theme;
146
+ const mv = 10;
147
+ const start = Math.max(0, Math.min(this.selectedIndex - Math.floor(mv / 2), Math.max(0, this.filtered.length - mv)));
148
+ const end = Math.min(start + mv, this.filtered.length);
149
+ for (let i = start; i < end; i++) {
150
+ const item = this.filtered[i]; if (!item) continue;
151
+ const isClear = item.model === null;
152
+ const isSel = i === this.selectedIndex;
153
+ const line = isClear
154
+ ? (isSel ? t.fg("accent", "→ ") + t.fg("error", "clear") + t.fg("muted", " (unset)") : " " + t.fg("muted", "clear (unset)"))
155
+ : (isSel ? t.fg("accent", "→ ") + t.fg("accent", item.label) + " " + t.fg("muted", item.desc) : " " + item.label + " " + t.fg("muted", item.desc));
156
+ this.listContainer.addChild(new Text(line, 0, 0));
157
+ }
158
+ if (start > 0 || end < this.filtered.length) this.listContainer.addChild(new Text(t.fg("muted", ` (${this.selectedIndex + 1}/${this.filtered.length})`), 0, 0));
159
+ const sel = this.filtered[this.selectedIndex];
160
+ if (sel?.modelName) { this.listContainer.addChild(new Spacer(1)); this.listContainer.addChild(new Text(t.fg("muted", ` Name: ${sel.modelName}`), 0, 0)); }
161
+ }
162
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Module Settings UI — used by /dp-settings command.
3
+ *
4
+ * Modules are grouped into three categories. The main view shows one
5
+ * row per category; Enter opens a submenu listing the modules in that
6
+ * category so the user can toggle each one.
7
+ */
8
+
9
+ import type { Theme as PiTheme } from "@earendil-works/pi-coding-agent";
10
+ import { Container, SettingsList, type TUI, type SettingsListTheme, type SettingItem, type Component } from "@earendil-works/pi-tui";
11
+ import { getAllModuleSettings, setModuleEnabled, type ModuleSettings } from "../settings.js";
12
+
13
+ type ModuleName =
14
+ | "patchOverrideEdit"
15
+ | "secretRedaction"
16
+ | "lsp"
17
+ | "atOverride"
18
+ | "mcp"
19
+ | "wakatime"
20
+ | "rtk"
21
+ | "ask"
22
+ | "retry"
23
+ | "usage";
24
+
25
+ const MODULE_LABELS: Record<ModuleName, string> = {
26
+ patchOverrideEdit: "patchOverrideEdit",
27
+ secretRedaction: "secretRedaction",
28
+ lsp: "LSP",
29
+ atOverride: "@ overload",
30
+ mcp: "MCP",
31
+ wakatime: "WakaTime",
32
+ "rtk": "RTK",
33
+ ask: "Ask",
34
+ retry: "Retry",
35
+ usage: "Usage",
36
+ };
37
+
38
+ const MODULE_DESCS: Record<ModuleName, string> = {
39
+ patchOverrideEdit: "Replace Pi native edit/write with patch tool (targeted string replacement)",
40
+ secretRedaction: "Redact secrets from read / bash output before they enter model context",
41
+ lsp: "Language server diagnostics, hover, definition, references, symbols, rename",
42
+ atOverride: "Project-aware file search replacing default autocomplete",
43
+ mcp: "MCP client with builtin servers (context7, exa, codegraph)",
44
+ wakatime: "Send coding activity heartbeats to WakaTime",
45
+ "rtk": "Rewrite bash through system RTK when available",
46
+ ask: "Interactive ask tool for user clarification (blocks loop until answered)",
47
+ retry: "/retry command to continue after interruption",
48
+ usage: "/usage command for token stats",
49
+ };
50
+
51
+ type CategoryId = "tools" | "hooks" | "commands";
52
+
53
+ interface CategoryDef {
54
+ label: string;
55
+ description: string;
56
+ modules: ModuleName[];
57
+ }
58
+
59
+ const CATEGORIES: Record<CategoryId, CategoryDef> = {
60
+ tools: {
61
+ label: "Tools",
62
+ description: "LLM-callable tools",
63
+ modules: ["patchOverrideEdit", "ask", "lsp", "mcp"],
64
+ },
65
+ hooks: {
66
+ label: "Hooks",
67
+ description: "Agent-loop event handlers",
68
+ modules: ["secretRedaction", "rtk", "wakatime"],
69
+ },
70
+ commands: {
71
+ label: "Commands",
72
+ description: "Slash commands",
73
+ modules: ["atOverride", "retry", "usage"],
74
+ },
75
+ };
76
+
77
+ class DynamicBorder implements Component {
78
+ private colorFn: (str: string) => string;
79
+ constructor(theme: PiTheme) { this.colorFn = (str: string) => theme.fg("border", str); }
80
+ invalidate() {}
81
+ render(width: number): string[] { return [this.colorFn("─".repeat(Math.max(1, width)))]; }
82
+ }
83
+
84
+ function getSettingsListTheme(theme: PiTheme): SettingsListTheme {
85
+ return {
86
+ label: (text: string, selected: boolean) => selected ? theme.fg("accent", text) : text,
87
+ value: (text: string, selected: boolean) => selected ? theme.fg("accent", text) : theme.fg("muted", text),
88
+ description: (text: string) => theme.fg("dim", text),
89
+ cursor: theme.fg("accent", "→ "),
90
+ hint: (text: string) => theme.fg("dim", text),
91
+ };
92
+ }
93
+
94
+ const MODULE_TO_CATEGORY: Record<ModuleName, CategoryId> = {
95
+ patchOverrideEdit: "tools",
96
+ ask: "tools",
97
+ lsp: "tools",
98
+ mcp: "tools",
99
+ secretRedaction: "hooks",
100
+ "rtk": "hooks",
101
+ wakatime: "hooks",
102
+ atOverride: "commands",
103
+ retry: "commands",
104
+ usage: "commands",
105
+ };
106
+
107
+ function summaryFor(modules: Required<ModuleSettings>, ids: ModuleName[]): string {
108
+ const onCount = ids.reduce((sum, id) => {
109
+ const cat = MODULE_TO_CATEGORY[id];
110
+ return sum + ((modules[cat] as Record<string, boolean>)[id] ? 1 : 0);
111
+ }, 0);
112
+ return `${onCount}/${ids.length} on`;
113
+ }
114
+
115
+ class CategorySubmenu extends Container {
116
+ private list: SettingsList;
117
+
118
+ constructor(categoryId: CategoryId, theme: PiTheme, done: (summary?: string) => void) {
119
+ super();
120
+ const modules = getAllModuleSettings();
121
+ const category = CATEGORIES[categoryId];
122
+
123
+ const items: SettingItem[] = category.modules.map((id) => ({
124
+ id,
125
+ label: MODULE_LABELS[id],
126
+ description: MODULE_DESCS[id],
127
+ currentValue: (modules[MODULE_TO_CATEGORY[id]] as Record<string, boolean>)[id] ? "on" : "off",
128
+ values: ["on", "off"],
129
+ }));
130
+
131
+ this.list = new SettingsList(
132
+ items,
133
+ 10,
134
+ getSettingsListTheme(theme),
135
+ (id: string, newValue: string) => {
136
+ setModuleEnabled(id, newValue === "on");
137
+ this.list.updateValue(id, newValue);
138
+ // Stay open so the user can toggle multiple items; the parent
139
+ // summary updates when the submenu is closed with Esc.
140
+ },
141
+ () => done(summaryFor(getAllModuleSettings(), category.modules)),
142
+ );
143
+ this.addChild(this.list);
144
+ }
145
+
146
+ handleInput(data: string) {
147
+ this.list.handleInput(data);
148
+ }
149
+
150
+ render(width: number): string[] {
151
+ return this.list.render(width);
152
+ }
153
+ }
154
+
155
+ export class ModuleSettingsComponent extends Container {
156
+ private settingsList: SettingsList;
157
+
158
+ constructor(tui: TUI, theme: PiTheme, onDone: () => void) {
159
+ super();
160
+ const modules = getAllModuleSettings();
161
+
162
+ const categoryItems: SettingItem[] = (Object.keys(CATEGORIES) as CategoryId[]).map((id) => ({
163
+ id,
164
+ label: CATEGORIES[id].label,
165
+ description: CATEGORIES[id].description,
166
+ currentValue: summaryFor(modules, CATEGORIES[id].modules),
167
+ submenu: (_currentValue, done) => new CategorySubmenu(id, theme, done),
168
+ }));
169
+
170
+ this.addChild(new DynamicBorder(theme));
171
+
172
+ this.settingsList = new SettingsList(
173
+ categoryItems,
174
+ 10,
175
+ getSettingsListTheme(theme),
176
+ () => {},
177
+ () => onDone(),
178
+ { enableSearch: true },
179
+ );
180
+
181
+ this.addChild(this.settingsList);
182
+ this.addChild(new DynamicBorder(theme));
183
+ }
184
+
185
+ handleInput(data: string) {
186
+ this.settingsList.handleInput(data);
187
+ }
188
+ }