@lazyingart/agintiflow 0.20.185 → 0.20.187

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.
@@ -75,6 +75,21 @@ That SVG request returns a PNG fallback contract rather than pretending SVG was
75
75
  }
76
76
  ```
77
77
 
78
+ The CLI exposes the same direct path for shell scripts and other local apps:
79
+
80
+ ```bash
81
+ aginti image --json --dry-run \
82
+ --provider venice \
83
+ --format svg \
84
+ --output-dir artifacts/images/robot-cover \
85
+ --output-stem cover.svg \
86
+ "A cyan robot painting a poster, clean product illustration"
87
+ ```
88
+
89
+ `aginti image ...` returns the same `requestedFormat`, `actualFormat`, and `formatNotice` fields as the web API. Use this direct CLI when
90
+ an app needs a deterministic tool call. Use `aginti --image "..."` when the user wants an agent-mediated image task that may plan, inspect
91
+ references, create files, or send artifacts to the canvas.
92
+
78
93
  With GRS AI, the tool uses the Nano Banana API:
79
94
 
80
95
  - `POST https://grsaiapi.com/v1/draw/nano-banana`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.185",
3
+ "version": "0.20.187",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
package/public/app.js CHANGED
@@ -1251,6 +1251,37 @@ function workspaceFilesByLane() {
1251
1251
  return groups;
1252
1252
  }
1253
1253
 
1254
+ function buildWorkspaceFolderTree(files = []) {
1255
+ const root = { name: "", path: "", dirs: new Map(), files: [] };
1256
+ for (const file of files) {
1257
+ const parts = String(file.path || "")
1258
+ .split("/")
1259
+ .filter(Boolean);
1260
+ if (!parts.length) continue;
1261
+ const fileName = parts.pop();
1262
+ let node = root;
1263
+ let currentPath = "";
1264
+ for (const part of parts) {
1265
+ currentPath = currentPath ? `${currentPath}/${part}` : part;
1266
+ if (!node.dirs.has(part)) node.dirs.set(part, { name: part, path: currentPath, dirs: new Map(), files: [] });
1267
+ node = node.dirs.get(part);
1268
+ }
1269
+ node.files.push({ ...file, name: file.name || fileName });
1270
+ }
1271
+ return root;
1272
+ }
1273
+
1274
+ function workspaceFolderCount(node) {
1275
+ if (!node) return 0;
1276
+ let count = node.files?.length || 0;
1277
+ for (const child of node.dirs?.values?.() || []) count += workspaceFolderCount(child);
1278
+ return count;
1279
+ }
1280
+
1281
+ function sortWorkspaceFiles(left, right) {
1282
+ return String(left.path || "").localeCompare(String(right.path || ""));
1283
+ }
1284
+
1254
1285
  function workspaceActiveFile() {
1255
1286
  return workspaceBrowser.files.find((file) => file.path === workspaceBrowser.activePath) || null;
1256
1287
  }
@@ -1279,12 +1310,18 @@ function renderWorkspaceExplorer() {
1279
1310
  const activeLane = workspaceBrowser.activeLane || "all";
1280
1311
  const warnings = workspaceBrowser.warnings?.length ? ` · ${workspaceBrowser.warnings.length} warning(s)` : "";
1281
1312
  const truncated = workspaceBrowser.truncated ? " · truncated" : "";
1282
- const status = workspaceBrowser.loading
1283
- ? "Loading workspace..."
1284
- : workspaceBrowser.error
1285
- ? `Workspace unavailable: ${workspaceBrowser.error}`
1286
- : `${workspaceBrowser.files.length} files · ${workspaceBrowser.root || workspaceRoot()}${warnings}${truncated}`;
1287
- workspaceBrowserStatusEl.textContent = status;
1313
+ if (workspaceBrowser.loading) {
1314
+ workspaceBrowserStatusEl.textContent = "Loading workspace...";
1315
+ } else if (workspaceBrowser.error) {
1316
+ workspaceBrowserStatusEl.textContent = `Workspace unavailable: ${workspaceBrowser.error}`;
1317
+ } else {
1318
+ workspaceBrowserStatusEl.innerHTML = `
1319
+ <span class="workspace-status-chip">${workspaceBrowser.files.length} files</span>
1320
+ <span class="workspace-status-chip">${escapeHtml(activeLane)}</span>
1321
+ <code class="workspace-root-path">${escapeHtml(workspaceBrowser.root || workspaceRoot())}</code>
1322
+ ${warnings || truncated ? `<span class="workspace-status-warn">${escapeHtml(`${warnings}${truncated}`.replace(/^ · /, ""))}</span>` : ""}
1323
+ `;
1324
+ }
1288
1325
 
1289
1326
  workspaceLanesEl.innerHTML = WORKSPACE_LANES.map((lane) => {
1290
1327
  const count = groups[lane.id]?.length || 0;
@@ -1302,34 +1339,69 @@ function renderWorkspaceExplorer() {
1302
1339
  `;
1303
1340
  }).join("");
1304
1341
 
1305
- const visibleFiles = (groups[activeLane] || []).slice().sort((left, right) => left.path.localeCompare(right.path));
1342
+ const visibleFiles = (groups[activeLane] || []).slice().sort(sortWorkspaceFiles);
1306
1343
  if (!visibleFiles.length) {
1307
1344
  workspaceTreeEl.innerHTML = `<p class="subtle workspace-empty">No files in this category.</p>`;
1308
1345
  return;
1309
1346
  }
1310
1347
 
1311
- workspaceTreeEl.innerHTML = visibleFiles
1312
- .map((file) => {
1313
- const lane = inferWorkspaceLane(file);
1314
- const active = file.path === workspaceBrowser.activePath;
1315
- const meta = [lane, formatBytes(file.size), file.binary ? "binary" : "text"].join(" · ");
1316
- return `
1317
- <button
1318
- type="button"
1319
- class="workspace-file-row"
1320
- data-workspace-file="${escapeHtml(file.path)}"
1321
- data-selected="${active}"
1322
- draggable="true"
1323
- >
1324
- <span class="workspace-file-main">
1325
- <strong>${escapeHtml(workspaceBaseName(file.path))}</strong>
1326
- <small>${escapeHtml(workspaceDir(file.path) || ".")}</small>
1327
- </span>
1328
- <span class="workspace-file-meta">${escapeHtml(meta)}</span>
1329
- </button>
1330
- `;
1331
- })
1332
- .join("");
1348
+ const tree = buildWorkspaceFolderTree(visibleFiles);
1349
+ workspaceTreeEl.innerHTML = `
1350
+ <div class="workspace-tree-toolbar">
1351
+ <span>${escapeHtml(WORKSPACE_LANES.find((lane) => lane.id === activeLane)?.label || "Files")}</span>
1352
+ <strong>${visibleFiles.length}</strong>
1353
+ </div>
1354
+ <div class="workspace-folder-root">
1355
+ ${renderWorkspaceFolderNode(tree, 0)}
1356
+ </div>
1357
+ `;
1358
+ }
1359
+
1360
+ function renderWorkspaceFileNode(file, depth = 0) {
1361
+ const lane = inferWorkspaceLane(file);
1362
+ const active = file.path === workspaceBrowser.activePath;
1363
+ const meta = [lane, formatBytes(file.size), file.binary ? "binary" : "text"].join(" · ");
1364
+ return `
1365
+ <button
1366
+ type="button"
1367
+ class="workspace-file-row workspace-tree-file"
1368
+ data-workspace-file="${escapeHtml(file.path)}"
1369
+ data-selected="${active}"
1370
+ draggable="true"
1371
+ style="--depth: ${depth};"
1372
+ >
1373
+ <span class="workspace-file-icon" aria-hidden="true">${file.binary ? "bin" : "txt"}</span>
1374
+ <span class="workspace-file-main">
1375
+ <strong>${escapeHtml(workspaceBaseName(file.path))}</strong>
1376
+ <small>${escapeHtml(workspaceDir(file.path) || ".")}</small>
1377
+ </span>
1378
+ <span class="workspace-file-meta">${escapeHtml(meta)}</span>
1379
+ </button>
1380
+ `;
1381
+ }
1382
+
1383
+ function renderWorkspaceFolderNode(node, depth = 0) {
1384
+ const dirs = [...(node.dirs?.values?.() || [])].sort((left, right) => left.name.localeCompare(right.name));
1385
+ const files = (node.files || []).slice().sort(sortWorkspaceFiles);
1386
+ const children = [
1387
+ ...dirs.map((child) => renderWorkspaceFolderNode(child, depth + 1)),
1388
+ ...files.map((file) => renderWorkspaceFileNode(file, depth)),
1389
+ ].join("");
1390
+ if (depth === 0) return children;
1391
+ const activeInside = workspaceBrowser.activePath && workspaceBrowser.activePath.startsWith(`${node.path}/`);
1392
+ const open = depth <= 2 || activeInside;
1393
+ return `
1394
+ <details class="workspace-folder" ${open ? "open" : ""} style="--depth: ${Math.max(depth - 1, 0)};">
1395
+ <summary>
1396
+ <span class="workspace-folder-icon" aria-hidden="true">&gt;</span>
1397
+ <span class="workspace-folder-name">${escapeHtml(node.name)}</span>
1398
+ <small>${workspaceFolderCount(node)}</small>
1399
+ </summary>
1400
+ <div class="workspace-folder-children">
1401
+ ${children}
1402
+ </div>
1403
+ </details>
1404
+ `;
1333
1405
  }
1334
1406
 
1335
1407
  function renderWorkspaceWorkbench() {
package/public/styles.css CHANGED
@@ -959,10 +959,43 @@ button.danger {
959
959
  }
960
960
 
961
961
  .workspace-browser-status {
962
+ display: flex;
963
+ flex-wrap: wrap;
964
+ align-items: center;
965
+ gap: 7px;
962
966
  line-height: 1.35;
963
967
  overflow-wrap: anywhere;
964
968
  }
965
969
 
970
+ .workspace-status-chip {
971
+ padding: 4px 8px;
972
+ border: 1px solid rgba(15, 118, 110, 0.16);
973
+ border-radius: 999px;
974
+ background: rgba(255, 253, 250, 0.86);
975
+ color: var(--ink);
976
+ font-size: 0.76rem;
977
+ font-weight: 700;
978
+ text-transform: capitalize;
979
+ }
980
+
981
+ .workspace-root-path {
982
+ min-width: 0;
983
+ max-width: 100%;
984
+ padding: 5px 8px;
985
+ border: 1px solid rgba(15, 118, 110, 0.14);
986
+ border-radius: 10px;
987
+ background: rgba(13, 148, 136, 0.07);
988
+ color: #155e75;
989
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
990
+ font-size: 0.76rem;
991
+ overflow-wrap: anywhere;
992
+ }
993
+
994
+ .workspace-status-warn {
995
+ color: #b45309;
996
+ font-size: 0.76rem;
997
+ }
998
+
966
999
  .workspace-lanes {
967
1000
  display: grid;
968
1001
  grid-template-columns: repeat(auto-fit, minmax(82px, 1fr));
@@ -995,11 +1028,105 @@ button.danger {
995
1028
  }
996
1029
 
997
1030
  .workspace-tree {
998
- display: grid;
999
- gap: 7px;
1000
1031
  max-height: 440px;
1001
1032
  overflow: auto;
1002
- padding-right: 2px;
1033
+ padding: 8px;
1034
+ border: 1px solid rgba(15, 118, 110, 0.12);
1035
+ border-radius: 16px;
1036
+ background:
1037
+ linear-gradient(180deg, rgba(255, 253, 250, 0.88), rgba(240, 253, 250, 0.58)),
1038
+ rgba(255, 255, 255, 0.72);
1039
+ }
1040
+
1041
+ .workspace-tree-toolbar {
1042
+ position: sticky;
1043
+ top: 0;
1044
+ z-index: 1;
1045
+ display: flex;
1046
+ justify-content: space-between;
1047
+ align-items: center;
1048
+ gap: 10px;
1049
+ margin: -8px -8px 8px;
1050
+ padding: 8px 10px;
1051
+ border-bottom: 1px solid rgba(15, 118, 110, 0.1);
1052
+ background: rgba(255, 253, 250, 0.95);
1053
+ color: var(--muted);
1054
+ font-size: 0.78rem;
1055
+ font-weight: 700;
1056
+ text-transform: uppercase;
1057
+ letter-spacing: 0.03em;
1058
+ }
1059
+
1060
+ .workspace-tree-toolbar strong {
1061
+ color: var(--accent);
1062
+ }
1063
+
1064
+ .workspace-folder-root,
1065
+ .workspace-folder-children {
1066
+ display: grid;
1067
+ gap: 4px;
1068
+ }
1069
+
1070
+ .workspace-folder {
1071
+ margin-left: calc(var(--depth, 0) * 12px);
1072
+ }
1073
+
1074
+ .workspace-folder summary {
1075
+ display: flex;
1076
+ align-items: center;
1077
+ gap: 7px;
1078
+ min-height: 32px;
1079
+ padding: 6px 8px;
1080
+ border: 1px solid transparent;
1081
+ border-radius: 11px;
1082
+ color: var(--ink);
1083
+ cursor: pointer;
1084
+ list-style: none;
1085
+ user-select: none;
1086
+ }
1087
+
1088
+ .workspace-folder summary::-webkit-details-marker {
1089
+ display: none;
1090
+ }
1091
+
1092
+ .workspace-folder summary:hover {
1093
+ border-color: rgba(15, 118, 110, 0.16);
1094
+ background: rgba(240, 253, 250, 0.72);
1095
+ }
1096
+
1097
+ .workspace-folder-icon {
1098
+ display: inline-grid;
1099
+ place-items: center;
1100
+ width: 18px;
1101
+ height: 18px;
1102
+ border: 1px solid rgba(15, 118, 110, 0.18);
1103
+ border-radius: 6px;
1104
+ color: var(--accent);
1105
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
1106
+ font-size: 0.68rem;
1107
+ transition: transform 0.16s ease;
1108
+ }
1109
+
1110
+ .workspace-folder[open] > summary .workspace-folder-icon {
1111
+ transform: rotate(90deg);
1112
+ }
1113
+
1114
+ .workspace-folder-name {
1115
+ min-width: 0;
1116
+ overflow: hidden;
1117
+ text-overflow: ellipsis;
1118
+ white-space: nowrap;
1119
+ font-weight: 750;
1120
+ }
1121
+
1122
+ .workspace-folder summary small {
1123
+ margin-left: auto;
1124
+ color: var(--muted);
1125
+ font-size: 0.72rem;
1126
+ }
1127
+
1128
+ .workspace-folder-children {
1129
+ margin-top: 4px;
1003
1130
  }
1004
1131
 
1005
1132
  .workspace-empty {
@@ -1011,18 +1138,23 @@ button.danger {
1011
1138
 
1012
1139
  .workspace-file-row {
1013
1140
  display: grid;
1014
- grid-template-columns: minmax(0, 1fr);
1015
- gap: 3px;
1141
+ grid-template-columns: auto minmax(0, 1fr);
1142
+ gap: 6px 8px;
1143
+ align-items: center;
1016
1144
  width: 100%;
1017
- padding: 10px 11px;
1145
+ padding: 8px 10px;
1018
1146
  border: 1px solid rgba(217, 119, 6, 0.16);
1019
- border-radius: 14px;
1147
+ border-radius: 12px;
1020
1148
  background: rgba(255, 253, 250, 0.72);
1021
1149
  color: var(--ink);
1022
1150
  cursor: pointer;
1023
1151
  text-align: left;
1024
1152
  }
1025
1153
 
1154
+ .workspace-tree-file {
1155
+ margin-left: calc(var(--depth, 0) * 12px);
1156
+ }
1157
+
1026
1158
  .workspace-file-row:hover {
1027
1159
  border-color: rgba(15, 118, 110, 0.34);
1028
1160
  background: rgba(240, 253, 250, 0.72);
@@ -1030,10 +1162,24 @@ button.danger {
1030
1162
 
1031
1163
  .workspace-file-row[data-selected="true"] {
1032
1164
  border-color: rgba(15, 118, 110, 0.58);
1033
- background: linear-gradient(135deg, rgba(15, 118, 110, 0.18), rgba(255, 255, 255, 0.92));
1165
+ background: linear-gradient(135deg, rgba(15, 118, 110, 0.22), rgba(255, 255, 255, 0.96));
1034
1166
  box-shadow: 0 0 0 2px rgba(15, 118, 110, 0.1);
1035
1167
  }
1036
1168
 
1169
+ .workspace-file-icon {
1170
+ display: inline-grid;
1171
+ place-items: center;
1172
+ width: 30px;
1173
+ height: 24px;
1174
+ border-radius: 8px;
1175
+ background: rgba(15, 118, 110, 0.09);
1176
+ color: #155e75;
1177
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
1178
+ font-size: 0.62rem;
1179
+ font-weight: 800;
1180
+ text-transform: uppercase;
1181
+ }
1182
+
1037
1183
  .workspace-file-main {
1038
1184
  display: grid;
1039
1185
  gap: 1px;
@@ -1050,6 +1196,7 @@ button.danger {
1050
1196
 
1051
1197
  .workspace-file-main small,
1052
1198
  .workspace-file-meta {
1199
+ grid-column: 2;
1053
1200
  color: var(--muted);
1054
1201
  font-size: 0.76rem;
1055
1202
  }
@@ -10,6 +10,28 @@ This note records the working publication route for AgInTiFlow so future release
10
10
 
11
11
  Latest verified release:
12
12
 
13
+ - Version: `0.20.186`
14
+ - Commit: `f32a748`
15
+ - GitHub Actions run: `https://github.com/lazyingart/AgInTiFlow/actions/runs/26700782132`
16
+ - Workflow result: success
17
+ - npm registry check: `npm view @lazyingart/agintiflow version dist-tags.latest --registry=https://registry.npmjs.org` returned `0.20.186`.
18
+ - Installed verification: `npm install -g @lazyingart/agintiflow@0.20.186` then `aginti --version` returned `0.20.186`.
19
+ - Webapp verification: `aginti webapp restart --port 3210` then `curl -fsS http://127.0.0.1:3210/health` returned `version":"0.20.186"` from the global npm package path.
20
+ - Direct image CLI verification: `aginti image --json --dry-run --format svg` returned `requestedFormat:"svg"`, `actualFormat:"png"`, and a clear raster PNG fallback notice.
21
+
22
+ Previous verified release:
23
+
24
+ - Version: `0.20.185`
25
+ - Commit: `58d6087`
26
+ - GitHub Actions run: `https://github.com/lazyingart/AgInTiFlow/actions/runs/26700387931`
27
+ - Workflow result: success
28
+ - npm registry check: `npm view @lazyingart/agintiflow version dist-tags.latest --registry=https://registry.npmjs.org` returned `0.20.185`.
29
+ - Installed verification: `npm install -g @lazyingart/agintiflow@0.20.185` then `aginti --version` returned `0.20.185`.
30
+ - Webapp verification: `aginti webapp restart --port 3210` then `curl -fsS http://127.0.0.1:3210/health` returned `version":"0.20.185"` from the global npm package path.
31
+ - Image API verification: `POST /api/auxiliary/generate-image` with `format:"svg"` and `dryRun:true` returned `requestedFormat:"svg"`, `actualFormat:"png"`, and a clear raster PNG fallback notice.
32
+
33
+ Previous verified release:
34
+
13
35
  - Version: `0.20.184`
14
36
  - Commit: `9b22f4c`
15
37
  - GitHub Actions run: `https://github.com/lazyingart/AgInTiFlow/actions/runs/26700154894`
@@ -3,6 +3,8 @@ import fs from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { execFile as execFileCallback } from "node:child_process";
7
+ import { promisify } from "node:util";
6
8
  import { runAgent } from "../src/agent-runner.js";
7
9
  import { generateImage, listAuxiliarySkills } from "../src/auxiliary-tools.js";
8
10
  import { resolveRuntimeConfig } from "../src/config.js";
@@ -10,6 +12,7 @@ import { providerKeyStatus, setProviderKey } from "../src/project.js";
10
12
  import { SessionStore } from "../src/session-store.js";
11
13
 
12
14
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
15
+ const execFile = promisify(execFileCallback);
13
16
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-auxiliary-"));
14
17
  process.env.AGINTIFLOW_HOME = path.join(tempRoot, ".agintiflow-home");
15
18
  const runtimeDir = path.join(tempRoot, "runtime");
@@ -82,6 +85,37 @@ try {
82
85
  );
83
86
  assert(svgFallbackManifest.requestedFormat === "svg" && svgFallbackManifest.actualFormat === "png", "SVG fallback manifest missing format contract");
84
87
 
88
+ const cliImage = await execFile(
89
+ process.execPath,
90
+ [
91
+ path.join(repoRoot, "bin/aginti-cli.js"),
92
+ "--no-auto-update",
93
+ "image",
94
+ "--json",
95
+ "--dry-run",
96
+ "--cwd",
97
+ workspace,
98
+ "--provider",
99
+ "venice",
100
+ "--format",
101
+ "svg",
102
+ "--output-dir",
103
+ "artifacts/images/cli-svg-fallback",
104
+ "--output-stem",
105
+ "diagram.svg",
106
+ "A simple geometric diagram requested as SVG.",
107
+ ],
108
+ {
109
+ cwd: repoRoot,
110
+ env: { ...process.env, AGINTIFLOW_HOME: process.env.AGINTIFLOW_HOME },
111
+ }
112
+ );
113
+ const cliImageResult = JSON.parse(cliImage.stdout);
114
+ assert(cliImageResult.ok && cliImageResult.requestedFormat === "svg", "direct image CLI did not record requestedFormat");
115
+ assert(cliImageResult.actualFormat === "png", "direct image CLI did not select PNG fallback");
116
+ assert(/raster PNG/i.test(cliImageResult.formatNotice || ""), "direct image CLI did not explain SVG-to-PNG fallback");
117
+ await fs.access(path.join(workspace, "artifacts/images/cli-svg-fallback/task_manifest.json"));
118
+
85
119
  const blocked = await generateImage(
86
120
  {
87
121
  prompt: "blocked",
@@ -131,6 +165,7 @@ try {
131
165
  "generate_image_dry_run",
132
166
  "venice_generate_image_dry_run",
133
167
  "svg_request_png_fallback",
168
+ "direct_image_cli_svg_request_png_fallback",
134
169
  "generate_image_guardrail",
135
170
  "mock_agent_image_tool",
136
171
  ],
@@ -93,6 +93,7 @@ try {
93
93
  await fs.mkdir(path.join(runtimeDir, "data"), { recursive: true });
94
94
  await fs.writeFile(path.join(runtimeDir, "notes", "workspace-smoke.md"), "# Workspace smoke\n\nEditable text.\n");
95
95
  await fs.writeFile(path.join(runtimeDir, "data", "workspace-smoke.csv"), "sample,value\nalpha,1\n");
96
+ await fs.writeFile(path.join(runtimeDir, ".aginti", ".env"), "DEEPSEEK_API_KEY=should-not-render\n");
96
97
 
97
98
  const webAppHtml = await fs.readFile(path.join(repoRoot, "public", "index.html"), "utf8");
98
99
  const chatThreadIndex = webAppHtml.indexOf('id="chat-thread"');
@@ -145,6 +146,9 @@ try {
145
146
  if (!workspaceSnapshot.files?.some((file) => file.path === "notes/workspace-smoke.md" && file.content.includes("Editable text."))) {
146
147
  throw new Error(`workspace snapshot did not include editable note: ${JSON.stringify(workspaceSnapshot.files?.slice(0, 5))}`);
147
148
  }
149
+ if (workspaceSnapshot.files?.some((file) => file.path === ".aginti/.env" || file.path === ".aginti/mcp.json" || file.path === ".env")) {
150
+ throw new Error(`workspace snapshot exposed protected internal/secret files: ${JSON.stringify(workspaceSnapshot.files?.slice(0, 10))}`);
151
+ }
148
152
  const rawWorkspaceResponse = await fetch(
149
153
  `${baseUrl}/api/workspace/raw?commandCwd=${encodeURIComponent(runtimeDir)}&path=${encodeURIComponent("notes/workspace-smoke.md")}`
150
154
  );
@@ -152,6 +156,20 @@ try {
152
156
  if (!rawWorkspaceResponse.ok || !rawWorkspaceText.includes("Workspace smoke")) {
153
157
  throw new Error(`workspace raw endpoint failed: status=${rawWorkspaceResponse.status} body=${rawWorkspaceText.slice(0, 120)}`);
154
158
  }
159
+ const protectedRawResponse = await fetch(
160
+ `${baseUrl}/api/workspace/raw?commandCwd=${encodeURIComponent(runtimeDir)}&path=${encodeURIComponent(".aginti/.env")}`
161
+ );
162
+ if (protectedRawResponse.ok) {
163
+ throw new Error("workspace raw endpoint exposed protected .aginti/.env");
164
+ }
165
+ const protectedWriteResponse = await fetch(`${baseUrl}/api/workspace/write`, {
166
+ method: "POST",
167
+ headers: { "Content-Type": "application/json" },
168
+ body: JSON.stringify({ commandCwd: runtimeDir, path: ".env", content: "SECRET=blocked\n" }),
169
+ });
170
+ if (protectedWriteResponse.ok) {
171
+ throw new Error("workspace write endpoint allowed protected .env");
172
+ }
155
173
  const writtenWorkspace = await fetchJson("/api/workspace/write", {
156
174
  method: "POST",
157
175
  headers: { "Content-Type": "application/json" },
@@ -73,8 +73,10 @@ let browser;
73
73
 
74
74
  try {
75
75
  await waitForHealth();
76
+ await fs.mkdir(path.join(runtimeDir, ".aginti"), { recursive: true });
76
77
  await fs.mkdir(path.join(runtimeDir, "notes"), { recursive: true });
77
78
  await fs.mkdir(path.join(runtimeDir, "data"), { recursive: true });
79
+ await fs.writeFile(path.join(runtimeDir, ".aginti", ".env"), "DEEPSEEK_API_KEY=should-not-render\n");
78
80
  await fs.writeFile(path.join(runtimeDir, "notes", "workspace-ui.md"), "# Workspace UI\n\ninitial body\n");
79
81
  await fs.writeFile(path.join(runtimeDir, "data", "workspace-ui.csv"), "sample,value\nalpha,1\n");
80
82
  browser = await chromium.launch({ headless: true });
@@ -102,6 +104,16 @@ try {
102
104
  if ((await page.locator("#workspace-lanes [data-workspace-lane]").count()) < 8) {
103
105
  throw new Error("workspace explorer did not render general category lanes");
104
106
  }
107
+ if ((await page.locator('[data-workspace-file=".aginti/.env"]').count()) !== 0) {
108
+ throw new Error("workspace explorer exposed protected .aginti/.env");
109
+ }
110
+ if ((await page.locator("#workspace-tree details.workspace-folder").count()) < 1) {
111
+ throw new Error("workspace explorer did not render a folder tree");
112
+ }
113
+ const statusText = await page.locator("#workspace-browser-status").innerText();
114
+ if (!statusText.includes(runtimeDir) || !/files/i.test(statusText)) {
115
+ throw new Error(`workspace explorer status did not show file count and root path: ${statusText}`);
116
+ }
105
117
  if ((await page.locator('[data-workspace-file="notes/workspace-ui.md"]').getAttribute("draggable")) !== "true") {
106
118
  throw new Error("workspace file rows are not draggable");
107
119
  }
@@ -305,6 +317,9 @@ try {
305
317
  "working-directory-search-top",
306
318
  "project-status-chips",
307
319
  "workspace-explorer-general-lanes",
320
+ "workspace-explorer-folder-tree",
321
+ "workspace-explorer-protected-files-hidden",
322
+ "workspace-explorer-status-root",
308
323
  "workspace-file-selection",
309
324
  "workspace-editor-save-persists",
310
325
  "workspace-file-drop-to-chat",
package/src/cli.js CHANGED
@@ -30,6 +30,7 @@ import { listTaskProfiles } from "./task-profiles.js";
30
30
  import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
31
31
  import { normalizeAuthProvider, promptHidden, runAuthWizard, shouldPromptForDeepSeek } from "./auth-onboarding.js";
32
32
  import { listSkills, selectSkillsForGoal } from "./skill-library.js";
33
+ import { generateImage } from "./auxiliary-tools.js";
33
34
  import { languageLabel, resolveLanguage } from "./i18n.js";
34
35
  import { maybeAutoUpdate } from "./auto-update.js";
35
36
  import { readHousekeepingSummary } from "./housekeeping.js";
@@ -761,7 +762,7 @@ function exitOnUnknownOptions(parsed) {
761
762
 
762
763
  function printUsage() {
763
764
  console.log(
764
- 'Usage: aginti [chat] OR aginti init [--template minimal|disciplined|coding|research|writing|design|aaps|supervision] OR aginti web [--port 3210] OR aginti docker [status|setup|install-host] OR aginti update OR aginti models OR aginti aaps [status|init|files|validate|compile|check|run] OR aginti mcp [status|config|inspect|tools|resources|read|prompts|prompt|call|restart] OR aginti skills [query] OR aginti skillmesh [status|off|record|share|sync|serve|service] OR aginti housekeeping [--json] OR aginti auth [deepseek|openai|qwen|venice|grsai] OR aginti resume [--all-sessions] [latest|<session-id>] ["prompt"] OR aginti --remove-empty-sessions OR aginti --remove-sessions OR aginti queue <session-id> "message" OR aginti [--no-auto-update] [-s safe|normal|danger] [--language en|ja|zh-Hans|zh-Hant|ko|fr|es|ar|vi|de|ru] [--image] [--latex] [--scs|--scs auto|--no-scs] [--dynamic-steps auto|on|off] [--routing smart|fast|complex|manual] [--provider deepseek|openai|qwen|venice|mock] [--model MODEL] [--route-model MODEL] [--main-model MODEL] [--spare-model MODEL --spare-reasoning medium] [--aux-provider grsai|venice --aux-model MODEL] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--mcp|--no-mcp] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex --wrapper-model gpt-5.5] [--list-models|--list-routes] "your task"'
765
+ 'Usage: aginti [chat] OR aginti init [--template minimal|disciplined|coding|research|writing|design|aaps|supervision] OR aginti web [--port 3210] OR aginti docker [status|setup|install-host] OR aginti update OR aginti image [--json] [--dry-run] [--format png|webp|svg] "prompt" OR aginti models OR aginti aaps [status|init|files|validate|compile|check|run] OR aginti mcp [status|config|inspect|tools|resources|read|prompts|prompt|call|restart] OR aginti skills [query] OR aginti skillmesh [status|off|record|share|sync|serve|service] OR aginti housekeeping [--json] OR aginti auth [deepseek|openai|qwen|venice|grsai] OR aginti resume [--all-sessions] [latest|<session-id>] ["prompt"] OR aginti --remove-empty-sessions OR aginti --remove-sessions OR aginti queue <session-id> "message" OR aginti [--no-auto-update] [-s safe|normal|danger] [--language en|ja|zh-Hans|zh-Hant|ko|fr|es|ar|vi|de|ru] [--image] [--latex] [--scs|--scs auto|--no-scs] [--dynamic-steps auto|on|off] [--routing smart|fast|complex|manual] [--provider deepseek|openai|qwen|venice|mock] [--model MODEL] [--route-model MODEL] [--main-model MODEL] [--spare-model MODEL --spare-reasoning medium] [--aux-provider grsai|venice --aux-model MODEL] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--mcp|--no-mcp] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex --wrapper-model gpt-5.5] [--list-models|--list-routes] "your task"'
765
766
  );
766
767
  console.log("Permission shortcuts: -s safe asks before writes/setup; -s normal allows current-project writes and Docker setup; -s danger enables trusted host/full-access mode.");
767
768
  console.log(`Languages: ${["en", "ja", "zh-Hans", "zh-Hant", "ko", "fr", "es", "ar", "vi", "de", "ru"].map((code) => `${code}=${languageLabel(code)}`).join(", ")}`);
@@ -790,6 +791,211 @@ function splitToolCommandArgv(argv = []) {
790
791
  return { actionArgv, optionArgv, json };
791
792
  }
792
793
 
794
+ function takeImageOptionValue(argv, index, option) {
795
+ const equals = String(option || "").match(/^([^=]+)=(.*)$/);
796
+ if (equals) return { value: equals[2], nextIndex: index + 1 };
797
+ const value = argv[index + 1];
798
+ return { value: value && !String(value).startsWith("--") ? value : "", nextIndex: value && !String(value).startsWith("--") ? index + 2 : index + 1 };
799
+ }
800
+
801
+ function parseImageCommandArgs(argv = []) {
802
+ const result = {
803
+ prompt: "",
804
+ provider: "",
805
+ model: "",
806
+ format: "",
807
+ outputDir: "",
808
+ outputStem: "",
809
+ aspectRatio: "",
810
+ imageSize: "",
811
+ host: "",
812
+ referenceImages: [],
813
+ commandCwd: "",
814
+ requestTimeoutMs: "",
815
+ pollTimeoutMs: "",
816
+ pollIntervalMs: "",
817
+ dryRun: false,
818
+ json: false,
819
+ stdin: false,
820
+ help: false,
821
+ unknownOptions: [],
822
+ };
823
+ const promptParts = [];
824
+ let index = 0;
825
+ if (["generate", "create", "make"].includes(String(argv[0] || "").toLowerCase())) index = 1;
826
+
827
+ while (index < argv.length) {
828
+ const arg = String(argv[index] || "");
829
+ const name = arg.split("=")[0];
830
+ if (arg === "--") {
831
+ promptParts.push(...argv.slice(index + 1));
832
+ break;
833
+ }
834
+ if (arg === "--help" || arg === "-h") {
835
+ result.help = true;
836
+ index += 1;
837
+ continue;
838
+ }
839
+ if (arg === "--json") {
840
+ result.json = true;
841
+ index += 1;
842
+ continue;
843
+ }
844
+ if (arg === "--dry-run" || arg === "--dryrun") {
845
+ result.dryRun = true;
846
+ index += 1;
847
+ continue;
848
+ }
849
+ if (arg === "--stdin") {
850
+ result.stdin = true;
851
+ index += 1;
852
+ continue;
853
+ }
854
+
855
+ const valueOptions = new Map([
856
+ ["--provider", "provider"],
857
+ ["--aux-provider", "provider"],
858
+ ["--auxiliary-provider", "provider"],
859
+ ["--model", "model"],
860
+ ["--aux-model", "model"],
861
+ ["--auxiliary-model", "model"],
862
+ ["--format", "format"],
863
+ ["--output-dir", "outputDir"],
864
+ ["--out-dir", "outputDir"],
865
+ ["--dir", "outputDir"],
866
+ ["--output-stem", "outputStem"],
867
+ ["--stem", "outputStem"],
868
+ ["--aspect-ratio", "aspectRatio"],
869
+ ["--ratio", "aspectRatio"],
870
+ ["--image-size", "imageSize"],
871
+ ["--size", "imageSize"],
872
+ ["--host", "host"],
873
+ ["--cwd", "commandCwd"],
874
+ ["--request-timeout-ms", "requestTimeoutMs"],
875
+ ["--poll-timeout-ms", "pollTimeoutMs"],
876
+ ["--poll-interval-ms", "pollIntervalMs"],
877
+ ]);
878
+ if (valueOptions.has(name)) {
879
+ const { value, nextIndex } = takeImageOptionValue(argv, index, arg);
880
+ if (!value) result.unknownOptions.push(arg);
881
+ else result[valueOptions.get(name)] = value;
882
+ index = nextIndex;
883
+ continue;
884
+ }
885
+ if (["--reference", "--reference-image", "--ref", "--image-ref"].includes(name)) {
886
+ const { value, nextIndex } = takeImageOptionValue(argv, index, arg);
887
+ if (!value) result.unknownOptions.push(arg);
888
+ else result.referenceImages.push(value);
889
+ index = nextIndex;
890
+ continue;
891
+ }
892
+ if (arg.startsWith("--") && promptParts.length === 0) {
893
+ result.unknownOptions.push(arg);
894
+ index += 1;
895
+ continue;
896
+ }
897
+ promptParts.push(arg);
898
+ index += 1;
899
+ }
900
+
901
+ result.prompt = promptParts.join(" ").trim();
902
+ return result;
903
+ }
904
+
905
+ function printImageCommandUsage() {
906
+ console.log(
907
+ 'Usage: aginti image [generate] [--json] [--dry-run] [--provider grsai|venice] [--model MODEL] [--format png|webp|svg] [--output-dir DIR] [--output-stem STEM] [--aspect-ratio 1:1] [--image-size 1K|2K|4K|1024x1024] [--reference path-or-url] "prompt"'
908
+ );
909
+ console.log("Direct image CLI calls the same generate_image tool as the web API. SVG/vector requests return PNG with requestedFormat/actualFormat/formatNotice.");
910
+ console.log('Agent-mediated image work is still available as: aginti --image "draw a poster"');
911
+ }
912
+
913
+ function directImageCommandArgv(argv = []) {
914
+ const first = String(argv[0] || "").toLowerCase();
915
+ const second = String(argv[1] || "").toLowerCase();
916
+ if (["image", "imagegen", "image-gen", "generate-image", "image-generate"].includes(first)) return argv.slice(1);
917
+ if (["aux", "auxiliary"].includes(first) && ["image", "imagegen", "image-gen", "generate-image", "generate"].includes(second)) {
918
+ return argv.slice(2);
919
+ }
920
+ return null;
921
+ }
922
+
923
+ function printImageCommandResult(result = {}) {
924
+ const status = result.dryRun ? "prepared" : result.ok ? "generated" : result.blocked ? "blocked" : "failed";
925
+ console.log(`image: ${status}`);
926
+ if (result.summary) console.log(`summary: ${result.summary}`);
927
+ if (result.provider) console.log(`provider: ${result.provider}`);
928
+ if (result.requestedFormat || result.actualFormat) {
929
+ const requested = result.requestedFormat || "auto";
930
+ const actual = result.actualFormat || "unknown";
931
+ console.log(`format: ${requested}${requested === actual ? "" : ` -> ${actual}`}`);
932
+ }
933
+ if (result.formatNotice) console.log(`notice: ${result.formatNotice}`);
934
+ if (result.path) console.log(`path: ${result.path}`);
935
+ if (result.imagePaths?.length) console.log(`images: ${result.imagePaths.join(", ")}`);
936
+ if (result.manifestPath) console.log(`manifest: ${result.manifestPath}`);
937
+ if (result.promptPath) console.log(`prompt: ${result.promptPath}`);
938
+ if (result.requestPayloadPath) console.log(`request: ${result.requestPayloadPath}`);
939
+ if (result.reason) console.log(`reason: ${result.reason}`);
940
+ }
941
+
942
+ async function handleImageCommand(argv, { commandCwd = process.cwd() } = {}) {
943
+ const parsed = parseImageCommandArgs(argv);
944
+ if (parsed.help) {
945
+ printImageCommandUsage();
946
+ return;
947
+ }
948
+ if (parsed.unknownOptions.length) {
949
+ printUnknownCliOptions(parsed.unknownOptions);
950
+ process.exit(1);
951
+ }
952
+ const prompt = parsed.stdin ? await readStdin() : parsed.prompt;
953
+ if (!prompt) {
954
+ printImageCommandUsage();
955
+ process.exit(1);
956
+ }
957
+ const effectiveCommandCwd = path.resolve(parsed.commandCwd || commandCwd || process.cwd());
958
+ const config = loadConfig(
959
+ {
960
+ goal: prompt,
961
+ taskProfile: "image",
962
+ commandCwd: effectiveCommandCwd,
963
+ auxiliaryProvider: parsed.provider,
964
+ auxiliaryModel: parsed.model,
965
+ allowFileTools: true,
966
+ allowAuxiliaryTools: true,
967
+ },
968
+ { packageDir, baseDir: effectiveCommandCwd }
969
+ );
970
+ const result = await generateImage(
971
+ {
972
+ prompt,
973
+ provider: parsed.provider,
974
+ model: parsed.model,
975
+ format: parsed.format,
976
+ outputDir: parsed.outputDir || undefined,
977
+ outputStem: parsed.outputStem || undefined,
978
+ aspectRatio: parsed.aspectRatio || undefined,
979
+ imageSize: parsed.imageSize || undefined,
980
+ host: parsed.host || undefined,
981
+ referenceImages: parsed.referenceImages,
982
+ requestTimeoutMs: parsed.requestTimeoutMs || undefined,
983
+ pollTimeoutMs: parsed.pollTimeoutMs || undefined,
984
+ pollIntervalMs: parsed.pollIntervalMs || undefined,
985
+ dryRun: parsed.dryRun,
986
+ },
987
+ {
988
+ ...config,
989
+ commandCwd: effectiveCommandCwd,
990
+ allowFileTools: true,
991
+ allowAuxiliaryTools: true,
992
+ }
993
+ );
994
+ if (parsed.json) console.log(JSON.stringify(result, null, 2));
995
+ else printImageCommandResult(result);
996
+ if (!result.ok) process.exit(1);
997
+ }
998
+
793
999
  function parseDockerCommandArgs(argv = [], fallbackCwd = process.cwd()) {
794
1000
  const result = {
795
1001
  action: "status",
@@ -1856,6 +2062,20 @@ export async function main(argv = process.argv.slice(2)) {
1856
2062
  return;
1857
2063
  }
1858
2064
 
2065
+ const imageCommand = directImageCommandArgv(commandArgv);
2066
+ if (imageCommand) {
2067
+ try {
2068
+ await handleImageCommand(imageCommand, { commandCwd });
2069
+ } catch (error) {
2070
+ const wantsJson = imageCommand.includes("--json");
2071
+ const message = error instanceof Error ? error.message : String(error);
2072
+ if (wantsJson) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
2073
+ else console.error(message);
2074
+ process.exit(1);
2075
+ }
2076
+ return;
2077
+ }
2078
+
1859
2079
  if (commandArgv.includes("--remove-empty-sessions") || commandArgv[0] === "remove-empty-sessions") {
1860
2080
  await handleRemoveSessionsCommand({ emptyOnly: true });
1861
2081
  return;
package/web.js CHANGED
@@ -778,8 +778,11 @@ const WORKSPACE_SKIP_NAMES = new Set([
778
778
  ".git",
779
779
  ".hg",
780
780
  ".svn",
781
+ ".aginti",
782
+ ".aginti-work",
781
783
  ".aginti-sessions",
782
784
  ".agintiflow-home",
785
+ ".sessions",
783
786
  ".cache",
784
787
  ".pytest_cache",
785
788
  ".ruff_cache",
@@ -790,9 +793,26 @@ const WORKSPACE_SKIP_NAMES = new Set([
790
793
  "dist",
791
794
  "node_modules",
792
795
  ]);
796
+ const WORKSPACE_PROTECTED_DIRS = new Set([".aginti", ".aginti-work", ".aginti-sessions", ".agintiflow-home", ".sessions", ".git", ".hg", ".svn"]);
797
+ const WORKSPACE_PROTECTED_FILES = new Set([".env", ".npmrc", ".pypirc", ".netrc"]);
793
798
  const WORKSPACE_MAX_FILES = 1600;
794
799
  const WORKSPACE_MAX_TEXT_BYTES = 512 * 1024;
795
800
 
801
+ function isProtectedEnvName(name = "") {
802
+ const value = String(name || "");
803
+ if (value === ".env") return true;
804
+ if (!value.startsWith(".env.")) return false;
805
+ return !/\.(example|sample|template)$/i.test(value);
806
+ }
807
+
808
+ function isProtectedWorkspacePath(relativePath = "") {
809
+ const normalized = String(relativePath || "")
810
+ .replace(/\\/g, "/")
811
+ .replace(/^\/+/, "");
812
+ const parts = normalized.split("/").filter(Boolean);
813
+ return parts.some((part) => WORKSPACE_PROTECTED_DIRS.has(part) || WORKSPACE_PROTECTED_FILES.has(part) || isProtectedEnvName(part));
814
+ }
815
+
796
816
  function isInsideDirectory(parent, candidate) {
797
817
  const relative = path.relative(parent, candidate);
798
818
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
@@ -820,6 +840,9 @@ async function resolveWorkspaceFilePath(root, relativePath = "", { mustExist = f
820
840
  .replace(/\\/g, "/")
821
841
  .replace(/^\/+/, "");
822
842
  if (!normalizedRelative || normalizedRelative.includes("\0")) throw new Error("Workspace file path is required.");
843
+ if (isProtectedWorkspacePath(normalizedRelative)) {
844
+ throw new Error("Workspace file is protected because it may contain secrets or AgInTiFlow internals.");
845
+ }
823
846
  const absolutePath = path.resolve(root, normalizedRelative);
824
847
  if (!isInsideDirectory(root, absolutePath)) throw new Error("Workspace file path escapes the selected folder.");
825
848
 
@@ -892,6 +915,7 @@ async function buildWorkspaceSnapshot(root) {
892
915
 
893
916
  const absolutePath = path.join(currentDir, entry.name);
894
917
  const relativePath = relativeWorkspacePath(root, absolutePath);
918
+ if (isProtectedWorkspacePath(relativePath)) continue;
895
919
  let stat;
896
920
  let realPath = absolutePath;
897
921
  let symlink = false;