@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 CHANGED
@@ -53,6 +53,10 @@ aginti login deepseek
53
53
  # inside chat, use /login or /auth
54
54
  # or non-interactively:
55
55
  printf '%s' "$DEEPSEEK_API_KEY" | aginti keys set deepseek --stdin
56
+
57
+ # optional image-generation auxiliary skill:
58
+ aginti login grsai
59
+ # inside chat, use /auxilliary grsai or /auxiliary grsai
56
60
  ```
57
61
 
58
62
  Start an interactive Codex-style CLI chat from any project folder:
@@ -67,6 +71,8 @@ Inside chat, type normal requests such as `write a small Python CLI app with tes
67
71
 
68
72
  For code edits, AgInTiFlow routes patch/refactor/database-style tasks to DeepSeek v4 pro by default and exposes `apply_patch` as a deterministic workspace tool. It supports exact replacements, Codex-style patch envelopes, and unified diffs, with preflight checks, path guardrails, hashes, and compact per-file diffs. See [docs/patch-tools.md](docs/patch-tools.md).
69
73
 
74
+ For raster image work, AgInTiFlow has an optional `image_generation` skill backed by the `generate_image` tool and a local `GRSAI` key. The skill tells DeepSeek when image generation is appropriate; the tool calls GRS AI Nano Banana, saves manifests/images under `artifacts/images`, and sends the result to the canvas. See [docs/auxiliary-image-generation.md](docs/auxiliary-image-generation.md).
75
+
70
76
  Launch the local web UI from an installed package:
71
77
 
72
78
  ```bash
@@ -0,0 +1,62 @@
1
+ # Auxiliary Image Generation
2
+
3
+ AgInTiFlow separates **skills** from **tools**:
4
+
5
+ - A skill is instruction and routing context that teaches the agent when a capability is useful.
6
+ - A tool is the deterministic callable function that performs the action and writes auditable artifacts.
7
+
8
+ The optional image-generation skill is `image_generation`. Its tool is `generate_image`.
9
+
10
+ ## Setup
11
+
12
+ Store the GRS AI key project-locally:
13
+
14
+ ```bash
15
+ aginti login grsai
16
+ # or
17
+ printf '%s' "$GRSAI" | aginti keys set grsai --stdin
18
+ ```
19
+
20
+ Inside interactive chat, use either spelling:
21
+
22
+ ```text
23
+ /auxilliary grsai
24
+ /auxiliary grsai
25
+ ```
26
+
27
+ The key is saved in `.aginti/.env` as `GRSAI` with `0600` permissions. The CLI and web app only report whether the key exists; they never return the raw value.
28
+
29
+ ## Runtime Flow
30
+
31
+ For image, cover, poster, illustration, photo, or logo-concept requests, the model can call:
32
+
33
+ ```json
34
+ {
35
+ "prompt": "A polished cyan robot painting a circuit-board river, high-end product illustration",
36
+ "outputDir": "artifacts/images/robot-cover",
37
+ "outputStem": "robot-cover",
38
+ "aspectRatio": "1:1",
39
+ "imageSize": "2K"
40
+ }
41
+ ```
42
+
43
+ The tool uses the GRS AI Nano Banana API:
44
+
45
+ - `POST https://grsaiapi.com/v1/draw/nano-banana`
46
+ - `POST https://grsaiapi.com/v1/draw/result`
47
+ - `Authorization: Bearer <GRSAI>`
48
+
49
+ Saved workspace artifacts:
50
+
51
+ - `prompt.txt`
52
+ - `request_payload.redacted.json`
53
+ - `submit_response.json`
54
+ - `result_response.json`
55
+ - `task_manifest.json`
56
+ - generated image files, for example `image.png`
57
+
58
+ Generated images are sent to the canvas automatically when available.
59
+
60
+ ## Guardrails
61
+
62
+ Output paths must stay inside the project workspace. Secret paths, `.git`, `node_modules` writes, and oversized reference images are blocked. Reference images may be workspace files, HTTPS URLs, or data URLs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.8.12",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a resumable Playwright website-control agent with OpenAI-compatible tool calling.",
6
6
  "license": "Apache-2.0",
@@ -41,6 +41,7 @@
41
41
  "scripts/install-docker-ubuntu.sh",
42
42
  "scripts/setup-agent-toolchain-docker.sh",
43
43
  "scripts/real-deepseek-capabilities.js",
44
+ "scripts/smoke-auxiliary-tools.js",
44
45
  "scripts/smoke-cli-chat.js",
45
46
  "scripts/smoke-coding-tools.js",
46
47
  "scripts/smoke-capabilities.js",
@@ -61,12 +62,13 @@
61
62
  "check": "node --check run.js && node --check web.js && node --check bin/aginti-cli.js && node --check src/*.js",
62
63
  "setup:toolchain-docker": "scripts/setup-agent-toolchain-docker.sh",
63
64
  "smoke:coding-tools": "node scripts/smoke-coding-tools.js",
65
+ "smoke:auxiliary-tools": "node scripts/smoke-auxiliary-tools.js",
64
66
  "smoke:cli-chat": "node scripts/smoke-cli-chat.js",
65
67
  "smoke:toolchain-docker": "node scripts/smoke-toolchain-docker.js",
66
68
  "smoke:inbox": "node scripts/smoke-inbox.js",
67
69
  "smoke:web-api": "node scripts/smoke-web-api.js",
68
70
  "real:deepseek": "node scripts/real-deepseek-capabilities.js",
69
- "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:capabilities && npm run smoke:cli-chat && npm run smoke:inbox",
71
+ "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:auxiliary-tools && npm run smoke:capabilities && npm run smoke:cli-chat && npm run smoke:inbox",
70
72
  "pack:dry-run": "npm pack --dry-run",
71
73
  "smoke:capabilities": "node scripts/smoke-capabilities.js"
72
74
  },
package/public/app.js CHANGED
@@ -24,7 +24,7 @@ const translations = {
24
24
  setupHelp:
25
25
  "DeepSeek/OpenAI keys are missing. Use mock mode, export an env var, or save a project-local DeepSeek key.",
26
26
  setupEnvHelp:
27
- "Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, or LLM_API_KEY. Mock mode remains available for local tests.",
27
+ "Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, LLM_API_KEY, and optional GRSAI for image generation. Mock mode remains available for local tests.",
28
28
  setupProviderLabel: "Provider",
29
29
  setupKeyLabel: "API key",
30
30
  saveKeyButton: "Save local key",
@@ -68,6 +68,7 @@ const translations = {
68
68
  headlessLabel: "Headless browser",
69
69
  shellToolLabel: "Enable shell tool",
70
70
  fileToolLabel: "Enable file tools",
71
+ auxiliaryToolLabel: "Enable auxiliary skills",
71
72
  wrapperToolLabel: "Enable agent wrappers",
72
73
  preferredWrapperLabel: "Preferred wrapper",
73
74
  dockerSandboxLabel: "Use Docker sandbox",
@@ -648,6 +649,7 @@ const logsEl = document.querySelector("#logs");
648
649
  const runMetaEl = document.querySelector("#run-meta");
649
650
  const stopRunButton = document.querySelector("#stop-run");
650
651
  const keyStatusEl = document.querySelector("#key-status");
652
+ const allowAuxiliaryToolsField = document.querySelector("#allowAuxiliaryTools");
651
653
  const allowWrapperToolsField = document.querySelector("#allowWrapperTools");
652
654
  const preferredWrapperField = document.querySelector("#preferredWrapper");
653
655
  const wrapperStatusEl = document.querySelector("#wrapper-status");
@@ -744,7 +746,9 @@ function renderKeyStatus(status = lastKeyStatus) {
744
746
  if (!status) return;
745
747
  keyStatusEl.textContent = `${t("keysLabel")}: OpenAI ${
746
748
  status.openai ? t("availableLabel") : t("missingLabel")
747
- } · DeepSeek ${status.deepseek ? t("availableLabel") : t("missingLabel")} · ${t("mockLabel")} ${
749
+ } · DeepSeek ${status.deepseek ? t("availableLabel") : t("missingLabel")} · GRS AI ${
750
+ status.grsai ? t("availableLabel") : t("missingLabel")
751
+ } · ${t("mockLabel")} ${
748
752
  status.mock ? t("availableLabel") : t("missingLabel")
749
753
  }`;
750
754
  if (setupCardEl) setupCardEl.hidden = Boolean(status.openai || status.deepseek);
@@ -979,6 +983,7 @@ function formPayload() {
979
983
  headless: document.querySelector("#headless").checked,
980
984
  allowShellTool: document.querySelector("#allowShellTool").checked,
981
985
  allowFileTools: document.querySelector("#allowFileTools").checked,
986
+ allowAuxiliaryTools: allowAuxiliaryToolsField?.checked ?? true,
982
987
  allowWrapperTools: allowWrapperToolsField.checked,
983
988
  preferredWrapper: preferredWrapperField.value,
984
989
  taskProfile: taskProfileField?.value || "auto",
@@ -2167,6 +2172,7 @@ async function loadConfig() {
2167
2172
  packageInstallPolicyField.value = prefs.packageInstallPolicy || "allow";
2168
2173
  document.querySelector("#allowShellTool").checked = prefs.allowShellTool ?? true;
2169
2174
  document.querySelector("#allowFileTools").checked = prefs.allowFileTools ?? true;
2175
+ if (allowAuxiliaryToolsField) allowAuxiliaryToolsField.checked = prefs.allowAuxiliaryTools ?? true;
2170
2176
  allowWrapperToolsField.checked = prefs.allowWrapperTools ?? false;
2171
2177
  preferredWrapperField.value = prefs.preferredWrapper || "codex";
2172
2178
  document.querySelector("#dockerSandboxImage").value = prefs.dockerSandboxImage || "agintiflow-sandbox:latest";
package/public/index.html CHANGED
@@ -57,6 +57,7 @@
57
57
  <select id="setup-provider">
58
58
  <option value="deepseek">DeepSeek</option>
59
59
  <option value="openai">OpenAI</option>
60
+ <option value="grsai">GRS AI image</option>
60
61
  </select>
61
62
  </label>
62
63
  <label>
@@ -175,6 +176,11 @@
175
176
  <span class="switch" aria-hidden="true"></span>
176
177
  <span data-i18n="fileToolLabel">Enable file tools</span>
177
178
  </label>
179
+ <label class="switch-label">
180
+ <input id="allowAuxiliaryTools" name="allowAuxiliaryTools" type="checkbox" checked />
181
+ <span class="switch" aria-hidden="true"></span>
182
+ <span data-i18n="auxiliaryToolLabel">Enable auxiliary skills</span>
183
+ </label>
178
184
  <label class="switch-label">
179
185
  <input id="allowPasswords" name="allowPasswords" type="checkbox" />
180
186
  <span class="switch" aria-hidden="true"></span>
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { runAgent } from "../src/agent-runner.js";
7
+ import { generateImage, listAuxiliarySkills } from "../src/auxiliary-tools.js";
8
+ import { resolveRuntimeConfig } from "../src/config.js";
9
+ import { providerKeyStatus, setProviderKey } from "../src/project.js";
10
+ import { SessionStore } from "../src/session-store.js";
11
+
12
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
13
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-auxiliary-"));
14
+ const runtimeDir = path.join(tempRoot, "runtime");
15
+ const workspace = path.join(tempRoot, "workspace");
16
+ await fs.mkdir(workspace, { recursive: true });
17
+
18
+ function assert(condition, message) {
19
+ if (!condition) throw new Error(message);
20
+ }
21
+
22
+ try {
23
+ await setProviderKey(workspace, "grsai", "test-grsai-key");
24
+ const keyStatus = providerKeyStatus(workspace);
25
+ assert(keyStatus.grsai, "GRSAI key status was not detected");
26
+ assert(keyStatus.envVars.grsai.includes("GRSAI"), "GRSAI env var name was not reported");
27
+ assert(listAuxiliarySkills().some((skill) => skill.id === "image_generation"), "image_generation skill missing");
28
+
29
+ const dryRun = await generateImage(
30
+ {
31
+ prompt: "A small cyan robot holding a paintbrush, clean bright product illustration.",
32
+ outputDir: "artifacts/images/dry-run",
33
+ outputStem: "robot",
34
+ dryRun: true,
35
+ },
36
+ {
37
+ commandCwd: workspace,
38
+ allowFileTools: true,
39
+ }
40
+ );
41
+ assert(dryRun.ok && dryRun.dryRun, "generate_image dry run failed");
42
+ await fs.access(path.join(workspace, "artifacts/images/dry-run/task_manifest.json"));
43
+ const payloadText = await fs.readFile(path.join(workspace, "artifacts/images/dry-run/request_payload.redacted.json"), "utf8");
44
+ assert(payloadText.includes("nano-banana-2"), "redacted image payload was not written");
45
+
46
+ const blocked = await generateImage(
47
+ {
48
+ prompt: "blocked",
49
+ outputDir: ".env/images",
50
+ dryRun: true,
51
+ },
52
+ {
53
+ commandCwd: workspace,
54
+ allowFileTools: true,
55
+ }
56
+ );
57
+ assert(blocked.blocked, "generate_image did not block sensitive output path");
58
+
59
+ const config = resolveRuntimeConfig(
60
+ {
61
+ provider: "mock",
62
+ routingMode: "manual",
63
+ model: "mock-agent",
64
+ goal: "Generate an image of a panda astronaut.",
65
+ commandCwd: workspace,
66
+ allowFileTools: true,
67
+ allowAuxiliaryTools: true,
68
+ maxSteps: 4,
69
+ },
70
+ {
71
+ baseDir: runtimeDir,
72
+ packageDir: repoRoot,
73
+ provider: "mock",
74
+ }
75
+ );
76
+ const run = await runAgent(config);
77
+ const store = new SessionStore(config.sessionsDir, run.sessionId);
78
+ const events = await store.loadEvents();
79
+ assert(events.some((event) => event.type === "tool.completed" && event.data?.toolName === "generate_image"), "mock run did not call generate_image");
80
+ await fs.access(path.join(workspace, "artifacts/images/mock-image/task_manifest.json"));
81
+
82
+ console.log(
83
+ JSON.stringify(
84
+ {
85
+ ok: true,
86
+ workspace,
87
+ checks: ["grsai_key_status", "image_skill_listed", "generate_image_dry_run", "generate_image_guardrail", "mock_agent_image_tool"],
88
+ },
89
+ null,
90
+ 2
91
+ )
92
+ );
93
+ } finally {
94
+ await fs.rm(tempRoot, { recursive: true, force: true });
95
+ }
@@ -17,6 +17,7 @@ import { redactSensitiveText, redactValue } from "./redaction.js";
17
17
  import { executeWorkspaceTool, resolveWorkspacePath, summarizeWorkspaceTools, WORKSPACE_TOOL_NAMES } from "./workspace-tools.js";
18
18
  import { normalizeCanvasPayload } from "./artifact-tunnel.js";
19
19
  import { getTaskProfile } from "./task-profiles.js";
20
+ import { generateImage, listAuxiliarySkills } from "./auxiliary-tools.js";
20
21
 
21
22
  const exec = promisify(execCallback);
22
23
  const BROWSER_TOOLS = new Set(["open_url", "open_workspace_file", "preview_workspace", "click", "type", "scroll", "press", "back"]);
@@ -276,6 +277,11 @@ function createInitialState(config, sessionId) {
276
277
  config.allowWrapperTools
277
278
  ? `External coding-agent wrappers are available as advisory tools only. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Wrapper status: ${wrapperStatusText()}.`
278
279
  : "External coding-agent wrappers are disabled.",
280
+ config.allowAuxiliaryTools
281
+ ? `Auxiliary skills are available: ${listAuxiliarySkills()
282
+ .map((skill) => `${skill.id} via ${skill.toolName} (${skill.available ? "key available" : `needs ${skill.keyName}`})`)
283
+ .join(", ")}. Use generate_image for real raster image/photo/illustration/cover/poster/logo requests when appropriate; if the key is missing, ask the user to run /auxilliary grsai or aginti login grsai.`
284
+ : "Auxiliary skills are disabled for this run.",
279
285
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
280
286
  "A frontend canvas/artifacts tunnel exists. Use send_to_canvas when important markdown, diffs, screenshots, images, or workspace files should be highlighted in the UI. It is optional and ordinary final text can still go directly to finish.",
281
287
  "For visual-output requests such as draw, plot, graph, chart, diagram, figure, image, or visualization, proactively publish a canvas artifact even when the user does not mention canvas. If workspace file tools are enabled, prefer creating a small SVG or markdown artifact and call send_to_canvas with selected=true.",
@@ -309,6 +315,11 @@ function createInitialState(config, sessionId) {
309
315
  config.allowWrapperTools
310
316
  ? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
311
317
  : "",
318
+ config.allowAuxiliaryTools
319
+ ? `Auxiliary skills: ${listAuxiliarySkills()
320
+ .map((skill) => `${skill.id}:${skill.available ? "available" : "missing-key"}`)
321
+ .join(" ")}`
322
+ : "",
312
323
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
313
324
  "Canvas/artifacts tunnel: available through send_to_canvas for optional frontend rendering.",
314
325
  "Visual-output requests should produce a canvas artifact without requiring the user to ask for canvas explicitly.",
@@ -504,6 +515,15 @@ function sanitizeToolArgs(toolName, args) {
504
515
  replace: typeof args.replace === "string" ? redactSensitiveText(args.replace).slice(0, 160) : safeArgs.replace,
505
516
  };
506
517
  }
518
+ if (toolName === "generate_image") {
519
+ return {
520
+ ...safeArgs,
521
+ prompt: typeof args.prompt === "string" ? `[${Buffer.byteLength(args.prompt, "utf8")} bytes sha256=${hashForLog(args.prompt)}]` : safeArgs.prompt,
522
+ referenceImages: Array.isArray(args.referenceImages)
523
+ ? args.referenceImages.map((item) => (String(item || "").startsWith("data:") ? `[data-uri ${String(item).length} chars]` : redactSensitiveText(item)))
524
+ : safeArgs.referenceImages,
525
+ };
526
+ }
507
527
  return safeArgs;
508
528
  }
509
529
 
@@ -874,6 +894,67 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
874
894
  observers.event("tool.completed", result);
875
895
  return result;
876
896
  }
897
+ case "generate_image": {
898
+ const imageResult = await generateImage(args, config);
899
+ const result = {
900
+ ok: Boolean(imageResult.ok),
901
+ toolName: "generate_image",
902
+ args: safeArgs,
903
+ ...imageResult,
904
+ };
905
+ const eventResult = sanitizeToolResult(result);
906
+ await store.appendEvent("tool.completed", eventResult);
907
+ observers.event("tool.completed", eventResult);
908
+
909
+ if (result.ok) {
910
+ const generated = {
911
+ path: result.path,
912
+ imagePaths: result.imagePaths || [],
913
+ manifestPath: result.manifestPath || "",
914
+ promptPath: result.promptPath || "",
915
+ requestPayloadPath: result.requestPayloadPath || "",
916
+ commandCwd: config.commandCwd,
917
+ };
918
+ await store.appendEvent("image.generated", generated);
919
+ observers.event("image.generated", generated);
920
+
921
+ const selectedPath = result.imagePaths?.[0] || result.manifestPath || "";
922
+ if (selectedPath) {
923
+ const normalized = normalizeCanvasPayload(
924
+ {
925
+ title: result.imagePaths?.length ? "Generated image" : "Image generation payload",
926
+ kind: result.imagePaths?.length ? "image" : "json",
927
+ path: selectedPath,
928
+ note: result.summary || "Generated image artifact.",
929
+ selected: Boolean(result.imagePaths?.length),
930
+ },
931
+ config
932
+ );
933
+ if (normalized.ok) {
934
+ const canvasItem = {
935
+ ...normalized.payload,
936
+ toolName: "generate_image",
937
+ commandCwd: config.commandCwd,
938
+ };
939
+ await store.appendEvent("canvas.item", canvasItem);
940
+ observers.event("canvas.item", canvasItem);
941
+ if (canvasItem.selected) {
942
+ await store.appendEvent("canvas.selected", {
943
+ artifactId: canvasItem.artifactId,
944
+ title: canvasItem.title,
945
+ source: "generate_image",
946
+ });
947
+ observers.event("canvas.selected", {
948
+ artifactId: canvasItem.artifactId,
949
+ title: canvasItem.title,
950
+ source: "generate_image",
951
+ });
952
+ }
953
+ }
954
+ }
955
+ }
956
+ return result;
957
+ }
877
958
  case "send_to_canvas": {
878
959
  const normalized = normalizeCanvasPayload(args, config);
879
960
  if (!normalized.ok) {