@nmzpy/pi-ember-stack 0.1.5 → 0.1.6
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
|
@@ -39,7 +39,7 @@ the TUI. Restart pi after changing the list. The available plugins are:
|
|
|
39
39
|
The Ember repository contains a project-local `.pi/settings.json` entry for:
|
|
40
40
|
|
|
41
41
|
```json
|
|
42
|
-
"npm:@nmzpy/pi-ember-stack@0.1.
|
|
42
|
+
"npm:@nmzpy/pi-ember-stack@0.1.6"
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
On a new clone, start pi from the project directory. Pi will ask for a
|
|
@@ -69,9 +69,9 @@ of this repository.
|
|
|
69
69
|
## Development
|
|
70
70
|
|
|
71
71
|
The package entrypoint is `plugins/index.ts`. Compact tools are under
|
|
72
|
-
`plugins/pi-compact-tools/`, while primary modes, plans,
|
|
73
|
-
agents are under `plugins/pi-custom-agents/`. Devin auth
|
|
74
|
-
`plugins/devin-auth/`.
|
|
72
|
+
`plugins/pi-compact-tools/`, while questionnaire, primary modes, plans,
|
|
73
|
+
subagents, and bundled agents are under `plugins/pi-custom-agents/`. Devin auth
|
|
74
|
+
is under `plugins/devin-auth/`.
|
|
75
75
|
|
|
76
76
|
Run the package typecheck with:
|
|
77
77
|
|
package/package.json
CHANGED
package/plugins/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ const PLUGINS: readonly StackPlugin[] = [
|
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
id: "pi-custom-agents",
|
|
35
|
-
description: "
|
|
35
|
+
description: "Questionnaire, primary modes, plans, subagents, and bundled agent definitions",
|
|
36
36
|
extension: piCustomAgentsPlugin,
|
|
37
37
|
},
|
|
38
38
|
{
|
|
@@ -1,60 +1,195 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
createBashTool,
|
|
5
|
+
createEditTool,
|
|
6
|
+
createFindTool,
|
|
7
|
+
createGrepTool,
|
|
8
|
+
createLsTool,
|
|
9
|
+
createReadTool,
|
|
10
|
+
createWriteTool,
|
|
11
|
+
type ExtensionAPI,
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { Text, type Component } from "@earendil-works/pi-tui";
|
|
5
14
|
|
|
6
15
|
const SOURCE_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const BULLET = "• ";
|
|
17
|
+
const DISCOVERY_TOOLS = new Set(["read", "grep", "find", "ls"]);
|
|
7
18
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
type ToolFactory = (cwd: string) => any;
|
|
20
|
+
type ToolRenderContext = {
|
|
21
|
+
args: any;
|
|
22
|
+
toolCallId: string;
|
|
23
|
+
invalidate: () => void;
|
|
24
|
+
};
|
|
25
|
+
type ToolRenderResultOptions = {
|
|
26
|
+
isPartial: boolean;
|
|
27
|
+
};
|
|
28
|
+
type CompactCall = {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
args: any;
|
|
32
|
+
group?: DiscoveryGroup;
|
|
33
|
+
invalidate?: () => void;
|
|
34
|
+
isError: boolean;
|
|
35
|
+
};
|
|
36
|
+
type DiscoveryGroup = {
|
|
37
|
+
records: CompactCall[];
|
|
38
|
+
};
|
|
16
39
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
onUpdate: any,
|
|
22
|
-
ctx: any,
|
|
23
|
-
) {
|
|
24
|
-
return createEditTool(ctx.cwd).execute(
|
|
25
|
-
toolCallId,
|
|
26
|
-
params,
|
|
27
|
-
signal,
|
|
28
|
-
onUpdate,
|
|
29
|
-
);
|
|
30
|
-
},
|
|
40
|
+
function textValue(value: unknown, fallback = ""): string {
|
|
41
|
+
if (value === undefined || value === null) return fallback;
|
|
42
|
+
return String(value).replace(/[\r\n]+/g, " ");
|
|
43
|
+
}
|
|
31
44
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
function toolPath(args: any): string {
|
|
46
|
+
return textValue(args?.file_path ?? args?.path, ".");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function errorText(result: any, isError: boolean): string | undefined {
|
|
50
|
+
const content = result?.content?.find((item: any) => item.type === "text");
|
|
51
|
+
if (!isError && !content?.text?.startsWith("Error")) return undefined;
|
|
52
|
+
return textValue(content?.text, "Tool failed").split("\n")[0];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function diffStats(result: any): { additions: number; removals: number } {
|
|
56
|
+
const diff = typeof result?.details?.diff === "string" ? result.details.diff : "";
|
|
57
|
+
let additions = 0;
|
|
58
|
+
let removals = 0;
|
|
59
|
+
for (const line of diff.split("\n")) {
|
|
60
|
+
if (line.startsWith("+") && !line.startsWith("+++")) additions++;
|
|
61
|
+
if (line.startsWith("-") && !line.startsWith("---")) removals++;
|
|
62
|
+
}
|
|
63
|
+
return { additions, removals };
|
|
64
|
+
}
|
|
41
65
|
|
|
42
|
-
|
|
43
|
-
|
|
66
|
+
function formatCallBody(name: string, args: any, theme: any): string {
|
|
67
|
+
const pathName = toolPath(args);
|
|
68
|
+
switch (name) {
|
|
69
|
+
case "read":
|
|
70
|
+
return theme.fg("toolTitle", theme.bold("Read")) +
|
|
71
|
+
theme.fg("accent", ` ${pathName}`);
|
|
72
|
+
case "grep":
|
|
73
|
+
return theme.fg("toolTitle", theme.bold("Search")) +
|
|
74
|
+
theme.fg("accent", ` ${textValue(args?.pattern)}`) +
|
|
75
|
+
theme.fg("toolOutput", ` in ${pathName}`);
|
|
76
|
+
case "find":
|
|
77
|
+
return theme.fg("toolTitle", theme.bold("Find")) +
|
|
78
|
+
theme.fg("accent", ` ${textValue(args?.pattern)}`) +
|
|
79
|
+
theme.fg("toolOutput", ` in ${pathName}`);
|
|
80
|
+
case "ls":
|
|
81
|
+
return theme.fg("toolTitle", theme.bold("List")) +
|
|
82
|
+
theme.fg("accent", ` ${pathName}`);
|
|
83
|
+
case "bash":
|
|
84
|
+
return theme.fg("toolTitle", theme.bold("Run")) +
|
|
85
|
+
theme.fg("accent", ` $ ${textValue(args?.command)}`);
|
|
86
|
+
case "edit":
|
|
87
|
+
return theme.fg("toolTitle", theme.bold("edit")) +
|
|
88
|
+
theme.fg("accent", ` ${pathName}`);
|
|
89
|
+
case "write":
|
|
90
|
+
return theme.fg("toolTitle", theme.bold("write")) +
|
|
91
|
+
theme.fg("accent", ` ${pathName}`);
|
|
92
|
+
default:
|
|
93
|
+
return theme.fg("toolTitle", theme.bold(name));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatGroup(group: DiscoveryGroup, theme: any): string {
|
|
98
|
+
const lines = [
|
|
99
|
+
theme.fg("muted", BULLET) + theme.fg("toolTitle", theme.bold("Explored")),
|
|
100
|
+
];
|
|
101
|
+
for (const [index, record] of group.records.entries()) {
|
|
102
|
+
const prefix = index === 0 ? " └ " : " ";
|
|
103
|
+
lines.push(theme.fg("dim", prefix) + formatCallBody(record.name, record.args, theme));
|
|
104
|
+
}
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
44
107
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
108
|
+
class CompactRenderer {
|
|
109
|
+
private readonly calls = new Map<string, CompactCall>();
|
|
110
|
+
private lastCall: CompactCall | undefined;
|
|
49
111
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
for (const line of diff.split("\n")) {
|
|
54
|
-
if (line.startsWith("+") && !line.startsWith("+++")) additions++;
|
|
55
|
-
if (line.startsWith("-") && !line.startsWith("---")) removals++;
|
|
56
|
-
}
|
|
112
|
+
beginTurn(): void {
|
|
113
|
+
this.lastCall = undefined;
|
|
114
|
+
}
|
|
57
115
|
|
|
116
|
+
observeCall(name: string, id: string, args: any): CompactCall {
|
|
117
|
+
return this.registerCall(name, id, args);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
registerCall(
|
|
121
|
+
name: string,
|
|
122
|
+
id: string,
|
|
123
|
+
args: any,
|
|
124
|
+
invalidate?: () => void,
|
|
125
|
+
): CompactCall {
|
|
126
|
+
const existing = this.calls.get(id);
|
|
127
|
+
if (existing) {
|
|
128
|
+
existing.args = args;
|
|
129
|
+
existing.invalidate = invalidate ?? existing.invalidate;
|
|
130
|
+
return existing;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const record: CompactCall = { id, name, args, isError: false };
|
|
134
|
+
this.calls.set(id, record);
|
|
135
|
+
if (DISCOVERY_TOOLS.has(name) && this.lastCall && DISCOVERY_TOOLS.has(this.lastCall.name)) {
|
|
136
|
+
const group = this.lastCall.group ?? { records: [this.lastCall] };
|
|
137
|
+
this.lastCall.group = group;
|
|
138
|
+
group.records.push(record);
|
|
139
|
+
record.group = group;
|
|
140
|
+
for (const groupedCall of group.records) groupedCall.invalidate?.();
|
|
141
|
+
}
|
|
142
|
+
this.lastCall = record;
|
|
143
|
+
record.invalidate = invalidate;
|
|
144
|
+
return record;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
setResult(record: CompactCall, result: any, isError: boolean): void {
|
|
148
|
+
record.isError = isError;
|
|
149
|
+
if (record.group) {
|
|
150
|
+
record.group.records[0]?.invalidate?.();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
renderCall(
|
|
155
|
+
name: string,
|
|
156
|
+
args: any,
|
|
157
|
+
theme: any,
|
|
158
|
+
context: ToolRenderContext,
|
|
159
|
+
): Component {
|
|
160
|
+
const record = this.registerCall(name, context.toolCallId, args, context.invalidate);
|
|
161
|
+
if (record.group && record.group.records.length > 1) {
|
|
162
|
+
if (record.group.records[0] !== record) return new Text("", 0, 0);
|
|
163
|
+
return new Text(formatGroup(record.group, theme), 0, 0);
|
|
164
|
+
}
|
|
165
|
+
return new Text(
|
|
166
|
+
theme.fg("muted", BULLET) + formatCallBody(name, args, theme),
|
|
167
|
+
0,
|
|
168
|
+
0,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
renderResult(
|
|
173
|
+
name: string,
|
|
174
|
+
args: any,
|
|
175
|
+
result: any,
|
|
176
|
+
options: ToolRenderResultOptions,
|
|
177
|
+
theme: any,
|
|
178
|
+
context: ToolRenderContext & { isError: boolean },
|
|
179
|
+
): Component {
|
|
180
|
+
const record = this.registerCall(name, context.toolCallId, args, context.invalidate);
|
|
181
|
+
this.setResult(record, result, context.isError);
|
|
182
|
+
if (record.group && record.group.records.length > 1) {
|
|
183
|
+
if (record.group.records[0] !== record) return new Text("", 0, 0);
|
|
184
|
+
const error = errorText(result, context.isError);
|
|
185
|
+
return error ? new Text(theme.fg("error", error), 0, 0) : new Text("", 0, 0);
|
|
186
|
+
}
|
|
187
|
+
if (options.isPartial) return new Text("", 0, 0);
|
|
188
|
+
|
|
189
|
+
const error = errorText(result, context.isError);
|
|
190
|
+
if (error) return new Text(theme.fg("error", error), 0, 0);
|
|
191
|
+
if (name === "edit") {
|
|
192
|
+
const { additions, removals } = diffStats(result);
|
|
58
193
|
return new Text(
|
|
59
194
|
theme.fg("success", `+${additions}`) +
|
|
60
195
|
theme.fg("dim", " / ") +
|
|
@@ -62,10 +197,71 @@ function registerCollapsedEditTool(extensionApi: any): void {
|
|
|
62
197
|
0,
|
|
63
198
|
0,
|
|
64
199
|
);
|
|
200
|
+
}
|
|
201
|
+
if (name === "bash") return new Text(theme.fg("success", "Done"), 0, 0);
|
|
202
|
+
if (name === "write") return new Text(theme.fg("success", "Written"), 0, 0);
|
|
203
|
+
return new Text("", 0, 0);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const TOOL_FACTORIES: Record<string, ToolFactory> = {
|
|
208
|
+
bash: createBashTool,
|
|
209
|
+
edit: createEditTool,
|
|
210
|
+
find: createFindTool,
|
|
211
|
+
grep: createGrepTool,
|
|
212
|
+
ls: createLsTool,
|
|
213
|
+
read: createReadTool,
|
|
214
|
+
write: createWriteTool,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
function registerCompactTool(
|
|
218
|
+
pi: ExtensionAPI,
|
|
219
|
+
name: string,
|
|
220
|
+
factory: ToolFactory,
|
|
221
|
+
renderer: CompactRenderer,
|
|
222
|
+
): void {
|
|
223
|
+
const definition = factory(SOURCE_ROOT);
|
|
224
|
+
pi.registerTool({
|
|
225
|
+
name,
|
|
226
|
+
label: name,
|
|
227
|
+
description: definition.description,
|
|
228
|
+
parameters: definition.parameters,
|
|
229
|
+
renderShell: "self",
|
|
230
|
+
|
|
231
|
+
async execute(
|
|
232
|
+
toolCallId: string,
|
|
233
|
+
params: any,
|
|
234
|
+
signal: AbortSignal,
|
|
235
|
+
onUpdate: any,
|
|
236
|
+
ctx: any,
|
|
237
|
+
) {
|
|
238
|
+
return factory(ctx.cwd).execute(toolCallId, params, signal, onUpdate);
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
renderCall(args: any, theme: any, context: ToolRenderContext): Component {
|
|
242
|
+
return renderer.renderCall(name, args, theme, context);
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
renderResult(
|
|
246
|
+
result: any,
|
|
247
|
+
options: ToolRenderResultOptions,
|
|
248
|
+
theme: any,
|
|
249
|
+
context: ToolRenderContext & { isError: boolean },
|
|
250
|
+
): Component {
|
|
251
|
+
return renderer.renderResult(name, context.args, result, options, theme, context);
|
|
65
252
|
},
|
|
66
253
|
});
|
|
67
254
|
}
|
|
68
255
|
|
|
69
|
-
export default function piCompactToolsPlugin(pi:
|
|
70
|
-
|
|
256
|
+
export default function piCompactToolsPlugin(pi: ExtensionAPI): void {
|
|
257
|
+
const renderer = new CompactRenderer();
|
|
258
|
+
pi.on("turn_start", () => renderer.beginTurn());
|
|
259
|
+
pi.on("tool_call", (event: any) => {
|
|
260
|
+
if (TOOL_FACTORIES[event.toolName]) {
|
|
261
|
+
renderer.observeCall(event.toolName, event.toolCallId, event.input);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
for (const [name, factory] of Object.entries(TOOL_FACTORIES)) {
|
|
265
|
+
registerCompactTool(pi, name, factory, renderer);
|
|
266
|
+
}
|
|
71
267
|
}
|
|
@@ -259,6 +259,7 @@ export function registerQuestionnaireTool(pi: any): void {
|
|
|
259
259
|
renderCall(args: { questions?: QuestionnaireQuestion[] }, theme: any): any {
|
|
260
260
|
const count = args.questions?.length ?? 0;
|
|
261
261
|
return new Text(
|
|
262
|
+
theme.fg("muted", "• ") +
|
|
262
263
|
theme.fg("toolTitle", theme.bold("questionnaire ")) +
|
|
263
264
|
theme.fg("muted", `${count} question${count === 1 ? "" : "s"}`),
|
|
264
265
|
0,
|
|
@@ -756,6 +756,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
756
756
|
// Chain
|
|
757
757
|
if (args.chain && args.chain.length > 0) {
|
|
758
758
|
let text =
|
|
759
|
+
fg("muted", "• ") +
|
|
759
760
|
fg("toolTitle", theme.bold("subagent ")) +
|
|
760
761
|
fg("accent", `chain (${args.chain.length} steps)`) +
|
|
761
762
|
fg("muted", ` [${scope}]`);
|
|
@@ -778,6 +779,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
778
779
|
// Parallel
|
|
779
780
|
if (args.tasks && args.tasks.length > 0) {
|
|
780
781
|
let text =
|
|
782
|
+
fg("muted", "• ") +
|
|
781
783
|
fg("toolTitle", theme.bold("subagent ")) +
|
|
782
784
|
fg("accent", `parallel (${args.tasks.length} tasks)`) +
|
|
783
785
|
fg("muted", ` [${scope}]`);
|
|
@@ -798,6 +800,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
798
800
|
: args.task
|
|
799
801
|
: "...";
|
|
800
802
|
let text =
|
|
803
|
+
fg("muted", "• ") +
|
|
801
804
|
fg("toolTitle", theme.bold("subagent ")) +
|
|
802
805
|
fg("accent", agentName) +
|
|
803
806
|
fg("muted", ` [${scope}]`);
|
|
@@ -1215,4 +1218,4 @@ export default function (pi: ExtensionAPI) {
|
|
|
1215
1218
|
}
|
|
1216
1219
|
}, { overlay: true, overlayOptions: { maxHeight: "70%" } }); // Overlay: editor stays visible below
|
|
1217
1220
|
}
|
|
1218
|
-
}
|
|
1221
|
+
}
|