@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.
@@ -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
+ }
@@ -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 === "openai" ? "OpenAI" : "DeepSeek"} API key: `);
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 === "openai" ? "OpenAI" : "DeepSeek"} API key: `);
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." };
@@ -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 normalized = ["openai", "deepseek"].includes(String(provider || "").toLowerCase())
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 labelText = normalized === "openai" ? "OpenAI" : "DeepSeek";
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(), normalized, key);
484
+ const result = await setProviderKey(process.cwd(), canonical, key);
477
485
  if (state) {
478
- state.provider = normalized;
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(`keys deepseek=${keys.deepseek ? "available" : "missing"} openai=${keys.openai ? "available" : "missing"}`);
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,