mono-pilot 0.2.10 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +260 -2
- package/dist/src/agents-paths.js +36 -0
- package/dist/src/brief/blocks.js +83 -0
- package/dist/src/brief/defaults.js +60 -0
- package/dist/src/brief/frontmatter.js +53 -0
- package/dist/src/brief/paths.js +10 -0
- package/dist/src/brief/reflection.js +27 -0
- package/dist/src/cli.js +62 -5
- package/dist/src/cluster/bus.js +102 -0
- package/dist/src/cluster/follower.js +137 -0
- package/dist/src/cluster/init.js +182 -0
- package/dist/src/cluster/leader.js +97 -0
- package/dist/src/cluster/log.js +49 -0
- package/dist/src/cluster/protocol.js +34 -0
- package/dist/src/cluster/services/bus.js +243 -0
- package/dist/src/cluster/services/embedding.js +12 -0
- package/dist/src/cluster/socket.js +86 -0
- package/dist/src/cluster/test-bus.js +175 -0
- package/dist/src/cluster_v2/connection-lifecycle.js +31 -0
- package/dist/src/cluster_v2/connection-lifecycle.test.js +24 -0
- package/dist/src/cluster_v2/connection.js +159 -0
- package/dist/src/cluster_v2/connection.test.js +55 -0
- package/dist/src/cluster_v2/events.js +102 -0
- package/dist/src/cluster_v2/index.js +2 -0
- package/dist/src/cluster_v2/observability.js +99 -0
- package/dist/src/cluster_v2/observability.test.js +46 -0
- package/dist/src/cluster_v2/rpc.js +389 -0
- package/dist/src/cluster_v2/rpc.test.js +110 -0
- package/dist/src/cluster_v2/runtime.failover.integration.test.js +156 -0
- package/dist/src/cluster_v2/runtime.js +531 -0
- package/dist/src/cluster_v2/runtime.lease-compromise.integration.test.js +91 -0
- package/dist/src/cluster_v2/runtime.lifecycle.integration.test.js +225 -0
- package/dist/src/cluster_v2/services/bus.integration.test.js +140 -0
- package/dist/src/cluster_v2/services/bus.js +450 -0
- package/dist/src/cluster_v2/services/discord/auth-store.js +82 -0
- package/dist/src/cluster_v2/services/discord/collector.js +569 -0
- package/dist/src/cluster_v2/services/discord/index.js +1 -0
- package/dist/src/cluster_v2/services/discord/oauth.js +87 -0
- package/dist/src/cluster_v2/services/discord/rpc-client.js +325 -0
- package/dist/src/cluster_v2/services/embedding.js +66 -0
- package/dist/src/cluster_v2/services/registry-cache.js +107 -0
- package/dist/src/cluster_v2/services/registry-cache.test.js +66 -0
- package/dist/src/cluster_v2/services/registry.js +36 -0
- package/dist/src/cluster_v2/services/twitter/collector.js +1055 -0
- package/dist/src/cluster_v2/services/twitter/index.js +1 -0
- package/dist/src/config/digest.js +78 -0
- package/dist/src/config/discord.js +143 -0
- package/dist/src/config/image-gen.js +48 -0
- package/dist/src/config/mono-pilot.js +31 -0
- package/dist/src/config/twitter.js +100 -0
- package/dist/src/extensions/cluster.js +311 -0
- package/dist/src/extensions/commands/build-memory.js +76 -0
- package/dist/src/extensions/commands/digest/backfill.js +779 -0
- package/dist/src/extensions/commands/digest/index.js +1133 -0
- package/dist/src/extensions/commands/image-model.js +214 -0
- package/dist/src/extensions/game/bus-injection.js +47 -0
- package/dist/src/extensions/game/identity.js +83 -0
- package/dist/src/extensions/game/mailbox.js +61 -0
- package/dist/src/extensions/game/system-prompt.js +134 -0
- package/dist/src/extensions/game/tools.js +28 -0
- package/dist/src/extensions/lifecycle.js +337 -0
- package/dist/src/extensions/mode-runtime.js +26 -2
- package/dist/src/extensions/mono-game.js +66 -0
- package/dist/src/extensions/mono-pilot.js +100 -18
- package/dist/src/extensions/nvim.js +47 -0
- package/dist/src/extensions/session-hints.js +1 -2
- package/dist/src/extensions/sftp.js +897 -0
- package/dist/src/extensions/status.js +676 -0
- package/dist/src/extensions/system-events.js +478 -0
- package/dist/src/extensions/system-prompt.js +24 -14
- package/dist/src/extensions/user-message.js +70 -1
- package/dist/src/lsp/client.js +235 -0
- package/dist/src/lsp/index.js +165 -0
- package/dist/src/lsp/runtime.js +67 -0
- package/dist/src/lsp/server.js +242 -0
- package/dist/src/memory/build-memory.js +103 -0
- package/dist/src/memory/config/defaults.js +55 -0
- package/dist/src/memory/config/loader.js +29 -0
- package/dist/src/memory/config/paths.js +9 -0
- package/dist/src/memory/config/resolve.js +90 -0
- package/dist/src/memory/config/types.js +1 -0
- package/dist/src/memory/embeddings/batch-runner.js +39 -0
- package/dist/src/memory/embeddings/cache.js +47 -0
- package/dist/src/memory/embeddings/chunk-limits.js +26 -0
- package/dist/src/memory/embeddings/input-limits.js +48 -0
- package/dist/src/memory/embeddings/local.js +108 -0
- package/dist/src/memory/embeddings/types.js +1 -0
- package/dist/src/memory/index-manager.js +552 -0
- package/dist/src/memory/indexing/embeddings.js +67 -0
- package/dist/src/memory/indexing/files.js +180 -0
- package/dist/src/memory/indexing/index-file.js +105 -0
- package/dist/src/memory/log.js +38 -0
- package/dist/src/memory/paths.js +15 -0
- package/dist/src/memory/runtime/index.js +299 -0
- package/dist/src/memory/runtime/thread.js +116 -0
- package/dist/src/memory/search/fts.js +57 -0
- package/dist/src/memory/search/hybrid.js +50 -0
- package/dist/src/memory/search/text.js +30 -0
- package/dist/src/memory/search/vector.js +43 -0
- package/dist/src/memory/session/content-hash.js +7 -0
- package/dist/src/memory/session/entry.js +33 -0
- package/dist/src/memory/session/flush-policy.js +34 -0
- package/dist/src/memory/session/hook.js +191 -0
- package/dist/src/memory/session/paths.js +15 -0
- package/dist/src/memory/session/session-reader.js +88 -0
- package/dist/src/memory/session/transcript/content-hash.js +7 -0
- package/dist/src/memory/session/transcript/entry.js +28 -0
- package/dist/src/memory/session/transcript/flush.js +56 -0
- package/dist/src/memory/session/transcript/paths.js +28 -0
- package/dist/src/memory/session/transcript/reader.js +112 -0
- package/dist/src/memory/session/transcript/state.js +31 -0
- package/dist/src/memory/store/schema.js +89 -0
- package/dist/src/memory/store/sqlite.js +89 -0
- package/dist/src/memory/types.js +1 -0
- package/dist/src/memory/warm.js +25 -0
- package/dist/{tools → src/tools}/README.md +28 -2
- package/dist/{tools → src/tools}/apply-patch-description.md +8 -2
- package/dist/{tools → src/tools}/apply-patch.js +174 -104
- package/dist/{tools → src/tools}/apply-patch.test.js +52 -1
- package/dist/{tools/ask-question.js → src/tools/ask-user-question.js} +3 -3
- package/dist/src/tools/ast-grep.js +357 -0
- package/dist/src/tools/brief-write.js +122 -0
- package/dist/src/tools/bus-send.js +100 -0
- package/dist/{tools → src/tools}/call-mcp-tool.js +20 -24
- package/dist/src/tools/codex-apply-patch-description.md +52 -0
- package/dist/src/tools/codex-apply-patch.js +540 -0
- package/dist/{tools → src/tools}/delete.js +24 -0
- package/dist/src/tools/exit-plan-mode.js +83 -0
- package/dist/{tools → src/tools}/fetch-mcp-resource.js +31 -3
- package/dist/src/tools/generate-image.js +567 -0
- package/dist/{tools → src/tools}/glob.js +55 -1
- package/dist/{tools → src/tools}/list-mcp-resources.js +32 -3
- package/dist/{tools → src/tools}/list-mcp-tools.js +38 -3
- package/dist/src/tools/ls.js +48 -0
- package/dist/src/tools/lsp-diagnostics.js +67 -0
- package/dist/src/tools/lsp-symbols.js +54 -0
- package/dist/src/tools/mailbox.js +85 -0
- package/dist/src/tools/memory-get.js +90 -0
- package/dist/src/tools/memory-search.js +180 -0
- package/dist/{tools → src/tools}/plan-mode-reminder.md +3 -4
- package/dist/{tools → src/tools}/read-file.js +8 -19
- package/dist/{tools → src/tools}/rg.js +10 -20
- package/dist/{tools → src/tools}/shell.js +19 -42
- package/dist/{tools → src/tools}/subagent.js +255 -6
- package/dist/{tools → src/tools}/switch-mode.js +37 -6
- package/dist/{tools → src/tools}/web-fetch.js +105 -7
- package/dist/{tools → src/tools}/web-search.js +29 -1
- package/package.json +21 -9
- package/dist/src/utils/mcp-client.js +0 -282
- /package/dist/{tools → src/tools}/ask-mode-reminder.md +0 -0
- /package/dist/{tools → src/tools}/rg.test.js +0 -0
- /package/dist/{tools → src/tools}/semantic-search-description.md +0 -0
- /package/dist/{tools → src/tools}/semantic-search.js +0 -0
- /package/dist/{tools → src/tools}/shell-description.md +0 -0
- /package/dist/{tools → src/tools}/subagent-description.md +0 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { extname } from "node:path";
|
|
3
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
4
|
+
import { keyHint } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import { Type } from "@sinclair/typebox";
|
|
6
|
+
import { loadMonoPilotConfigObject } from "../config/mono-pilot.js";
|
|
7
|
+
import { extractImageGenConfig } from "../config/image-gen.js";
|
|
8
|
+
const DESCRIPTION = `Generate images using the Google Gemini API or OpenRouter (default model \`gemini-3.1-flash-image-preview\`).
|
|
9
|
+
|
|
10
|
+
Use when you need to create or edit an image from a text prompt. Optionally provide an input image for edits.
|
|
11
|
+
|
|
12
|
+
Requirements:
|
|
13
|
+
- Provider \`gemini\`: set an API key in \`~/.mono-pilot/config.json\`.
|
|
14
|
+
- Provider \`openrouter\`: set an API key in \`~/.mono-pilot/config.json\`.
|
|
15
|
+
- Default provider/model can be set in \`~/.mono-pilot/config.json\` under \`imageGen\`.
|
|
16
|
+
|
|
17
|
+
Inputs:
|
|
18
|
+
- \`prompt\` is required.
|
|
19
|
+
- \`image_path\` or \`image_base64\` (with \`image_mime_type\`) are optional for image editing.
|
|
20
|
+
- \`output_path\` is optional to save the first generated image.
|
|
21
|
+
|
|
22
|
+
Outputs:
|
|
23
|
+
- Returns text parts (if any) and one or more image parts.
|
|
24
|
+
- Image parts use \`ImageContent\` format: { "type": "image", "data": "<base64>", "mimeType": "image/png" }.`;
|
|
25
|
+
const DEFAULT_MODEL = "gemini-3.1-flash-image-preview";
|
|
26
|
+
const DEFAULT_OPENROUTER_MODEL = "google/gemini-3.1-flash-image-preview";
|
|
27
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
28
|
+
const generateImageSchema = Type.Object({
|
|
29
|
+
prompt: Type.String({
|
|
30
|
+
description: "Text prompt used to generate or edit the image.",
|
|
31
|
+
}),
|
|
32
|
+
image_path: Type.Optional(Type.String({
|
|
33
|
+
description: "Optional path to an input image for editing.",
|
|
34
|
+
})),
|
|
35
|
+
image_base64: Type.Optional(Type.String({
|
|
36
|
+
description: "Optional base64-encoded input image data.",
|
|
37
|
+
})),
|
|
38
|
+
image_mime_type: Type.Optional(Type.String({
|
|
39
|
+
description: "MIME type for image_base64 (e.g., image/png).",
|
|
40
|
+
})),
|
|
41
|
+
output_path: Type.Optional(Type.String({
|
|
42
|
+
description: "Optional output path to save the first generated image.",
|
|
43
|
+
})),
|
|
44
|
+
});
|
|
45
|
+
function normalizeProvider(value) {
|
|
46
|
+
return value === "openrouter" ? "openrouter" : "gemini";
|
|
47
|
+
}
|
|
48
|
+
function normalizePrompt(prompt) {
|
|
49
|
+
return prompt.trim();
|
|
50
|
+
}
|
|
51
|
+
function detectMimeType(path) {
|
|
52
|
+
const ext = extname(path).toLowerCase();
|
|
53
|
+
if (ext === ".png")
|
|
54
|
+
return "image/png";
|
|
55
|
+
if (ext === ".jpg" || ext === ".jpeg")
|
|
56
|
+
return "image/jpeg";
|
|
57
|
+
if (ext === ".gif")
|
|
58
|
+
return "image/gif";
|
|
59
|
+
if (ext === ".webp")
|
|
60
|
+
return "image/webp";
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
function collectInputImageInfo(params) {
|
|
64
|
+
if (params.image_path) {
|
|
65
|
+
return { ref: params.image_path, source: "path" };
|
|
66
|
+
}
|
|
67
|
+
if (params.image_base64) {
|
|
68
|
+
const mimeType = params.image_mime_type;
|
|
69
|
+
const ref = mimeType
|
|
70
|
+
? `data:${mimeType};base64,${params.image_base64}`
|
|
71
|
+
: params.image_base64;
|
|
72
|
+
return { ref, source: "base64", mimeType };
|
|
73
|
+
}
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
function buildParts(params) {
|
|
77
|
+
const parts = [];
|
|
78
|
+
if (params.image_path) {
|
|
79
|
+
const buffer = readFileSync(params.image_path);
|
|
80
|
+
const mimeType = detectMimeType(params.image_path);
|
|
81
|
+
if (!mimeType) {
|
|
82
|
+
throw new Error("Unsupported image_path extension. Use png/jpg/gif/webp.");
|
|
83
|
+
}
|
|
84
|
+
parts.push({
|
|
85
|
+
inlineData: {
|
|
86
|
+
mimeType,
|
|
87
|
+
data: buffer.toString("base64"),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (params.image_base64) {
|
|
92
|
+
if (!params.image_mime_type) {
|
|
93
|
+
throw new Error("image_mime_type is required when image_base64 is provided.");
|
|
94
|
+
}
|
|
95
|
+
parts.push({
|
|
96
|
+
inlineData: {
|
|
97
|
+
mimeType: params.image_mime_type,
|
|
98
|
+
data: params.image_base64,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
parts.push({ text: normalizePrompt(params.prompt) });
|
|
103
|
+
return { parts, usedInput: Boolean(params.image_path || params.image_base64) };
|
|
104
|
+
}
|
|
105
|
+
function collectResponseParts(parts) {
|
|
106
|
+
const content = [];
|
|
107
|
+
let imageCount = 0;
|
|
108
|
+
let textCount = 0;
|
|
109
|
+
for (const part of parts) {
|
|
110
|
+
const text = typeof part.text === "string" ? part.text : undefined;
|
|
111
|
+
const inlineData = (typeof part.inlineData === "object" && part.inlineData !== null && part.inlineData) ||
|
|
112
|
+
(typeof part.inline_data === "object" && part.inline_data !== null && part.inline_data);
|
|
113
|
+
if (text) {
|
|
114
|
+
content.push({ type: "text", text });
|
|
115
|
+
textCount += 1;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (inlineData && typeof inlineData === "object") {
|
|
119
|
+
const data = inlineData.data;
|
|
120
|
+
const mimeType = inlineData.mimeType ?? inlineData.mime_type;
|
|
121
|
+
if (typeof data === "string" && typeof mimeType === "string") {
|
|
122
|
+
content.push({ type: "image", data, mimeType });
|
|
123
|
+
imageCount += 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { content, imageCount, textCount };
|
|
128
|
+
}
|
|
129
|
+
function parseDataUrl(url) {
|
|
130
|
+
if (!url.startsWith("data:")) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const match = url.match(/^data:([^;]+);base64,(.*)$/);
|
|
134
|
+
if (!match) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return { mimeType: match[1], data: match[2] };
|
|
138
|
+
}
|
|
139
|
+
function collectOpenRouterParts(message) {
|
|
140
|
+
const content = [];
|
|
141
|
+
let imageCount = 0;
|
|
142
|
+
let textCount = 0;
|
|
143
|
+
const messageContent = message.content;
|
|
144
|
+
const text = typeof messageContent === "string" ? messageContent : undefined;
|
|
145
|
+
if (text) {
|
|
146
|
+
content.push({ type: "text", text });
|
|
147
|
+
textCount += 1;
|
|
148
|
+
}
|
|
149
|
+
if (Array.isArray(messageContent)) {
|
|
150
|
+
for (const part of messageContent) {
|
|
151
|
+
if (!part || typeof part !== "object") {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const partType = part.type;
|
|
155
|
+
if (partType === "text" && typeof part.text === "string") {
|
|
156
|
+
content.push({ type: "text", text: part.text });
|
|
157
|
+
textCount += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (partType === "image_url") {
|
|
161
|
+
const imageUrl = (typeof part.image_url === "object" &&
|
|
162
|
+
part.image_url !== null
|
|
163
|
+
? part.image_url.url
|
|
164
|
+
: undefined) ??
|
|
165
|
+
(typeof part.url === "string" ? part.url : undefined);
|
|
166
|
+
if (typeof imageUrl !== "string") {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const parsed = parseDataUrl(imageUrl);
|
|
170
|
+
if (parsed) {
|
|
171
|
+
content.push({ type: "image", data: parsed.data, mimeType: parsed.mimeType });
|
|
172
|
+
imageCount += 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
content.push({ type: "text", text: `OpenRouter image URL: ${imageUrl}` });
|
|
176
|
+
textCount += 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const images = Array.isArray(message.images) ? message.images : [];
|
|
181
|
+
for (const image of images) {
|
|
182
|
+
const imageUrl = (typeof image.image_url === "object" && image.image_url !== null
|
|
183
|
+
? image.image_url.url
|
|
184
|
+
: undefined) ??
|
|
185
|
+
(typeof image.url === "string" ? image.url : undefined);
|
|
186
|
+
if (typeof imageUrl !== "string") {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const parsed = parseDataUrl(imageUrl);
|
|
190
|
+
if (parsed) {
|
|
191
|
+
content.push({ type: "image", data: parsed.data, mimeType: parsed.mimeType });
|
|
192
|
+
imageCount += 1;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
content.push({ type: "text", text: `OpenRouter image URL: ${imageUrl}` });
|
|
196
|
+
textCount += 1;
|
|
197
|
+
}
|
|
198
|
+
return { content, imageCount, textCount };
|
|
199
|
+
}
|
|
200
|
+
function findFirstImage(content) {
|
|
201
|
+
return content.find((entry) => entry.type === "image");
|
|
202
|
+
}
|
|
203
|
+
function buildOpenRouterMessages(prompt, parts, usedInput) {
|
|
204
|
+
if (!usedInput) {
|
|
205
|
+
return [{ role: "user", content: prompt }];
|
|
206
|
+
}
|
|
207
|
+
const content = parts.map((part) => {
|
|
208
|
+
if ("inlineData" in part && typeof part.inlineData === "object" && part.inlineData) {
|
|
209
|
+
const inlineData = part.inlineData;
|
|
210
|
+
if (typeof inlineData.mimeType === "string" && typeof inlineData.data === "string") {
|
|
211
|
+
return {
|
|
212
|
+
type: "image_url",
|
|
213
|
+
image_url: {
|
|
214
|
+
url: `data:${inlineData.mimeType};base64,${inlineData.data}`,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if ("text" in part && typeof part.text === "string") {
|
|
220
|
+
return { type: "text", text: part.text };
|
|
221
|
+
}
|
|
222
|
+
return { type: "text", text: JSON.stringify(part) };
|
|
223
|
+
});
|
|
224
|
+
return [{ role: "user", content }];
|
|
225
|
+
}
|
|
226
|
+
function resolveOpenRouterEndpoint(baseUrl) {
|
|
227
|
+
const fallback = "https://openrouter.ai/api/v1/chat/completions";
|
|
228
|
+
if (!baseUrl)
|
|
229
|
+
return fallback;
|
|
230
|
+
const trimmed = baseUrl.trim();
|
|
231
|
+
if (!trimmed)
|
|
232
|
+
return fallback;
|
|
233
|
+
const normalized = trimmed.replace(/\/+$/, "");
|
|
234
|
+
if (normalized.endsWith("/chat/completions")) {
|
|
235
|
+
return normalized;
|
|
236
|
+
}
|
|
237
|
+
return `${normalized}/chat/completions`;
|
|
238
|
+
}
|
|
239
|
+
function chunkText(text, size) {
|
|
240
|
+
if (text.length <= size)
|
|
241
|
+
return [text];
|
|
242
|
+
const chunks = [];
|
|
243
|
+
for (let i = 0; i < text.length; i += size) {
|
|
244
|
+
chunks.push(text.slice(i, i + size));
|
|
245
|
+
}
|
|
246
|
+
return chunks;
|
|
247
|
+
}
|
|
248
|
+
function buildResponseTextParts(payload, chunkSize = 1800) {
|
|
249
|
+
const json = JSON.stringify(payload, null, 2);
|
|
250
|
+
return chunkText(json, chunkSize).map((text) => ({ type: "text", text }));
|
|
251
|
+
}
|
|
252
|
+
export default function generateImageExtension(pi) {
|
|
253
|
+
// System prompt injection is handled centrally by system-prompt extension.
|
|
254
|
+
pi.registerTool({
|
|
255
|
+
name: "GenerateImage",
|
|
256
|
+
label: "GenerateImage",
|
|
257
|
+
description: DESCRIPTION,
|
|
258
|
+
parameters: generateImageSchema,
|
|
259
|
+
renderCall(args, theme) {
|
|
260
|
+
const input = args;
|
|
261
|
+
const prompt = typeof input.prompt === "string" ? input.prompt.trim() : "";
|
|
262
|
+
const displayPrompt = prompt.length > 80 ? `${prompt.slice(0, 79)}…` : prompt || "(missing prompt)";
|
|
263
|
+
let text = theme.fg("toolTitle", theme.bold("GenerateImage"));
|
|
264
|
+
text += ` ${theme.fg("toolOutput", displayPrompt)}`;
|
|
265
|
+
return new Text(text, 0, 0);
|
|
266
|
+
},
|
|
267
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
268
|
+
if (isPartial) {
|
|
269
|
+
return new Text(theme.fg("muted", "Generating image..."), 0, 0);
|
|
270
|
+
}
|
|
271
|
+
const details = (result.details ?? {});
|
|
272
|
+
const textBlocks = result.content.filter((entry) => entry.type === "text" && typeof entry.text === "string");
|
|
273
|
+
if (!expanded) {
|
|
274
|
+
if (details.output_image_written && details.output_image_path) {
|
|
275
|
+
const summary = `saved: ${details.output_image_path}`;
|
|
276
|
+
return new Text(theme.fg("muted", `${summary} (click or ${keyHint("expandTools", "to expand")})`), 0, 0);
|
|
277
|
+
}
|
|
278
|
+
const imageCount = result.content.filter((entry) => entry.type === "image").length;
|
|
279
|
+
const summary = `${imageCount} image${imageCount === 1 ? "" : "s"}`;
|
|
280
|
+
return new Text(theme.fg("muted", `${summary} (click or ${keyHint("expandTools", "to expand")})`), 0, 0);
|
|
281
|
+
}
|
|
282
|
+
const lines = [];
|
|
283
|
+
for (const block of textBlocks) {
|
|
284
|
+
lines.push(...block.text.split("\n"));
|
|
285
|
+
}
|
|
286
|
+
if (details.prompt) {
|
|
287
|
+
lines.push(theme.fg("muted", `prompt: ${details.prompt}`));
|
|
288
|
+
}
|
|
289
|
+
if (details.input_image_ref) {
|
|
290
|
+
lines.push(theme.fg("muted", `input image: ${details.input_image_ref}`));
|
|
291
|
+
}
|
|
292
|
+
if (details.output_image_path) {
|
|
293
|
+
const suffix = details.output_image_written === false ? " (failed)" : "";
|
|
294
|
+
lines.push(theme.fg("muted", `output image: ${details.output_image_path}${suffix}`));
|
|
295
|
+
}
|
|
296
|
+
if (details.provider) {
|
|
297
|
+
lines.push(theme.fg("muted", `provider: ${details.provider}`));
|
|
298
|
+
}
|
|
299
|
+
lines.push(theme.fg("muted", `(click or ${keyHint("expandTools", "to collapse")})`));
|
|
300
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
301
|
+
},
|
|
302
|
+
async execute(_toolCallId, params, signal) {
|
|
303
|
+
const normalizedPrompt = normalizePrompt(params.prompt ?? "");
|
|
304
|
+
const inputImageInfo = collectInputImageInfo(params);
|
|
305
|
+
const baseDetails = {
|
|
306
|
+
prompt: normalizedPrompt || undefined,
|
|
307
|
+
input_image_ref: inputImageInfo.ref,
|
|
308
|
+
input_image_source: inputImageInfo.source,
|
|
309
|
+
input_image_mime_type: inputImageInfo.mimeType,
|
|
310
|
+
output_image_path: params.output_path,
|
|
311
|
+
};
|
|
312
|
+
let config = {};
|
|
313
|
+
try {
|
|
314
|
+
config = await loadMonoPilotConfigObject();
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
318
|
+
const fallbackProvider = "gemini";
|
|
319
|
+
return {
|
|
320
|
+
content: [{ type: "text", text: `GenerateImage error: ${message}` }],
|
|
321
|
+
details: {
|
|
322
|
+
provider: fallbackProvider,
|
|
323
|
+
model: DEFAULT_MODEL,
|
|
324
|
+
...baseDetails,
|
|
325
|
+
error: message,
|
|
326
|
+
api_key_source: "missing",
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const imageGenConfig = extractImageGenConfig(config);
|
|
331
|
+
const provider = normalizeProvider(imageGenConfig.selection.provider);
|
|
332
|
+
const providerConfig = imageGenConfig.providers[provider];
|
|
333
|
+
const apiKeyFromConfig = providerConfig?.apiKey?.trim();
|
|
334
|
+
const apiKey = apiKeyFromConfig;
|
|
335
|
+
const apiKeySource = apiKeyFromConfig ? "config" : "missing";
|
|
336
|
+
const model = imageGenConfig.selection.model?.trim() ||
|
|
337
|
+
providerConfig?.models?.[0]?.id ||
|
|
338
|
+
(provider === "openrouter" ? DEFAULT_OPENROUTER_MODEL : DEFAULT_MODEL);
|
|
339
|
+
const apiKeyInfo = {
|
|
340
|
+
provider,
|
|
341
|
+
api_key: apiKey ?? "",
|
|
342
|
+
api_key_source: apiKeySource,
|
|
343
|
+
};
|
|
344
|
+
if (!apiKey) {
|
|
345
|
+
return {
|
|
346
|
+
content: [
|
|
347
|
+
{
|
|
348
|
+
type: "text",
|
|
349
|
+
text: "GenerateImage error: missing API key. Provide api_key or set it in ~/.mono-pilot/config.json.",
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
details: {
|
|
353
|
+
model,
|
|
354
|
+
...baseDetails,
|
|
355
|
+
error: "missing api key",
|
|
356
|
+
...apiKeyInfo,
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (params.image_path && params.image_base64) {
|
|
361
|
+
return {
|
|
362
|
+
content: [
|
|
363
|
+
{
|
|
364
|
+
type: "text",
|
|
365
|
+
text: "GenerateImage error: provide only one of image_path or image_base64.",
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
details: {
|
|
369
|
+
model,
|
|
370
|
+
...baseDetails,
|
|
371
|
+
error: "invalid input",
|
|
372
|
+
...apiKeyInfo,
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const prompt = normalizedPrompt;
|
|
377
|
+
if (prompt.length === 0) {
|
|
378
|
+
return {
|
|
379
|
+
content: [{ type: "text", text: "GenerateImage error: prompt cannot be empty." }],
|
|
380
|
+
details: {
|
|
381
|
+
model,
|
|
382
|
+
...baseDetails,
|
|
383
|
+
error: "empty prompt",
|
|
384
|
+
...apiKeyInfo,
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
let parts = [];
|
|
389
|
+
let usedInput = false;
|
|
390
|
+
try {
|
|
391
|
+
const built = buildParts(params);
|
|
392
|
+
parts = built.parts;
|
|
393
|
+
usedInput = built.usedInput;
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
397
|
+
return {
|
|
398
|
+
content: [{ type: "text", text: `GenerateImage error: ${message}` }],
|
|
399
|
+
details: {
|
|
400
|
+
model,
|
|
401
|
+
...baseDetails,
|
|
402
|
+
error: message,
|
|
403
|
+
...apiKeyInfo,
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const openRouterMessages = buildOpenRouterMessages(prompt, parts, usedInput);
|
|
408
|
+
const openRouterBaseUrl = provider === "openrouter" ? resolveOpenRouterEndpoint(providerConfig?.baseUrl) : "";
|
|
409
|
+
const useAuthHeader = providerConfig?.authHeader !== false;
|
|
410
|
+
const controller = new AbortController();
|
|
411
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
412
|
+
const onAbort = () => controller.abort();
|
|
413
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
414
|
+
try {
|
|
415
|
+
const response = provider === "openrouter"
|
|
416
|
+
? await fetch(openRouterBaseUrl, {
|
|
417
|
+
method: "POST",
|
|
418
|
+
headers: {
|
|
419
|
+
...(useAuthHeader ? { Authorization: `Bearer ${apiKey}` } : { "x-api-key": apiKey }),
|
|
420
|
+
"Content-Type": "application/json",
|
|
421
|
+
},
|
|
422
|
+
body: JSON.stringify({
|
|
423
|
+
model,
|
|
424
|
+
messages: openRouterMessages,
|
|
425
|
+
modalities: ["image", "text"],
|
|
426
|
+
}),
|
|
427
|
+
signal: controller.signal,
|
|
428
|
+
})
|
|
429
|
+
: await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`, {
|
|
430
|
+
method: "POST",
|
|
431
|
+
headers: {
|
|
432
|
+
"x-goog-api-key": apiKey,
|
|
433
|
+
"Content-Type": "application/json",
|
|
434
|
+
},
|
|
435
|
+
body: JSON.stringify({
|
|
436
|
+
contents: [{ parts }],
|
|
437
|
+
generationConfig: {
|
|
438
|
+
responseModalities: ["TEXT", "IMAGE"],
|
|
439
|
+
},
|
|
440
|
+
}),
|
|
441
|
+
signal: controller.signal,
|
|
442
|
+
});
|
|
443
|
+
const payload = (await response.json());
|
|
444
|
+
if (!response.ok) {
|
|
445
|
+
const errorMessage = (typeof payload.error === "object" && payload.error !== null && "message" in payload.error
|
|
446
|
+
? String(payload.error.message ?? "")
|
|
447
|
+
: "") || JSON.stringify(payload);
|
|
448
|
+
const responseParts = buildResponseTextParts(payload);
|
|
449
|
+
return {
|
|
450
|
+
content: [
|
|
451
|
+
{
|
|
452
|
+
type: "text",
|
|
453
|
+
text: `GenerateImage error: HTTP ${response.status} ${response.statusText}\n${errorMessage}`,
|
|
454
|
+
},
|
|
455
|
+
...responseParts,
|
|
456
|
+
],
|
|
457
|
+
details: {
|
|
458
|
+
model,
|
|
459
|
+
...baseDetails,
|
|
460
|
+
response: JSON.stringify(payload),
|
|
461
|
+
error: JSON.stringify(payload),
|
|
462
|
+
...apiKeyInfo,
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
const candidates = Array.isArray(payload.candidates) ? payload.candidates : [];
|
|
467
|
+
const firstCandidate = candidates[0];
|
|
468
|
+
const openRouterChoices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
469
|
+
const openRouterMessageRaw = openRouterChoices[0]?.message ?? {};
|
|
470
|
+
const openRouterMessage = typeof openRouterMessageRaw === "object" && openRouterMessageRaw !== null
|
|
471
|
+
? openRouterMessageRaw
|
|
472
|
+
: {};
|
|
473
|
+
const collected = provider === "openrouter"
|
|
474
|
+
? collectOpenRouterParts(openRouterMessage)
|
|
475
|
+
: collectResponseParts(Array.isArray(firstCandidate?.content?.parts)
|
|
476
|
+
? (firstCandidate?.content ?? {}).parts
|
|
477
|
+
: []);
|
|
478
|
+
if (collected.content.length === 0) {
|
|
479
|
+
const responseParts = buildResponseTextParts(payload);
|
|
480
|
+
return {
|
|
481
|
+
content: [
|
|
482
|
+
{ type: "text", text: "GenerateImage: no image data returned." },
|
|
483
|
+
...responseParts,
|
|
484
|
+
],
|
|
485
|
+
details: {
|
|
486
|
+
model,
|
|
487
|
+
...baseDetails,
|
|
488
|
+
image_count: 0,
|
|
489
|
+
text_count: 0,
|
|
490
|
+
used_input_image: usedInput || undefined,
|
|
491
|
+
response: JSON.stringify(payload),
|
|
492
|
+
...apiKeyInfo,
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
let outputWritten;
|
|
497
|
+
if (params.output_path) {
|
|
498
|
+
const image = findFirstImage(collected.content);
|
|
499
|
+
if (image) {
|
|
500
|
+
try {
|
|
501
|
+
writeFileSync(params.output_path, Buffer.from(image.data, "base64"));
|
|
502
|
+
outputWritten = true;
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
506
|
+
return {
|
|
507
|
+
content: [{ type: "text", text: `GenerateImage error: ${message}` }, ...collected.content],
|
|
508
|
+
details: {
|
|
509
|
+
model,
|
|
510
|
+
...baseDetails,
|
|
511
|
+
output_image_written: false,
|
|
512
|
+
image_count: collected.imageCount,
|
|
513
|
+
text_count: collected.textCount,
|
|
514
|
+
used_input_image: usedInput || undefined,
|
|
515
|
+
response: JSON.stringify(payload),
|
|
516
|
+
error: message,
|
|
517
|
+
...apiKeyInfo,
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
outputWritten = false;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const outputContent = outputWritten && params.output_path
|
|
527
|
+
? [
|
|
528
|
+
{ type: "text", text: `GenerateImage saved: ${params.output_path}` },
|
|
529
|
+
...collected.content.filter((entry) => entry.type === "text"),
|
|
530
|
+
]
|
|
531
|
+
: collected.content;
|
|
532
|
+
return {
|
|
533
|
+
content: outputContent,
|
|
534
|
+
details: {
|
|
535
|
+
model,
|
|
536
|
+
...baseDetails,
|
|
537
|
+
output_image_written: outputWritten,
|
|
538
|
+
image_count: collected.imageCount,
|
|
539
|
+
text_count: collected.textCount,
|
|
540
|
+
used_input_image: usedInput || undefined,
|
|
541
|
+
response: JSON.stringify(payload),
|
|
542
|
+
...apiKeyInfo,
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
548
|
+
const timeoutMessage = controller.signal.aborted
|
|
549
|
+
? `GenerateImage error: request timed out after ${REQUEST_TIMEOUT_MS}ms.`
|
|
550
|
+
: `GenerateImage error: ${message}`;
|
|
551
|
+
return {
|
|
552
|
+
content: [{ type: "text", text: timeoutMessage }],
|
|
553
|
+
details: {
|
|
554
|
+
model,
|
|
555
|
+
...baseDetails,
|
|
556
|
+
error: message,
|
|
557
|
+
...apiKeyInfo,
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
finally {
|
|
562
|
+
clearTimeout(timeoutId);
|
|
563
|
+
signal?.removeEventListener("abort", onAbort);
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
|
-
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, keyHint, truncateHead, } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
4
5
|
import { Type } from "@sinclair/typebox";
|
|
5
6
|
import { globSync } from "glob";
|
|
6
7
|
const DESCRIPTION = `
|
|
@@ -11,6 +12,8 @@ Tool to search for files matching a glob pattern
|
|
|
11
12
|
- Use this tool when you need to find files by name patterns
|
|
12
13
|
- You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches that are potentially useful as a batch.
|
|
13
14
|
`.trim();
|
|
15
|
+
const MAX_RENDER_PATH_CHARS = 120;
|
|
16
|
+
const MAX_RENDER_PATTERN_CHARS = 160;
|
|
14
17
|
const globSchema = Type.Object({
|
|
15
18
|
target_directory: Type.Optional(Type.String({
|
|
16
19
|
description: "Absolute path to directory to search for files in. If not provided, defaults to mono-pilot workspace root.",
|
|
@@ -25,6 +28,19 @@ Examples:
|
|
|
25
28
|
- "**/test/**/test_*.ts" - find all test_*.ts files in any test directory`,
|
|
26
29
|
}),
|
|
27
30
|
});
|
|
31
|
+
function compactForCommandArg(value, maxLength) {
|
|
32
|
+
const normalized = value.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\\n").trim();
|
|
33
|
+
if (normalized.length <= maxLength)
|
|
34
|
+
return normalized;
|
|
35
|
+
return `${normalized.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
36
|
+
}
|
|
37
|
+
function shellQuoteArg(value) {
|
|
38
|
+
if (value.length === 0)
|
|
39
|
+
return "''";
|
|
40
|
+
if (/^[A-Za-z0-9_./:=,+-]+$/.test(value))
|
|
41
|
+
return value;
|
|
42
|
+
return `'${value.replace(/'/g, `"'"'`)}'`;
|
|
43
|
+
}
|
|
28
44
|
function normalizeGlobPattern(pattern) {
|
|
29
45
|
const trimmed = pattern.trim();
|
|
30
46
|
if (trimmed.length === 0) {
|
|
@@ -84,6 +100,44 @@ export default function (pi) {
|
|
|
84
100
|
label: "Glob",
|
|
85
101
|
description: DESCRIPTION,
|
|
86
102
|
parameters: globSchema,
|
|
103
|
+
renderCall(args, theme) {
|
|
104
|
+
const input = args;
|
|
105
|
+
const pattern = typeof input.glob_pattern === "string" && input.glob_pattern.trim().length > 0
|
|
106
|
+
? compactForCommandArg(input.glob_pattern, MAX_RENDER_PATTERN_CHARS)
|
|
107
|
+
: "(missing glob_pattern)";
|
|
108
|
+
const targetDirectory = typeof input.target_directory === "string" && input.target_directory.trim().length > 0
|
|
109
|
+
? compactForCommandArg(input.target_directory, MAX_RENDER_PATH_CHARS)
|
|
110
|
+
: undefined;
|
|
111
|
+
const commandArgs = [pattern];
|
|
112
|
+
if (targetDirectory)
|
|
113
|
+
commandArgs.push("--target-directory", targetDirectory);
|
|
114
|
+
const commandText = commandArgs.map(shellQuoteArg).join(" ");
|
|
115
|
+
let text = theme.fg("toolTitle", theme.bold("Glob"));
|
|
116
|
+
text += ` ${theme.fg("toolOutput", commandText)}`;
|
|
117
|
+
return new Text(text, 0, 0);
|
|
118
|
+
},
|
|
119
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
120
|
+
if (isPartial) {
|
|
121
|
+
return new Text(theme.fg("muted", "Searching..."), 0, 0);
|
|
122
|
+
}
|
|
123
|
+
const textBlock = result.content.find((entry) => entry.type === "text" && typeof entry.text === "string");
|
|
124
|
+
if (!textBlock || typeof textBlock.text !== "string") {
|
|
125
|
+
return new Text(theme.fg("error", "No text result returned."), 0, 0);
|
|
126
|
+
}
|
|
127
|
+
const fullText = textBlock.text;
|
|
128
|
+
const details = result.details;
|
|
129
|
+
const fileCount = details?.returned_matches ?? fullText.split("\n").length;
|
|
130
|
+
if (!expanded) {
|
|
131
|
+
const summary = `${fileCount} files (click or ${keyHint("expandTools", "to expand")})`;
|
|
132
|
+
return new Text(theme.fg("muted", summary), 0, 0);
|
|
133
|
+
}
|
|
134
|
+
let text = fullText
|
|
135
|
+
.split("\n")
|
|
136
|
+
.map((line) => theme.fg("toolOutput", line))
|
|
137
|
+
.join("\n");
|
|
138
|
+
text += theme.fg("muted", `\n(click or ${keyHint("expandTools", "to collapse")})`);
|
|
139
|
+
return new Text(text, 0, 0);
|
|
140
|
+
},
|
|
87
141
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
88
142
|
const targetDirectory = resolveTargetDirectory(params.target_directory, ctx.cwd);
|
|
89
143
|
const normalizedPattern = normalizeGlobPattern(params.glob_pattern);
|