@lazyingart/agintiflow 0.20.126 → 0.20.127

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.126",
3
+ "version": "0.20.127",
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
@@ -2061,6 +2061,12 @@ function resetArtifactViewer() {
2061
2061
  artifactViewerBodyEl.innerHTML = `<p class="subtle">${t("artifactViewerEmpty")}</p>`;
2062
2062
  }
2063
2063
 
2064
+ function artifactRawUrl(artifactId, { download = false } = {}) {
2065
+ if (!currentSessionId || !artifactId) return "";
2066
+ const base = `/api/sessions/${encodeURIComponent(currentSessionId)}/artifacts/${encodeURIComponent(artifactId)}/raw`;
2067
+ return download ? `${base}?download=1` : base;
2068
+ }
2069
+
2064
2070
  function renderArtifactShell() {
2065
2071
  renderArtifactBadge();
2066
2072
  renderArtifactList();
@@ -2075,23 +2081,40 @@ function renderArtifactContent(content) {
2075
2081
  artifactViewerTitleEl.textContent = content.title || item?.title || t("artifactViewerEmptyTitle");
2076
2082
  artifactViewerMetaEl.textContent = [content.path || item?.path, item ? artifactLabel(item) : ""].filter(Boolean).join(" · ");
2077
2083
  artifactViewerKindEl.textContent = content.kind || item?.kind || "";
2084
+ const streamedUrl = content.url || (content.id ? artifactRawUrl(content.id) : "");
2085
+ const downloadUrl = content.downloadUrl || (content.id ? artifactRawUrl(content.id, { download: true }) : "");
2086
+ const renderUrl = content.dataUrl || streamedUrl;
2078
2087
 
2079
- if (content.dataUrl && (content.kind === "pdf" || content.mime === "application/pdf")) {
2088
+ if (renderUrl && (content.kind === "pdf" || content.mime === "application/pdf")) {
2080
2089
  artifactViewerBodyEl.innerHTML = `
2081
- <iframe class="artifact-pdf-frame" src="${content.dataUrl}" title="${escapeHtml(content.title || "Artifact PDF")}"></iframe>
2090
+ <iframe class="artifact-pdf-frame" src="${renderUrl}" title="${escapeHtml(content.title || "Artifact PDF")}"></iframe>
2082
2091
  `;
2083
2092
  return;
2084
2093
  }
2085
2094
 
2086
- if (content.dataUrl) {
2095
+ if (renderUrl && (content.kind === "image" || String(content.mime || "").startsWith("image/"))) {
2087
2096
  artifactViewerBodyEl.innerHTML = `
2088
2097
  <figure class="artifact-image-frame">
2089
- <img class="artifact-preview-image" src="${content.dataUrl}" alt="${escapeHtml(content.title || "Artifact image")}" />
2098
+ <img class="artifact-preview-image" src="${renderUrl}" alt="${escapeHtml(content.title || "Artifact image")}" />
2090
2099
  </figure>
2091
2100
  `;
2092
2101
  return;
2093
2102
  }
2094
2103
 
2104
+ if ((content.tooLargeForInline || content.binary) && streamedUrl) {
2105
+ artifactViewerBodyEl.innerHTML = `
2106
+ <div class="artifact-large-file">
2107
+ <strong>${escapeHtml(content.preview || "Artifact is available as a streamed file.")}</strong>
2108
+ <p>${escapeHtml(content.path || item?.path || content.title || "Generated artifact")}</p>
2109
+ <div class="artifact-file-actions">
2110
+ <a class="secondary-button" href="${streamedUrl}" target="_blank" rel="noreferrer">Open preview</a>
2111
+ <a class="secondary-button" href="${downloadUrl}" download="${escapeHtml(artifactDownloadName(item, content))}">Download</a>
2112
+ </div>
2113
+ </div>
2114
+ `;
2115
+ return;
2116
+ }
2117
+
2095
2118
  const text = typeof content.text === "string" ? content.text : "";
2096
2119
  if (content.kind === "markdown" || /markdown/i.test(content.mime || "")) {
2097
2120
  artifactViewerBodyEl.innerHTML = `<div class="artifact-markdown markdown-body">${renderMarkdown(text)}</div>`;
@@ -2153,6 +2176,21 @@ async function downloadArtifact(artifactId) {
2153
2176
  const data = await response.json().catch(() => ({}));
2154
2177
  if (!response.ok) throw new Error(data.error || t("artifactDownloadFailed"));
2155
2178
 
2179
+ if ((data.downloadUrl || data.url) && !data.dataUrl && typeof data.text !== "string") {
2180
+ const link = document.createElement("a");
2181
+ link.href = data.downloadUrl || artifactRawUrl(artifactId, { download: true }) || data.url;
2182
+ link.download = artifactDownloadName(item, data);
2183
+ document.body.append(link);
2184
+ link.click();
2185
+ link.remove();
2186
+
2187
+ markArtifactRead(artifactId);
2188
+ renderArtifactBadge();
2189
+ renderArtifactList();
2190
+ artifactStatusEl.textContent = t("artifactDownloadReady");
2191
+ return;
2192
+ }
2193
+
2156
2194
  const blob = data.dataUrl
2157
2195
  ? blobFromDataUrl(data.dataUrl)
2158
2196
  : new Blob([typeof data.text === "string" ? data.text : JSON.stringify(data, null, 2)], {
package/public/styles.css CHANGED
@@ -1602,6 +1602,29 @@ button.danger {
1602
1602
  background: white;
1603
1603
  }
1604
1604
 
1605
+ .artifact-large-file {
1606
+ display: grid;
1607
+ gap: 10px;
1608
+ align-content: start;
1609
+ margin: 0;
1610
+ padding: 18px;
1611
+ border: 1px solid var(--line);
1612
+ border-radius: 16px;
1613
+ background: rgba(255, 255, 255, 0.72);
1614
+ }
1615
+
1616
+ .artifact-large-file p {
1617
+ margin: 0;
1618
+ color: var(--muted);
1619
+ overflow-wrap: anywhere;
1620
+ }
1621
+
1622
+ .artifact-file-actions {
1623
+ display: flex;
1624
+ flex-wrap: wrap;
1625
+ gap: 8px;
1626
+ }
1627
+
1605
1628
  .artifact-editor {
1606
1629
  min-height: 100%;
1607
1630
  height: 100%;
@@ -14,7 +14,7 @@ AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-a
14
14
 
15
15
  Project-oriented programming for agentic workflows.
16
16
 
17
- AAPS is a project oriented and prompt-native programming language and visual studio for turning prompts into structured, verifiable pipelines. It connects wet and dry experiments, hardware and software, and human intent with executable agent work through tasks, typed inputs, declared outputs, validation gates, recovery steps, and durable artifacts.
17
+ AAPS is a prompt-native programming language and visual studio for turning prompts into structured, verifiable pipelines. It connects wet and dry experiments, hardware and software, and human intent with executable agent work through tasks, typed inputs, declared outputs, validation gates, recovery steps, and durable artifacts.
18
18
 
19
19
  ## Shorter Version
20
20
 
@@ -1,7 +1,13 @@
1
1
  import fs from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { buildArtifacts, normalizeCanvasPayload, persistCanvasPayloadFile, readArtifactContent } from "../src/artifact-tunnel.js";
4
+ import {
5
+ buildArtifacts,
6
+ normalizeCanvasPayload,
7
+ persistCanvasPayloadFile,
8
+ readArtifactContent,
9
+ resolveArtifactFile,
10
+ } from "../src/artifact-tunnel.js";
5
11
  import { SessionStore } from "../src/session-store.js";
6
12
 
7
13
  async function main() {
@@ -54,6 +60,45 @@ async function main() {
54
60
  if (!content.ok) throw new Error(content.error || "persisted artifact could not be read");
55
61
  if (!String(content.text || "").includes("Durable report")) throw new Error("persisted artifact content mismatch");
56
62
 
63
+ const largeImagePath = path.join(workspace, "large-preview.png");
64
+ const pngHeader = Buffer.from("89504e470d0a1a0a", "hex");
65
+ await fs.writeFile(largeImagePath, Buffer.concat([pngHeader, Buffer.alloc(4_200_000)]));
66
+ const largeNormalized = normalizeCanvasPayload(
67
+ {
68
+ title: "Large preview image",
69
+ kind: "image",
70
+ path: "large-preview.png",
71
+ selected: true,
72
+ },
73
+ config
74
+ );
75
+ if (!largeNormalized.ok) throw new Error(largeNormalized.reason || "large image canvas payload normalization failed");
76
+ const largePersisted = await persistCanvasPayloadFile(largeNormalized.payload, { config, store });
77
+ if (!largePersisted.ok) throw new Error(largePersisted.reason || "large image canvas artifact persistence failed");
78
+ if (!largePersisted.payload.artifactPersisted) throw new Error("large image was not persisted into session artifacts");
79
+
80
+ const largeEvents = [
81
+ {
82
+ timestamp: new Date().toISOString(),
83
+ type: "canvas.item",
84
+ data: {
85
+ ...largePersisted.payload,
86
+ commandCwd: workspace,
87
+ },
88
+ },
89
+ ];
90
+ const { items: largeItems } = buildArtifacts({ sessionId: store.sessionId, events: largeEvents, store });
91
+ const largeContent = await readArtifactContent(largeItems[0], { store, config });
92
+ if (!largeContent.ok) throw new Error(largeContent.error || "large artifact metadata could not be read");
93
+ if (largeContent.dataUrl) throw new Error("large artifact should not be inlined as a data URL");
94
+ if (!largeContent.url || !largeContent.downloadUrl || !largeContent.tooLargeForInline) {
95
+ throw new Error("large artifact did not expose streamed preview URLs");
96
+ }
97
+ const largeFile = await resolveArtifactFile(largeItems[0], { store, config });
98
+ if (!largeFile.ok || largeFile.size <= 4_000_000 || largeFile.mime !== "image/png") {
99
+ throw new Error("large artifact file resolver returned invalid metadata");
100
+ }
101
+
57
102
  const missing = await persistCanvasPayloadFile({ ...normalized.payload, path: "missing.png" }, { config, store });
58
103
  if (missing.ok) throw new Error("missing canvas path should fail persistence");
59
104
 
@@ -350,6 +350,37 @@ try {
350
350
  throw new Error("artifact content endpoint did not return renderable content");
351
351
  }
352
352
 
353
+ const largeSessionId = "large-artifact-smoke";
354
+ const largeStore = new SessionStore(paths.globalSessionsDir, largeSessionId, sessionStoreOptions(runtimeDir, largeSessionId));
355
+ await largeStore.ensure();
356
+ const largeCanvasDir = path.join(largeStore.artifactsDir, "canvas");
357
+ await fs.mkdir(largeCanvasDir, { recursive: true });
358
+ const largeImagePath = path.join(largeCanvasDir, "large-image.png");
359
+ await fs.writeFile(largeImagePath, Buffer.concat([Buffer.from("89504e470d0a1a0a", "hex"), Buffer.alloc(4_200_000)]));
360
+ await largeStore.appendEvent("canvas.item", {
361
+ artifactId: "large-image",
362
+ title: "Large streamed image",
363
+ kind: "image",
364
+ path: "large-image.png",
365
+ sessionFilePath: largeImagePath,
366
+ selected: true,
367
+ });
368
+ const largeArtifacts = await fetchJson(`/api/sessions/${encodeURIComponent(largeSessionId)}/artifacts`);
369
+ if (!largeArtifacts.items?.some((item) => item.id === "large-image")) {
370
+ throw new Error("large artifact endpoint did not list session artifact");
371
+ }
372
+ const largeContent = await fetchJson(`/api/sessions/${encodeURIComponent(largeSessionId)}/artifacts/large-image`);
373
+ if (!largeContent.tooLargeForInline || !largeContent.url || largeContent.dataUrl) {
374
+ throw new Error("large artifact metadata did not switch to streamed preview");
375
+ }
376
+ const rawResponse = await fetch(`${baseUrl}/api/sessions/${encodeURIComponent(largeSessionId)}/artifacts/large-image/raw`);
377
+ if (!rawResponse.ok) throw new Error(`large artifact raw endpoint failed: ${rawResponse.status}`);
378
+ if (!/^image\/png\b/i.test(rawResponse.headers.get("content-type") || "")) {
379
+ throw new Error("large artifact raw endpoint did not preserve image content type");
380
+ }
381
+ const rawBytes = await rawResponse.arrayBuffer();
382
+ if (rawBytes.byteLength <= 4_000_000) throw new Error("large artifact raw endpoint returned truncated content");
383
+
353
384
  const deleted = await fetchJson(`/api/sessions/${encodeURIComponent(runStart.sessionId)}`, {
354
385
  method: "DELETE",
355
386
  });
@@ -384,6 +415,7 @@ try {
384
415
  "/api/workspace/changes",
385
416
  "/api/sessions/:id/artifacts",
386
417
  "/api/sessions/:id/artifacts/:artifactId",
418
+ "/api/sessions/:id/artifacts/:artifactId/raw",
387
419
  "POST /api/sessions/:id/artifacts/select",
388
420
  ],
389
421
  provider: run.provider,
@@ -60,6 +60,16 @@ function mimeForPath(filePath) {
60
60
  return IMAGE_MIME_BY_EXT.get(ext) || BINARY_RENDER_MIME_BY_EXT.get(ext) || "text/plain; charset=utf-8";
61
61
  }
62
62
 
63
+ function isRenderableBinaryMime(mime) {
64
+ return String(mime || "").startsWith("image/") || mime === "application/pdf";
65
+ }
66
+
67
+ function artifactRawUrl(item, download = false) {
68
+ if (!item?.sessionId || !item?.id) return "";
69
+ const base = `/api/sessions/${encodeURIComponent(item.sessionId)}/artifacts/${encodeURIComponent(item.id)}/raw`;
70
+ return download ? `${base}?download=1` : base;
71
+ }
72
+
63
73
  function safeCanvasFilename(artifactId, filePath) {
64
74
  const rawBase = path.basename(String(filePath || "artifact"));
65
75
  const safeBase = rawBase
@@ -285,24 +295,13 @@ export function findArtifact(items, artifactId) {
285
295
  return items.find((item) => item.id === artifactId) || null;
286
296
  }
287
297
 
288
- export async function readArtifactContent(item, { store, config }) {
298
+ export async function resolveArtifactFile(item, { store, config } = {}) {
289
299
  if (!item) {
290
300
  return { ok: false, error: "Artifact not found." };
291
301
  }
292
302
 
293
- if (item.ref?.type === "inline") {
294
- return {
295
- ok: true,
296
- id: item.id,
297
- kind: item.kind,
298
- title: item.title,
299
- path: item.path || "",
300
- mime: item.mime || "text/plain; charset=utf-8",
301
- text: redactSensitiveText(String(item.ref.text || "")),
302
- };
303
- }
304
-
305
303
  if (item.ref?.type === "session-file") {
304
+ if (!store?.sessionDir) return { ok: false, error: "Artifact session store is unavailable." };
306
305
  const absolutePath = path.resolve(item.ref.path);
307
306
  if (!isInside(store.sessionDir, absolutePath)) {
308
307
  return { ok: false, error: "Artifact path is outside this session." };
@@ -311,32 +310,14 @@ export async function readArtifactContent(item, { store, config }) {
311
310
  const stat = await fs.stat(absolutePath).catch(() => null);
312
311
  if (!stat?.isFile()) return { ok: false, error: "Artifact file is missing." };
313
312
  const mime = mimeForPath(absolutePath);
314
- const isBinaryRenderable = mime.startsWith("image/") || mime === "application/pdf";
315
- const maxBytes = isBinaryRenderable ? MAX_ARTIFACT_IMAGE_BYTES : MAX_ARTIFACT_TEXT_BYTES;
316
- if (stat.size > maxBytes) return { ok: false, error: "Artifact is too large to preview safely." };
317
-
318
- const buffer = await fs.readFile(absolutePath);
319
- if (isBinaryRenderable) {
320
- return {
321
- ok: true,
322
- id: item.id,
323
- kind: mime === "application/pdf" ? "pdf" : "image",
324
- title: item.title,
325
- path: item.path || "",
326
- mime,
327
- dataUrl: `data:${mime};base64,${buffer.toString("base64")}`,
328
- };
329
- }
330
-
331
- if (buffer.includes(0)) return { ok: false, error: "Binary artifact cannot be rendered as text." };
332
313
  return {
333
314
  ok: true,
334
- id: item.id,
335
- kind: item.kind,
315
+ absolutePath,
316
+ size: stat.size,
317
+ mime,
318
+ kind: mime === "application/pdf" ? "pdf" : mime.startsWith("image/") ? "image" : item.kind,
336
319
  title: item.title,
337
320
  path: item.path || "",
338
- mime,
339
- text: redactSensitiveText(buffer.toString("utf8")),
340
321
  };
341
322
  }
342
323
 
@@ -350,33 +331,87 @@ export async function readArtifactContent(item, { store, config }) {
350
331
  const target = resolveWorkspacePath(itemConfig, item.ref.path);
351
332
  const stat = await fs.stat(target.absolutePath).catch(() => null);
352
333
  if (!stat?.isFile()) return { ok: false, error: "Workspace file is missing." };
353
-
354
334
  const mime = mimeForPath(target.absolutePath);
355
- const isBinaryRenderable = mime.startsWith("image/") || mime === "application/pdf";
335
+ return {
336
+ ok: true,
337
+ absolutePath: target.absolutePath,
338
+ size: stat.size,
339
+ mime,
340
+ kind: mime === "application/pdf" ? "pdf" : mime.startsWith("image/") ? "image" : item.kind,
341
+ title: item.title,
342
+ path: item.path || "",
343
+ };
344
+ }
345
+
346
+ return { ok: false, error: "Artifact has no readable file." };
347
+ }
348
+
349
+ export async function readArtifactContent(item, { store, config }) {
350
+ if (!item) {
351
+ return { ok: false, error: "Artifact not found." };
352
+ }
353
+
354
+ if (item.ref?.type === "inline") {
355
+ return {
356
+ ok: true,
357
+ id: item.id,
358
+ kind: item.kind,
359
+ title: item.title,
360
+ path: item.path || "",
361
+ mime: item.mime || "text/plain; charset=utf-8",
362
+ text: redactSensitiveText(String(item.ref.text || "")),
363
+ };
364
+ }
365
+
366
+ if (item.ref?.type === "session-file" || item.ref?.type === "workspace-file") {
367
+ const resolved = await resolveArtifactFile(item, { store, config });
368
+ if (!resolved.ok) return resolved;
369
+
370
+ const isBinaryRenderable = isRenderableBinaryMime(resolved.mime);
356
371
  const maxBytes = isBinaryRenderable ? MAX_ARTIFACT_IMAGE_BYTES : MAX_ARTIFACT_TEXT_BYTES;
357
- if (stat.size > maxBytes) return { ok: false, error: "Workspace file is too large to preview safely." };
372
+ const url = artifactRawUrl(item);
373
+ const downloadUrl = artifactRawUrl(item, true);
374
+ const base = {
375
+ ok: true,
376
+ id: item.id,
377
+ kind: resolved.kind,
378
+ title: item.title,
379
+ path: item.path || "",
380
+ mime: resolved.mime,
381
+ size: resolved.size,
382
+ url,
383
+ downloadUrl,
384
+ };
385
+
386
+ if (resolved.size > maxBytes) {
387
+ return {
388
+ ...base,
389
+ tooLargeForInline: true,
390
+ preview: isBinaryRenderable
391
+ ? "Large renderable artifact is available through the streamed artifact endpoint."
392
+ : "Large artifact is available through the streamed artifact endpoint.",
393
+ };
394
+ }
358
395
 
359
- const buffer = await fs.readFile(target.absolutePath);
396
+ const buffer = await fs.readFile(resolved.absolutePath);
360
397
  if (isBinaryRenderable) {
361
398
  return {
362
- ok: true,
363
- id: item.id,
364
- kind: mime === "application/pdf" ? "pdf" : "image",
365
- title: item.title,
366
- path: item.path || "",
367
- mime,
368
- dataUrl: `data:${mime};base64,${buffer.toString("base64")}`,
399
+ ...base,
400
+ dataUrl: `data:${resolved.mime};base64,${buffer.toString("base64")}`,
401
+ };
402
+ }
403
+
404
+ if (buffer.includes(0)) {
405
+ return {
406
+ ...base,
407
+ binary: true,
408
+ preview: "Binary artifact is available through the streamed artifact endpoint.",
369
409
  };
370
410
  }
371
411
 
372
- if (buffer.includes(0)) return { ok: false, error: "Binary workspace file cannot be rendered as text." };
373
412
  return {
374
- ok: true,
375
- id: item.id,
413
+ ...base,
376
414
  kind: item.kind,
377
- title: item.title,
378
- path: item.path || "",
379
- mime,
380
415
  text: redactSensitiveText(buffer.toString("utf8")),
381
416
  };
382
417
  }
package/web.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createReadStream } from "node:fs";
1
2
  import fs from "node:fs/promises";
2
3
  import express from "express";
3
4
  import path from "node:path";
@@ -43,6 +44,7 @@ import {
43
44
  countUnreadArtifacts,
44
45
  findArtifact,
45
46
  readArtifactContent,
47
+ resolveArtifactFile,
46
48
  serializeArtifacts,
47
49
  } from "./src/artifact-tunnel.js";
48
50
 
@@ -75,6 +77,17 @@ function isSafeSessionId(sessionId) {
75
77
  return /^[A-Za-z0-9._:-]+$/.test(String(sessionId || "")) && !String(sessionId || "").includes("..");
76
78
  }
77
79
 
80
+ function safeDownloadFilename(value) {
81
+ const basename = path.basename(String(value || "artifact").replace(/\\/g, "/"));
82
+ return (
83
+ basename
84
+ .replace(/[\r\n"\\]+/g, "_")
85
+ .replace(/^\.+/, "")
86
+ .replace(/[^A-Za-z0-9._ -]+/g, "-")
87
+ .slice(0, 120) || "artifact"
88
+ );
89
+ }
90
+
78
91
  function mapEventLogs(events) {
79
92
  return events.map((event) => ({
80
93
  at: event.timestamp,
@@ -877,6 +890,39 @@ app.get("/api/sessions/:sessionId/artifacts/:artifactId", async (req, res) => {
877
890
  res.json(content);
878
891
  });
879
892
 
893
+ app.get("/api/sessions/:sessionId/artifacts/:artifactId/raw", async (req, res) => {
894
+ const bundle = await loadArtifactBundle(req.params.sessionId);
895
+ if (bundle.error) {
896
+ res.status(bundle.status || 500).json({ error: bundle.error });
897
+ return;
898
+ }
899
+
900
+ const artifact = findArtifact(bundle.items, req.params.artifactId);
901
+ const file = await resolveArtifactFile(artifact, bundle);
902
+ if (!file.ok) {
903
+ res.status(artifact ? 400 : 404).json({ error: file.error || "Artifact not found." });
904
+ return;
905
+ }
906
+
907
+ const filename = safeDownloadFilename(file.path || file.title || file.absolutePath);
908
+ const disposition = req.query.download === "1" ? "attachment" : "inline";
909
+ res.setHeader("Content-Type", file.mime || "application/octet-stream");
910
+ res.setHeader("Content-Length", String(file.size));
911
+ res.setHeader("Content-Disposition", `${disposition}; filename="${filename}"`);
912
+ res.setHeader("Cache-Control", "private, max-age=0, no-cache");
913
+ res.setHeader("X-Content-Type-Options", "nosniff");
914
+
915
+ const stream = createReadStream(file.absolutePath);
916
+ stream.on("error", (error) => {
917
+ if (!res.headersSent) {
918
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
919
+ return;
920
+ }
921
+ res.destroy(error);
922
+ });
923
+ stream.pipe(res);
924
+ });
925
+
880
926
  app.post("/api/sessions/:sessionId/artifacts/select", async (req, res) => {
881
927
  const bundle = await loadArtifactBundle(req.params.sessionId);
882
928
  if (bundle.error) {