@mariozechner/pi-coding-agent 0.49.1 → 0.49.3
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/CHANGELOG.md +50 -1
- package/README.md +5 -11
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +6 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/export-html/template.css +19 -0
- package/dist/core/export-html/template.js +70 -5
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/types.d.ts +10 -3
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +1 -3
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +11 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +5 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +3 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +3 -2
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +5 -3
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts +3 -2
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +4 -2
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts +3 -2
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +4 -2
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts +3 -2
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +4 -2
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +5 -0
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +9 -0
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +84 -38
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +7 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +2 -2
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts +2 -2
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +2 -2
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +10 -2
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +81 -38
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +7 -3
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +2 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +3 -2
- package/dist/utils/shell.js.map +1 -1
- package/docs/extensions.md +5 -3
- package/docs/tui.md +6 -3
- package/examples/extensions/README.md +3 -0
- package/examples/extensions/antigravity-image-gen.ts +413 -0
- package/examples/extensions/inline-bash.ts +94 -0
- package/examples/extensions/question.ts +9 -22
- package/examples/extensions/space-invaders.ts +560 -0
- package/examples/extensions/widget-placement.ts +17 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
package/docs/extensions.md
CHANGED
|
@@ -184,7 +184,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
184
184
|
const ok = await ctx.ui.confirm("Title", "Are you sure?");
|
|
185
185
|
ctx.ui.notify("Done!", "success");
|
|
186
186
|
ctx.ui.setStatus("my-ext", "Processing..."); // Footer status
|
|
187
|
-
ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]); // Widget above editor
|
|
187
|
+
ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]); // Widget above editor (default)
|
|
188
188
|
});
|
|
189
189
|
|
|
190
190
|
// Register tools, commands, shortcuts, flags
|
|
@@ -1366,7 +1366,7 @@ Extensions can interact with users via `ctx.ui` methods and customize how messag
|
|
|
1366
1366
|
- Settings toggles (SettingsList)
|
|
1367
1367
|
- Status indicators (setStatus)
|
|
1368
1368
|
- Working message during streaming (setWorkingMessage)
|
|
1369
|
-
- Widgets above editor (setWidget)
|
|
1369
|
+
- Widgets above/below editor (setWidget)
|
|
1370
1370
|
- Custom footers (setFooter)
|
|
1371
1371
|
|
|
1372
1372
|
### Dialogs
|
|
@@ -1456,8 +1456,10 @@ ctx.ui.setStatus("my-ext", undefined); // Clear
|
|
|
1456
1456
|
ctx.ui.setWorkingMessage("Thinking deeply...");
|
|
1457
1457
|
ctx.ui.setWorkingMessage(); // Restore default
|
|
1458
1458
|
|
|
1459
|
-
// Widget above editor (
|
|
1459
|
+
// Widget above editor (default)
|
|
1460
1460
|
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
|
|
1461
|
+
// Widget below editor
|
|
1462
|
+
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });
|
|
1461
1463
|
ctx.ui.setWidget("my-widget", (tui, theme) => new Text(theme.fg("accent", "Custom"), 0, 0));
|
|
1462
1464
|
ctx.ui.setWidget("my-widget", undefined); // Clear
|
|
1463
1465
|
|
package/docs/tui.md
CHANGED
|
@@ -704,14 +704,17 @@ ctx.ui.setStatus("my-ext", undefined);
|
|
|
704
704
|
|
|
705
705
|
**Examples:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
|
|
706
706
|
|
|
707
|
-
### Pattern 5:
|
|
707
|
+
### Pattern 5: Widgets Above/Below Editor
|
|
708
708
|
|
|
709
|
-
Show persistent content above the input editor. Good for todo lists, progress.
|
|
709
|
+
Show persistent content above or below the input editor. Good for todo lists, progress.
|
|
710
710
|
|
|
711
711
|
```typescript
|
|
712
|
-
// Simple string array
|
|
712
|
+
// Simple string array (above editor by default)
|
|
713
713
|
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
|
|
714
714
|
|
|
715
|
+
// Render below the editor
|
|
716
|
+
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });
|
|
717
|
+
|
|
715
718
|
// Or with theme
|
|
716
719
|
ctx.ui.setWidget("my-widget", (_tui, theme) => {
|
|
717
720
|
const lines = items.map((item, i) =>
|
|
@@ -34,6 +34,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
|
|
34
34
|
| `questionnaire.ts` | Multi-question input with tab bar navigation between questions |
|
|
35
35
|
| `tool-override.ts` | Override built-in tools (e.g., add logging/access control to `read`) |
|
|
36
36
|
| `truncated-tool.ts` | Wraps ripgrep with proper output truncation (50KB/2000 lines) |
|
|
37
|
+
| `antigravity-image-gen.ts` | Generate images via Google Antigravity with optional save-to-disk modes |
|
|
37
38
|
| `ssh.ts` | Delegate all tools to a remote machine via SSH using pluggable operations |
|
|
38
39
|
| `subagent/` | Delegate tasks to specialized subagents with isolated context windows |
|
|
39
40
|
|
|
@@ -47,6 +48,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
|
|
47
48
|
| `handoff.ts` | Transfer context to a new focused session via `/handoff <goal>` |
|
|
48
49
|
| `qna.ts` | Extracts questions from last response into editor via `ctx.ui.setEditorText()` |
|
|
49
50
|
| `status-line.ts` | Shows turn progress in footer via `ctx.ui.setStatus()` with themed colors |
|
|
51
|
+
| `widget-placement.ts` | Shows widgets above and below the editor via `ctx.ui.setWidget()` placement |
|
|
50
52
|
| `model-status.ts` | Shows model changes in status bar via `model_select` hook |
|
|
51
53
|
| `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence |
|
|
52
54
|
| `send-user-message.ts` | Demonstrates `pi.sendUserMessage()` for sending user messages from extensions |
|
|
@@ -62,6 +64,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
|
|
62
64
|
| `doom-overlay/` | DOOM game running as an overlay at 35 FPS (demonstrates real-time game rendering) |
|
|
63
65
|
| `shutdown-command.ts` | Adds `/quit` command demonstrating `ctx.shutdown()` |
|
|
64
66
|
| `interactive-shell.ts` | Run interactive commands (vim, htop) with full terminal via `user_bash` hook |
|
|
67
|
+
| `inline-bash.ts` | Expands `!{command}` patterns in prompts via `input` event transformation |
|
|
65
68
|
|
|
66
69
|
### Git Integration
|
|
67
70
|
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity Image Generation
|
|
3
|
+
*
|
|
4
|
+
* Generates images via Google Antigravity's image models (gemini-3-pro-image, imagen-3).
|
|
5
|
+
* Returns images as tool result attachments for inline terminal rendering.
|
|
6
|
+
* Requires OAuth login via /login for google-antigravity.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* "Generate an image of a sunset over mountains"
|
|
10
|
+
* "Create a 16:9 wallpaper of a cyberpunk city"
|
|
11
|
+
*
|
|
12
|
+
* Save modes (tool param, env var, or config file):
|
|
13
|
+
* save=none - Don't save to disk (default)
|
|
14
|
+
* save=project - Save to <repo>/.pi/generated-images/
|
|
15
|
+
* save=global - Save to ~/.pi/agent/generated-images/
|
|
16
|
+
* save=custom - Save to saveDir param or PI_IMAGE_SAVE_DIR
|
|
17
|
+
*
|
|
18
|
+
* Environment variables:
|
|
19
|
+
* PI_IMAGE_SAVE_MODE - Default save mode (none|project|global|custom)
|
|
20
|
+
* PI_IMAGE_SAVE_DIR - Directory for custom save mode
|
|
21
|
+
*
|
|
22
|
+
* Config files (project overrides global):
|
|
23
|
+
* ~/.pi/agent/extensions/antigravity-image-gen.json
|
|
24
|
+
* <repo>/.pi/extensions/antigravity-image-gen.json
|
|
25
|
+
* Example: { "save": "global" }
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { randomUUID } from "node:crypto";
|
|
29
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
30
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
31
|
+
import { homedir } from "node:os";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
34
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
35
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
36
|
+
|
|
37
|
+
const PROVIDER = "google-antigravity";
|
|
38
|
+
|
|
39
|
+
const ASPECT_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"] as const;
|
|
40
|
+
|
|
41
|
+
type AspectRatio = (typeof ASPECT_RATIOS)[number];
|
|
42
|
+
|
|
43
|
+
const DEFAULT_MODEL = "gemini-3-pro-image";
|
|
44
|
+
const DEFAULT_ASPECT_RATIO: AspectRatio = "1:1";
|
|
45
|
+
const DEFAULT_SAVE_MODE = "none";
|
|
46
|
+
|
|
47
|
+
const SAVE_MODES = ["none", "project", "global", "custom"] as const;
|
|
48
|
+
type SaveMode = (typeof SAVE_MODES)[number];
|
|
49
|
+
|
|
50
|
+
const ANTIGRAVITY_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
|
51
|
+
|
|
52
|
+
const ANTIGRAVITY_HEADERS = {
|
|
53
|
+
"User-Agent": "antigravity/1.11.5 darwin/arm64",
|
|
54
|
+
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
55
|
+
"Client-Metadata": JSON.stringify({
|
|
56
|
+
ideType: "IDE_UNSPECIFIED",
|
|
57
|
+
platform: "PLATFORM_UNSPECIFIED",
|
|
58
|
+
pluginType: "GEMINI",
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const IMAGE_SYSTEM_INSTRUCTION =
|
|
63
|
+
"You are an AI image generator. Generate images based on user descriptions. Focus on creating high-quality, visually appealing images that match the user's request.";
|
|
64
|
+
|
|
65
|
+
const TOOL_PARAMS = Type.Object({
|
|
66
|
+
prompt: Type.String({ description: "Image description." }),
|
|
67
|
+
model: Type.Optional(
|
|
68
|
+
Type.String({
|
|
69
|
+
description: "Image model id (e.g., gemini-3-pro-image, imagen-3). Default: gemini-3-pro-image.",
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
aspectRatio: Type.Optional(StringEnum(ASPECT_RATIOS)),
|
|
73
|
+
save: Type.Optional(StringEnum(SAVE_MODES)),
|
|
74
|
+
saveDir: Type.Optional(
|
|
75
|
+
Type.String({
|
|
76
|
+
description: "Directory to save image when save=custom. Defaults to PI_IMAGE_SAVE_DIR if set.",
|
|
77
|
+
}),
|
|
78
|
+
),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
type ToolParams = Static<typeof TOOL_PARAMS>;
|
|
82
|
+
|
|
83
|
+
interface CloudCodeAssistRequest {
|
|
84
|
+
project: string;
|
|
85
|
+
model: string;
|
|
86
|
+
request: {
|
|
87
|
+
contents: Content[];
|
|
88
|
+
sessionId?: string;
|
|
89
|
+
systemInstruction?: { role?: string; parts: { text: string }[] };
|
|
90
|
+
generationConfig?: {
|
|
91
|
+
maxOutputTokens?: number;
|
|
92
|
+
temperature?: number;
|
|
93
|
+
imageConfig?: { aspectRatio?: string };
|
|
94
|
+
candidateCount?: number;
|
|
95
|
+
};
|
|
96
|
+
safetySettings?: Array<{ category: string; threshold: string }>;
|
|
97
|
+
};
|
|
98
|
+
requestType?: string;
|
|
99
|
+
userAgent?: string;
|
|
100
|
+
requestId?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface CloudCodeAssistResponseChunk {
|
|
104
|
+
response?: {
|
|
105
|
+
candidates?: Array<{
|
|
106
|
+
content?: {
|
|
107
|
+
role: string;
|
|
108
|
+
parts?: Array<{
|
|
109
|
+
text?: string;
|
|
110
|
+
inlineData?: {
|
|
111
|
+
mimeType?: string;
|
|
112
|
+
data?: string;
|
|
113
|
+
};
|
|
114
|
+
}>;
|
|
115
|
+
};
|
|
116
|
+
}>;
|
|
117
|
+
usageMetadata?: {
|
|
118
|
+
promptTokenCount?: number;
|
|
119
|
+
candidatesTokenCount?: number;
|
|
120
|
+
thoughtsTokenCount?: number;
|
|
121
|
+
totalTokenCount?: number;
|
|
122
|
+
cachedContentTokenCount?: number;
|
|
123
|
+
};
|
|
124
|
+
modelVersion?: string;
|
|
125
|
+
responseId?: string;
|
|
126
|
+
};
|
|
127
|
+
traceId?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface Content {
|
|
131
|
+
role: "user" | "model";
|
|
132
|
+
parts: Part[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface Part {
|
|
136
|
+
text?: string;
|
|
137
|
+
inlineData?: {
|
|
138
|
+
mimeType?: string;
|
|
139
|
+
data?: string;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface ParsedCredentials {
|
|
144
|
+
accessToken: string;
|
|
145
|
+
projectId: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface ExtensionConfig {
|
|
149
|
+
save?: SaveMode;
|
|
150
|
+
saveDir?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface SaveConfig {
|
|
154
|
+
mode: SaveMode;
|
|
155
|
+
outputDir?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseOAuthCredentials(raw: string): ParsedCredentials {
|
|
159
|
+
let parsed: { token?: string; projectId?: string };
|
|
160
|
+
try {
|
|
161
|
+
parsed = JSON.parse(raw) as { token?: string; projectId?: string };
|
|
162
|
+
} catch {
|
|
163
|
+
throw new Error("Invalid Google OAuth credentials. Run /login to re-authenticate.");
|
|
164
|
+
}
|
|
165
|
+
if (!parsed.token || !parsed.projectId) {
|
|
166
|
+
throw new Error("Missing token or projectId in Google OAuth credentials. Run /login.");
|
|
167
|
+
}
|
|
168
|
+
return { accessToken: parsed.token, projectId: parsed.projectId };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function readConfigFile(path: string): ExtensionConfig {
|
|
172
|
+
if (!existsSync(path)) {
|
|
173
|
+
return {};
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const content = readFileSync(path, "utf-8");
|
|
177
|
+
const parsed = JSON.parse(content) as ExtensionConfig;
|
|
178
|
+
return parsed ?? {};
|
|
179
|
+
} catch {
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function loadConfig(cwd: string): ExtensionConfig {
|
|
185
|
+
const globalConfig = readConfigFile(join(homedir(), ".pi", "agent", "extensions", "antigravity-image-gen.json"));
|
|
186
|
+
const projectConfig = readConfigFile(join(cwd, ".pi", "extensions", "antigravity-image-gen.json"));
|
|
187
|
+
return { ...globalConfig, ...projectConfig };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function resolveSaveConfig(params: ToolParams, cwd: string): SaveConfig {
|
|
191
|
+
const config = loadConfig(cwd);
|
|
192
|
+
const envMode = (process.env.PI_IMAGE_SAVE_MODE || "").toLowerCase();
|
|
193
|
+
const paramMode = params.save;
|
|
194
|
+
const mode = (paramMode || envMode || config.save || DEFAULT_SAVE_MODE) as SaveMode;
|
|
195
|
+
|
|
196
|
+
if (!SAVE_MODES.includes(mode)) {
|
|
197
|
+
return { mode: DEFAULT_SAVE_MODE as SaveMode };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (mode === "project") {
|
|
201
|
+
return { mode, outputDir: join(cwd, ".pi", "generated-images") };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (mode === "global") {
|
|
205
|
+
return { mode, outputDir: join(homedir(), ".pi", "agent", "generated-images") };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (mode === "custom") {
|
|
209
|
+
const dir = params.saveDir || process.env.PI_IMAGE_SAVE_DIR || config.saveDir;
|
|
210
|
+
if (!dir || !dir.trim()) {
|
|
211
|
+
throw new Error("save=custom requires saveDir or PI_IMAGE_SAVE_DIR.");
|
|
212
|
+
}
|
|
213
|
+
return { mode, outputDir: dir };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { mode };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function imageExtension(mimeType: string): string {
|
|
220
|
+
const lower = mimeType.toLowerCase();
|
|
221
|
+
if (lower.includes("jpeg") || lower.includes("jpg")) return "jpg";
|
|
222
|
+
if (lower.includes("gif")) return "gif";
|
|
223
|
+
if (lower.includes("webp")) return "webp";
|
|
224
|
+
return "png";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function saveImage(base64Data: string, mimeType: string, outputDir: string): Promise<string> {
|
|
228
|
+
await mkdir(outputDir, { recursive: true });
|
|
229
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
230
|
+
const ext = imageExtension(mimeType);
|
|
231
|
+
const filename = `image-${timestamp}-${randomUUID().slice(0, 8)}.${ext}`;
|
|
232
|
+
const filePath = join(outputDir, filename);
|
|
233
|
+
await writeFile(filePath, Buffer.from(base64Data, "base64"));
|
|
234
|
+
return filePath;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function buildRequest(prompt: string, model: string, projectId: string, aspectRatio: string): CloudCodeAssistRequest {
|
|
238
|
+
return {
|
|
239
|
+
project: projectId,
|
|
240
|
+
model,
|
|
241
|
+
request: {
|
|
242
|
+
contents: [
|
|
243
|
+
{
|
|
244
|
+
role: "user",
|
|
245
|
+
parts: [{ text: prompt }],
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
systemInstruction: {
|
|
249
|
+
parts: [{ text: IMAGE_SYSTEM_INSTRUCTION }],
|
|
250
|
+
},
|
|
251
|
+
generationConfig: {
|
|
252
|
+
imageConfig: { aspectRatio },
|
|
253
|
+
candidateCount: 1,
|
|
254
|
+
},
|
|
255
|
+
safetySettings: [
|
|
256
|
+
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_ONLY_HIGH" },
|
|
257
|
+
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_ONLY_HIGH" },
|
|
258
|
+
{ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_ONLY_HIGH" },
|
|
259
|
+
{ category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_ONLY_HIGH" },
|
|
260
|
+
{ category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "BLOCK_ONLY_HIGH" },
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
requestType: "agent",
|
|
264
|
+
requestId: `agent-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
|
265
|
+
userAgent: "antigravity",
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function parseSseForImage(
|
|
270
|
+
response: Response,
|
|
271
|
+
signal?: AbortSignal,
|
|
272
|
+
): Promise<{ image: { data: string; mimeType: string }; text: string[] }> {
|
|
273
|
+
if (!response.body) {
|
|
274
|
+
throw new Error("No response body");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const reader = response.body.getReader();
|
|
278
|
+
const decoder = new TextDecoder();
|
|
279
|
+
let buffer = "";
|
|
280
|
+
const textParts: string[] = [];
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
while (true) {
|
|
284
|
+
if (signal?.aborted) {
|
|
285
|
+
throw new Error("Request was aborted");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const { done, value } = await reader.read();
|
|
289
|
+
if (done) break;
|
|
290
|
+
|
|
291
|
+
buffer += decoder.decode(value, { stream: true });
|
|
292
|
+
const lines = buffer.split("\n");
|
|
293
|
+
buffer = lines.pop() || "";
|
|
294
|
+
|
|
295
|
+
for (const line of lines) {
|
|
296
|
+
if (!line.startsWith("data:")) continue;
|
|
297
|
+
const jsonStr = line.slice(5).trim();
|
|
298
|
+
if (!jsonStr) continue;
|
|
299
|
+
|
|
300
|
+
let chunk: CloudCodeAssistResponseChunk;
|
|
301
|
+
try {
|
|
302
|
+
chunk = JSON.parse(jsonStr) as CloudCodeAssistResponseChunk;
|
|
303
|
+
} catch {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const responseData = chunk.response;
|
|
308
|
+
if (!responseData?.candidates) continue;
|
|
309
|
+
|
|
310
|
+
for (const candidate of responseData.candidates) {
|
|
311
|
+
const parts = candidate.content?.parts;
|
|
312
|
+
if (!parts) continue;
|
|
313
|
+
for (const part of parts) {
|
|
314
|
+
if (part.text) {
|
|
315
|
+
textParts.push(part.text);
|
|
316
|
+
}
|
|
317
|
+
if (part.inlineData?.data) {
|
|
318
|
+
await reader.cancel();
|
|
319
|
+
return {
|
|
320
|
+
image: {
|
|
321
|
+
data: part.inlineData.data,
|
|
322
|
+
mimeType: part.inlineData.mimeType || "image/png",
|
|
323
|
+
},
|
|
324
|
+
text: textParts,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} finally {
|
|
332
|
+
reader.releaseLock();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
throw new Error("No image data returned by the model");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function getCredentials(ctx: {
|
|
339
|
+
modelRegistry: { getApiKeyForProvider: (provider: string) => Promise<string | undefined> };
|
|
340
|
+
}): Promise<ParsedCredentials> {
|
|
341
|
+
const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER);
|
|
342
|
+
if (!apiKey) {
|
|
343
|
+
throw new Error("Missing Google Antigravity OAuth credentials. Run /login for google-antigravity.");
|
|
344
|
+
}
|
|
345
|
+
return parseOAuthCredentials(apiKey);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export default function antigravityImageGen(pi: ExtensionAPI) {
|
|
349
|
+
pi.registerTool({
|
|
350
|
+
name: "generate_image",
|
|
351
|
+
label: "Generate image",
|
|
352
|
+
description:
|
|
353
|
+
"Generate an image via Google Antigravity image models. Returns the image as a tool result attachment. Optional saving via save=project|global|custom|none, or PI_IMAGE_SAVE_MODE/PI_IMAGE_SAVE_DIR.",
|
|
354
|
+
parameters: TOOL_PARAMS,
|
|
355
|
+
async execute(_toolCallId, params: ToolParams, onUpdate, ctx, signal) {
|
|
356
|
+
const { accessToken, projectId } = await getCredentials(ctx);
|
|
357
|
+
const model = params.model || DEFAULT_MODEL;
|
|
358
|
+
const aspectRatio = params.aspectRatio || DEFAULT_ASPECT_RATIO;
|
|
359
|
+
|
|
360
|
+
const requestBody = buildRequest(params.prompt, model, projectId, aspectRatio);
|
|
361
|
+
onUpdate?.({
|
|
362
|
+
content: [{ type: "text", text: `Requesting image from ${PROVIDER}/${model}...` }],
|
|
363
|
+
details: { provider: PROVIDER, model, aspectRatio },
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const response = await fetch(`${ANTIGRAVITY_ENDPOINT}/v1internal:streamGenerateContent?alt=sse`, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
headers: {
|
|
369
|
+
Authorization: `Bearer ${accessToken}`,
|
|
370
|
+
"Content-Type": "application/json",
|
|
371
|
+
Accept: "text/event-stream",
|
|
372
|
+
...ANTIGRAVITY_HEADERS,
|
|
373
|
+
},
|
|
374
|
+
body: JSON.stringify(requestBody),
|
|
375
|
+
signal,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
if (!response.ok) {
|
|
379
|
+
const errorText = await response.text();
|
|
380
|
+
throw new Error(`Image request failed (${response.status}): ${errorText}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const parsed = await parseSseForImage(response, signal);
|
|
384
|
+
const saveConfig = resolveSaveConfig(params, ctx.cwd);
|
|
385
|
+
let savedPath: string | undefined;
|
|
386
|
+
let saveError: string | undefined;
|
|
387
|
+
if (saveConfig.mode !== "none" && saveConfig.outputDir) {
|
|
388
|
+
try {
|
|
389
|
+
savedPath = await saveImage(parsed.image.data, parsed.image.mimeType, saveConfig.outputDir);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
saveError = error instanceof Error ? error.message : String(error);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const summaryParts = [`Generated image via ${PROVIDER}/${model}.`, `Aspect ratio: ${aspectRatio}.`];
|
|
395
|
+
if (savedPath) {
|
|
396
|
+
summaryParts.push(`Saved image to: ${savedPath}`);
|
|
397
|
+
} else if (saveError) {
|
|
398
|
+
summaryParts.push(`Failed to save image: ${saveError}`);
|
|
399
|
+
}
|
|
400
|
+
if (parsed.text.length > 0) {
|
|
401
|
+
summaryParts.push(`Model notes: ${parsed.text.join(" ")}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
content: [
|
|
406
|
+
{ type: "text", text: summaryParts.join(" ") },
|
|
407
|
+
{ type: "image", data: parsed.image.data, mimeType: parsed.image.mimeType },
|
|
408
|
+
],
|
|
409
|
+
details: { provider: PROVIDER, model, aspectRatio, savedPath, saveMode: saveConfig.mode },
|
|
410
|
+
};
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline Bash Extension - expands inline bash commands in user prompts.
|
|
3
|
+
*
|
|
4
|
+
* Start pi with this extension:
|
|
5
|
+
* pi -e ./examples/extensions/inline-bash.ts
|
|
6
|
+
*
|
|
7
|
+
* Then type prompts with inline bash:
|
|
8
|
+
* What's in !{pwd}?
|
|
9
|
+
* The current branch is !{git branch --show-current} and status: !{git status --short}
|
|
10
|
+
* My node version is !{node --version}
|
|
11
|
+
*
|
|
12
|
+
* The !{command} patterns are executed and replaced with their output before
|
|
13
|
+
* the prompt is sent to the agent.
|
|
14
|
+
*
|
|
15
|
+
* Note: Regular !command syntax (whole-line bash) is preserved and works as before.
|
|
16
|
+
*/
|
|
17
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
18
|
+
|
|
19
|
+
export default function (pi: ExtensionAPI) {
|
|
20
|
+
const PATTERN = /!\{([^}]+)\}/g;
|
|
21
|
+
const TIMEOUT_MS = 30000;
|
|
22
|
+
|
|
23
|
+
pi.on("input", async (event, ctx) => {
|
|
24
|
+
const text = event.text;
|
|
25
|
+
|
|
26
|
+
// Don't process if it's a whole-line bash command (starts with !)
|
|
27
|
+
// This preserves the existing !command behavior
|
|
28
|
+
if (text.trimStart().startsWith("!") && !text.trimStart().startsWith("!{")) {
|
|
29
|
+
return { action: "continue" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check if there are any inline bash patterns
|
|
33
|
+
if (!PATTERN.test(text)) {
|
|
34
|
+
return { action: "continue" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Reset regex state after test()
|
|
38
|
+
PATTERN.lastIndex = 0;
|
|
39
|
+
|
|
40
|
+
let result = text;
|
|
41
|
+
const expansions: Array<{ command: string; output: string; error?: string }> = [];
|
|
42
|
+
|
|
43
|
+
// Find all matches first (to avoid issues with replacing while iterating)
|
|
44
|
+
const matches: Array<{ full: string; command: string }> = [];
|
|
45
|
+
let match = PATTERN.exec(text);
|
|
46
|
+
while (match) {
|
|
47
|
+
matches.push({ full: match[0], command: match[1] });
|
|
48
|
+
match = PATTERN.exec(text);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Execute each command and collect results
|
|
52
|
+
for (const { full, command } of matches) {
|
|
53
|
+
try {
|
|
54
|
+
const bashResult = await pi.exec("bash", ["-c", command], {
|
|
55
|
+
timeout: TIMEOUT_MS,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const output = bashResult.stdout || bashResult.stderr || "";
|
|
59
|
+
const trimmed = output.trim();
|
|
60
|
+
|
|
61
|
+
if (bashResult.code !== 0 && bashResult.stderr) {
|
|
62
|
+
expansions.push({
|
|
63
|
+
command,
|
|
64
|
+
output: trimmed,
|
|
65
|
+
error: `exit code ${bashResult.code}`,
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
expansions.push({ command, output: trimmed });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
result = result.replace(full, trimmed);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
74
|
+
expansions.push({ command, output: "", error: errorMsg });
|
|
75
|
+
result = result.replace(full, `[error: ${errorMsg}]`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Show what was expanded (if UI available)
|
|
80
|
+
if (ctx.hasUI && expansions.length > 0) {
|
|
81
|
+
const summary = expansions
|
|
82
|
+
.map((e) => {
|
|
83
|
+
const status = e.error ? ` (${e.error})` : "";
|
|
84
|
+
const preview = e.output.length > 50 ? `${e.output.slice(0, 50)}...` : e.output;
|
|
85
|
+
return `!{${e.command}}${status} -> "${preview}"`;
|
|
86
|
+
})
|
|
87
|
+
.join("\n");
|
|
88
|
+
|
|
89
|
+
ctx.ui.notify(`Expanded ${expansions.length} inline command(s):\n${summary}`, "info");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { action: "transform", text: result, images: event.images };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -22,28 +22,17 @@ interface QuestionDetails {
|
|
|
22
22
|
wasCustom?: boolean;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
const OptionSchema = Type.
|
|
27
|
-
Type.String(),
|
|
28
|
-
Type.
|
|
29
|
-
|
|
30
|
-
description: Type.Optional(Type.String({ description: "Optional description shown below label" })),
|
|
31
|
-
}),
|
|
32
|
-
]);
|
|
25
|
+
// Options with labels and optional descriptions
|
|
26
|
+
const OptionSchema = Type.Object({
|
|
27
|
+
label: Type.String({ description: "Display label for the option" }),
|
|
28
|
+
description: Type.Optional(Type.String({ description: "Optional description shown below label" })),
|
|
29
|
+
});
|
|
33
30
|
|
|
34
31
|
const QuestionParams = Type.Object({
|
|
35
32
|
question: Type.String({ description: "The question to ask the user" }),
|
|
36
33
|
options: Type.Array(OptionSchema, { description: "Options for the user to choose from" }),
|
|
37
34
|
});
|
|
38
35
|
|
|
39
|
-
// Normalize option to { label, description? }
|
|
40
|
-
function normalizeOption(opt: string | { label: string; description?: string }): OptionWithDesc {
|
|
41
|
-
if (typeof opt === "string") {
|
|
42
|
-
return { label: opt };
|
|
43
|
-
}
|
|
44
|
-
return opt;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
36
|
export default function question(pi: ExtensionAPI) {
|
|
48
37
|
pi.registerTool({
|
|
49
38
|
name: "question",
|
|
@@ -57,7 +46,7 @@ export default function question(pi: ExtensionAPI) {
|
|
|
57
46
|
content: [{ type: "text", text: "Error: UI not available (running in non-interactive mode)" }],
|
|
58
47
|
details: {
|
|
59
48
|
question: params.question,
|
|
60
|
-
options: params.options.map((o) =>
|
|
49
|
+
options: params.options.map((o) => o.label),
|
|
61
50
|
answer: null,
|
|
62
51
|
} as QuestionDetails,
|
|
63
52
|
};
|
|
@@ -70,9 +59,7 @@ export default function question(pi: ExtensionAPI) {
|
|
|
70
59
|
};
|
|
71
60
|
}
|
|
72
61
|
|
|
73
|
-
|
|
74
|
-
const normalizedOptions = params.options.map(normalizeOption);
|
|
75
|
-
const allOptions: DisplayOption[] = [...normalizedOptions, { label: "Type something.", isOther: true }];
|
|
62
|
+
const allOptions: DisplayOption[] = [...params.options, { label: "Type something.", isOther: true }];
|
|
76
63
|
|
|
77
64
|
const result = await ctx.ui.custom<{ answer: string; wasCustom: boolean; index?: number } | null>(
|
|
78
65
|
(tui, theme, _kb, done) => {
|
|
@@ -209,7 +196,7 @@ export default function question(pi: ExtensionAPI) {
|
|
|
209
196
|
);
|
|
210
197
|
|
|
211
198
|
// Build simple options list for details
|
|
212
|
-
const simpleOptions =
|
|
199
|
+
const simpleOptions = params.options.map((o) => o.label);
|
|
213
200
|
|
|
214
201
|
if (!result) {
|
|
215
202
|
return {
|
|
@@ -244,7 +231,7 @@ export default function question(pi: ExtensionAPI) {
|
|
|
244
231
|
let text = theme.fg("toolTitle", theme.bold("question ")) + theme.fg("muted", args.question);
|
|
245
232
|
const opts = Array.isArray(args.options) ? args.options : [];
|
|
246
233
|
if (opts.length) {
|
|
247
|
-
const labels = opts.map((o:
|
|
234
|
+
const labels = opts.map((o: OptionWithDesc) => o.label);
|
|
248
235
|
const numbered = [...labels, "Type something."].map((o, i) => `${i + 1}. ${o}`);
|
|
249
236
|
text += `\n${theme.fg("dim", ` Options: ${numbered.join(", ")}`)}`;
|
|
250
237
|
}
|