pi-shorthand 0.9.0 → 0.11.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/pi-shorthand/main/docs/screenshot.png?v=2)
3
+ ![A code 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
 
@@ -36,10 +36,18 @@ sudo apt install bubblewrap build-essential lsof
36
36
 
37
37
  pi-shorthand gives Pi a programming environment instead of a patch format. This makes multi-file and structural edits possible in one call, but does not guarantee that Pi will find it easier or more reliable than its built-in edit tool. Which works better depends on the model and the task.
38
38
 
39
- Programs edit a private workspace, with host files outside the repository kept read-only. By default, a failure keeps completed files and rolls back failed or interrupted edits. Concurrent edits can cause a run to be rejected; conflict detection is best-effort, not an atomic commit.
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
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.
42
42
 
43
43
  ## Without Pi
44
44
 
45
- The engine is published separately as `shorthand-code`, with a `shorthand` command that runs a program from a file or stdin and prints the same result. See `shorthand --help`.
45
+ The engine is published separately as [shorthand-code](https://github.com/sebinsua/shortsight/tree/main/packages/shorthand-code), with a `shorthand` command that runs a program from a file or stdin and prints the same result. See `shorthand --help`.
46
+
47
+ ## Code graph (optional)
48
+
49
+ Install [`sightread`](https://github.com/sebinsua/shortsight/tree/main/packages/sightread) and programs can also ask the TypeScript compiler what calls what, with exact line ranges, before they edit:
50
+
51
+ ```sh
52
+ npm i -g sightread
53
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-shorthand",
3
- "version": "0.9.0",
3
+ "version": "0.11.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",
@@ -11,26 +11,22 @@
11
11
  "pi-extension",
12
12
  "pi-package"
13
13
  ],
14
- "homepage": "https://github.com/sebinsua/pi-shorthand/tree/main/packages/pi-shorthand#readme",
15
- "bugs": "https://github.com/sebinsua/pi-shorthand/issues",
14
+ "homepage": "https://github.com/sebinsua/shortsight/tree/main/packages/pi-shorthand#readme",
15
+ "bugs": "https://github.com/sebinsua/shortsight/issues",
16
16
  "license": "MIT",
17
17
  "author": "Seb Insua",
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "git+https://github.com/sebinsua/pi-shorthand.git",
20
+ "url": "git+https://github.com/sebinsua/shortsight.git",
21
21
  "directory": "packages/pi-shorthand"
22
22
  },
23
23
  "files": [
24
24
  "src",
25
- "skills",
26
25
  "README.md"
27
26
  ],
28
27
  "type": "module",
29
- "scripts": {
30
- "prepack": "cp ../../README.md README.md"
31
- },
32
28
  "dependencies": {
33
- "shorthand-code": "0.9.0"
29
+ "shorthand-code": "0.11.0"
34
30
  },
35
31
  "peerDependencies": {
36
32
  "@earendil-works/pi-ai": "*",
@@ -59,9 +55,6 @@
59
55
  "extensions": [
60
56
  "./src/index.ts"
61
57
  ],
62
- "skills": [
63
- "./skills"
64
- ],
65
- "image": "https://raw.githubusercontent.com/sebinsua/pi-shorthand/main/docs/screenshot.png?v=2"
58
+ "image": "https://raw.githubusercontent.com/sebinsua/shortsight/main/docs/shorthand.png"
66
59
  }
67
60
  }
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@
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
 
6
+ import { createRequire } from "node:module";
6
7
  import * as path from "node:path";
7
8
  import { StringEnum } from "@earendil-works/pi-ai";
8
9
  import { type ExtensionAPI, type Theme, truncateHead, truncateTail } from "@earendil-works/pi-coding-agent";
@@ -16,6 +17,7 @@ import {
16
17
  diagnosticLines,
17
18
  runFailed,
18
19
  runWithBun,
20
+ sightreadAvailable,
19
21
  textForModel,
20
22
  } from "shorthand-code";
21
23
  import { callLine, resultLines, unstructuredResultText } from "./display.ts";
@@ -29,12 +31,30 @@ Common operations:
29
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.
30
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.
31
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).
32
35
  - await refactor.renameFile({ from, to }) moves a TypeScript file and updates module paths that resolve to it.
33
36
  - await refactor.move({ file, symbol, to }) moves a top-level declaration to another file and updates the imports that follow it.
34
37
 
35
38
  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 two seconds; request more for longer programs. Time spent inside the helpers above does not count toward it, up to 60 extra seconds.`;
36
39
 
37
- export default function (pi: ExtensionAPI) {
40
+ const GRAPH_DESCRIPTION =
41
+ "- await graph.query(request | request[]) queries symbols and relationships; graph results can be passed to sg as scope. The graph shows the repository before this program's edits.\n";
42
+
43
+ export const SKILLS_DIRECTORY = path.join(
44
+ path.dirname(createRequire(import.meta.url).resolve("shorthand-code/package.json")),
45
+ "skills",
46
+ );
47
+
48
+ export function codeDescription(graphAvailable: boolean): string {
49
+ return graphAvailable
50
+ ? DESCRIPTION.replace("\n\nSee the shorthand skill", `\n${GRAPH_DESCRIPTION}\nSee the shorthand skill`)
51
+ : DESCRIPTION;
52
+ }
53
+
54
+ export default async function (pi: ExtensionAPI, findGraph: () => Promise<unknown> = sightreadAvailable) {
55
+ // The shorthand skill ships with shorthand-code, whose `shorthand --skill` prints it for other agents.
56
+ pi.on("resources_discover", () => ({ skillPaths: [SKILLS_DIRECTORY] }));
57
+ const graphAvailable = Boolean(await findGraph());
38
58
  // A failed run is an error, both for the model and for how Pi shows it. (execute() returns its details
39
59
  // rather than throwing, since a thrown error loses them.)
40
60
  pi.on("tool_result", async (event) => {
@@ -45,7 +65,7 @@ export default function (pi: ExtensionAPI) {
45
65
  pi.registerTool({
46
66
  name: "code",
47
67
  label: "Code",
48
- description: DESCRIPTION,
68
+ description: codeDescription(graphAvailable),
49
69
  promptSnippet:
50
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",
51
71
 
@@ -91,6 +111,7 @@ export default function (pi: ExtensionAPI) {
91
111
  program: params.program,
92
112
  timeoutMs: (params.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1000,
93
113
  rollback: params.rollback ?? "file",
114
+ graph: graphAvailable,
94
115
  },
95
116
  signal,
96
117
  (step) => (latest = step),
@@ -1,117 +0,0 @@
1
- ---
2
- name: shorthand
3
- description: Edit repository files with Bun programs using plain text edits or structural matching. Covers renames, file and declaration moves, and call-site migrations; the advanced guide covers extracting code, moving syntax and rule objects.
4
- ---
5
-
6
- # Shorthand
7
-
8
- Use text replacement for known source; use structural matching when it saves enumerating
9
- occurrences or preserves varying syntax. Ordinary JavaScript strings, loops and Bun APIs work.
10
- Run tests, type-checks and builds separately with the shell tool after editing.
11
-
12
- ## Replace known text
13
-
14
- ```ts
15
- edit({ path: "src/config.ts", oldText: "timeoutMs: 1000", newText: "timeoutMs: 3000" });
16
- ```
17
-
18
- `edit` replaces exactly one literal occurrence, throwing if it is missing or ambiguous. Include
19
- surrounding text to distinguish repeated occurrences. Replacement text is literal, including `$`.
20
- Line-ending differences are accepted when matching.
21
- `edit` calls are synchronous and can be combined in one program; later calls see earlier changes.
22
-
23
- ## Refactors
24
-
25
- Rename symbols, move files, or move a declaration to another file, updating the project to match:
26
-
27
- ```ts
28
- await refactor.rename({ file: "src/users.ts", symbol: "parseUser", to: "decodeUser" });
29
- await refactor.renameFile({ from: "src/users.ts", to: "src/models/users.ts" });
30
- await refactor.move({ file: "src/api.ts", symbol: "parseUser", to: "src/users/parse.ts" });
31
- ```
32
-
33
- `refactor.rename` requires the declaration name to be unique in its file and leaves unrelated symbols
34
- alone. `refactor.renameFile` moves the file and updates imports and exports that resolve to it. Read
35
- [Semantic TypeScript refactors](advanced-refactors.md#semantic-typescript-refactors) for selection
36
- rules, updated paths and failure conditions.
37
-
38
- `refactor.move` appends the top-level declaration of `symbol` to `to`, which may be a new file. The
39
- target imports what the declaration uses, exporting helpers from the source when needed; the source
40
- imports it back if it still uses it; and files importing it from the source import it from the target.
41
- Importers keep their style, including `tsconfig` path aliases. Default exports, overloads, namespace
42
- imports that use it and dynamic imports of the source are refused before anything is written.
43
-
44
- ## Insert before or after a statement
45
-
46
- Validate an order immediately before saving it:
47
-
48
- ```ts
49
- const save = sg.one("await saveOrder(order);", "checkout.ts");
50
- sg.insert("validateOrder(order);", { before: save });
51
- ```
52
-
53
- Use `{ after: save }` to insert after it instead. `sg.one(pattern, files?)` requires exactly one
54
- match. Select the whole statement, including its semicolon, for before/after insertion in JS/TS.
55
-
56
- ## Replace calls while keeping their arguments
57
-
58
- Switch logging calls without reproducing their arguments:
59
-
60
- ```ts
61
- sg.rewrite("console.log($$$ARGS)", "logger.info($$$ARGS)", "src");
62
- ```
63
-
64
- `sg.rewrite(pattern, replacement, files?)` discovers, parses and writes matching files. Omit the
65
- scope for the working directory, or pass a file, directory or glob. `$X` captures one syntax node;
66
- `$$$X` captures a sequence. Captures can appear in the replacement text.
67
-
68
- ## Change one argument and preserve the rest
69
-
70
- Migrate numeric retry limits to options objects, leaving existing options alone:
71
-
72
- ```ts
73
- sg.rewrite("connect($URL, $RETRIES)", (m) => {
74
- const retries = m.node.getMatch("RETRIES")!;
75
- return retries.kind() === "number" ? retries.replace(`{ retries: ${retries.text()} }`) : null;
76
- });
77
- ```
78
-
79
- A callback returns text to replace the whole match, a native edit for part of it, or `null` to skip.
80
- Rewrites apply one after another. A pattern rewrite skips places an earlier rewrite produced, so one
81
- rewrite per call shape is safe in any order; select them with `sg.find` to rewrite them again.
82
- **`node.replace()` constructs an edit; return it from the callback so `sg.rewrite` applies it.**
83
-
84
- ## Replace an implementation while keeping its signature
85
-
86
- Make `calculateTotal` sum its prices, preserving parameter and return types:
87
-
88
- ```ts
89
- const fn = sg.one(
90
- { rule: { kind: "function_declaration", has: { field: "name", regex: "^calculateTotal$" } } },
91
- "prices.ts",
92
- );
93
- sg.rewrite(fn, (m) => m.node.field("body")!.replace("{ return prices.reduce((total, price) => total + price, 0); }"));
94
- ```
95
-
96
- Use `kind: "method_definition"` with the same `has` to select a class method; match names with
97
- `regex`, since a method's name is not an identifier pattern. `sg.rewrite(match, "new source")`
98
- replaces the whole selected node instead. `sg.find` returns an
99
- array: pass it once to `sg.rewrite(matches, callback)` for independent edits. Each call writes
100
- immediately within the editing workspace; select again if a later edit depends on that write.
101
-
102
- ## Write files or replace plain text
103
-
104
- ```ts
105
- await Bun.write("src/defaults.ts", "export const retryLimit = 3;\n");
106
- const source = await Bun.file("src/config.ts").text();
107
- await Bun.write("src/config.ts", source.replace("timeoutMs: 1000", "timeoutMs: 3000"));
108
- ```
109
-
110
- Use repository-relative paths. Changes apply on successful exit by default; the tool reports the
111
- diff, preserves existing UTF-8 BOMs and uniform line endings across write methods, then uses a
112
- detected project formatter. Counters and console summaries aren't required.
113
-
114
- This page covers renames, file and declaration moves, and call-site migrations. Read
115
- [advanced-refactors.md](advanced-refactors.md) only to extract code into a new function or file,
116
- insert, move or remove other statements, or use rule objects or the native ast-grep API. The bundled
117
- TypeScript 7 package has no legacy compiler API; use the supplied structural tools instead.
@@ -1,146 +0,0 @@
1
- # Advanced refactors
2
-
3
- Use this guide for extraction, complex structural rewrites, syntax placement or other languages.
4
- The everyday file and replacement operations are in [SKILL.md](SKILL.md).
5
-
6
- ## Semantic TypeScript refactors
7
-
8
- Prefer `refactor.rename` to a structural rewrite when changing a TypeScript symbol. It asks the TypeScript
9
- language server to rename the resolved symbol across the project, so unrelated names and strings are
10
- left alone. `file` is the declaration's file, and `symbol` must name exactly one declaration there.
11
- Missing names, repeated declarations and overloads are rejected without writing.
12
-
13
- Use `refactor.renameFile` to move a whole TypeScript file. It updates imports and exports that resolve to
14
- the file, as well as relative module paths inside the moved file, then performs the move. The source
15
- must exist, the destination must not exist, and both paths must remain inside the repository.
16
-
17
- Await each operation; when it resolves, all of its edits are complete. Use `sg.move` below for
18
- moving syntax between files; it does not repair imports or bindings. `refactor.move` moves a top-level
19
- declaration with its imports.
20
-
21
- ## Extract existing source
22
-
23
- Select and reuse the implementation rather than copying it into the program or searching for braces.
24
- This extracts a state-independent method, leaves a delegate, and updates its direct caller:
25
-
26
- ```ts
27
- const pattern = { rule: { kind: "method_definition", has: { field: "name", regex: "^format$" } } };
28
- const method = sg.one(pattern, "writer.ts");
29
- await Bun.write(
30
- "table.ts",
31
- 'import type { Cell, FormatOptions } from "./types";\n' +
32
- method.text.replace(/^format\b/, "export function renderTable"),
33
- );
34
- sg.rewrite(method, (m) => m.node.field("body")!.replace("{ return renderTable(rows, options); }"));
35
- await Bun.write("writer.ts", 'import { renderTable } from "./table";\n' + (await Bun.file("writer.ts").text()));
36
- sg.rewrite('import { TableWriter } from "./writer"', 'import { renderTable } from "./table"', "export.ts");
37
- sg.rewrite("new TableWriter().format($$$ARGS)", "renderTable($$$ARGS)", "export.ts");
38
- ```
39
-
40
- Preserve imports and dependencies; moving source does not remove its dependence on instance state.
41
- `field("body")` selects the actual body even when defaults or comments contain braces.
42
-
43
- ## Structural matching details
44
-
45
- Patterns must parse as one syntax node; a standalone class method such as
46
- `format($$$PARAMS) { $$$BODY }` is matched as a method. `$X` captures one node, `$$$X` a sequence, and `$_`
47
- matches without capturing. Capture text is available as `m.X` or `m.vars.X`.
48
-
49
- Callbacks are synchronous. They can return an array of native edits, or `null`/`undefined`/`false`
50
- to skip. `sg.rewrite` counts matches producing edits, not individual edits. Overlaps are rejected;
51
- an empty selection returns zero. Syntax fields and node kinds depend on the language.
52
-
53
- `sg.find` and `sg.rewrite` handle JS, TS, TSX, HTML and CSS. Scopes accept files, directories,
54
- globs or arrays. Paths may be relative or absolute within the editing workspace; results use
55
- repository-relative paths. String scopes use Git's tracked and non-ignored files. An explicit
56
- `sg.file()` target can also select an ignored JS/TS file; only files Git sees are applied afterward.
57
-
58
- ## Inserting, moving and removing syntax
59
-
60
- For JS/TS, use matches from `sg.find` or `sg.one(pattern, files)`, which requires exactly one match.
61
- `sg.file(path)` selects a JS/TS file root for placement or for scoping `find`, `one` and `rewrite`.
62
- Pass paths or file targets individually or in mixed arrays. Missing targets are valid insertion destinations,
63
- but cannot be searched.
64
-
65
- ```ts
66
- sg.insert("initialize();", { before: sg.one("run();", "src/app.ts") });
67
- sg.move(sg.one("function helper() { $$$BODY }", "src/old.ts"), {
68
- endOf: sg.file("src/new.ts"),
69
- });
70
- sg.remove(sg.find("obsolete();", "src")); // single match or array; validates the batch before writing
71
- ```
72
-
73
- Choose one destination: `before`/`after` a whole statement or declaration (`"run();"`), or
74
- `startOf`/`endOf` a file root or `statement_block`. For a function, select its body explicitly.
75
- Argument lists and class bodies aren't supported.
76
-
77
- To copy, use `sg.insert(source.text, destination)`. `sg.move(source, destination, transform?)`
78
- accepts an optional `(text) => string` returning non-empty replacement text.
79
-
80
- **Rematch placement targets after each edit**; use array removal for matches from one search. Adjacent comments stay in
81
- place, and interior whitespace is preserved. Imports and bindings aren't repaired; to move a top-level
82
- declaration with its imports, use `refactor.move`. If placement
83
- rejects joined statement boundaries, add explicit semicolons.
84
-
85
- ## Rule objects
86
-
87
- When a pattern alone can't say it, pass a rule instead:
88
-
89
- ```ts
90
- sg.find({ rule: { kind: "import_statement" } }, "src");
91
- sg.find({ rule: { pattern: "console.log($$$A)", inside: { kind: "function_declaration", stopBy: "end" } } }, "src");
92
- sg.find(
93
- { rule: { pattern: "console.log($$$A)", not: { inside: { kind: "function_declaration", stopBy: "end" } } } },
94
- "src",
95
- );
96
- // constraints restrict what a capture may match
97
- sg.rewrite({ rule: { pattern: "$F($X)" }, constraints: { F: { regex: "^(oldApi|legacyApi)$" } } }, "newApi($X)", "src");
98
- ```
99
-
100
- `kind` names come from tree-sitter (`function_declaration`, `call_expression`, `import_statement`,
101
- …). `stopBy: "end"` searches all ancestors, not just the parent.
102
-
103
- ## Native ast-grep API
104
-
105
- For transformations outside file-backed rewrites, ast-grep's JavaScript API is exposed on `sg`:
106
-
107
- ```ts
108
- const root = sg.parse(sg.Lang.TypeScript, sourceText).root();
109
- const edits = root.findAll("oldApi($$$ARGS)").map((node) => node.replace("newApi()"));
110
- await Bun.write("src/app.ts", root.commitEdits(edits));
111
- ```
112
-
113
- `import { parse, Lang } from "@ast-grep/napi"` also works. Native `replace` constructs edits;
114
- `commitEdits` returns updated source text. Neither writes files by itself.
115
- The bundled TypeScript 7 package does not expose the legacy compiler API (`createSourceFile`,
116
- `ScriptTarget`); changing import syntax won't make it available.
117
-
118
- ## File discovery and commands
119
-
120
- ```ts
121
- glob("src/**/*.ts"); // sorted paths Git sees
122
- grep("oldApi(", "src"); // [{ file, line, text }]; accepts a RegExp too
123
- const files = await $`git ls-files`.text();
124
- ```
125
-
126
- `glob`, `grep` and `sg` are synchronous. Bun's `$` needs `await`; interpolated values are
127
- quoted as single arguments, and arrays become multiple arguments. These globals and bundled CLIs
128
- belong to `code`, not necessarily ordinary shell calls. `node:fs` and ordinary Bun APIs also work.
129
-
130
- ## Execution options
131
-
132
- Programs run in an isolated repository workspace. Use relative paths: the live checkout's absolute
133
- path is inaccessible on macOS. On both platforms, host paths outside the workspace are read-only
134
- (including external symlink targets). `$TMPDIR` is private to the run.
135
- Writes to `.git` are blocked.
136
-
137
- The default timeout is two seconds; request more for longer transformations. Time inside helpers
138
- (`edit`, `glob`, `grep`, `sg`, `refactor`) does not count toward it, up to 60 extra seconds per run.
139
- By default, rollback happens per file: files involved in failed or interrupted edits are discarded,
140
- while the others are retained, including after exceptions. Unattributed failures
141
- (such as failed checks) preserve completed edits and report the failure. A failed file loses all
142
- its edits from this run, even if the error is caught. Multi-file operations such as moves share
143
- one outcome. Arbitrary shell failures cannot identify failed closed
144
- files. Open writers are inspected before failure exits and timeout termination; inspection failures
145
- or crashes that bypass exit handling retain nothing. Cancellation still discards everything.
146
- Use `rollback: "all"` when the whole change must be atomic.