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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-shorthand
2
2
 
3
- ![A code call in Pi](https://raw.githubusercontent.com/sebinsua/shortsight/main/docs/shorthand.png)
3
+ ![A shorthand call in Pi](https://raw.githubusercontent.com/sebinsua/shortsight/main/docs/shorthand.png)
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 `code` 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.
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.12.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.12.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 code call looks in Pi. (The model gets a separate plain-text version: see textForModel.)
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("code"))} ${args.title ?? ""}${suffix ? theme.fg("muted", ` (${suffix})`) : ""}`;
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 || "Code failed without result details";
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 `code` tool: the model writes one Bun program that makes a multi-step change to the repository.
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. Run tests, type-checks and builds separately afterward with the shell tool.
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 codeDescription(graphAvailable: boolean): string {
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 === "code" && run && runFailed(run)) return { isError: true };
116
+ if (event.toolName === "shorthand" && run && runFailed(run)) return { isError: true };
63
117
  });
64
118
 
65
119
  pi.registerTool({
66
- name: "code",
67
- label: "Code",
68
- description: codeDescription(graphAvailable),
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. Run verification separately afterward",
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: `pi-shorthand-${toolCallId}`, truncateHead, truncateTail }),
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 renderCodeResult(result, options, theme);
202
+ return renderShorthandResult(result, options, theme);
146
203
  },
147
204
  });
148
205
  }
149
206
 
150
- export function renderCodeResult(
207
+ export function renderShorthandResult(
151
208
  result: { content: readonly unknown[]; details?: unknown },
152
209
  { expanded, isPartial }: { expanded: boolean; isPartial: boolean },
153
210
  theme: Theme,