@lazyingart/agintiflow 0.8.12 → 0.9.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.
- package/README.md +6 -0
- package/docs/auxiliary-image-generation.md +62 -0
- package/package.json +4 -2
- package/public/app.js +8 -2
- package/public/index.html +6 -0
- package/scripts/smoke-auxiliary-tools.js +95 -0
- package/src/agent-runner.js +81 -0
- package/src/auxiliary-tools.js +344 -0
- package/src/capabilities.js +14 -1
- package/src/cli.js +31 -7
- package/src/config.js +4 -0
- package/src/guardrails.js +15 -0
- package/src/interactive-cli.js +58 -8
- package/src/model-client.js +54 -0
- package/src/project.js +35 -13
- package/src/redaction.js +1 -1
- package/src/task-profiles.js +7 -0
- package/src/workspace-tools.js +2 -0
- package/web.js +4 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { redactSensitiveText } from "./redaction.js";
|
|
5
|
+
import { checkWorkspaceToolUse, resolveWorkspacePath } from "./workspace-tools.js";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_GRS_HOST = "https://grsaiapi.com";
|
|
8
|
+
const DEFAULT_IMAGE_MODEL = "nano-banana-2";
|
|
9
|
+
const DEFAULT_ASPECT_RATIO = "1:1";
|
|
10
|
+
const DEFAULT_IMAGE_SIZE = "2K";
|
|
11
|
+
const TRANSIENT_HTTP_CODES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
12
|
+
const MAX_PROMPT_BYTES = 120_000;
|
|
13
|
+
const MAX_REFERENCE_BYTES = 4_000_000;
|
|
14
|
+
|
|
15
|
+
export const AUXILIARY_SKILLS = [
|
|
16
|
+
{
|
|
17
|
+
id: "image_generation",
|
|
18
|
+
label: "Image generation",
|
|
19
|
+
provider: "grsai",
|
|
20
|
+
keyName: "GRSAI",
|
|
21
|
+
toolName: "generate_image",
|
|
22
|
+
description:
|
|
23
|
+
"Generate raster image artifacts through GRS AI Nano Banana, save the manifest and image files in the workspace, then send selected results to the canvas.",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function timestampSlug() {
|
|
28
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function safeStem(value = "image") {
|
|
32
|
+
const stem = String(value || "image")
|
|
33
|
+
.trim()
|
|
34
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
35
|
+
.replace(/^-+|-+$/g, "");
|
|
36
|
+
return stem || "image";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeAspectRatio(value) {
|
|
40
|
+
const text = String(value || DEFAULT_ASPECT_RATIO).trim();
|
|
41
|
+
return /^(?:\d+:\d+|1:1|2:3|3:2|3:4|4:3|9:16|16:9)$/.test(text) ? text : DEFAULT_ASPECT_RATIO;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeImageSize(value) {
|
|
45
|
+
const text = String(value || DEFAULT_IMAGE_SIZE).trim();
|
|
46
|
+
return /^(?:1K|2K|4K|1024x1024|1024x1536|1536x1024)$/i.test(text) ? text : DEFAULT_IMAGE_SIZE;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hashBuffer(buffer) {
|
|
50
|
+
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mimeFromPath(filePath) {
|
|
54
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
55
|
+
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
|
56
|
+
if (ext === ".webp") return "image/webp";
|
|
57
|
+
if (ext === ".gif") return "image/gif";
|
|
58
|
+
if (ext === ".svg") return "image/svg+xml";
|
|
59
|
+
return "image/png";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseJsonResponse(raw) {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(raw);
|
|
65
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
66
|
+
} catch {
|
|
67
|
+
// Some providers stream server-sent JSON chunks; fall through to line parsing.
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let lastObject = null;
|
|
71
|
+
for (const line of String(raw || "").split(/\r?\n/)) {
|
|
72
|
+
const chunk = line.trim().replace(/^data:\s*/, "");
|
|
73
|
+
if (!chunk) continue;
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(chunk);
|
|
76
|
+
if (parsed && typeof parsed === "object") lastObject = parsed;
|
|
77
|
+
} catch {
|
|
78
|
+
// Ignore non-JSON progress lines.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!lastObject) throw new Error("Failed to parse image API response as JSON.");
|
|
82
|
+
return lastObject;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function redactPayload(payload) {
|
|
86
|
+
const copy = { ...payload };
|
|
87
|
+
copy.urls = (copy.urls || []).map((item) => {
|
|
88
|
+
const text = String(item || "");
|
|
89
|
+
if (text.startsWith("data:")) return `<data-uri length=${text.length}>`;
|
|
90
|
+
return redactSensitiveText(text);
|
|
91
|
+
});
|
|
92
|
+
return copy;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function grsaiKey() {
|
|
96
|
+
return String(process.env.GRSAI || process.env.GRSAI_API_KEY || "").trim();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function listAuxiliarySkills() {
|
|
100
|
+
return AUXILIARY_SKILLS.map((skill) => ({
|
|
101
|
+
...skill,
|
|
102
|
+
available: skill.provider === "grsai" ? Boolean(grsaiKey()) : false,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function referenceToUrl(item, config) {
|
|
107
|
+
const value = String(item || "").trim();
|
|
108
|
+
if (!value) return { url: "", redacted: "" };
|
|
109
|
+
if (/^https?:\/\//i.test(value)) return { url: value, redacted: value };
|
|
110
|
+
if (value.startsWith("data:")) return { url: value, redacted: `<data-uri length=${value.length}>` };
|
|
111
|
+
|
|
112
|
+
const target = resolveWorkspacePath(config, value);
|
|
113
|
+
const stat = await fs.stat(target.absolutePath);
|
|
114
|
+
if (!stat.isFile()) throw new Error(`Reference image is not a file: ${target.relativePath}`);
|
|
115
|
+
if (stat.size > MAX_REFERENCE_BYTES) throw new Error(`Reference image is too large: ${target.relativePath}`);
|
|
116
|
+
const buffer = await fs.readFile(target.absolutePath);
|
|
117
|
+
return {
|
|
118
|
+
url: `data:${mimeFromPath(target.relativePath)};base64,${buffer.toString("base64")}`,
|
|
119
|
+
redacted: `<${target.relativePath} data-uri bytes=${buffer.length}>`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function writeJson(filePath, payload) {
|
|
124
|
+
await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function requestJson(url, payload, apiKey, { timeoutMs = 300000, retries = 2 } = {}) {
|
|
128
|
+
let lastError = null;
|
|
129
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
132
|
+
try {
|
|
133
|
+
const response = await fetch(url, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: {
|
|
136
|
+
"Content-Type": "application/json",
|
|
137
|
+
Authorization: `Bearer ${apiKey}`,
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify(payload),
|
|
140
|
+
signal: controller.signal,
|
|
141
|
+
});
|
|
142
|
+
const text = await response.text();
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
const error = new Error(`Image API HTTP ${response.status}: ${redactSensitiveText(text).slice(0, 600)}`);
|
|
145
|
+
error.status = response.status;
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
return parseJsonResponse(text);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
lastError = error;
|
|
151
|
+
const retryable = TRANSIENT_HTTP_CODES.has(Number(error?.status)) || /aborted|timeout|fetch failed/i.test(String(error?.message || ""));
|
|
152
|
+
if (!retryable || attempt >= retries) throw error;
|
|
153
|
+
await new Promise((resolve) => setTimeout(resolve, 2000 + attempt * 2000));
|
|
154
|
+
} finally {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
throw lastError || new Error("Image API request failed.");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function pollResult({ host, taskId, apiKey, outputDir, intervalMs = 5000, timeoutMs = 900000, requestTimeoutMs = 300000 }) {
|
|
162
|
+
const started = Date.now();
|
|
163
|
+
const pollUrl = `${host.replace(/\/+$/, "")}/v1/draw/result`;
|
|
164
|
+
let attempt = 0;
|
|
165
|
+
while (true) {
|
|
166
|
+
const result = await requestJson(pollUrl, { id: taskId }, apiKey, {
|
|
167
|
+
timeoutMs: requestTimeoutMs,
|
|
168
|
+
retries: 4,
|
|
169
|
+
});
|
|
170
|
+
await writeJson(path.join(outputDir, "result_response.json"), result);
|
|
171
|
+
|
|
172
|
+
const data = result.data || {};
|
|
173
|
+
const status = result.status || data.status;
|
|
174
|
+
if (status === "succeeded" || status === "failed") return result;
|
|
175
|
+
if (Date.now() - started > timeoutMs) throw new Error(`Image generation polling timed out for task ${taskId}.`);
|
|
176
|
+
attempt += 1;
|
|
177
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs + attempt * 250, 12000)));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function downloadImage(url, destination) {
|
|
182
|
+
const response = await fetch(url, {
|
|
183
|
+
headers: {
|
|
184
|
+
"User-Agent": "AgInTiFlow/1.0",
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) throw new Error(`Image download failed with HTTP ${response.status}.`);
|
|
188
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
189
|
+
await fs.writeFile(destination, buffer);
|
|
190
|
+
return {
|
|
191
|
+
bytes: buffer.length,
|
|
192
|
+
sha256: hashBuffer(buffer),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function resultUrls(payload) {
|
|
197
|
+
const data = payload.data || payload;
|
|
198
|
+
const results = Array.isArray(data.results) ? data.results : [];
|
|
199
|
+
return results.map((item) => item?.url).filter(Boolean);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function generateImage(args = {}, config = {}) {
|
|
203
|
+
const prompt = String(args.prompt || "").trim();
|
|
204
|
+
if (!prompt) throw new Error("Image prompt is required.");
|
|
205
|
+
if (Buffer.byteLength(prompt, "utf8") > MAX_PROMPT_BYTES) throw new Error("Image prompt is too large.");
|
|
206
|
+
|
|
207
|
+
const outputDirInput = args.outputDir || `artifacts/images/${timestampSlug()}`;
|
|
208
|
+
const target = resolveWorkspacePath(config, outputDirInput);
|
|
209
|
+
const writePolicy = checkWorkspaceToolUse("write_file", { path: path.join(target.relativePath, "task_manifest.json") }, config);
|
|
210
|
+
if (!writePolicy.allowed) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
blocked: true,
|
|
214
|
+
reason: writePolicy.reason,
|
|
215
|
+
category: writePolicy.category,
|
|
216
|
+
toolName: "generate_image",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
await fs.mkdir(target.absolutePath, { recursive: true });
|
|
221
|
+
const referenceInputs = Array.isArray(args.referenceImages) ? args.referenceImages : [];
|
|
222
|
+
const references = [];
|
|
223
|
+
for (const item of referenceInputs) {
|
|
224
|
+
const converted = await referenceToUrl(item, config);
|
|
225
|
+
if (converted.url) references.push(converted);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const host = String(args.host || DEFAULT_GRS_HOST).trim().replace(/\/+$/, "") || DEFAULT_GRS_HOST;
|
|
229
|
+
const model = String(args.model || DEFAULT_IMAGE_MODEL).trim() || DEFAULT_IMAGE_MODEL;
|
|
230
|
+
const outputStem = safeStem(args.outputStem || "image");
|
|
231
|
+
const payload = {
|
|
232
|
+
model,
|
|
233
|
+
prompt,
|
|
234
|
+
urls: references.map((item) => item.url),
|
|
235
|
+
aspectRatio: normalizeAspectRatio(args.aspectRatio),
|
|
236
|
+
imageSize: normalizeImageSize(args.imageSize),
|
|
237
|
+
webHook: "-1",
|
|
238
|
+
shutProgress: Boolean(args.shutProgress),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const redactedPayload = redactPayload(payload);
|
|
242
|
+
redactedPayload.urls = references.map((item) => item.redacted);
|
|
243
|
+
const promptPath = path.join(target.absolutePath, "prompt.txt");
|
|
244
|
+
const requestPath = path.join(target.absolutePath, "request_payload.redacted.json");
|
|
245
|
+
const manifestPath = path.join(target.absolutePath, "task_manifest.json");
|
|
246
|
+
await fs.writeFile(promptPath, `${prompt}\n`, "utf8");
|
|
247
|
+
await writeJson(requestPath, redactedPayload);
|
|
248
|
+
|
|
249
|
+
const manifest = {
|
|
250
|
+
tool: "generate_image",
|
|
251
|
+
provider: "grsai",
|
|
252
|
+
host,
|
|
253
|
+
model,
|
|
254
|
+
outputDir: target.relativePath,
|
|
255
|
+
promptFile: path.posix.join(target.relativePath, "prompt.txt"),
|
|
256
|
+
requestPayloadRedacted: path.posix.join(target.relativePath, "request_payload.redacted.json"),
|
|
257
|
+
status: args.dryRun ? "prepared" : "started",
|
|
258
|
+
createdAt: new Date().toISOString(),
|
|
259
|
+
};
|
|
260
|
+
await writeJson(manifestPath, manifest);
|
|
261
|
+
|
|
262
|
+
if (args.dryRun) {
|
|
263
|
+
return {
|
|
264
|
+
ok: true,
|
|
265
|
+
dryRun: true,
|
|
266
|
+
toolName: "generate_image",
|
|
267
|
+
path: target.relativePath,
|
|
268
|
+
manifestPath: path.posix.join(target.relativePath, "task_manifest.json"),
|
|
269
|
+
promptPath: path.posix.join(target.relativePath, "prompt.txt"),
|
|
270
|
+
requestPayloadPath: path.posix.join(target.relativePath, "request_payload.redacted.json"),
|
|
271
|
+
imagePaths: [],
|
|
272
|
+
summary: "Prepared redacted image-generation payload without calling the provider.",
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const apiKey = grsaiKey();
|
|
277
|
+
if (!apiKey) {
|
|
278
|
+
throw new Error("Missing GRSAI key. Run `aginti login grsai`, `aginti keys set grsai --stdin`, or `/auxilliary grsai`.");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const submitUrl = `${host}/v1/draw/nano-banana`;
|
|
282
|
+
const submitPayload = await requestJson(submitUrl, payload, apiKey, {
|
|
283
|
+
timeoutMs: Number(args.requestTimeoutMs) || 300000,
|
|
284
|
+
retries: 2,
|
|
285
|
+
});
|
|
286
|
+
await writeJson(path.join(target.absolutePath, "submit_response.json"), submitPayload);
|
|
287
|
+
const taskId = submitPayload?.data?.id || submitPayload?.id;
|
|
288
|
+
if (!taskId) throw new Error("Image API did not return a task id.");
|
|
289
|
+
|
|
290
|
+
manifest.taskId = String(taskId);
|
|
291
|
+
manifest.status = "submitted";
|
|
292
|
+
manifest.submittedAt = new Date().toISOString();
|
|
293
|
+
await writeJson(manifestPath, manifest);
|
|
294
|
+
|
|
295
|
+
const resultPayload = await pollResult({
|
|
296
|
+
host,
|
|
297
|
+
taskId: String(taskId),
|
|
298
|
+
apiKey,
|
|
299
|
+
outputDir: target.absolutePath,
|
|
300
|
+
intervalMs: Number(args.pollIntervalMs) || 5000,
|
|
301
|
+
timeoutMs: Number(args.pollTimeoutMs) || 900000,
|
|
302
|
+
requestTimeoutMs: Number(args.requestTimeoutMs) || 300000,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const status = resultPayload.status || resultPayload?.data?.status;
|
|
306
|
+
if (status !== "succeeded") {
|
|
307
|
+
manifest.status = status || "failed";
|
|
308
|
+
manifest.finishedAt = new Date().toISOString();
|
|
309
|
+
await writeJson(manifestPath, manifest);
|
|
310
|
+
throw new Error(`Image generation failed with status ${status || "unknown"}. See ${manifestPath}.`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const urls = resultUrls(resultPayload);
|
|
314
|
+
if (urls.length === 0) throw new Error("Image API succeeded but returned no image URLs.");
|
|
315
|
+
const imagePaths = [];
|
|
316
|
+
const downloads = [];
|
|
317
|
+
for (let index = 0; index < urls.length; index += 1) {
|
|
318
|
+
const parsed = new URL(urls[index]);
|
|
319
|
+
const suffix = path.extname(parsed.pathname) || ".png";
|
|
320
|
+
const filename = urls.length === 1 ? `${outputStem}${suffix}` : `${outputStem}_${String(index + 1).padStart(2, "0")}${suffix}`;
|
|
321
|
+
const absolutePath = path.join(target.absolutePath, filename);
|
|
322
|
+
const info = await downloadImage(urls[index], absolutePath);
|
|
323
|
+
const relativePath = path.posix.join(target.relativePath, filename);
|
|
324
|
+
imagePaths.push(relativePath);
|
|
325
|
+
downloads.push({ path: relativePath, ...info });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
manifest.status = "succeeded";
|
|
329
|
+
manifest.finishedAt = new Date().toISOString();
|
|
330
|
+
manifest.downloadedFiles = downloads;
|
|
331
|
+
await writeJson(manifestPath, manifest);
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
ok: true,
|
|
335
|
+
toolName: "generate_image",
|
|
336
|
+
path: imagePaths[0] || target.relativePath,
|
|
337
|
+
imagePaths,
|
|
338
|
+
manifestPath: path.posix.join(target.relativePath, "task_manifest.json"),
|
|
339
|
+
promptPath: path.posix.join(target.relativePath, "prompt.txt"),
|
|
340
|
+
requestPayloadPath: path.posix.join(target.relativePath, "request_payload.redacted.json"),
|
|
341
|
+
taskId: String(taskId),
|
|
342
|
+
summary: `${imagePaths.length} image(s) generated`,
|
|
343
|
+
};
|
|
344
|
+
}
|
package/src/capabilities.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getModelPresets } from "./model-routing.js";
|
|
|
7
7
|
import { listProjectSessions, projectPaths, providerKeyStatus } from "./project.js";
|
|
8
8
|
import { listTaskProfiles } from "./task-profiles.js";
|
|
9
9
|
import { listAgentWrappers } from "./tool-wrappers.js";
|
|
10
|
+
import { listAuxiliarySkills } from "./auxiliary-tools.js";
|
|
10
11
|
|
|
11
12
|
const execFileAsync = promisify(execFile);
|
|
12
13
|
|
|
@@ -139,6 +140,10 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
|
|
|
139
140
|
capability("docker", Boolean(dockerStatus?.dockerAvailable), dockerStatus || {}),
|
|
140
141
|
capability("deepseek-key", keyStatus.deepseek, { envVars: keyStatus.envVars.deepseek }),
|
|
141
142
|
capability("openai-key", keyStatus.openai, { envVars: keyStatus.envVars.openai }),
|
|
143
|
+
capability("grsai-key", keyStatus.grsai, {
|
|
144
|
+
envVars: keyStatus.envVars.grsai,
|
|
145
|
+
setup: "Optional for image generation. Run `aginti login grsai` or use `/auxilliary grsai` in chat.",
|
|
146
|
+
}),
|
|
142
147
|
capability("file-tools", Boolean(config.allowFileTools), { workspace: config.commandCwd }),
|
|
143
148
|
capability("shell-tool", Boolean(config.allowShellTool), {
|
|
144
149
|
sandboxMode: config.sandboxMode,
|
|
@@ -178,6 +183,7 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
|
|
|
178
183
|
keys: {
|
|
179
184
|
deepseek: keyStatus.deepseek,
|
|
180
185
|
openai: keyStatus.openai,
|
|
186
|
+
grsai: keyStatus.grsai,
|
|
181
187
|
mock: true,
|
|
182
188
|
localEnv: keyStatus.localEnv,
|
|
183
189
|
envVars: keyStatus.envVars,
|
|
@@ -194,6 +200,13 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
|
|
|
194
200
|
label: profile.label,
|
|
195
201
|
tools: profile.tools,
|
|
196
202
|
})),
|
|
203
|
+
auxiliarySkills: listAuxiliarySkills().map((skill) => ({
|
|
204
|
+
id: skill.id,
|
|
205
|
+
label: skill.label,
|
|
206
|
+
provider: skill.provider,
|
|
207
|
+
toolName: skill.toolName,
|
|
208
|
+
available: skill.available,
|
|
209
|
+
})),
|
|
197
210
|
},
|
|
198
211
|
checks,
|
|
199
212
|
maintenancePolicy: maintenancePolicyChecks(config),
|
|
@@ -221,7 +234,7 @@ export function printCapabilityReport(report) {
|
|
|
221
234
|
console.log(
|
|
222
235
|
`keys: deepseek=${report.keys.deepseek ? "available" : "missing"} openai=${
|
|
223
236
|
report.keys.openai ? "available" : "missing"
|
|
224
|
-
} mock=available localEnv=${report.keys.localEnv}`
|
|
237
|
+
} grsai=${report.keys.grsai ? "available" : "missing"} mock=available localEnv=${report.keys.localEnv}`
|
|
225
238
|
);
|
|
226
239
|
for (const check of report.checks) {
|
|
227
240
|
const suffix = check.version ? ` ${check.version}` : check.reason ? ` ${check.reason}` : check.hint ? ` ${check.hint}` : "";
|
package/src/cli.js
CHANGED
|
@@ -44,6 +44,7 @@ export function parseArgs(argv) {
|
|
|
44
44
|
allowShellTool: undefined,
|
|
45
45
|
allowFileTools: undefined,
|
|
46
46
|
allowWrapperTools: undefined,
|
|
47
|
+
allowAuxiliaryTools: undefined,
|
|
47
48
|
allowDestructive: undefined,
|
|
48
49
|
preferredWrapper: "",
|
|
49
50
|
taskProfile: "",
|
|
@@ -60,6 +61,7 @@ export function parseArgs(argv) {
|
|
|
60
61
|
host: "",
|
|
61
62
|
listProfiles: false,
|
|
62
63
|
latex: false,
|
|
64
|
+
image: false,
|
|
63
65
|
};
|
|
64
66
|
|
|
65
67
|
const parts = [];
|
|
@@ -141,6 +143,12 @@ export function parseArgs(argv) {
|
|
|
141
143
|
result.maxSteps = result.maxSteps || 30;
|
|
142
144
|
continue;
|
|
143
145
|
}
|
|
146
|
+
if (arg === "--image" || arg === "--image-gen" || arg === "--image-generation") {
|
|
147
|
+
result.image = true;
|
|
148
|
+
result.taskProfile = "image";
|
|
149
|
+
result.allowAuxiliaryTools = true;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
144
152
|
if (arg === "--max-steps") {
|
|
145
153
|
result.maxSteps = Number(readOption(argv, i));
|
|
146
154
|
i += 1;
|
|
@@ -166,6 +174,14 @@ export function parseArgs(argv) {
|
|
|
166
174
|
result.allowFileTools = false;
|
|
167
175
|
continue;
|
|
168
176
|
}
|
|
177
|
+
if (arg === "--allow-auxiliary-tools" || arg === "--allow-auxiliary") {
|
|
178
|
+
result.allowAuxiliaryTools = true;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (arg === "--no-auxiliary-tools" || arg === "--no-auxiliary") {
|
|
182
|
+
result.allowAuxiliaryTools = false;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
169
185
|
if (arg === "--allow-wrappers") {
|
|
170
186
|
result.allowWrapperTools = true;
|
|
171
187
|
continue;
|
|
@@ -220,15 +236,23 @@ export function parseArgs(argv) {
|
|
|
220
236
|
|
|
221
237
|
function printUsage() {
|
|
222
238
|
console.log(
|
|
223
|
-
'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti login deepseek OR aginti resume [latest|<session-id>] ["prompt"] OR aginti queue <session-id> "message" OR aginti [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-destructive] [--allow-file-tools|--no-file-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
|
|
239
|
+
'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti login deepseek|openai|grsai OR aginti resume [latest|<session-id>] ["prompt"] OR aginti queue <session-id> "message" OR aginti [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-destructive] [--allow-file-tools|--no-file-tools] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
|
|
224
240
|
);
|
|
225
241
|
}
|
|
226
242
|
|
|
243
|
+
function providerLabel(provider) {
|
|
244
|
+
const normalized = String(provider || "").toLowerCase();
|
|
245
|
+
if (normalized === "openai") return "OpenAI";
|
|
246
|
+
if (normalized === "grsai" || normalized === "auxiliary" || normalized === "auxilliary") return "GRSAI";
|
|
247
|
+
return "DeepSeek";
|
|
248
|
+
}
|
|
249
|
+
|
|
227
250
|
function agentDefaults(args) {
|
|
228
251
|
const defaults = {
|
|
229
252
|
...args,
|
|
230
253
|
allowShellTool: args.allowShellTool ?? true,
|
|
231
254
|
allowFileTools: args.allowFileTools ?? true,
|
|
255
|
+
allowAuxiliaryTools: args.allowAuxiliaryTools ?? true,
|
|
232
256
|
sandboxMode: args.sandboxMode || "docker-workspace",
|
|
233
257
|
packageInstallPolicy: args.packageInstallPolicy || "allow",
|
|
234
258
|
useDockerSandbox: args.useDockerSandbox ?? true,
|
|
@@ -279,7 +303,7 @@ function printDoctorReport(report) {
|
|
|
279
303
|
console.log(
|
|
280
304
|
`keys: deepseek=${report.keys.deepseek ? "available" : "missing"} openai=${
|
|
281
305
|
report.keys.openai ? "available" : "missing"
|
|
282
|
-
} mock=available localEnv=${report.project.localEnvPresent}`
|
|
306
|
+
} grsai=${report.keys.grsai ? "available" : "missing"} mock=available localEnv=${report.project.localEnvPresent}`
|
|
283
307
|
);
|
|
284
308
|
console.log(
|
|
285
309
|
`sandbox=${report.sandbox?.sandboxMode || "unknown"} docker=${
|
|
@@ -321,15 +345,15 @@ async function handleKeyCommand(argv) {
|
|
|
321
345
|
console.log(
|
|
322
346
|
`keys: deepseek=${status.deepseek ? "available" : "missing"} openai=${
|
|
323
347
|
status.openai ? "available" : "missing"
|
|
324
|
-
} mock=available localEnv=${status.localEnv}`
|
|
348
|
+
} grsai=${status.grsai ? "available" : "missing"} mock=available localEnv=${status.localEnv}`
|
|
325
349
|
);
|
|
326
|
-
console.log("env vars: DeepSeek=DEEPSEEK_API_KEY or LLM_API_KEY; OpenAI=OPENAI_API_KEY or LLM_API_KEY");
|
|
350
|
+
console.log("env vars: DeepSeek=DEEPSEEK_API_KEY or LLM_API_KEY; OpenAI=OPENAI_API_KEY or LLM_API_KEY; image=GRSAI or GRSAI_API_KEY");
|
|
327
351
|
return;
|
|
328
352
|
}
|
|
329
353
|
|
|
330
354
|
if (verb === "set") {
|
|
331
355
|
const target = provider || "deepseek";
|
|
332
|
-
const key = argv.includes("--stdin") ? await readStdin() : await promptHidden(`${target
|
|
356
|
+
const key = argv.includes("--stdin") ? await readStdin() : await promptHidden(`${providerLabel(target)} API key/token: `);
|
|
333
357
|
if (!key) {
|
|
334
358
|
console.error("No key saved.");
|
|
335
359
|
process.exit(1);
|
|
@@ -339,7 +363,7 @@ async function handleKeyCommand(argv) {
|
|
|
339
363
|
return;
|
|
340
364
|
}
|
|
341
365
|
|
|
342
|
-
console.error("Usage: aginti keys status OR aginti keys set deepseek [--stdin]");
|
|
366
|
+
console.error("Usage: aginti keys status OR aginti keys set deepseek|openai|grsai [--stdin]");
|
|
343
367
|
process.exit(1);
|
|
344
368
|
}
|
|
345
369
|
|
|
@@ -460,7 +484,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
460
484
|
const provider = argv[1] || "deepseek";
|
|
461
485
|
const key = argv.includes("--stdin") || !process.stdin.isTTY
|
|
462
486
|
? await readStdin()
|
|
463
|
-
: await promptHidden(`${provider
|
|
487
|
+
: await promptHidden(`${providerLabel(provider)} API key/token: `);
|
|
464
488
|
if (!key) {
|
|
465
489
|
console.error("No key saved.");
|
|
466
490
|
process.exit(1);
|
package/src/config.js
CHANGED
|
@@ -78,6 +78,10 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
78
78
|
overrides.allowWrapperTools ?? args.allowWrapperTools ?? process.env.ALLOW_WRAPPER_TOOLS,
|
|
79
79
|
false
|
|
80
80
|
),
|
|
81
|
+
allowAuxiliaryTools: parseBoolean(
|
|
82
|
+
overrides.allowAuxiliaryTools ?? args.allowAuxiliaryTools ?? process.env.ALLOW_AUXILIARY_TOOLS,
|
|
83
|
+
true
|
|
84
|
+
),
|
|
81
85
|
preferredWrapper: normalizeWrapperName(
|
|
82
86
|
overrides.preferredWrapper ?? args.preferredWrapper ?? process.env.PREFERRED_WRAPPER ?? process.env.AGENT_WRAPPER
|
|
83
87
|
),
|
package/src/guardrails.js
CHANGED
|
@@ -151,6 +151,21 @@ export function checkToolUse({ toolName, args, snapshot, config }) {
|
|
|
151
151
|
return { allowed: true };
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
if (toolName === "generate_image") {
|
|
155
|
+
if (!config.allowAuxiliaryTools) {
|
|
156
|
+
return { allowed: false, reason: "Auxiliary tools are disabled for this run.", category: "auxiliary-tools" };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const prompt = String(args.prompt || "").trim();
|
|
160
|
+
if (!prompt) return { allowed: false, reason: "Image prompt is required.", category: "auxiliary-tools" };
|
|
161
|
+
if (Buffer.byteLength(prompt, "utf8") > MAX_CANVAS_CONTENT_BYTES) {
|
|
162
|
+
return { allowed: false, reason: "Image prompt is too large.", category: "auxiliary-tools" };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const outputDir = String(args.outputDir || "artifacts/images/generated").trim();
|
|
166
|
+
return checkWorkspaceToolUse("write_file", { path: `${outputDir.replace(/\/+$/, "")}/task_manifest.json` }, config);
|
|
167
|
+
}
|
|
168
|
+
|
|
154
169
|
if (toolName === "send_to_canvas") {
|
|
155
170
|
const title = String(args.title || "").trim();
|
|
156
171
|
if (!title) return { allowed: false, reason: "Canvas title is required." };
|
package/src/interactive-cli.js
CHANGED
|
@@ -30,6 +30,8 @@ const SLASH_COMMANDS = [
|
|
|
30
30
|
"/status",
|
|
31
31
|
"/login",
|
|
32
32
|
"/auth",
|
|
33
|
+
"/auxilliary",
|
|
34
|
+
"/auxiliary",
|
|
33
35
|
"/new",
|
|
34
36
|
"/resume",
|
|
35
37
|
"/sessions",
|
|
@@ -206,8 +208,10 @@ function printHelp() {
|
|
|
206
208
|
"Commands:",
|
|
207
209
|
" /help Show this help.",
|
|
208
210
|
" /status Show active route, workspace, sandbox, and session.",
|
|
209
|
-
" /login [deepseek|openai] Paste and save a project-local API key.",
|
|
210
|
-
" /auth [deepseek|openai] Alias for /login.",
|
|
211
|
+
" /login [deepseek|openai|grsai] Paste and save a project-local API key.",
|
|
212
|
+
" /auth [deepseek|openai|grsai] Alias for /login.",
|
|
213
|
+
" /auxilliary [status|grsai|on|off|image]",
|
|
214
|
+
" Manage optional auxiliary skills, including GRS AI image generation.",
|
|
211
215
|
" /new Start a fresh session on the next message.",
|
|
212
216
|
" /resume <session-id> Continue a saved session.",
|
|
213
217
|
" /sessions List recent sessions in this project.",
|
|
@@ -345,7 +349,7 @@ function printStatus(state) {
|
|
|
345
349
|
printSystemLine(`provider=${state.provider || "auto"} routing=${state.routingMode} model=${state.model || "auto"}`);
|
|
346
350
|
printSystemLine(`profile=${state.taskProfile} maxSteps=${state.maxSteps}`);
|
|
347
351
|
printSystemLine(
|
|
348
|
-
`shell=${state.allowShellTool} files=${state.allowFileTools} sandbox=${state.sandboxMode} installs=${state.packageInstallPolicy}`
|
|
352
|
+
`shell=${state.allowShellTool} files=${state.allowFileTools} auxiliary=${state.allowAuxiliaryTools} sandbox=${state.sandboxMode} installs=${state.packageInstallPolicy}`
|
|
349
353
|
);
|
|
350
354
|
if (state.sandboxMode !== "host") {
|
|
351
355
|
printSystemLine(`dockerWorkspace=/workspace -> ${state.commandCwd || process.cwd()}`);
|
|
@@ -429,6 +433,7 @@ function createState(args = {}) {
|
|
|
429
433
|
packageInstallPolicy: normalizePackageInstallPolicy(args.packageInstallPolicy || "allow"),
|
|
430
434
|
allowShellTool: args.allowShellTool ?? true,
|
|
431
435
|
allowFileTools: args.allowFileTools ?? true,
|
|
436
|
+
allowAuxiliaryTools: args.allowAuxiliaryTools ?? true,
|
|
432
437
|
allowWrapperTools: args.allowWrapperTools ?? false,
|
|
433
438
|
allowDestructive: args.allowDestructive ?? false,
|
|
434
439
|
preferredWrapper: args.preferredWrapper || "codex",
|
|
@@ -463,23 +468,27 @@ async function maybeOnboardDeepSeekKey(state) {
|
|
|
463
468
|
}
|
|
464
469
|
|
|
465
470
|
async function promptAndSaveProviderKey(provider = "deepseek", state = null) {
|
|
466
|
-
const
|
|
471
|
+
const aliases = { auxiliary: "grsai", auxilliary: "grsai", image: "grsai", imagegen: "grsai" };
|
|
472
|
+
const candidate = aliases[String(provider || "").toLowerCase()] || String(provider || "").toLowerCase();
|
|
473
|
+
const normalized = ["openai", "deepseek", "grsai"].includes(candidate)
|
|
467
474
|
? String(provider || "").toLowerCase()
|
|
468
475
|
: "deepseek";
|
|
469
|
-
const
|
|
476
|
+
const canonical = aliases[normalized] || normalized;
|
|
477
|
+
const labelText = canonical === "openai" ? "OpenAI" : canonical === "grsai" ? "GRSAI" : "DeepSeek";
|
|
470
478
|
const key = await promptHidden(`${labelText} API key/token (paste, Enter to save): `);
|
|
471
479
|
if (!key) {
|
|
472
480
|
printAgentMessage("No key saved.");
|
|
473
481
|
return;
|
|
474
482
|
}
|
|
475
483
|
|
|
476
|
-
const result = await setProviderKey(process.cwd(),
|
|
484
|
+
const result = await setProviderKey(process.cwd(), canonical, key);
|
|
477
485
|
if (state) {
|
|
478
|
-
state.provider =
|
|
486
|
+
if (canonical !== "grsai") state.provider = canonical;
|
|
479
487
|
if (state.routingMode === "manual" && state.model === "mock-agent") {
|
|
480
488
|
state.routingMode = "smart";
|
|
481
489
|
state.model = "";
|
|
482
490
|
}
|
|
491
|
+
if (canonical === "grsai") state.allowAuxiliaryTools = true;
|
|
483
492
|
}
|
|
484
493
|
printAgentMessage(`Saved ${result.keyName} to project-local ignored env. Raw key was not printed.`);
|
|
485
494
|
}
|
|
@@ -496,13 +505,53 @@ async function handleCommand(line, state, packageDir) {
|
|
|
496
505
|
if (command === "status") {
|
|
497
506
|
printStatus(state);
|
|
498
507
|
const keys = providerKeyStatus(process.cwd());
|
|
499
|
-
printSystemLine(
|
|
508
|
+
printSystemLine(
|
|
509
|
+
`keys deepseek=${keys.deepseek ? "available" : "missing"} openai=${keys.openai ? "available" : "missing"} grsai=${
|
|
510
|
+
keys.grsai ? "available" : "missing"
|
|
511
|
+
}`
|
|
512
|
+
);
|
|
500
513
|
return true;
|
|
501
514
|
}
|
|
502
515
|
if (command === "login" || command === "auth") {
|
|
503
516
|
await promptAndSaveProviderKey(value || "deepseek", state);
|
|
504
517
|
return true;
|
|
505
518
|
}
|
|
519
|
+
if (command === "auxilliary" || command === "auxiliary") {
|
|
520
|
+
const action = value || "status";
|
|
521
|
+
if (action === "status") {
|
|
522
|
+
const keys = providerKeyStatus(process.cwd());
|
|
523
|
+
printAgentMessage(
|
|
524
|
+
[
|
|
525
|
+
`Auxiliary tools: ${state.allowAuxiliaryTools ? "on" : "off"}`,
|
|
526
|
+
`Image generation: ${keys.grsai ? "GRSAI key available" : "missing GRSAI key"}`,
|
|
527
|
+
"Use `/auxilliary grsai` to paste the image key, `/auxilliary image` to switch to the image profile, or `/auxilliary off` to hide auxiliary tools.",
|
|
528
|
+
].join("\n")
|
|
529
|
+
);
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
if (action === "on") {
|
|
533
|
+
state.allowAuxiliaryTools = true;
|
|
534
|
+
printSystemLine("auxiliary=on");
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
if (action === "off") {
|
|
538
|
+
state.allowAuxiliaryTools = false;
|
|
539
|
+
printSystemLine("auxiliary=off");
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
if (action === "image") {
|
|
543
|
+
state.allowAuxiliaryTools = true;
|
|
544
|
+
state.taskProfile = "image";
|
|
545
|
+
printSystemLine("auxiliary=on profile=image");
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
if (["grsai", "login", "key", "token"].includes(action)) {
|
|
549
|
+
await promptAndSaveProviderKey("grsai", state);
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
printAgentMessage("Usage: /auxilliary [status|grsai|on|off|image]");
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
506
555
|
if (command === "new") {
|
|
507
556
|
state.sessionId = "";
|
|
508
557
|
printAgentMessage("Next message will start a new session.");
|
|
@@ -630,6 +679,7 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
630
679
|
packageInstallPolicy: state.packageInstallPolicy,
|
|
631
680
|
allowShellTool: state.allowShellTool,
|
|
632
681
|
allowFileTools: state.allowFileTools,
|
|
682
|
+
allowAuxiliaryTools: state.allowAuxiliaryTools,
|
|
633
683
|
allowWrapperTools: state.allowWrapperTools,
|
|
634
684
|
allowDestructive: state.allowDestructive,
|
|
635
685
|
preferredWrapper: state.preferredWrapper,
|