shariq-pi-extensions 0.1.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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/THIRD_PARTY_NOTICES.md +11 -0
  4. package/docs/ARCHITECTURE.md +54 -0
  5. package/docs/DEVELOPMENT.md +88 -0
  6. package/docs/EXTENSIONS.md +89 -0
  7. package/extensions/antigravity-provider/LICENSE +21 -0
  8. package/extensions/antigravity-provider/README.md +23 -0
  9. package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
  10. package/extensions/antigravity-provider/antigravity/index.ts +67 -0
  11. package/extensions/antigravity-provider/antigravity/models.ts +184 -0
  12. package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
  13. package/extensions/antigravity-provider/index.ts +3 -0
  14. package/extensions/ask-user/README.md +21 -0
  15. package/extensions/ask-user/index.ts +417 -0
  16. package/extensions/background-terminals/README.md +43 -0
  17. package/extensions/background-terminals/index.ts +400 -0
  18. package/extensions/background-terminals/src/manager.ts +480 -0
  19. package/extensions/background-terminals/src/output-buffer.ts +110 -0
  20. package/extensions/background-terminals/src/presentation.ts +74 -0
  21. package/extensions/background-terminals/src/types.ts +47 -0
  22. package/extensions/background-terminals/src/ui.ts +478 -0
  23. package/extensions/context-usage/LICENSE +21 -0
  24. package/extensions/context-usage/README.md +20 -0
  25. package/extensions/context-usage/index.ts +2006 -0
  26. package/extensions/context-usage/lib/ansi.ts +60 -0
  27. package/extensions/context-usage/lib/boundary.ts +27 -0
  28. package/extensions/context-usage/lib/chat.ts +190 -0
  29. package/extensions/context-usage/lib/config.ts +30 -0
  30. package/extensions/context-usage/lib/fmt.ts +59 -0
  31. package/extensions/context-usage/lib/style.ts +260 -0
  32. package/extensions/copy-all/README.md +15 -0
  33. package/extensions/copy-all/index.ts +72 -0
  34. package/extensions/factory-provider/API_KEYS.md +17 -0
  35. package/extensions/factory-provider/AUTH.md +15 -0
  36. package/extensions/factory-provider/README.md +90 -0
  37. package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
  38. package/extensions/factory-provider/UPDATING.md +97 -0
  39. package/extensions/factory-provider/factory/api-keys.ts +407 -0
  40. package/extensions/factory-provider/factory/auth.ts +132 -0
  41. package/extensions/factory-provider/factory/constants.ts +15 -0
  42. package/extensions/factory-provider/factory/dashboard.ts +363 -0
  43. package/extensions/factory-provider/factory/droid.ts +84 -0
  44. package/extensions/factory-provider/factory/limits.ts +342 -0
  45. package/extensions/factory-provider/factory/models.ts +283 -0
  46. package/extensions/factory-provider/factory/responses.ts +331 -0
  47. package/extensions/factory-provider/factory/warm-theme.ts +53 -0
  48. package/extensions/factory-provider/factory/websocket.ts +130 -0
  49. package/extensions/factory-provider/index.ts +204 -0
  50. package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
  51. package/extensions/firecrawl-web/README.md +31 -0
  52. package/extensions/firecrawl-web/auth.ts +92 -0
  53. package/extensions/firecrawl-web/client.ts +238 -0
  54. package/extensions/firecrawl-web/index.ts +96 -0
  55. package/extensions/firecrawl-web/output.ts +125 -0
  56. package/extensions/git-info/README.md +25 -0
  57. package/extensions/git-info/index.ts +162 -0
  58. package/extensions/git-info/src/git.ts +206 -0
  59. package/extensions/git-info/src/ui.ts +291 -0
  60. package/extensions/goal/README.md +44 -0
  61. package/extensions/goal/constants.ts +4 -0
  62. package/extensions/goal/extension.ts +1064 -0
  63. package/extensions/goal/index.ts +2 -0
  64. package/extensions/goal/types.ts +56 -0
  65. package/extensions/goal/ui.ts +334 -0
  66. package/extensions/orchestration/README.md +58 -0
  67. package/extensions/orchestration/dashboard.ts +192 -0
  68. package/extensions/orchestration/engine.ts +706 -0
  69. package/extensions/orchestration/index.ts +243 -0
  70. package/extensions/orchestration/prompts.ts +124 -0
  71. package/extensions/orchestration/protocol.ts +116 -0
  72. package/extensions/orchestration/settings-ui.ts +41 -0
  73. package/extensions/orchestration/settings.ts +59 -0
  74. package/extensions/orchestration/storage.ts +49 -0
  75. package/extensions/orchestration/types.ts +105 -0
  76. package/extensions/performance-status/README.md +5 -0
  77. package/extensions/performance-status/index.ts +171 -0
  78. package/extensions/pi-memory/README.md +53 -0
  79. package/extensions/pi-memory/index.ts +326 -0
  80. package/extensions/pi-memory/src/config.ts +75 -0
  81. package/extensions/pi-memory/src/database.ts +617 -0
  82. package/extensions/pi-memory/src/extraction.ts +178 -0
  83. package/extensions/pi-memory/src/project.ts +80 -0
  84. package/extensions/pi-memory/src/projection.ts +59 -0
  85. package/extensions/pi-memory/src/redaction.ts +60 -0
  86. package/extensions/pi-memory/src/retrieval.ts +30 -0
  87. package/extensions/pi-memory/src/service.ts +194 -0
  88. package/extensions/pi-memory/src/session.ts +146 -0
  89. package/extensions/pi-memory/src/types.ts +82 -0
  90. package/extensions/shared/README.md +18 -0
  91. package/extensions/shared/activity-status.ts +31 -0
  92. package/extensions/shared/child-session.ts +150 -0
  93. package/extensions/shared/context-utilization.ts +47 -0
  94. package/extensions/shared/dashboard-state.ts +99 -0
  95. package/extensions/shared/tool-call-timeout.ts +104 -0
  96. package/extensions/shared/tui-dashboard.ts +139 -0
  97. package/extensions/shell-shortcuts/README.md +17 -0
  98. package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
  99. package/extensions/shell-shortcuts/index.ts +6 -0
  100. package/extensions/subagents/README.md +89 -0
  101. package/extensions/subagents/index.ts +1386 -0
  102. package/extensions/subagents/src/backend.ts +69 -0
  103. package/extensions/subagents/src/backends/pi.ts +781 -0
  104. package/extensions/subagents/src/catalog.ts +85 -0
  105. package/extensions/subagents/src/config.ts +241 -0
  106. package/extensions/subagents/src/context.ts +57 -0
  107. package/extensions/subagents/src/coordinator.ts +64 -0
  108. package/extensions/subagents/src/domain.ts +294 -0
  109. package/extensions/subagents/src/format.ts +74 -0
  110. package/extensions/subagents/src/manager.ts +813 -0
  111. package/extensions/subagents/src/prompt.ts +94 -0
  112. package/extensions/subagents/src/result-delivery.ts +20 -0
  113. package/extensions/subagents/src/runtime.ts +48 -0
  114. package/extensions/subagents/src/ui/takeover.ts +695 -0
  115. package/extensions/subagents/src/ui/transcript.ts +182 -0
  116. package/extensions/subagents/src/worktree.ts +175 -0
  117. package/extensions/web-fetch/README.md +13 -0
  118. package/extensions/web-fetch/index.ts +441 -0
  119. package/package.json +117 -0
  120. package/skills/background-terminals/SKILL.md +42 -0
  121. package/skills/orchestration/SKILL.md +33 -0
  122. package/skills/subagents/SKILL.md +89 -0
  123. package/themes/ember-warm-dark.json +85 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Transcript rendering for the takeover view: turns a SubagentSnapshot's
3
+ * normalized transcript + live state into plain wrapped lines. Ported from
4
+ * v1, with the session-poking replaced by snapshot reads.
5
+ */
6
+
7
+ import type { Theme } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ truncateToWidth,
10
+ visibleWidth,
11
+ wrapTextWithAnsi,
12
+ } from "@earendil-works/pi-tui";
13
+ import type { SubagentSnapshot, TranscriptItem } from "../domain.ts";
14
+
15
+ const ANSI_PATTERN =
16
+ // eslint-disable-next-line no-control-regex
17
+ /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
18
+
19
+ /**
20
+ * Strip raw ANSI codes, expand tabs, and drop control chars. Terminal-expanded
21
+ * tabs (and stray escapes) make lines wider than the width we declare to the
22
+ * TUI, which desyncs the renderer and smears the overlay.
23
+ */
24
+ export function sanitizeText(text: string): string {
25
+ return text
26
+ .replace(ANSI_PATTERN, "")
27
+ .replaceAll("\t", " ")
28
+ .replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "");
29
+ }
30
+
31
+ function renderUserText(
32
+ theme: Theme,
33
+ text: string,
34
+ width: number,
35
+ out: string[],
36
+ ) {
37
+ const clean = sanitizeText(text).trim();
38
+ if (!clean) return;
39
+ const wrapped = wrapTextWithAnsi(clean, Math.max(10, width - 2));
40
+ for (let i = 0; i < wrapped.length; i++) {
41
+ const prefix = i === 0 ? theme.fg("accent", "> ") : " ";
42
+ out.push(
43
+ truncateToWidth(prefix + theme.fg("userMessageText", wrapped[i]), width),
44
+ );
45
+ }
46
+ }
47
+
48
+ function renderThinking(
49
+ theme: Theme,
50
+ text: string,
51
+ width: number,
52
+ out: string[],
53
+ ) {
54
+ const reasoning = sanitizeText(text).trim();
55
+ if (!reasoning) return;
56
+ const prefix = theme.fg("dim", "~ ");
57
+ const wrapped = wrapTextWithAnsi(reasoning, Math.max(10, width - 2));
58
+ for (let i = 0; i < wrapped.length; i++) {
59
+ out.push(
60
+ truncateToWidth(
61
+ (i === 0 ? prefix : " ") + theme.fg("muted", theme.italic(wrapped[i])),
62
+ width,
63
+ ),
64
+ );
65
+ }
66
+ }
67
+
68
+ function renderAssistantItem(
69
+ theme: Theme,
70
+ item: Extract<TranscriptItem, { kind: "assistant" }>,
71
+ width: number,
72
+ out: string[],
73
+ ) {
74
+ for (const part of item.parts) {
75
+ if (part.type === "text") {
76
+ const text = sanitizeText(part.text).trim();
77
+ if (!text) continue;
78
+ out.push(...wrapTextWithAnsi(text, width));
79
+ } else if (part.type === "thinking") {
80
+ renderThinking(
81
+ theme,
82
+ part.redacted ? "[redacted reasoning]" : part.text,
83
+ width,
84
+ out,
85
+ );
86
+ } else if (part.type === "toolCall") {
87
+ const preview = part.argsPreview ? sanitizeText(part.argsPreview) : "";
88
+ const line =
89
+ theme.fg("muted", "→ ") +
90
+ theme.fg("toolTitle", part.name) +
91
+ (preview && preview !== "{}" ? theme.fg("dim", ` ${preview}`) : "");
92
+ out.push(truncateToWidth(line, width));
93
+ }
94
+ }
95
+ }
96
+
97
+ function renderToolResultItem(
98
+ theme: Theme,
99
+ item: Extract<TranscriptItem, { kind: "toolResult" }>,
100
+ width: number,
101
+ out: string[],
102
+ ) {
103
+ const firstLine =
104
+ sanitizeText(item.outputPreview ?? "")
105
+ .split("\n")
106
+ .find((line) => line.trim()) ?? "";
107
+ const label = item.isError
108
+ ? theme.fg("error", " error: ")
109
+ : theme.fg("dim", " output: ");
110
+ out.push(
111
+ truncateToWidth(label + theme.fg("dim", firstLine || "(no output)"), width),
112
+ );
113
+ }
114
+
115
+ /** Render a subagent's conversation as plain lines, wrapped to `width`. */
116
+ export function buildTranscriptLines(
117
+ snap: SubagentSnapshot,
118
+ width: number,
119
+ theme: Theme,
120
+ ): string[] {
121
+ const out: string[] = [];
122
+
123
+ for (const item of snap.transcript) {
124
+ const before = out.length;
125
+ if (item.kind === "user") {
126
+ renderUserText(theme, item.text, width, out);
127
+ } else if (item.kind === "assistant") {
128
+ renderAssistantItem(theme, item, width, out);
129
+ } else {
130
+ renderToolResultItem(theme, item, width, out);
131
+ }
132
+ if (out.length > before) out.push("");
133
+ }
134
+ while (out.length > 0 && out[out.length - 1] === "") out.pop();
135
+
136
+ // Live streaming assistant buffers (cleared when the finalized message lands).
137
+ if (snap.liveAssistant) {
138
+ const { thinking, text } = snap.liveAssistant;
139
+ const before = out.length;
140
+ if (out.length > 0) out.push("");
141
+ if (thinking.trim()) renderThinking(theme, thinking, width, out);
142
+ if (text.trim())
143
+ out.push(...wrapTextWithAnsi(sanitizeText(text).trim(), width));
144
+ if (out.length === before + 1) out.pop();
145
+ }
146
+
147
+ // Live tool executions (present until the ToolEnd lands in the transcript).
148
+ for (const tool of snap.liveTools) {
149
+ if (out.length > 0) out.push("");
150
+ const marker = tool.done
151
+ ? tool.isError
152
+ ? theme.fg("error", "error")
153
+ : theme.fg("success", "done")
154
+ : theme.fg("warning", "running");
155
+ let line = `${theme.fg("toolTitle", tool.name)} · ${marker}`;
156
+ const preview = tool.outputPreview && sanitizeText(tool.outputPreview);
157
+ if (preview) line += theme.fg("dim", ` · ${preview}`);
158
+ out.push(truncateToWidth(line, width));
159
+ }
160
+
161
+ // Queued steering/follow-up messages: show them immediately so Enter
162
+ // visibly acknowledges the user's input instead of appearing to do nothing.
163
+ for (const message of snap.queued) {
164
+ if (out.length > 0) out.push("");
165
+ const prefix = theme.fg("warning", `> [queued ${message.kind}] `);
166
+ const wrapped = wrapTextWithAnsi(
167
+ sanitizeText(message.text),
168
+ Math.max(10, width - visibleWidth(prefix)),
169
+ );
170
+ for (let i = 0; i < wrapped.length; i++) {
171
+ out.push(
172
+ truncateToWidth(
173
+ (i === 0 ? prefix : " ".repeat(visibleWidth(prefix))) +
174
+ theme.fg("muted", wrapped[i]),
175
+ width,
176
+ ),
177
+ );
178
+ }
179
+ }
180
+
181
+ return out;
182
+ }
@@ -0,0 +1,175 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
+
7
+ export interface WorktreeInfo {
8
+ baseCwd: string;
9
+ repoRoot: string;
10
+ path: string;
11
+ branch: string;
12
+ /** Commit from which the isolated branch was created. Optional for v1 records. */
13
+ baseCommit?: string;
14
+ }
15
+
16
+ export const WORKTREE_ACTIONS = ["inspect", "patch", "cherry-pick", "merge", "discard"] as const;
17
+ export type WorktreeAction = (typeof WORKTREE_ACTIONS)[number];
18
+
19
+ type Exec = ExtensionAPI["exec"];
20
+
21
+ function commandError(command: string, result: { code: number | null; stderr: string; stdout: string }) {
22
+ const detail = (result.stderr || result.stdout).trim();
23
+ return new Error(`${command} failed${detail ? `: ${detail}` : ""}`);
24
+ }
25
+
26
+ async function checked(exec: Exec, command: string, args: string[]) {
27
+ const result = await exec(command, args);
28
+ if (result.code !== 0) throw commandError(`${command} ${args.join(" ")}`, result);
29
+ return result.stdout.trim();
30
+ }
31
+
32
+ export async function createAgentWorktree(exec: Exec, cwd: string, label: string): Promise<WorktreeInfo> {
33
+ const repoRoot = await checked(exec, "git", ["-C", cwd, "rev-parse", "--show-toplevel"]);
34
+ const sourceStatus = await checked(exec, "git", ["-C", repoRoot, "status", "--porcelain", "--untracked-files=all"]);
35
+ if (sourceStatus) {
36
+ throw new Error(
37
+ "Worktree isolation requires a clean source checkout because isolated branches start from HEAD and cannot safely inherit uncommitted changes. Commit or stash the changes, or use the shared workspace.",
38
+ );
39
+ }
40
+ const baseCommit = await checked(exec, "git", ["-C", cwd, "rev-parse", "HEAD"]);
41
+ const repoHash = createHash("sha256").update(repoRoot).digest("hex").slice(0, 12);
42
+ const slug = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "agent";
43
+ const suffix = randomUUID().slice(0, 8);
44
+ const worktreePath = path.join(getAgentDir(), "subagent-worktrees", repoHash, `${slug}-${suffix}`);
45
+ const branch = `pi-subagent/${slug}-${suffix}`;
46
+ fs.mkdirSync(path.dirname(worktreePath), { recursive: true });
47
+ const result = await exec("git", ["-C", repoRoot, "worktree", "add", "-b", branch, worktreePath, baseCommit]);
48
+ if (result.code !== 0) throw commandError("git worktree add", result);
49
+ return { baseCwd: cwd, repoRoot, path: worktreePath, branch, baseCommit };
50
+ }
51
+
52
+ export async function discardAgentWorktree(exec: Exec, info: WorktreeInfo) {
53
+ const remove = await exec("git", ["-C", info.repoRoot, "worktree", "remove", "--force", info.path]);
54
+ if (remove.code !== 0) throw commandError("git worktree remove", remove);
55
+ const branch = await exec("git", ["-C", info.repoRoot, "branch", "-D", info.branch]);
56
+ if (branch.code !== 0) throw commandError("git branch -D", branch);
57
+ }
58
+
59
+ export async function inspectAgentWorktree(exec: Exec, info: WorktreeInfo) {
60
+ const baseCommit = info.baseCommit ?? await checked(exec, "git", ["-C", info.repoRoot, "merge-base", "HEAD", info.branch]);
61
+ const status = await checked(exec, "git", ["-C", info.path, "status", "--porcelain"]);
62
+ const names = await checked(exec, "git", ["-C", info.path, "diff", "--name-only", baseCommit]);
63
+ const untracked = status.split(/\r?\n/).filter((line) => line.startsWith("?? ")).map((line) => line.slice(3));
64
+ const commitsText = await checked(exec, "git", ["-C", info.repoRoot, "rev-list", "--reverse", `${baseCommit}..${info.branch}`]);
65
+ return {
66
+ baseCommit,
67
+ dirty: !!status,
68
+ files: [...new Set([...names.split(/\r?\n/).filter(Boolean), ...untracked])],
69
+ commits: commitsText.split(/\r?\n/).filter(Boolean),
70
+ };
71
+ }
72
+
73
+ export async function applyAgentWorktreeTo(
74
+ exec: Exec,
75
+ info: WorktreeInfo,
76
+ targetCwd: string,
77
+ ) {
78
+ const mark = await exec("git", ["-C", info.path, "add", "-N", "--", "."]);
79
+ if (mark.code !== 0) throw commandError("git add -N", mark);
80
+ const baseCommit = info.baseCommit ?? await checked(exec, "git", ["-C", info.repoRoot, "merge-base", "HEAD", info.branch]);
81
+ const diff = await exec("git", ["-C", info.path, "diff", "--binary", baseCommit]);
82
+ if (diff.code !== 0) throw commandError("git diff", diff);
83
+ if (!diff.stdout.trim()) return { action: "patch" as const, changed: false, files: [] as string[] };
84
+
85
+ const names = await checked(exec, "git", ["-C", info.path, "diff", "--name-only", baseCommit]);
86
+ const patchPath = path.join(getAgentDir(), "subagent-worktrees", `.apply-${randomUUID()}.patch`);
87
+ fs.writeFileSync(patchPath, diff.stdout, { mode: 0o600 });
88
+ try {
89
+ const check = await exec("git", ["-C", targetCwd, "apply", "--check", "--whitespace=nowarn", patchPath]);
90
+ if (check.code !== 0) throw commandError("git apply --check", check);
91
+ const apply = await exec("git", ["-C", targetCwd, "apply", "--whitespace=nowarn", patchPath]);
92
+ if (apply.code !== 0) throw commandError("git apply", apply);
93
+ } finally {
94
+ fs.rmSync(patchPath, { force: true });
95
+ }
96
+ return { action: "patch" as const, changed: true, files: names.split(/\r?\n/).filter(Boolean) };
97
+ }
98
+
99
+ export function applyAgentWorktree(exec: Exec, info: WorktreeInfo) {
100
+ return applyAgentWorktreeTo(exec, info, info.repoRoot);
101
+ }
102
+
103
+ async function commitDirtyWorktree(exec: Exec, info: WorktreeInfo, label: string) {
104
+ const status = await checked(exec, "git", ["-C", info.path, "status", "--porcelain"]);
105
+ if (!status) return;
106
+ await checked(exec, "git", ["-C", info.path, "add", "-A", "--", "."]);
107
+ await checked(exec, "git", [
108
+ "-C", info.path,
109
+ "-c", "user.name=Pi Subagent",
110
+ "-c", "user.email=pi-subagent@localhost",
111
+ "commit", "-m", `Apply ${label} subagent changes`,
112
+ ]);
113
+ }
114
+
115
+ async function ensureCleanSource(exec: Exec, info: WorktreeInfo) {
116
+ const status = await checked(exec, "git", ["-C", info.repoRoot, "status", "--porcelain"]);
117
+ if (status) {
118
+ throw new Error("The source repository must be clean for cherry-pick or merge. Use action=patch to preserve unrelated working-tree changes.");
119
+ }
120
+ }
121
+
122
+ async function integrationArgs(
123
+ exec: Exec,
124
+ info: WorktreeInfo,
125
+ action: "cherry-pick" | "merge",
126
+ ) {
127
+ const inspected = await inspectAgentWorktree(exec, info);
128
+ if (inspected.commits.length === 0) return { inspected, args: [] as string[] };
129
+ if (action === "merge") return { inspected, args: ["merge", "--no-ff", "--no-edit", info.branch] };
130
+ return { inspected, args: ["cherry-pick", ...inspected.commits] };
131
+ }
132
+
133
+ /** Preflight commit-aware integration in a temporary worktree before touching the source checkout. */
134
+ export async function integrateAgentWorktree(
135
+ exec: Exec,
136
+ info: WorktreeInfo,
137
+ action: "cherry-pick" | "merge",
138
+ label: string,
139
+ ) {
140
+ await ensureCleanSource(exec, info);
141
+ await commitDirtyWorktree(exec, info, label);
142
+ const { inspected, args } = await integrationArgs(exec, info, action);
143
+ if (args.length === 0) {
144
+ return { action, changed: false, files: inspected.files, commits: [] as string[] };
145
+ }
146
+
147
+ const preflightPath = path.join(getAgentDir(), "subagent-worktrees", `.preflight-${randomUUID()}`);
148
+ fs.mkdirSync(path.dirname(preflightPath), { recursive: true });
149
+ await checked(exec, "git", ["-C", info.repoRoot, "worktree", "add", "--detach", preflightPath, "HEAD"]);
150
+ try {
151
+ const preflight = await exec("git", [
152
+ "-C", preflightPath,
153
+ "-c", "user.name=Pi Subagent",
154
+ "-c", "user.email=pi-subagent@localhost",
155
+ ...args,
156
+ ]);
157
+ if (preflight.code !== 0) {
158
+ throw new Error(`Preflight found ${action} conflicts; the source repository was not changed. ${(preflight.stderr || preflight.stdout).trim()}`);
159
+ }
160
+ } finally {
161
+ await exec("git", ["-C", info.repoRoot, "worktree", "remove", "--force", preflightPath]);
162
+ }
163
+
164
+ const applied = await exec("git", [
165
+ "-C", info.repoRoot,
166
+ "-c", "user.name=Pi Subagent",
167
+ "-c", "user.email=pi-subagent@localhost",
168
+ ...args,
169
+ ]);
170
+ if (applied.code !== 0) {
171
+ await exec("git", ["-C", info.repoRoot, action === "merge" ? "merge" : "cherry-pick", "--abort"]);
172
+ throw commandError(`git ${action}`, applied);
173
+ }
174
+ return { action, changed: true, files: inspected.files, commits: inspected.commits };
175
+ }
@@ -0,0 +1,13 @@
1
+ # Web Fetch
2
+
3
+ Pi local extension that registers `web_fetch`, a read-only exact-URL retrieval tool.
4
+
5
+ Use `web_search` for discovery and `web_fetch` for retrieval from a specific URL.
6
+
7
+ Parameters:
8
+
9
+ - `url` — required `http://` or `https://` URL
10
+ - `format` — `markdown` (default), `text`, or `html`
11
+ - `timeoutSeconds` — default 30, max 120
12
+ - `maxBytes` — default 5 MiB, max 25 MiB
13
+ - `userAgent` — optional override