pi-graphite 0.2.3 → 0.3.1

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/src/tools/pr.ts DELETED
@@ -1,298 +0,0 @@
1
- import { spawn, type ChildProcessByStdio } from "node:child_process";
2
- import type { Readable } from "node:stream";
3
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
- import { DEFAULT_COMMAND_TIMEOUT_MS, killProcessGroup, runGt, safeNoninteractiveEnv } from "../lib/exec";
5
- import { ensureSuccess, renderText } from "../lib/result";
6
- import {
7
- CwdParam,
8
- StringEnum,
9
- Type,
10
- requireConfirm,
11
- type ToolReturn,
12
- } from "../lib/schema";
13
-
14
- /* ------------------------------ pr_submit ------------------------------ */
15
-
16
- function shellQuote(s: string): string {
17
- if (s === "") return "''";
18
- if (/^[A-Za-z0-9_./:@%+=-]+$/.test(s)) return s;
19
- return `'${s.replace(/'/g, `'"'"'`)}'`;
20
- }
21
-
22
- interface CmdResult {
23
- command: string;
24
- args: string[];
25
- cwd: string;
26
- exitCode: number;
27
- stdout: string;
28
- stderr: string;
29
- timedOut: boolean;
30
- spawnError?: string;
31
- }
32
-
33
- function runGh(args: string[], cwd: string, signal?: AbortSignal): Promise<CmdResult> {
34
- return new Promise((resolve) => {
35
- let child: ChildProcessByStdio<null, Readable, Readable>;
36
- try {
37
- child = spawn("gh", args, {
38
- cwd,
39
- env: safeNoninteractiveEnv(),
40
- stdio: ["ignore", "pipe", "pipe"],
41
- detached: process.platform !== "win32",
42
- });
43
- } catch (e) {
44
- resolve({ command: "gh", args, cwd, exitCode: -1, stdout: "", stderr: "", timedOut: false, spawnError: (e as Error).message });
45
- return;
46
- }
47
-
48
- let stdout = "";
49
- let stderr = "";
50
- let killed = false;
51
- let settled = false;
52
- const killChild = () => {
53
- killed = true;
54
- killProcessGroup(child, "SIGTERM");
55
- setTimeout(() => killProcessGroup(child, "SIGKILL"), 1500).unref?.();
56
- };
57
- const timeout = setTimeout(killChild, DEFAULT_COMMAND_TIMEOUT_MS);
58
- timeout.unref?.();
59
- const onAbort = killChild;
60
- signal?.addEventListener("abort", onAbort, { once: true });
61
-
62
- child.stdout.on("data", (d) => (stdout += d.toString()));
63
- child.stderr.on("data", (d) => (stderr += d.toString()));
64
- child.on("error", (e) => {
65
- if (settled) return;
66
- settled = true;
67
- clearTimeout(timeout);
68
- signal?.removeEventListener("abort", onAbort);
69
- resolve({ command: "gh", args, cwd, exitCode: -1, stdout, stderr, timedOut: killed, spawnError: e.message });
70
- });
71
- child.on("close", (code) => {
72
- if (settled) return;
73
- settled = true;
74
- clearTimeout(timeout);
75
- signal?.removeEventListener("abort", onAbort);
76
- resolve({ command: "gh", args, cwd, exitCode: code ?? -1, stdout, stderr, timedOut: killed });
77
- });
78
- });
79
- }
80
-
81
- function renderGhText(label: string, r: CmdResult): string {
82
- const lines = [`[${label}] fail`, `$ gh ${r.args.join(" ")}`, `# cwd=${r.cwd} exit=${r.exitCode}${r.timedOut ? " (aborted)" : ""}${r.spawnError ? ` (spawn-error: ${r.spawnError})` : ""}`];
83
- if (r.stdout.trim()) lines.push("--- stdout ---", r.stdout.trimEnd());
84
- if (r.stderr.trim()) lines.push("--- stderr ---", r.stderr.trimEnd());
85
- return lines.join("\n");
86
- }
87
-
88
- export function registerPrSubmit(pi: ExtensionAPI) {
89
- pi.registerTool({
90
- name: "graphite_pr_submit",
91
- label: "Graphite: PR submit",
92
- description:
93
- "Submit branches as pull requests via `gt submit`. Defaults to a dry-run plan; set apply:true (with confirmRemote:true) to actually push and create/update PRs.",
94
- promptSnippet:
95
- "graphite_pr_submit: plan or apply `gt submit` for a branch or stack",
96
- promptGuidelines: [
97
- "Always call graphite_pr_submit with apply:false (default) first to see the dry-run plan, then call again with apply:true and confirmRemote:true to actually submit.",
98
- "`gt submit` cannot set PR title/body inline. If you pass `title`/`body` to graphite_pr_submit, the tool will return a `gh pr edit` command for you to run via bash to apply the metadata.",
99
- ],
100
- parameters: Type.Object({
101
- cwd: CwdParam,
102
- apply: Type.Optional(
103
- Type.Boolean({
104
- description: "false => --dry-run (default). true => actually push (requires confirmRemote).",
105
- }),
106
- ),
107
- stack: Type.Optional(
108
- Type.Boolean({
109
- description:
110
- "true => --stack (include descendants). false => --no-stack. Omitted => gt default behavior.",
111
- }),
112
- ),
113
- branch: Type.Optional(Type.String({ description: "Run from this branch (--branch)." })),
114
- updateOnly: Type.Optional(Type.Boolean({ description: "--update-only" })),
115
- draft: Type.Optional(Type.Boolean({ description: "--draft for new PRs" })),
116
- publish: Type.Optional(Type.Boolean({ description: "--publish all PRs" })),
117
- mergeWhenReady: Type.Optional(Type.Boolean({ description: "--merge-when-ready" })),
118
- rerequestReview: Type.Optional(Type.Boolean()),
119
- reviewers: Type.Optional(Type.Array(Type.String(), { description: "User reviewers (--reviewers)." })),
120
- teamReviewers: Type.Optional(Type.Array(Type.String())),
121
- comment: Type.Optional(Type.String({ description: "--comment <msg>" })),
122
- targetTrunk: Type.Optional(Type.String()),
123
- editMode: Type.Optional(
124
- StringEnum(["none", "cli"] as const, {
125
- description:
126
- "none (default) => --no-edit; cli => --edit --cli. Browser/web edit mode is disabled.",
127
- }),
128
- ),
129
- ai: Type.Optional(
130
- Type.Boolean({
131
- description: "true => --ai (let gt generate PR title/body). Default false (--no-ai).",
132
- }),
133
- ),
134
- forcePush: Type.Optional(
135
- Type.Boolean({ description: "--force (instead of default --force-with-lease). Requires confirmRemote." }),
136
- ),
137
- ignoreOutOfSyncTrunk: Type.Optional(Type.Boolean()),
138
- view: Type.Optional(Type.Boolean({ description: "Rejected: browser viewing is disabled." })),
139
- confirmRemote: Type.Optional(Type.Boolean()),
140
-
141
- title: Type.Optional(
142
- Type.String({
143
- description:
144
- "Desired PR title. `gt submit` has no inline flag for this; the tool emits a `gh pr edit` command to run after submit.",
145
- }),
146
- ),
147
- body: Type.Optional(
148
- Type.String({
149
- description:
150
- "Desired PR body. `gt submit` has no inline flag for this; the tool emits a `gh pr edit` command to run after submit.",
151
- }),
152
- ),
153
- }),
154
- async execute(_id, p, signal): Promise<ToolReturn> {
155
- const apply = p.apply === true;
156
- if (apply) requireConfirm(p.confirmRemote, "gt submit (push branches + create/update PRs)");
157
- if (p.forcePush) requireConfirm(p.confirmRemote, "gt submit --force");
158
- if ((p.editMode as string | undefined) === "web") throw new Error("editMode:'web' is disabled; browser launch is not exposed.");
159
- if (p.view) throw new Error("view:true is disabled; browser launch is not exposed. Use graphite_pr_lifecycle action='view_url'.");
160
-
161
- const wantsCustomMetadata = p.title != null || p.body != null;
162
-
163
- const args: string[] = ["submit"];
164
- if (!apply) args.push("--dry-run");
165
-
166
- if (p.stack === true) args.push("--stack");
167
- else if (p.stack === false) args.push("--no-stack");
168
-
169
- if (p.branch) args.push("--branch", p.branch);
170
- if (p.updateOnly) args.push("--update-only");
171
- if (p.draft) args.push("--draft");
172
- if (p.publish) args.push("--publish");
173
- if (p.mergeWhenReady) args.push("--merge-when-ready");
174
- if (p.rerequestReview) args.push("--rerequest-review");
175
-
176
- if (p.reviewers && p.reviewers.length)
177
- args.push("--reviewers", p.reviewers.join(","));
178
- if (p.teamReviewers && p.teamReviewers.length)
179
- args.push("--team-reviewers", p.teamReviewers.join(","));
180
- if (p.comment) args.push("--comment", p.comment);
181
- if (p.targetTrunk) args.push("--target-trunk", p.targetTrunk);
182
-
183
- // If the caller supplied title/body, force --no-edit so gt doesn't try
184
- // to prompt or open a web editor with conflicting metadata. The actual
185
- // metadata is applied via the suggested `gh pr edit` command instead.
186
- const editMode = wantsCustomMetadata ? "none" : (p.editMode ?? "none");
187
- if (editMode === "none") args.push("--no-edit");
188
- else if (editMode === "cli") args.push("--edit", "--cli");
189
-
190
- args.push(p.ai ? "--ai" : "--no-ai");
191
-
192
- if (p.forcePush) args.push("--force");
193
- if (p.ignoreOutOfSyncTrunk) args.push("--ignore-out-of-sync-trunk");
194
-
195
- const label = `gt ${args.join(" ")}`;
196
- const r = await runGt(args, { cwd: p.cwd, signal });
197
- const f = await ensureSuccess(label, r, p.cwd);
198
-
199
- const blocks: string[] = [renderText(label, f)];
200
-
201
- let metadataNote: string | undefined;
202
- if (wantsCustomMetadata) {
203
- const ghParts: string[] = ["gh", "pr", "edit"];
204
- if (p.branch) ghParts.push(p.branch);
205
- if (p.title != null) ghParts.push("--title", shellQuote(p.title));
206
- if (p.body != null) ghParts.push("--body", shellQuote(p.body));
207
- const ghCmd = ghParts.join(" ");
208
-
209
- metadataNote = [
210
- "## metadata note",
211
- "`gt submit` has no flag to set PR title/body inline.",
212
- "To apply the title/body you supplied, run the following via the bash tool" +
213
- " (gt does not run gh for you; this keeps the tool composable):",
214
- ghCmd,
215
- p.stack === true || (!p.branch && p.stack !== false)
216
- ? "If this submit covered multiple PRs, repeat `gh pr edit <branch>` for each PR that needs metadata."
217
- : undefined,
218
- ]
219
- .filter((x): x is string => Boolean(x))
220
- .join("\n");
221
-
222
- blocks.push(metadataNote);
223
- }
224
-
225
- return {
226
- content: [{ type: "text", text: blocks.join("\n\n") }],
227
- details: {
228
- apply,
229
- editMode,
230
- wantsCustomMetadata,
231
- metadataNote,
232
- result: f,
233
- },
234
- };
235
- },
236
- });
237
- }
238
-
239
- /* ----------------------------- pr_lifecycle ----------------------------- */
240
-
241
- export function registerPrLifecycle(pi: ExtensionAPI) {
242
- pi.registerTool({
243
- name: "graphite_pr_lifecycle",
244
- label: "Graphite: PR lifecycle",
245
- description:
246
- "PR lifecycle actions: return PR URL, merge the stack via Graphite, or unlink a branch from its PR.",
247
- promptSnippet:
248
- "graphite_pr_lifecycle: view_url | merge | unlink for a PR/branch",
249
- parameters: Type.Object({
250
- cwd: CwdParam,
251
- action: StringEnum(["view_url", "merge", "unlink"] as const),
252
- branch: Type.Optional(Type.String({ description: "Branch name or PR number." })),
253
- stack: Type.Optional(
254
- Type.Boolean({ description: "Rejected: stack browser page is disabled." }),
255
- ),
256
- apply: Type.Optional(
257
- Type.Boolean({
258
- description: "action=merge: false (default) => --dry-run; true => actually merge (requires confirmRemote).",
259
- }),
260
- ),
261
- confirm: Type.Optional(
262
- Type.Boolean({
263
- description: "Rejected: interactive confirmation prompts are disabled.",
264
- }),
265
- ),
266
- confirmRemote: Type.Optional(Type.Boolean()),
267
- }),
268
- async execute(_id, p, signal): Promise<ToolReturn> {
269
- let args: string[];
270
- if (p.action === "view_url") {
271
- if (p.stack) throw new Error("stack:true is disabled; browser stack page is not exposed.");
272
- const branchArgs = p.branch ? [p.branch] : [];
273
- const r = await runGh(["pr", "view", ...branchArgs, "--json", "url", "--jq", ".url"], p.cwd, signal);
274
- if (r.exitCode !== 0) throw new Error(renderGhText("gh pr view", r));
275
- return {
276
- content: [{ type: "text", text: r.stdout.trim() }],
277
- details: { action: p.action, url: r.stdout.trim(), result: r },
278
- };
279
- } else if (p.action === "merge") {
280
- const apply = p.apply === true;
281
- if (apply) requireConfirm(p.confirmRemote, "gt merge (merges PRs on GitHub)");
282
- args = ["merge"];
283
- if (!apply) args.push("--dry-run");
284
- if (p.confirm) throw new Error("confirm:true is disabled; interactive confirmation prompts are not exposed.");
285
- } else {
286
- args = ["unlink"];
287
- if (p.branch) args.push(p.branch);
288
- }
289
- const label = `gt ${args.join(" ")}`;
290
- const r = await runGt(args, { cwd: p.cwd, signal });
291
- const f = await ensureSuccess(label, r, p.cwd);
292
- return {
293
- content: [{ type: "text", text: renderText(label, f) }],
294
- details: { action: p.action, result: f },
295
- };
296
- },
297
- });
298
- }
@@ -1,108 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { runGt } from "../lib/exec";
3
- import { ensureSuccess, renderText } from "../lib/result";
4
- import {
5
- CwdParam,
6
- StringEnum,
7
- Type,
8
- requireConfirm,
9
- type ToolReturn,
10
- } from "../lib/schema";
11
-
12
- export function registerRemoteSync(pi: ExtensionAPI) {
13
- pi.registerTool({
14
- name: "graphite_remote_sync",
15
- label: "Graphite: remote sync",
16
- description:
17
- "Sync local branches with remote: `gt sync` (pull trunk + restack + cleanup) or `gt get` (fetch a branch / PR locally).",
18
- promptSnippet:
19
- "graphite_remote_sync: `gt sync` for cleanup+restack, or `gt get` to import a branch/PR",
20
- promptGuidelines: [
21
- "Run graphite_remote_sync action=sync at the start of a session to update trunk and restack open stacks.",
22
- "graphite_remote_sync action=sync with force=true or deleteAll=true is destructive; pass confirmDestructive:true.",
23
- ],
24
- parameters: Type.Object({
25
- cwd: CwdParam,
26
- action: StringEnum(["sync", "get"] as const),
27
-
28
- // sync
29
- allTrunks: Type.Optional(Type.Boolean({ description: "sync: --all" })),
30
- deleteAll: Type.Optional(
31
- Type.Boolean({
32
- description:
33
- "sync|get: delete all merged/closed branches without prompting (--delete-all). Requires confirmDestructive.",
34
- }),
35
- ),
36
- force: Type.Optional(
37
- Type.Boolean({
38
- description:
39
- "sync|get: overwrite local branches with remote (--force). Requires confirmDestructive.",
40
- }),
41
- ),
42
- restack: Type.Optional(
43
- Type.Boolean({
44
- description: "sync|get: restack after fetching (default true; pass false for --no-restack).",
45
- }),
46
- ),
47
-
48
- // get
49
- target: Type.Optional(
50
- Type.String({ description: "get: branch name or PR number to fetch." }),
51
- ),
52
- downstack: Type.Optional(
53
- Type.Boolean({ description: "get: --downstack (don't sync upstack)." }),
54
- ),
55
- remoteUpstack: Type.Optional(
56
- Type.Boolean({ description: "get: --remote-upstack (include remote-only upstack)." }),
57
- ),
58
- checkout: Type.Optional(
59
- Type.Boolean({ description: "get: check out target after sync (default true; false => --no-checkout)." }),
60
- ),
61
- unfrozen: Type.Optional(
62
- Type.Boolean({ description: "get: --unfrozen (new branches editable)." }),
63
- ),
64
-
65
- confirmDestructive: Type.Optional(Type.Boolean()),
66
- }),
67
- async execute(_id, p, signal): Promise<ToolReturn> {
68
- const args: string[] = [p.action];
69
-
70
- if (p.action === "sync") {
71
- if (p.force || p.deleteAll) {
72
- requireConfirm(
73
- p.confirmDestructive,
74
- "gt sync with --force/--delete-all (may overwrite branches)",
75
- );
76
- }
77
- if (p.allTrunks) args.push("--all");
78
- if (p.deleteAll) args.push("--delete-all");
79
- if (p.force) args.push("--force");
80
- if (p.restack === false) args.push("--no-restack");
81
- } else {
82
- if (!p.target) throw new Error("action=get requires `target` (branch name or PR number).");
83
- args.push(p.target);
84
- if (p.downstack) args.push("--downstack");
85
- if (p.remoteUpstack) args.push("--remote-upstack");
86
- if (p.force) {
87
- requireConfirm(p.confirmDestructive, "gt get --force (overwrites local branches)");
88
- args.push("--force");
89
- }
90
- if (p.deleteAll) {
91
- requireConfirm(p.confirmDestructive, "gt get --delete-all");
92
- args.push("--delete-all");
93
- }
94
- if (p.checkout === false) args.push("--no-checkout");
95
- if (p.restack === false) args.push("--no-restack");
96
- if (p.unfrozen) args.push("--unfrozen");
97
- }
98
-
99
- const label = `gt ${args.join(" ")}`;
100
- const r = await runGt(args, { cwd: p.cwd, signal });
101
- const f = await ensureSuccess(label, r, p.cwd);
102
- return {
103
- content: [{ type: "text", text: renderText(label, f) }],
104
- details: { action: p.action, result: f },
105
- };
106
- },
107
- });
108
- }
package/src/tools/repo.ts DELETED
@@ -1,114 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { runGt } from "../lib/exec";
3
- import { ensureAllSuccess, ensureSuccess, renderText } from "../lib/result";
4
- import { CwdParam, StringEnum, Type, type ToolReturn } from "../lib/schema";
5
-
6
- const params = Type.Object({
7
- cwd: CwdParam,
8
- action: StringEnum(["status", "init", "set_trunk", "show_config"] as const),
9
- trunk: Type.Optional(
10
- Type.String({
11
- description:
12
- "Trunk branch name. Required for action=set_trunk. Optional for action=init (gt default behavior).",
13
- }),
14
- ),
15
- addAdditionalTrunk: Type.Optional(
16
- Type.Boolean({
17
- description:
18
- "Rejected: `gt trunk --add` is interactive and not exposed to agents.",
19
- }),
20
- ),
21
- reset: Type.Optional(
22
- Type.Boolean({
23
- description: "If true with action=init, untrack all branches (gt init --reset).",
24
- }),
25
- ),
26
- });
27
-
28
- export function registerRepo(pi: ExtensionAPI) {
29
- pi.registerTool({
30
- name: "graphite_repo",
31
- label: "Graphite: repo",
32
- description:
33
- "Repo-level Graphite operations: status snapshot (log + trunk), init, set trunk, and show config.",
34
- promptSnippet:
35
- "graphite_repo: inspect repo state, initialize Graphite, configure trunk(s)",
36
- parameters: params,
37
- async execute(_id, p, signal): Promise<ToolReturn> {
38
- const cwd = p.cwd;
39
-
40
- if (p.action === "status") {
41
- const [trunk, log] = await Promise.all([
42
- runGt(["trunk"], { cwd, signal }),
43
- runGt(["log", "short"], { cwd, signal }),
44
- ]);
45
- const [ft, fl] = await ensureAllSuccess(
46
- [
47
- { label: "gt trunk", result: trunk },
48
- { label: "gt log short", result: log },
49
- ],
50
- cwd,
51
- );
52
- return {
53
- content: [
54
- {
55
- type: "text",
56
- text: [renderText("gt trunk", ft), renderText("gt log short", fl)].join("\n\n"),
57
- },
58
- ],
59
- details: { trunk: ft, log: fl },
60
- };
61
- }
62
-
63
- if (p.action === "init") {
64
- const args = ["init"];
65
- if (p.trunk) args.push("--trunk", p.trunk);
66
- if (p.reset) args.push("--reset");
67
- const r = await runGt(args, { cwd, signal });
68
- const f = await ensureSuccess(`gt ${args.join(" ")}`, r, cwd);
69
- return {
70
- content: [{ type: "text", text: renderText(`gt ${args.join(" ")}`, f) }],
71
- details: { result: f },
72
- };
73
- }
74
-
75
- if (p.action === "set_trunk") {
76
- if (!p.trunk) throw new Error("action=set_trunk requires `trunk`.");
77
- if (!p.addAdditionalTrunk) {
78
- const args = ["init", "--trunk", p.trunk];
79
- const r = await runGt(args, { cwd, signal });
80
- const f = await ensureSuccess(`gt ${args.join(" ")}`, r, cwd);
81
- return {
82
- content: [
83
- { type: "text", text: renderText(`gt ${args.join(" ")}`, f) },
84
- ],
85
- details: { result: f },
86
- };
87
- }
88
- throw new Error("gt trunk --add is interactive; not exposed to agents.");
89
- }
90
-
91
- // show_config
92
- const [trunk, trunkAll] = await Promise.all([
93
- runGt(["trunk"], { cwd, signal }),
94
- runGt(["trunk", "--all"], { cwd, signal }),
95
- ]);
96
- const [ft, fta] = await ensureAllSuccess(
97
- [
98
- { label: "gt trunk", result: trunk },
99
- { label: "gt trunk --all", result: trunkAll },
100
- ],
101
- cwd,
102
- );
103
- return {
104
- content: [
105
- {
106
- type: "text",
107
- text: [renderText("gt trunk", ft), renderText("gt trunk --all", fta)].join("\n\n"),
108
- },
109
- ],
110
- details: { trunk: ft, trunkAll: fta },
111
- };
112
- },
113
- });
114
- }