pi-shorthand 0.12.0 → 0.14.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 +4 -2
- package/package.json +2 -2
- package/src/display.ts +3 -3
- package/src/index.ts +71 -14
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-shorthand
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
5
|
A [Pi](https://github.com/earendil-works/pi) tool for editing repositories with Bun programs, with first-class support for TypeScript and JavaScript.
|
|
6
6
|
|
|
@@ -38,7 +38,9 @@ pi-shorthand gives Pi a programming environment instead of a patch format. This
|
|
|
38
38
|
|
|
39
39
|
Programs edit a private workspace, with host files outside the repository kept read-only. Agents are good at improving things a step at a time, so changes are applied a whole file at a time and the agent is told which worked and which didn't. Concurrent edits can cause a run to be rejected; conflict detection is best-effort, not an atomic commit.
|
|
40
40
|
|
|
41
|
-
The `
|
|
41
|
+
The `shorthand` tool uses Pi's working directory by default. Pass `cwd` to target another checkout; relative paths are resolved from Pi's working directory. For example, when Pi starts in a bare worktree container, `cwd: "child"` targets its `child` worktree. The chosen directory must be inside a Git worktree.
|
|
42
|
+
|
|
43
|
+
Scripts run by Pi's `codemode` tool can call `shorthand` too. They get back what the run changed as data, not text.
|
|
42
44
|
|
|
43
45
|
## Without Pi
|
|
44
46
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-shorthand",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Pi tool for editing repositories with Bun programs, with first-class support for TypeScript and JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ast-grep",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"type": "module",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"shorthand-code": "0.
|
|
29
|
+
"shorthand-code": "0.14.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@earendil-works/pi-ai": "*",
|
package/src/display.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* How a
|
|
2
|
+
* How a shorthand call looks in Pi. (The model gets a separate plain-text version: see textForModel.)
|
|
3
3
|
*
|
|
4
4
|
* A verdict line first, whose colour carries the outcome. Lines indented under it belong to it: what
|
|
5
5
|
* went wrong, or what to watch out for. A blank line starts a new section. The sections come in the
|
|
@@ -38,7 +38,7 @@ export function callLine(
|
|
|
38
38
|
args.timeout && `program timeout ${args.timeout}s`,
|
|
39
39
|
];
|
|
40
40
|
const suffix = settings.filter(Boolean).join(", ");
|
|
41
|
-
return `${theme.fg("toolTitle", theme.bold("
|
|
41
|
+
return `${theme.fg("toolTitle", theme.bold("shorthand"))} ${args.title ?? ""}${suffix ? theme.fg("muted", ` (${suffix})`) : ""}`;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** Text to show when a completed tool result has no structured RunResult details. */
|
|
@@ -51,7 +51,7 @@ export function unstructuredResultText(content: readonly unknown[]): string {
|
|
|
51
51
|
)
|
|
52
52
|
.join("\n")
|
|
53
53
|
.trim();
|
|
54
|
-
return text || "
|
|
54
|
+
return text || "Shorthand failed without result details";
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** Lines that belong to the verdict line above them. */
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `
|
|
2
|
+
* The `shorthand` tool: the model writes one Bun program that makes a multi-step change to the repository.
|
|
3
3
|
* Its writes go to a copy-on-write overlay; if it succeeds, they're applied and the diff is returned.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -22,17 +22,18 @@ import {
|
|
|
22
22
|
} from "shorthand-code";
|
|
23
23
|
import { callLine, resultLines, unstructuredResultText } from "./display.ts";
|
|
24
24
|
|
|
25
|
-
const DESCRIPTION = `Edit repository files with a TypeScript program run by Bun. Best for changes across many files, repeated edits and semantic TypeScript renames or moves; a small change to one file is quicker as a direct edit. Top-level await and ordinary Bun/Node APIs work. Use repository-relative paths. Set cwd to a checkout path when Pi's working directory is outside the repository, such as a child worktree in a bare worktree container. The program runs in an isolated workspace; changes apply on successful exit by default and the tool reports the diff.
|
|
25
|
+
const DESCRIPTION = `Edit repository files with a TypeScript program run by Bun. Best for changes across many files, repeated edits and semantic TypeScript renames or moves; a small change to one file is quicker as a direct edit. Top-level await and ordinary Bun/Node APIs work. Use repository-relative paths. Only edits files inside a git repository; use edit or write for anything else. Set cwd to a checkout path when Pi's working directory is outside the repository, such as a child worktree in a bare worktree container. The program runs in an isolated workspace; changes apply on successful exit by default and the tool reports the diff. Tests, type-checks and builds belong outside the program: in a shell call, or, when a codemode script calls shorthand, later in that script.
|
|
26
26
|
|
|
27
27
|
Common operations:
|
|
28
28
|
- edit({ path, oldText, newText }) replaces exactly one literal occurrence; missing or ambiguous text is an error. Use text edits for known source, structural matching when it saves enumerating occurrences or preserves varying syntax.
|
|
29
29
|
- await Bun.file(path).text(); await Bun.write(path, text)
|
|
30
30
|
- sg.rewrite(pattern, replacement, files?) discovers and rewrites matching code; omit files for the working directory. $X captures one node; $$$X captures a sequence.
|
|
31
|
-
- sg.one(pattern, files?) selects exactly one match; sg.find returns an array. sg.rewrite also accepts a selected match or array without a file scope.
|
|
31
|
+
- sg.one(pattern, files?) selects exactly one match; sg.find returns an array. sg.rewrite also accepts a selected match or array without a file scope, or a batch of [pattern, replacement] rules applied together to the original code.
|
|
32
32
|
- A rewrite callback receives a match and returns text, a native node.replace(text) edit, or null to skip. Return native edits to apply them. Pass selected arrays together for independent edits; select again after changing their file.
|
|
33
|
-
- await refactor.rename({ file, symbol, to }) renames one resolved TypeScript symbol across the project without changing unrelated names.
|
|
34
|
-
- await refactor.references({ file, symbol }) returns resolved identifier matches that pass straight to sg.rewrite(matches, callback).
|
|
33
|
+
- await refactor.rename({ file, symbol, to }) renames one resolved TypeScript symbol across the project without changing unrelated names; a parameter is named through its function ("load.id"), or give a place with { at: match | { file, line, column }, to }.
|
|
34
|
+
- await refactor.references({ file, symbol } | { at }) returns resolved identifier matches that pass straight to sg.rewrite(matches, callback).
|
|
35
35
|
- await refactor.renameFile({ from, to }) moves a TypeScript file and updates module paths that resolve to it.
|
|
36
|
+
- rename, move and renameFile check the result with TypeScript, refuse what would break it, and return { files }; pass dryRun: true to get { files, diff } and change nothing.
|
|
36
37
|
- await refactor.move({ file, symbol, to }) moves a top-level declaration to another file and updates the imports that follow it.
|
|
37
38
|
|
|
38
39
|
See the shorthand skill for renames, moves and call-site migrations. Read its advanced-refactors.md guide only to extract code, move syntax or use rule objects. The default timeout is five seconds; request more for longer programs. Time spent inside the helpers above does not count toward it, up to 60 extra seconds.`;
|
|
@@ -45,12 +46,65 @@ export const SKILLS_DIRECTORY = path.join(
|
|
|
45
46
|
"skills",
|
|
46
47
|
);
|
|
47
48
|
|
|
48
|
-
export function
|
|
49
|
+
export function shorthandDescription(graphAvailable: boolean): string {
|
|
49
50
|
return graphAvailable
|
|
50
51
|
? DESCRIPTION.replace("\n\nSee the shorthand skill", `\n${GRAPH_DESCRIPTION}\nSee the shorthand skill`)
|
|
51
52
|
: DESCRIPTION;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
const paths = (description: string) => Type.Array(Type.String(), { description });
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* What a script gets back from `shorthand`, for example one run by Pi's codemode tool. The model still sees the text.
|
|
59
|
+
* A failed run resolves to this too, so a script checks `exitCode`.
|
|
60
|
+
*/
|
|
61
|
+
export const SHORTHAND_OUTPUT_SCHEMA = Type.Object({
|
|
62
|
+
exitCode: Type.Optional(
|
|
63
|
+
Type.Union([Type.Number(), Type.Null()], { description: "0 on success; null when the program was killed" }),
|
|
64
|
+
),
|
|
65
|
+
timedOut: Type.Optional(Type.Boolean()),
|
|
66
|
+
output: Type.Optional(Type.String({ description: "The program's stdout and stderr" })),
|
|
67
|
+
errorLine: Type.Optional(Type.String({ description: "On failure, the program line the error came from" })),
|
|
68
|
+
warnings: Type.Optional(Type.Array(Type.String(), { description: "Likely mistakes spotted before it ran" })),
|
|
69
|
+
changes: Type.Optional(
|
|
70
|
+
Type.Array(
|
|
71
|
+
Type.Object({
|
|
72
|
+
path: Type.String(),
|
|
73
|
+
kind: StringEnum(["added", "modified", "deleted"] as const),
|
|
74
|
+
patch: Type.String(),
|
|
75
|
+
}),
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
applied: Type.Optional(paths("Changed files written to the repository")),
|
|
79
|
+
conflicts: Type.Optional(paths("Files that changed outside the run while it ran; if any, nothing was applied")),
|
|
80
|
+
rolledBack: Type.Optional(paths("Changed files not kept after a failure")),
|
|
81
|
+
infrastructureError: Type.Optional(Type.String({ description: "Set, alone, when the program couldn't be run" })),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export function shorthandOutput(result: RunResult) {
|
|
85
|
+
const { exitCode, timedOut, output, errorLine, warnings, applied, conflicts, rolledBack } = result;
|
|
86
|
+
const changes = result.changes.map((change) => ({ path: change.path, kind: change.kind, patch: change.patch }));
|
|
87
|
+
return {
|
|
88
|
+
exitCode,
|
|
89
|
+
timedOut,
|
|
90
|
+
output,
|
|
91
|
+
...(errorLine === undefined ? {} : { errorLine }),
|
|
92
|
+
warnings,
|
|
93
|
+
changes,
|
|
94
|
+
applied,
|
|
95
|
+
conflicts,
|
|
96
|
+
rolledBack,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Base name of the temporary files that hold a large run's full diff or output. A call a codemode script makes has
|
|
102
|
+
* an id like `<parent id>/1`, and ids can hold `|`, so anything but a safe file-name character is replaced.
|
|
103
|
+
*/
|
|
104
|
+
export function spillName(toolCallId: string): string {
|
|
105
|
+
return `pi-shorthand-${toolCallId.replace(/[^\w.-]/g, "_")}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
54
108
|
export default async function (pi: ExtensionAPI, findGraph: () => Promise<unknown> = sightreadAvailable) {
|
|
55
109
|
// The shorthand skill ships with shorthand-code, whose `shorthand --skill` prints it for other agents.
|
|
56
110
|
pi.on("resources_discover", () => ({ skillPaths: [SKILLS_DIRECTORY] }));
|
|
@@ -59,15 +113,15 @@ export default async function (pi: ExtensionAPI, findGraph: () => Promise<unknow
|
|
|
59
113
|
// rather than throwing, since a thrown error loses them.)
|
|
60
114
|
pi.on("tool_result", async (event) => {
|
|
61
115
|
const run = event.details as RunResult | undefined;
|
|
62
|
-
if (event.toolName === "
|
|
116
|
+
if (event.toolName === "shorthand" && run && runFailed(run)) return { isError: true };
|
|
63
117
|
});
|
|
64
118
|
|
|
65
119
|
pi.registerTool({
|
|
66
|
-
name: "
|
|
67
|
-
label: "
|
|
68
|
-
description:
|
|
120
|
+
name: "shorthand",
|
|
121
|
+
label: "Shorthand",
|
|
122
|
+
description: shorthandDescription(graphAvailable),
|
|
69
123
|
promptSnippet:
|
|
70
|
-
"Make multi-file, repetitive or rename/move changes with one Bun program; a small change to one file is quicker as a direct edit.
|
|
124
|
+
"Make multi-file, repetitive or rename/move changes with one Bun program; a small change to one file is quicker as a direct edit. Keep verification outside the program",
|
|
71
125
|
|
|
72
126
|
parameters: Type.Object({
|
|
73
127
|
title: Type.String({ description: "A few words describing the change, shown to the user" }),
|
|
@@ -90,6 +144,7 @@ export default async function (pi: ExtensionAPI, findGraph: () => Promise<unknow
|
|
|
90
144
|
}),
|
|
91
145
|
),
|
|
92
146
|
}),
|
|
147
|
+
outputSchema: SHORTHAND_OUTPUT_SCHEMA,
|
|
93
148
|
|
|
94
149
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
95
150
|
// While it runs, show how long it's been going and the latest reported step.
|
|
@@ -122,6 +177,7 @@ export default async function (pi: ExtensionAPI, findGraph: () => Promise<unknow
|
|
|
122
177
|
isError: true,
|
|
123
178
|
content: [{ type: "text" as const, text: [error.message, ...diagnosticLines(error.diagnostics)].join("\n") }],
|
|
124
179
|
details: { infrastructureError: error.message, diagnostics: error.diagnostics },
|
|
180
|
+
structuredContent: { infrastructureError: error.message },
|
|
125
181
|
};
|
|
126
182
|
} finally {
|
|
127
183
|
clearInterval(progress);
|
|
@@ -130,10 +186,11 @@ export default async function (pi: ExtensionAPI, findGraph: () => Promise<unknow
|
|
|
130
186
|
content: [
|
|
131
187
|
{
|
|
132
188
|
type: "text",
|
|
133
|
-
text: textForModel(result, { name:
|
|
189
|
+
text: textForModel(result, { name: spillName(toolCallId), truncateHead, truncateTail }),
|
|
134
190
|
},
|
|
135
191
|
],
|
|
136
192
|
details: result,
|
|
193
|
+
structuredContent: shorthandOutput(result),
|
|
137
194
|
};
|
|
138
195
|
},
|
|
139
196
|
|
|
@@ -142,12 +199,12 @@ export default async function (pi: ExtensionAPI, findGraph: () => Promise<unknow
|
|
|
142
199
|
},
|
|
143
200
|
|
|
144
201
|
renderResult(result, options, theme) {
|
|
145
|
-
return
|
|
202
|
+
return renderShorthandResult(result, options, theme);
|
|
146
203
|
},
|
|
147
204
|
});
|
|
148
205
|
}
|
|
149
206
|
|
|
150
|
-
export function
|
|
207
|
+
export function renderShorthandResult(
|
|
151
208
|
result: { content: readonly unknown[]; details?: unknown },
|
|
152
209
|
{ expanded, isPartial }: { expanded: boolean; isPartial: boolean },
|
|
153
210
|
theme: Theme,
|