@shanepadgett/tau-agent 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 (138) hide show
  1. package/README.md +30 -0
  2. package/docs/extending-tau-agent.md +119 -0
  3. package/docs/subagents.md +57 -0
  4. package/docs/tui.md +107 -0
  5. package/extensions/appshot/README.md +11 -0
  6. package/extensions/appshot/capture.swift +191 -0
  7. package/extensions/appshot/index.ts +245 -0
  8. package/extensions/appshot/native-helper.ts +102 -0
  9. package/extensions/attention/README.md +23 -0
  10. package/extensions/attention/index.ts +71 -0
  11. package/extensions/auto-name/README.md +26 -0
  12. package/extensions/auto-name/index.ts +163 -0
  13. package/extensions/branch/README.md +22 -0
  14. package/extensions/branch/index.ts +145 -0
  15. package/extensions/branch/panel.ts +148 -0
  16. package/extensions/clear-screen/README.md +29 -0
  17. package/extensions/clear-screen/index.ts +75 -0
  18. package/extensions/commit/README.md +15 -0
  19. package/extensions/commit/commit-plan.ts +328 -0
  20. package/extensions/commit/git-change-set.ts +268 -0
  21. package/extensions/commit/index.ts +138 -0
  22. package/extensions/commit/review-ui.ts +745 -0
  23. package/extensions/explore/README.md +7 -0
  24. package/extensions/explore/autoread.ts +140 -0
  25. package/extensions/explore/find.ts +147 -0
  26. package/extensions/explore/grep.ts +749 -0
  27. package/extensions/explore/index.ts +17 -0
  28. package/extensions/explore/limits.ts +22 -0
  29. package/extensions/explore/ls.ts +113 -0
  30. package/extensions/explore/path-display.ts +41 -0
  31. package/extensions/explore/path-tree.ts +150 -0
  32. package/extensions/explore/read.ts +160 -0
  33. package/extensions/explore/result.ts +29 -0
  34. package/extensions/explore/traverse.ts +226 -0
  35. package/extensions/footer/README.md +23 -0
  36. package/extensions/footer/index.ts +534 -0
  37. package/extensions/footer/settings.ts +13 -0
  38. package/extensions/ideas/README.md +10 -0
  39. package/extensions/ideas/browser.ts +66 -0
  40. package/extensions/ideas/index.ts +31 -0
  41. package/extensions/ideas/store.ts +32 -0
  42. package/extensions/image-gen/README.md +11 -0
  43. package/extensions/image-gen/client.ts +186 -0
  44. package/extensions/image-gen/index.ts +152 -0
  45. package/extensions/manage-sessions/README.md +12 -0
  46. package/extensions/manage-sessions/index.ts +58 -0
  47. package/extensions/manage-sessions/manager-ui.ts +323 -0
  48. package/extensions/manage-sessions/sessions.ts +126 -0
  49. package/extensions/patch/README.md +48 -0
  50. package/extensions/patch/executor.ts +355 -0
  51. package/extensions/patch/index.ts +170 -0
  52. package/extensions/patch/matcher.ts +236 -0
  53. package/extensions/patch/parser.ts +347 -0
  54. package/extensions/patch/render.ts +247 -0
  55. package/extensions/patch/summary.ts +35 -0
  56. package/extensions/publish/README.md +11 -0
  57. package/extensions/publish/index.ts +225 -0
  58. package/extensions/qna/README.md +45 -0
  59. package/extensions/qna/additional-context-body.ts +51 -0
  60. package/extensions/qna/body-render.ts +6 -0
  61. package/extensions/qna/choice-question-body.ts +322 -0
  62. package/extensions/qna/index.ts +245 -0
  63. package/extensions/qna/inline-editor-row.ts +56 -0
  64. package/extensions/qna/input-question-body.ts +89 -0
  65. package/extensions/qna/model.ts +317 -0
  66. package/extensions/qna/panel.ts +240 -0
  67. package/extensions/qna/ui.ts +16 -0
  68. package/extensions/reference/README.md +47 -0
  69. package/extensions/reference/index.ts +76 -0
  70. package/extensions/reference/panel.ts +874 -0
  71. package/extensions/reference/settings.ts +29 -0
  72. package/extensions/run-summary/README.md +5 -0
  73. package/extensions/run-summary/index.ts +98 -0
  74. package/extensions/silent-command-runner/README.md +25 -0
  75. package/extensions/silent-command-runner/index.ts +468 -0
  76. package/extensions/silent-command-runner/settings.ts +65 -0
  77. package/extensions/soul/README.md +19 -0
  78. package/extensions/soul/index.ts +20 -0
  79. package/extensions/soul/prompt.ts +254 -0
  80. package/extensions/soul/settings.ts +13 -0
  81. package/extensions/stash/README.md +48 -0
  82. package/extensions/stash/browser.ts +51 -0
  83. package/extensions/stash/index.ts +55 -0
  84. package/extensions/stash/store.ts +36 -0
  85. package/extensions/subagent/README.md +34 -0
  86. package/extensions/subagent/agents/scout.md +85 -0
  87. package/extensions/subagent/agents/web-research.md +95 -0
  88. package/extensions/subagent/agents.ts +196 -0
  89. package/extensions/subagent/index.ts +214 -0
  90. package/extensions/subagent/render.ts +89 -0
  91. package/extensions/subagent/run.ts +325 -0
  92. package/extensions/tau/README.md +17 -0
  93. package/extensions/tau/index.ts +122 -0
  94. package/extensions/tau-help/README.md +3 -0
  95. package/extensions/tau-help/help.md +125 -0
  96. package/extensions/tau-help/index.ts +68 -0
  97. package/extensions/turn-budget/README.md +14 -0
  98. package/extensions/turn-budget/index.ts +150 -0
  99. package/extensions/turn-budget/settings.ts +35 -0
  100. package/extensions/web/README.md +13 -0
  101. package/extensions/web/codesearch.ts +79 -0
  102. package/extensions/web/exa.ts +101 -0
  103. package/extensions/web/html.ts +67 -0
  104. package/extensions/web/index.ts +13 -0
  105. package/extensions/web/limits.ts +11 -0
  106. package/extensions/web/tool-output.ts +54 -0
  107. package/extensions/web/webfetch.ts +177 -0
  108. package/extensions/web/websearch.ts +100 -0
  109. package/package.json +57 -0
  110. package/prompts/.gitkeep +1 -0
  111. package/prompts/cavemanify.md +25 -0
  112. package/prompts/implement.md +25 -0
  113. package/prompts/interview.md +48 -0
  114. package/prompts/plan-feature.md +30 -0
  115. package/prompts/plan-implementation.md +50 -0
  116. package/schemas/tau.schema.json +205 -0
  117. package/shared/agent-blocked.ts +12 -0
  118. package/shared/description.ts +28 -0
  119. package/shared/events.ts +135 -0
  120. package/shared/git.ts +41 -0
  121. package/shared/injected-context.ts +69 -0
  122. package/shared/jsonl-store.ts +108 -0
  123. package/shared/model-fallback/index.ts +278 -0
  124. package/shared/model-fallback/settings.ts +18 -0
  125. package/shared/model-fallback/types.ts +8 -0
  126. package/shared/ranges.ts +21 -0
  127. package/shared/settings/define.ts +15 -0
  128. package/shared/settings/files.ts +20 -0
  129. package/shared/settings/json.ts +41 -0
  130. package/shared/settings/load.ts +59 -0
  131. package/shared/settings/merge.ts +21 -0
  132. package/shared/settings/paths.ts +41 -0
  133. package/shared/settings/schema.ts +25 -0
  134. package/shared/settings/specs.ts +23 -0
  135. package/shared/text.ts +11 -0
  136. package/shared/tool-row-state.ts +43 -0
  137. package/skills/writing-preferences/SKILL.md +136 -0
  138. package/themes/.gitkeep +1 -0
@@ -0,0 +1,186 @@
1
+ const CODEX_IMAGES_URL = "https://chatgpt.com/backend-api/codex/images";
2
+ const MODEL = "gpt-image-2";
3
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
4
+ const MAX_ERROR_BODY_BYTES = 8192;
5
+ const MAX_ERROR_MESSAGE_LENGTH = 2000;
6
+
7
+ interface FetchResponse {
8
+ ok: boolean;
9
+ status: number;
10
+ body: ReadableStream<Uint8Array> | null;
11
+ json(): Promise<unknown>;
12
+ }
13
+
14
+ export interface CodexAuth {
15
+ token: string;
16
+ accountId: string;
17
+ }
18
+
19
+ export interface EditImage {
20
+ mimeType: "image/png" | "image/jpeg" | "image/webp";
21
+ data: string;
22
+ }
23
+
24
+ interface GeneratedImage {
25
+ bytes: Buffer;
26
+ base64: string;
27
+ mimeType: "image/png";
28
+ }
29
+
30
+ function isRecord(value: unknown): value is Record<string, unknown> {
31
+ return typeof value === "object" && value !== null && !Array.isArray(value);
32
+ }
33
+
34
+ export function resolveCodexAuth(token: string): CodexAuth {
35
+ const invalidCredential = () =>
36
+ new Error("The OpenAI Codex credential does not contain a usable ChatGPT account ID. Run /login again.");
37
+ const segments = token.split(".");
38
+ if (segments.length !== 3 || !segments[1]) throw invalidCredential();
39
+ if (!/^[A-Za-z0-9_-]+$/.test(segments[1]) || segments[1].length % 4 === 1) throw invalidCredential();
40
+
41
+ let payload: unknown;
42
+ try {
43
+ const decoded = Buffer.from(segments[1], "base64url");
44
+ if (decoded.toString("base64url") !== segments[1]) throw invalidCredential();
45
+ payload = JSON.parse(decoded.toString("utf8"));
46
+ } catch {
47
+ throw invalidCredential();
48
+ }
49
+ if (!isRecord(payload)) throw invalidCredential();
50
+ const authClaim = payload["https://api.openai.com/auth"];
51
+ if (!isRecord(authClaim)) throw invalidCredential();
52
+ const accountId = authClaim.chatgpt_account_id;
53
+ if (typeof accountId !== "string" || !accountId.trim()) throw invalidCredential();
54
+ return { token, accountId: accountId.trim() };
55
+ }
56
+
57
+ async function readBoundedError(response: FetchResponse): Promise<string> {
58
+ if (!response.body) return "";
59
+ const reader = response.body.getReader();
60
+ const chunks: Uint8Array[] = [];
61
+ let length = 0;
62
+ try {
63
+ while (length < MAX_ERROR_BODY_BYTES) {
64
+ const result = await reader.read();
65
+ if (result.done) break;
66
+ const remaining = MAX_ERROR_BODY_BYTES - length;
67
+ const chunk = result.value.subarray(0, remaining);
68
+ chunks.push(chunk);
69
+ length += chunk.length;
70
+ if (chunk.length < result.value.length) break;
71
+ }
72
+ } finally {
73
+ await reader.cancel().catch(() => undefined);
74
+ }
75
+ const bytes = new Uint8Array(length);
76
+ let offset = 0;
77
+ for (const chunk of chunks) {
78
+ bytes.set(chunk, offset);
79
+ offset += chunk.length;
80
+ }
81
+ return new TextDecoder().decode(bytes);
82
+ }
83
+
84
+ function serverErrorMessage(body: string, token: string): string {
85
+ let message = body.trim();
86
+ try {
87
+ const parsed: unknown = JSON.parse(message);
88
+ if (isRecord(parsed)) {
89
+ const error = parsed.error;
90
+ if (isRecord(error) && typeof error.message === "string") message = error.message;
91
+ else if (typeof parsed.message === "string") message = parsed.message;
92
+ }
93
+ } catch {
94
+ // Plain-text error body.
95
+ }
96
+ return message.replaceAll(token, "[redacted]").slice(0, MAX_ERROR_MESSAGE_LENGTH).trim();
97
+ }
98
+
99
+ function decodeImageResponse(value: unknown): GeneratedImage {
100
+ if (!isRecord(value) || !Array.isArray(value.data) || value.data.length === 0 || !isRecord(value.data[0])) {
101
+ throw new Error("OpenAI Codex returned an invalid image response");
102
+ }
103
+ const encoded = value.data[0].b64_json;
104
+ if (typeof encoded !== "string" || !encoded.trim()) {
105
+ throw new Error("OpenAI Codex returned an invalid image response");
106
+ }
107
+ const base64 = encoded.trim();
108
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64) || base64.length % 4 === 1) {
109
+ throw new Error("OpenAI Codex returned invalid base64 image data");
110
+ }
111
+ const bytes = Buffer.from(base64, "base64");
112
+ if (bytes.toString("base64").replace(/=+$/, "") !== base64.replace(/=+$/, "")) {
113
+ throw new Error("OpenAI Codex returned invalid base64 image data");
114
+ }
115
+ if (bytes.length < PNG_SIGNATURE.length || !bytes.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) {
116
+ throw new Error("OpenAI Codex returned image data that is not PNG");
117
+ }
118
+ return { bytes, base64: bytes.toString("base64"), mimeType: "image/png" };
119
+ }
120
+
121
+ async function requestImage(
122
+ operation: "generation" | "edit",
123
+ body: Record<string, unknown>,
124
+ auth: CodexAuth,
125
+ signal: AbortSignal | undefined,
126
+ ): Promise<GeneratedImage> {
127
+ const route = operation === "generation" ? "generations" : "edits";
128
+ const response = (await fetch(`${CODEX_IMAGES_URL}/${route}`, {
129
+ method: "POST",
130
+ headers: {
131
+ Authorization: `Bearer ${auth.token}`,
132
+ "chatgpt-account-id": auth.accountId,
133
+ originator: "pi",
134
+ Accept: "application/json",
135
+ "Content-Type": "application/json",
136
+ },
137
+ body: JSON.stringify(body),
138
+ signal,
139
+ })) as FetchResponse;
140
+ if (!response.ok) {
141
+ const message = serverErrorMessage(await readBoundedError(response), auth.token);
142
+ throw new Error(`Image ${operation} failed with status ${response.status}${message ? `: ${message}` : ""}`);
143
+ }
144
+
145
+ let value: unknown;
146
+ try {
147
+ value = await response.json();
148
+ } catch {
149
+ throw new Error("OpenAI Codex returned a non-JSON image response");
150
+ }
151
+ return decodeImageResponse(value);
152
+ }
153
+
154
+ export function generateImage(
155
+ prompt: string,
156
+ auth: CodexAuth,
157
+ signal: AbortSignal | undefined,
158
+ ): Promise<GeneratedImage> {
159
+ return requestImage(
160
+ "generation",
161
+ { prompt, model: MODEL, background: "auto", quality: "auto", size: "auto" },
162
+ auth,
163
+ signal,
164
+ );
165
+ }
166
+
167
+ export function editImage(
168
+ prompt: string,
169
+ images: readonly EditImage[],
170
+ auth: CodexAuth,
171
+ signal: AbortSignal | undefined,
172
+ ): Promise<GeneratedImage> {
173
+ return requestImage(
174
+ "edit",
175
+ {
176
+ images: images.map((image) => ({ image_url: `data:${image.mimeType};base64,${image.data}` })),
177
+ prompt,
178
+ model: MODEL,
179
+ background: "auto",
180
+ quality: "auto",
181
+ size: "auto",
182
+ },
183
+ auth,
184
+ signal,
185
+ );
186
+ }
@@ -0,0 +1,152 @@
1
+ import { defineTool, withFileMutationQueue, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { randomUUID } from "node:crypto";
3
+ import { link, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
6
+ import { type Static, Type } from "typebox";
7
+ import { editImage, generateImage, resolveCodexAuth, type EditImage } from "./client.ts";
8
+
9
+ const MODEL = "gpt-image-2";
10
+ const MAX_INPUT_BYTES = 50 * 1024 * 1024;
11
+ const MAX_INLINE_BYTES = 12 * 1024 * 1024;
12
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
13
+
14
+ const imageGenSchema = Type.Object(
15
+ {
16
+ prompt: Type.String({ minLength: 1 }),
17
+ path: Type.Optional(
18
+ Type.String({ description: "Explicit PNG destination path; defaults to Tau's external image store" }),
19
+ ),
20
+ referenced_image_paths: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { minItems: 1, maxItems: 5 })),
21
+ },
22
+ { additionalProperties: false },
23
+ );
24
+
25
+ type ImageGenParams = Static<typeof imageGenSchema>;
26
+
27
+ interface ImageGenDetails {
28
+ path: string;
29
+ model: typeof MODEL;
30
+ operation: "generate" | "edit";
31
+ }
32
+
33
+ function detectImageMimeType(bytes: Buffer): EditImage["mimeType"] | undefined {
34
+ if (bytes.length >= PNG_SIGNATURE.length && bytes.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) {
35
+ return "image/png";
36
+ }
37
+ if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return "image/jpeg";
38
+ if (
39
+ bytes.length >= 12 &&
40
+ bytes.subarray(0, 4).toString("ascii") === "RIFF" &&
41
+ bytes.subarray(8, 12).toString("ascii") === "WEBP"
42
+ ) {
43
+ return "image/webp";
44
+ }
45
+ return undefined;
46
+ }
47
+
48
+ export default function imageGenExtension(pi: ExtensionAPI): void {
49
+ pi.registerTool(
50
+ defineTool<typeof imageGenSchema, ImageGenDetails | undefined>({
51
+ name: "image_gen",
52
+ label: "Image Generation",
53
+ description:
54
+ "Generate a new raster image from a prompt, or edit one to five local raster images. Uses the existing OpenAI Codex OAuth login, saves a PNG under Tau's external image store unless an explicit path is provided, and returns the image for inspection.",
55
+ promptSnippet: "Generate or edit raster images with OpenAI Codex",
56
+ promptGuidelines: [
57
+ "Use image_gen when the user asks for a generated raster image or an AI edit of local raster images.",
58
+ "Omit referenced_image_paths when image_gen should create a new image.",
59
+ "Pass one to five local paths in referenced_image_paths when image_gen should edit or compose existing images.",
60
+ "Omit path for temporary external storage. Pass path only when the user wants the generated image saved in their repository or another explicit location.",
61
+ ],
62
+ parameters: imageGenSchema,
63
+ async execute(_toolCallId, params: ImageGenParams, signal, onUpdate, ctx) {
64
+ signal?.throwIfAborted();
65
+ const prompt = params.prompt.trim();
66
+ if (!prompt) throw new Error("Image prompt cannot be empty");
67
+ const requestedPath = params.path?.startsWith("@") ? params.path.slice(1) : params.path;
68
+ if (requestedPath !== undefined && !requestedPath.trim()) throw new Error("Image path cannot be empty");
69
+ const absolutePath = requestedPath
70
+ ? isAbsolute(requestedPath)
71
+ ? requestedPath
72
+ : resolve(ctx.cwd, requestedPath)
73
+ : join(homedir(), ".local", "share", "tau-agent", "images", `image-${randomUUID()}.png`);
74
+ if (!absolutePath.toLowerCase().endsWith(".png")) throw new Error("Image path must end in .png");
75
+
76
+ const token = await ctx.modelRegistry.getApiKeyForProvider("openai-codex");
77
+ if (!token) {
78
+ throw new Error("OpenAI Codex OAuth is unavailable. Run /login and select OpenAI Codex.");
79
+ }
80
+ const auth = resolveCodexAuth(token);
81
+ const images: EditImage[] = [];
82
+ for (const path of params.referenced_image_paths ?? []) {
83
+ const rawPath = path.startsWith("@") ? path.slice(1) : path;
84
+ if (!rawPath.trim()) throw new Error("Referenced image path cannot be empty");
85
+ const absolutePath = isAbsolute(rawPath) ? rawPath : resolve(ctx.cwd, rawPath);
86
+ const metadata = await stat(absolutePath);
87
+ if (!metadata.isFile()) throw new Error(`Referenced image is not a file: ${absolutePath}`);
88
+ if (metadata.size > MAX_INPUT_BYTES) throw new Error(`Referenced image exceeds 50 MiB: ${absolutePath}`);
89
+ const bytes = await readFile(absolutePath);
90
+ if (bytes.length > MAX_INPUT_BYTES) throw new Error(`Referenced image exceeds 50 MiB: ${absolutePath}`);
91
+ const mimeType = detectImageMimeType(bytes);
92
+ if (!mimeType)
93
+ throw new Error(`Referenced image is not a supported PNG, JPEG, or WebP file: ${absolutePath}`);
94
+ images.push({ mimeType, data: bytes.toString("base64") });
95
+ }
96
+
97
+ const operation = images.length === 0 ? "generate" : "edit";
98
+ await onUpdate?.({
99
+ content: [
100
+ {
101
+ type: "text",
102
+ text:
103
+ operation === "generate"
104
+ ? `Generating image with ${MODEL}...`
105
+ : `Editing image with ${MODEL}...`,
106
+ },
107
+ ],
108
+ details: undefined,
109
+ });
110
+ const generated =
111
+ operation === "generate"
112
+ ? await generateImage(prompt, auth, signal)
113
+ : await editImage(prompt, images, auth, signal);
114
+ signal?.throwIfAborted();
115
+
116
+ const outputDirectory = dirname(absolutePath);
117
+ await withFileMutationQueue(absolutePath, async () => {
118
+ await mkdir(outputDirectory, { recursive: true });
119
+ const temporaryPath = join(outputDirectory, `.${basename(absolutePath)}.${randomUUID()}.tmp.png`);
120
+ try {
121
+ await writeFile(temporaryPath, generated.bytes, { flag: "wx" });
122
+ signal?.throwIfAborted();
123
+ await link(temporaryPath, absolutePath);
124
+ } finally {
125
+ await rm(temporaryPath, { force: true });
126
+ }
127
+ });
128
+
129
+ const verb = operation === "generate" ? "Generated" : "Edited";
130
+ const details: ImageGenDetails = { path: absolutePath, model: MODEL, operation };
131
+ if (generated.bytes.length > MAX_INLINE_BYTES) {
132
+ return {
133
+ content: [
134
+ {
135
+ type: "text",
136
+ text: `${verb} image saved to ${absolutePath}. The PNG exceeds the 12 MiB attachment limit, so it was not added to model context.`,
137
+ },
138
+ ],
139
+ details,
140
+ };
141
+ }
142
+ return {
143
+ content: [
144
+ { type: "text", text: `${verb} image saved to ${absolutePath}` },
145
+ { type: "image", data: generated.base64, mimeType: generated.mimeType },
146
+ ],
147
+ details,
148
+ };
149
+ },
150
+ }),
151
+ );
152
+ }
@@ -0,0 +1,12 @@
1
+ # Manage sessions
2
+
3
+ Clean up saved Pi sessions from the TUI.
4
+
5
+ ## Commands
6
+
7
+ - `/manage-sessions` opens a session manager with active and archived sessions.
8
+ - `/sweep` starts a new blank session, then archives or deletes the session you were just using.
9
+
10
+ The manager lets you archive, unarchive, and delete saved sessions in bulk. It can show sessions for the current folder or all projects.
11
+
12
+ Archived sessions stay out of the active session list and can be restored later.
@@ -0,0 +1,58 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { showSessionManager } from "./manager-ui.ts";
3
+ import { archiveSession, deleteSessionFile } from "./sessions.ts";
4
+
5
+ export default function manageSessionsExtension(pi: ExtensionAPI): void {
6
+ pi.registerCommand("manage-sessions", {
7
+ description: "Manage saved sessions in the TUI",
8
+ handler: async (_args, ctx) => {
9
+ await ctx.waitForIdle();
10
+
11
+ if (ctx.mode !== "tui") {
12
+ ctx.ui.notify("Session manager requires TUI mode.", "error");
13
+ return;
14
+ }
15
+
16
+ await showSessionManager(ctx);
17
+ },
18
+ });
19
+
20
+ pi.registerCommand("sweep", {
21
+ description: "Archive or delete the current session after starting a new one",
22
+ handler: async (_args, ctx) => {
23
+ await ctx.waitForIdle();
24
+
25
+ if (!ctx.hasUI) {
26
+ ctx.ui.notify("Sweep requires interactive UI.", "error");
27
+ return;
28
+ }
29
+
30
+ const sessionFile = ctx.sessionManager.getSessionFile();
31
+ if (!sessionFile) {
32
+ ctx.ui.notify("Current session is not persisted.", "error");
33
+ return;
34
+ }
35
+
36
+ const choice = await ctx.ui.select("Sweep current session:", ["Archive", "Delete"]);
37
+ if (choice !== "Archive" && choice !== "Delete") return;
38
+
39
+ const confirmed = await ctx.ui.confirm(`${choice} this session?`, "Will launch a new session.");
40
+ if (!confirmed) return;
41
+
42
+ await ctx.newSession({
43
+ parentSession: sessionFile,
44
+ withSession: async (newCtx) => {
45
+ try {
46
+ if (choice === "Archive") await archiveSession(sessionFile);
47
+ else await deleteSessionFile(sessionFile);
48
+
49
+ newCtx.ui.notify(`Session ${choice.toLowerCase()}d.`, "info");
50
+ } catch (error) {
51
+ const message = error instanceof Error ? error.message : String(error);
52
+ newCtx.ui.notify(`Sweep failed: ${message}`, "error");
53
+ }
54
+ },
55
+ });
56
+ },
57
+ });
58
+ }