diff --git a/package-lock.json b/package-lock.json index 9527d44ee..b2f1920df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,10 @@ "typescript-eslint": "^8.62.0" } }, + "node_modules/@argent/artifacts": { + "resolved": "packages/artifacts", + "link": true + }, "node_modules/@argent/cli": { "resolved": "packages/argent-cli", "link": true @@ -143,40 +147,6 @@ "undici": "^7.24.4" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", @@ -1564,6 +1534,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~8.3.0" } @@ -1691,6 +1662,7 @@ "integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/types": "8.62.0", @@ -2031,6 +2003,7 @@ "version": "8.16.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2826,6 +2799,7 @@ "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", "dev": true, "license": "MIT", + "peer": true, "workspaces": [ "packages/*" ], @@ -3055,6 +3029,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -3556,6 +3531,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -5489,6 +5465,7 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5525,6 +5502,7 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -5593,6 +5571,7 @@ "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -5946,6 +5925,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -6008,6 +5988,7 @@ "picocolors": "^1.1.1" }, "devDependencies": { + "@argent/artifacts": "file:../artifacts", "@types/node": "^26.0.1", "typescript": "^6.0.3", "vitest": "^4.1.9" @@ -6058,6 +6039,7 @@ "@modelcontextprotocol/sdk": "^1.29.0" }, "devDependencies": { + "@argent/artifacts": "file:../artifacts", "@types/node": "^26.0.1", "typescript": "^6.0.3", "vitest": "^4.1.9" @@ -6066,12 +6048,22 @@ "packages/argent-tools-client": { "name": "@argent/tools-client", "version": "0.14.0", + "dependencies": { + "@argent/artifacts": "file:../artifacts" + }, "devDependencies": { "@types/node": "^26.0.1", "typescript": "^6.0.3", "vitest": "^4.1.9" } }, + "packages/artifacts": { + "name": "@argent/artifacts", + "version": "0.14.0", + "devDependencies": { + "typescript": "^6.0.3" + } + }, "packages/configuration-core": { "name": "@argent/configuration-core", "version": "0.14.0", @@ -6112,6 +6104,7 @@ "name": "@argent/registry", "version": "0.14.0", "dependencies": { + "@argent/artifacts": "file:../artifacts", "zod": "^4.0.0" }, "devDependencies": { @@ -6145,6 +6138,7 @@ "name": "@argent/tool-server", "version": "0.14.0", "dependencies": { + "@argent/artifacts": "file:../artifacts", "@argent/configuration-core": "file:../configuration-core", "@argent/native-devtools-android": "file:../native-devtools-android", "@argent/native-devtools-ios": "file:../native-devtools-ios", diff --git a/packages/argent-cli/package.json b/packages/argent-cli/package.json index 15ce5442a..e225489d4 100644 --- a/packages/argent-cli/package.json +++ b/packages/argent-cli/package.json @@ -20,6 +20,7 @@ "picocolors": "^1.1.1" }, "devDependencies": { + "@argent/artifacts": "file:../artifacts", "@types/node": "^26.0.1", "typescript": "^6.0.3", "vitest": "^4.1.9" diff --git a/packages/argent-cli/test/run-artifacts.test.ts b/packages/argent-cli/test/run-artifacts.test.ts index f11aebdd5..6937c9753 100644 --- a/packages/argent-cli/test/run-artifacts.test.ts +++ b/packages/argent-cli/test/run-artifacts.test.ts @@ -5,7 +5,8 @@ import { join } from "node:path"; import { tmpdir } from "node:os"; import { mkdtemp, rm, writeFile, stat } from "node:fs/promises"; import { run, type RunCommandOptions } from "../src/run.js"; -import { ARTIFACT_MARKER, artifactsRoot, type ArtifactHandle } from "@argent/tools-client"; +import { artifactsRoot } from "@argent/tools-client"; +import { ARTIFACT_MARKER, type ArtifactHandle } from "@argent/artifacts"; const PNG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x11, 0x22, 0x33]); @@ -129,6 +130,7 @@ describe("CLI run — artifact materialization end-to-end", () => { return { [ARTIFACT_MARKER]: true, id: "loc-1", + kind: "screenshot", filename: "shot.png", mimeType: "image/png", size: st.size, @@ -159,6 +161,7 @@ describe("CLI run — artifact materialization end-to-end", () => { image: { [ARTIFACT_MARKER]: true, id: "rem-1", + kind: "screenshot", filename: "shot.png", mimeType: "image/png", size: PNG.length, diff --git a/packages/argent-mcp/package.json b/packages/argent-mcp/package.json index 95e3c4d0a..d8d93597f 100644 --- a/packages/argent-mcp/package.json +++ b/packages/argent-mcp/package.json @@ -18,6 +18,7 @@ "@modelcontextprotocol/sdk": "^1.29.0" }, "devDependencies": { + "@argent/artifacts": "file:../artifacts", "@types/node": "^26.0.1", "typescript": "^6.0.3", "vitest": "^4.1.9" diff --git a/packages/argent-mcp/test/content.test.ts b/packages/argent-mcp/test/content.test.ts index 25c8d158a..cd05fad21 100644 --- a/packages/argent-mcp/test/content.test.ts +++ b/packages/argent-mcp/test/content.test.ts @@ -12,12 +12,21 @@ import { flowRunToMcpContent, type FlowExecuteResult, } from "../src/content.js"; -import { ARTIFACT_MARKER, type ArtifactHandle } from "@argent/tools-client"; +import { ARTIFACT_MARKER, type ArtifactHandle, type ArtifactKind } from "@argent/artifacts"; const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; +function artifactKind(filename: string, mimeType: string): ArtifactKind { + if (mimeType.startsWith("image/")) return "screenshot"; + if (filename.includes("cpu")) return "native-profile-cpu"; + if (filename.includes("hangs")) return "native-profile-hangs"; + if (filename.includes("leaks")) return "native-profile-leaks"; + return "native-profile-trace"; +} + function artifactHandle(id: string, filename: string, mimeType: string): ArtifactHandle { - return { [ARTIFACT_MARKER]: true, id, filename, mimeType, size: 0 }; + const kind = artifactKind(filename, mimeType); + return { [ARTIFACT_MARKER]: true, id, kind, filename, mimeType, size: 0 }; } function fetchReturning(bytes: number[]): typeof fetch { diff --git a/packages/argent-tools-client/package.json b/packages/argent-tools-client/package.json index 93897d3b7..35eaa02fa 100644 --- a/packages/argent-tools-client/package.json +++ b/packages/argent-tools-client/package.json @@ -11,6 +11,9 @@ "test": "vitest run", "typecheck:tests": "tsc --noEmit -p tsconfig.test.json" }, + "dependencies": { + "@argent/artifacts": "file:../artifacts" + }, "devDependencies": { "@types/node": "^26.0.1", "typescript": "^6.0.3", diff --git a/packages/argent-tools-client/src/artifacts.ts b/packages/argent-tools-client/src/artifacts.ts index 2767de70b..0754fda2f 100644 --- a/packages/argent-tools-client/src/artifacts.ts +++ b/packages/argent-tools-client/src/artifacts.ts @@ -33,37 +33,10 @@ import { basename, join } from "node:path"; import { createHash } from "node:crypto"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; +import { ARTIFACT_MARKER, type ArtifactHandle } from "@argent/artifacts"; const execFileAsync = promisify(execFile); -/** Must match the tool-server's wire contract (`tool-server/src/artifacts.ts`). */ -export const ARTIFACT_MARKER = "__argentArtifact" as const; - -export interface ArtifactHandle { - [ARTIFACT_MARKER]: true; - id: string; - filename: string; - mimeType: string; - size: number; - /** - * Absolute path of the file on the tool-server host. When the tool-server is - * co-located with this client, the file is already on disk here — the gate - * reads it directly instead of downloading it over `/artifacts/:id`, avoiding - * a redundant second copy. Verified against {@link size}/{@link mtimeMs} - * before it is trusted; any mismatch (or a remote host) falls back to the - * download path. Absent on older tool-servers that don't emit it. - */ - hostPath?: string; - /** mtime of {@link hostPath} (ms) at registration, for the integrity check. */ - mtimeMs?: number; - /** - * Present when the artifact is a directory bundle (e.g. an Instruments - * `.trace`). Locally the gate uses the directory in place; on a remote miss - * the download is a gzipped tar that the client unpacks back into a directory. - */ - archive?: "tar.gz"; -} - export function isArtifactHandle(value: unknown): value is ArtifactHandle { return ( !!value && diff --git a/packages/argent-tools-client/src/index.ts b/packages/argent-tools-client/src/index.ts index d9d542de3..4f0924369 100644 --- a/packages/argent-tools-client/src/index.ts +++ b/packages/argent-tools-client/src/index.ts @@ -52,11 +52,10 @@ export { isArtifactHandle, getDeviceIdFromArgs, artifactsRoot, - ARTIFACT_MARKER, - type ArtifactHandle, type MaterializeContext, type MaterializedImage, } from "./artifacts.js"; +export type { ArtifactHandle } from "@argent/artifacts"; export { prepareFileInputs, diff --git a/packages/argent-tools-client/src/tools-client.ts b/packages/argent-tools-client/src/tools-client.ts index d441fa832..66229fcc2 100644 --- a/packages/argent-tools-client/src/tools-client.ts +++ b/packages/argent-tools-client/src/tools-client.ts @@ -1,12 +1,14 @@ import { ensureToolsServer, type ToolsServerHandle, type ToolsServerPaths } from "./launcher.js"; import { getResolvedToolsUrl } from "./link-config.js"; import { prepareFileInputs, applyClientFileDirectives, type FileInputSpec } from "./file-inputs.js"; +import type { ArtifactOutputMap } from "@argent/artifacts"; export interface ToolMeta { name: string; description: string; inputSchema: Record; outputHint?: string; + artifacts?: ArtifactOutputMap; /** Args that name files on the CALLER's machine — see file-inputs.ts. */ fileInputs?: FileInputSpec[]; alwaysLoad?: boolean; diff --git a/packages/argent-tools-client/test/artifacts.test.ts b/packages/argent-tools-client/test/artifacts.test.ts index 4e9d9a871..e64c3498f 100644 --- a/packages/argent-tools-client/test/artifacts.test.ts +++ b/packages/argent-tools-client/test/artifacts.test.ts @@ -8,12 +8,20 @@ import { isArtifactHandle, getDeviceIdFromArgs, artifactDir, - ARTIFACT_MARKER, - type ArtifactHandle, } from "../src/artifacts.js"; +import { ARTIFACT_MARKER, type ArtifactHandle, type ArtifactKind } from "@argent/artifacts"; + +function artifactKind(filename: string, mimeType: string): ArtifactKind { + if (mimeType.startsWith("image/")) return "screenshot"; + if (filename.includes("cpu")) return "native-profile-cpu"; + if (filename.includes("hangs")) return "native-profile-hangs"; + if (filename.includes("leaks")) return "native-profile-leaks"; + return "native-profile-trace"; +} function handle(id: string, filename: string, mimeType: string): ArtifactHandle { - return { [ARTIFACT_MARKER]: true, id, filename, mimeType, size: 0 }; + const kind = artifactKind(filename, mimeType); + return { [ARTIFACT_MARKER]: true, id, kind, filename, mimeType, size: 0 }; } const PNG = [0x89, 0x50, 0x4e, 0x47, 0x01, 0x02]; @@ -141,6 +149,7 @@ describe("materializeArtifacts", () => { const h: ArtifactHandle = { [ARTIFACT_MARKER]: true, id: "trunc", + kind: "native-profile-cpu", filename: "data.xml", mimeType: "application/xml", size: 99, // server announced 99 bytes… @@ -190,6 +199,7 @@ describe("materializeArtifacts local short-circuit", () => { return { [ARTIFACT_MARKER]: true, id, + kind: artifactKind(filename, mimeType), filename, mimeType, size: st.size, @@ -237,6 +247,7 @@ describe("materializeArtifacts local short-circuit", () => { const h: ArtifactHandle = { [ARTIFACT_MARKER]: true, id: "img2", + kind: "screenshot", filename: "shot.png", mimeType: "image/png", size: PNG.length, @@ -258,6 +269,7 @@ describe("materializeArtifacts local short-circuit", () => { const h: ArtifactHandle = { [ARTIFACT_MARKER]: true, id: "img3", + kind: "screenshot", filename: "shot.png", mimeType: "image/png", size: PNG.length, @@ -320,6 +332,7 @@ describe("materializeArtifacts directory bundles", () => { return { [ARTIFACT_MARKER]: true, id, + kind: "native-profile-trace", filename: "session.trace", mimeType: "application/octet-stream", size: 0, diff --git a/packages/artifacts/package.json b/packages/artifacts/package.json new file mode 100644 index 000000000..da0acbebd --- /dev/null +++ b/packages/artifacts/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "name": "@argent/artifacts", + "version": "0.14.0", + "description": "Shared artifact wire contract for Argent tools", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "rm -rf dist tsconfig.tsbuildinfo && tsc", + "typecheck:tests": "tsc --noEmit -p tsconfig.json" + }, + "devDependencies": { + "typescript": "^6.0.3" + } +} diff --git a/packages/artifacts/src/index.ts b/packages/artifacts/src/index.ts new file mode 100644 index 000000000..2c0382499 --- /dev/null +++ b/packages/artifacts/src/index.ts @@ -0,0 +1,53 @@ +/** Discriminant key identifying an artifact handle inside a tool result. */ +export const ARTIFACT_MARKER = "__argentArtifact" as const; + +/** + * Semantic artifact category. MIME type tells consumers how to read the bytes; + * kind tells them what the artifact represents. + */ +export type ArtifactKind = + | "screenshot" + | "screenshot-diff" + | "screenshot-diff-context" + | "native-profile-trace" + | "native-profile-cpu" + | "native-profile-hangs" + | "native-profile-leaks" + | "native-profile-report" + | "react-profile-cpu" + | "react-profile-commits" + | "react-profile-report"; + +/** Wire contract: what a tool returns in place of a host path. */ +export interface ArtifactHandle { + [ARTIFACT_MARKER]: true; + id: string; + kind: ArtifactKind; + filename: string; + mimeType: string; + size: number; + /** + * Absolute path of the file on the tool-server host. Present on current + * servers so co-located clients can read the file directly; optional for + * compatibility with older servers and remote-only handles. + */ + hostPath?: string; + /** mtime of {@link hostPath} (ms) at registration, for client integrity checks. */ + mtimeMs?: number; + /** + * Present when the artifact is a directory bundle (e.g. an Instruments + * `.trace`). Remote downloads stream it as a gzipped tar. + */ + archive?: "tar.gz"; +} + +export interface ArtifactOutputSpec { + /** Semantic category of this declared tool output, distinct from MIME type. */ + kind: ArtifactKind; + /** Optional human-readable explanation for tool-listing consumers. */ + description?: string; + /** Optional MIME types this output normally produces. */ + mimeTypes?: string[]; +} + +export type ArtifactOutputMap = Record; diff --git a/packages/artifacts/tsconfig.json b/packages/artifacts/tsconfig.json new file mode 100644 index 000000000..03d52d62a --- /dev/null +++ b/packages/artifacts/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/registry/package.json b/packages/registry/package.json index a06a15535..8d1c6d535 100644 --- a/packages/registry/package.json +++ b/packages/registry/package.json @@ -12,6 +12,7 @@ "typecheck:tests": "tsc --noEmit -p tsconfig.test.json" }, "dependencies": { + "@argent/artifacts": "file:../artifacts", "zod": "^4.0.0" }, "devDependencies": { diff --git a/packages/registry/src/artifacts.ts b/packages/registry/src/artifacts.ts index a347b2916..48de5cebb 100644 --- a/packages/registry/src/artifacts.ts +++ b/packages/registry/src/artifacts.ts @@ -25,40 +25,19 @@ import { stat } from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { basename, extname } from "node:path"; +import { + ARTIFACT_MARKER, + type ArtifactHandle, + type ArtifactKind, + type ArtifactOutputMap, +} from "@argent/artifacts"; -/** Discriminant key identifying an artifact handle inside a tool result. */ -export const ARTIFACT_MARKER = "__argentArtifact" as const; - -/** Wire contract: what a tool returns in place of a host path. */ -export interface ArtifactHandle { - [ARTIFACT_MARKER]: true; - id: string; - filename: string; - mimeType: string; - size: number; - /** - * Absolute path of the file on this (tool-server) host. A co-located client - * uses it to read the file directly instead of downloading it over - * `/artifacts/:id`. The client verifies it against `size`/`mtimeMs` first, so - * a remote client (where the path is meaningless or absent) simply falls back - * to the download route. - */ - hostPath: string; - /** mtime of `hostPath` (ms) at registration, for the client's integrity check. */ - mtimeMs?: number; - /** - * Set when `hostPath` is a directory (e.g. an Instruments `.trace` bundle). - * A single file can't represent a directory, so `GET /artifacts/:id` streams - * it as a gzipped tar **only when a remote client actually requests it** — - * never in local mode, where the client uses the directory in place via the - * gate. The client unpacks the tar back into a directory after download. - */ - archive?: "tar.gz"; -} +export type RegisteredArtifactHandle = ArtifactHandle & { hostPath: string }; /** Internal entry the HTTP route reads to stream a registered artifact. */ export interface ArtifactEntry { path: string; + kind: ArtifactKind; filename: string; mimeType: string; size: number; @@ -68,6 +47,7 @@ export interface ArtifactEntry { /** Public metadata returned by the artifact inventory endpoint. */ export interface ArtifactListItem { id: string; + kind: ArtifactKind; filename: string; mimeType: string; size: number; @@ -92,6 +72,10 @@ function inferMimeType(filePath: string): string { } export interface RegisterArtifactOptions { + /** Absolute path of the file or directory on the tool-server host. */ + hostPath: string; + /** Semantic category of the artifact, distinct from its MIME type. */ + kind: ArtifactKind; /** Override the basename presented to the client. Defaults to the host basename. */ filename?: string; /** Override the inferred MIME type. */ @@ -104,6 +88,15 @@ export interface RegisterArtifactOptions { archive?: "tar.gz"; } +export type RegisterArtifactFileOptions = Omit; + +export interface ArtifactRegistrar { + register( + outputName: TOutputName, + opts: RegisterArtifactFileOptions + ): Promise; +} + /** * Process-scoped artifact store, owned by a {@link Registry}. A tool registers * an entry during `execute` and the `/artifacts/:id` route — resolving the same @@ -112,12 +105,13 @@ export interface RegisterArtifactOptions { export class ArtifactStore { private readonly entries = new Map(); - async register(hostPath: string, opts?: RegisterArtifactOptions): Promise { - const filename = opts?.filename ?? basename(hostPath); - const mimeType = opts?.mimeType ?? inferMimeType(hostPath); + async register(opts: RegisterArtifactOptions): Promise { + const { hostPath } = opts; + const filename = opts.filename ?? basename(hostPath); + const mimeType = opts.mimeType ?? inferMimeType(hostPath); let size = 0; let mtimeMs: number | undefined; - let isDirectory = opts?.archive === "tar.gz"; + let isDirectory = opts.archive === "tar.gz"; try { const st = await stat(hostPath); size = st.size; @@ -129,10 +123,18 @@ export class ArtifactStore { // they don't match what's on disk at read time. } const id = randomUUID(); - this.entries.set(id, { path: hostPath, filename, mimeType, size, isDirectory }); - const handle: ArtifactHandle = { + this.entries.set(id, { + path: hostPath, + kind: opts.kind, + filename, + mimeType, + size, + isDirectory, + }); + const handle: RegisteredArtifactHandle = { [ARTIFACT_MARKER]: true, id, + kind: opts.kind, filename, mimeType, size, @@ -150,6 +152,7 @@ export class ArtifactStore { list(): ArtifactListItem[] { return [...this.entries.entries()].map(([id, entry]) => ({ id, + kind: entry.kind, filename: entry.filename, mimeType: entry.mimeType, size: entry.size, @@ -157,3 +160,19 @@ export class ArtifactStore { })); } } + +export function createArtifactRegistrar( + store: ArtifactStore, + outputs: ArtifactOutputMap | undefined, + toolId: string +): ArtifactRegistrar { + return { + register(outputName, opts) { + const output = outputs?.[outputName]; + if (!output) { + throw new Error(`Tool "${toolId}" did not declare artifact output "${outputName}"`); + } + return store.register({ ...opts, kind: output.kind }); + }, + }; +} diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index 140591c0c..feb4b981d 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -18,11 +18,13 @@ export type { ToolCapability, ToolDependency, } from "./types"; -export { ArtifactStore, ARTIFACT_MARKER } from "./artifacts"; +export { ArtifactStore, createArtifactRegistrar } from "./artifacts"; export type { - ArtifactHandle, ArtifactEntry, ArtifactListItem, + ArtifactRegistrar, + RegisteredArtifactHandle, + RegisterArtifactFileOptions, RegisterArtifactOptions, } from "./artifacts"; export { diff --git a/packages/registry/src/registry.ts b/packages/registry/src/registry.ts index de39ebcae..a886f0261 100644 --- a/packages/registry/src/registry.ts +++ b/packages/registry/src/registry.ts @@ -11,7 +11,7 @@ import { InvokeToolOptions, ToolContext, } from "./types"; -import { ArtifactStore } from "./artifacts"; +import { ArtifactStore, createArtifactRegistrar } from "./artifacts"; import { ServiceNotFoundError, ServiceInitializationError, @@ -134,10 +134,13 @@ export class Registry { options: typeof ref === "string" ? undefined : ref.options, })); - // Build the per-invocation context: caller options (e.g. signal) plus the - // registry-owned artifact store, so any tool can register host files via - // `ctx.artifacts` without declaring a per-tool service. - const ctx: ToolContext = { ...options, artifacts: this.artifacts }; + // Build the per-invocation context: caller options (e.g. signal) plus a + // tool-aware artifact registrar. The registrar writes into the + // registry-owned store but only accepts outputs declared by this tool. + const ctx: ToolContext = { + ...options, + artifacts: createArtifactRegistrar(this.artifacts, definition.artifacts, definition.id), + }; const runOnce = async (): Promise => { const resolvedServices: Record = {}; diff --git a/packages/registry/src/types.ts b/packages/registry/src/types.ts index a60583cc1..449f09075 100644 --- a/packages/registry/src/types.ts +++ b/packages/registry/src/types.ts @@ -1,6 +1,7 @@ import { TypedEventEmitter } from "./event-emitter"; import { z } from "zod"; -import type { ArtifactStore } from "./artifacts"; +import type { ArtifactOutputMap, ArtifactOutputSpec } from "@argent/artifacts"; +import type { ArtifactRegistrar } from "./artifacts"; import type { FileInputSpec, ResolvedFileInput } from "./file-inputs"; // ── Service Types ── @@ -122,14 +123,15 @@ export interface InvokeToolOptions { /** * What a tool's `execute` receives as its third argument. The registry builds * this for every invocation: it carries the caller's {@link InvokeToolOptions} - * (e.g. `signal`) plus cross-cutting context the registry owns — currently the - * {@link ArtifactStore}, so any tool that produces a host file can register it - * (`ctx.artifacts.register(path)`) without declaring a per-tool service. The - * registry always populates `artifacts`; it is only ever absent when `execute` - * is called directly (bypassing `invokeTool`), e.g. in a unit test. + * (e.g. `signal`) plus cross-cutting context the registry owns. Tools register + * produced files by declared output name (`ctx.artifacts.register("report", + * { hostPath })`); the registry maps that name through the tool's + * {@link ToolDefinition.artifacts} contract to the concrete artifact kind. + * The registry always populates `artifacts`; it is only ever absent when + * `execute` is called directly (bypassing `invokeTool`), e.g. in a unit test. */ -export interface ToolContext extends InvokeToolOptions { - artifacts: ArtifactStore; +export interface ToolContext extends InvokeToolOptions { + artifacts: ArtifactRegistrar; } // ── Device + Capability Types ── @@ -211,7 +213,11 @@ export type ToolDependency = "adb" | "xcrun" | "emulator" | "sim-remote" | "vega // ── Tool Types ── -export interface ToolDefinition { +export interface ToolDefinition< + TParams = void, + TResult = unknown, + TArtifacts extends ArtifactOutputMap = ArtifactOutputMap, +> { id: string; description?: string; /** Zod schema for tool input; used for runtime validation. When provided, inputSchema is auto-derived at registration time. */ @@ -220,6 +226,12 @@ export interface ToolDefinition { inputSchema?: Record; /** Optional hint for adapters (e.g. "image" for MCP to return base64 image content). */ outputHint?: string; + /** + * Declares named artifact outputs this tool may return. Tool code registers + * concrete files by these names, keeping artifact kinds in the tool contract + * instead of scattered through producer call sites. + */ + artifacts?: TArtifacts; /** * When true, the MCP adapter marks the tool with `_meta["anthropic/alwaysLoad"] = true` * so Claude Code opts it out of progressive tool loading (ToolSearch). Use for the @@ -282,7 +294,11 @@ export interface ToolDefinition { fileInputs?: FileInputSpec[]; /** Returns alias → URN or { urn, options }; registry resolves each and passes alias → API into execute. */ services: (params: TParams) => Record; - execute(services: Record, params: TParams, ctx?: ToolContext): Promise; + execute( + services: Record, + params: TParams, + ctx?: ToolContext> + ): Promise; } export interface ToolRecord { diff --git a/packages/tool-server/package.json b/packages/tool-server/package.json index bf911b76b..106329808 100644 --- a/packages/tool-server/package.json +++ b/packages/tool-server/package.json @@ -13,6 +13,7 @@ "typecheck:tests": "tsc --noEmit -p tsconfig.test.json" }, "dependencies": { + "@argent/artifacts": "file:../artifacts", "@argent/configuration-core": "file:../configuration-core", "@argent/native-devtools-android": "file:../native-devtools-android", "@argent/native-devtools-ios": "file:../native-devtools-ios", diff --git a/packages/tool-server/src/artifacts.ts b/packages/tool-server/src/artifacts.ts index 12e9667c8..a519b4f22 100644 --- a/packages/tool-server/src/artifacts.ts +++ b/packages/tool-server/src/artifacts.ts @@ -21,18 +21,21 @@ import type { Registry, ArtifactEntry, ArtifactListItem, + ArtifactRegistrar, ArtifactStore, ToolContext, } from "@argent/registry"; export { ArtifactStore, - ARTIFACT_MARKER, - type ArtifactHandle, + createArtifactRegistrar, type ArtifactEntry, type ArtifactListItem, + type ArtifactRegistrar, + type RegisterArtifactFileOptions, type RegisterArtifactOptions, } from "@argent/registry"; +export type { ArtifactHandle } from "@argent/artifacts"; /** * Pull the registry-owned artifact store from a tool's `execute` context. @@ -40,10 +43,12 @@ export { * tool's `execute` is called directly (bypassing the registry) without a * context — i.e. a misconfigured unit test, not a real invocation. */ -export function requireArtifacts(ctx?: Partial): ArtifactStore { +export function requireArtifacts( + ctx?: Partial> +): ArtifactRegistrar { if (!ctx?.artifacts) { throw new Error( - "Artifact store missing from tool context. Invoke this tool via registry.invokeTool " + + "Artifact registrar missing from tool context. Invoke this tool via registry.invokeTool " + "(which injects ctx.artifacts), or pass { artifacts } when calling execute directly." ); } diff --git a/packages/tool-server/src/http.ts b/packages/tool-server/src/http.ts index 1246b81cf..90603b764 100644 --- a/packages/tool-server/src/http.ts +++ b/packages/tool-server/src/http.ts @@ -9,6 +9,7 @@ import { type Registry, type ResolvedFileInput, } from "@argent/registry"; +import type { ArtifactOutputSpec } from "@argent/artifacts"; import { AI_CLIENTS, type AiTelemetryProps } from "@argent/telemetry"; import { ToolNotFoundError } from "@argent/registry"; import { createIdleTimer, IDLE_CHECK_INTERVAL_MS } from "./utils/idle-timer"; @@ -442,6 +443,7 @@ export function createHttpApp(registry: Registry, options?: HttpAppOptions): Htt description: string; inputSchema: Record; outputHint?: string; + artifacts?: Record; fileInputs?: FileInputSpec[]; alwaysLoad?: boolean; searchHint?: string; @@ -452,6 +454,7 @@ export function createHttpApp(registry: Registry, options?: HttpAppOptions): Htt inputSchema: def.inputSchema ?? { type: "object", properties: {} }, }; if (def.outputHint) entry.outputHint = def.outputHint; + if (def.artifacts && Object.keys(def.artifacts).length > 0) entry.artifacts = def.artifacts; if (def.fileInputs && def.fileInputs.length > 0) entry.fileInputs = def.fileInputs; if (def.alwaysLoad) entry.alwaysLoad = true; if (def.searchHint) entry.searchHint = def.searchHint; diff --git a/packages/tool-server/src/tools/profiler/native-profiler/native-profiler-analyze.ts b/packages/tool-server/src/tools/profiler/native-profiler/native-profiler-analyze.ts index 182fe1420..161c94537 100644 --- a/packages/tool-server/src/tools/profiler/native-profiler/native-profiler-analyze.ts +++ b/packages/tool-server/src/tools/profiler/native-profiler/native-profiler-analyze.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import type { ToolDefinition } from "@argent/registry"; +import type { ArtifactOutputMap } from "@argent/artifacts"; import { nativeProfilerSessionRef, type NativeProfilerSessionApi, @@ -33,9 +34,17 @@ type NativeProfilerAnalyzeToolResult = Omit, - NativeProfilerAnalyzeToolResult + NativeProfilerAnalyzeToolResult, + typeof artifacts > = { id: "native-profiler-analyze", capability, @@ -49,6 +58,7 @@ Call native-profiler-stop first to export the trace data. Use when you need to interpret a completed native profiling recording. Fails if native-profiler-stop has not been called first to export trace data.`, zodSchema, + artifacts, services: (params) => ({ session: nativeProfilerSessionRef(resolveDevice(params.device_id)), }), @@ -69,7 +79,9 @@ Fails if native-profiler-stop has not been called first to export trace data.`, return { ...result, reportFile: result.reportFile - ? await requireArtifacts(ctx).register(result.reportFile) + ? await requireArtifacts(ctx).register("report", { + hostPath: result.reportFile, + }) : null, }; }, diff --git a/packages/tool-server/src/tools/profiler/native-profiler/native-profiler-stop.ts b/packages/tool-server/src/tools/profiler/native-profiler/native-profiler-stop.ts index 6d7d87cdd..fee9d586f 100644 --- a/packages/tool-server/src/tools/profiler/native-profiler/native-profiler-stop.ts +++ b/packages/tool-server/src/tools/profiler/native-profiler/native-profiler-stop.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import type { ToolDefinition } from "@argent/registry"; +import type { ArtifactOutputMap } from "@argent/artifacts"; import { nativeProfilerSessionRef, type NativeProfilerSessionApi, @@ -11,7 +12,7 @@ import { stopNativeProfilerIos, type IosStopResult } from "./platforms/ios"; import { stopNativeProfilerAndroid, type AndroidStopResult } from "./platforms/android"; import type { ExportDiagnostics } from "../../../utils/ios-profiler/export"; import { requireArtifacts, type ArtifactHandle } from "../../../artifacts"; -import type { ArtifactStore } from "@argent/registry"; +import type { ArtifactRegistrar } from "@argent/registry"; const zodSchema = z.object({ device_id: z @@ -55,14 +56,45 @@ const capability = { android: { emulator: true, device: true, unknown: true }, } as const; +const artifacts = { + trace: { + kind: "native-profile-trace", + }, + pftrace: { + kind: "native-profile-trace", + }, + cpu: { + kind: "native-profile-cpu", + }, + hangs: { + kind: "native-profile-hangs", + }, + leaks: { + kind: "native-profile-leaks", + }, +} satisfies ArtifactOutputMap; + +type ArtifactOutputName = Extract; + +const EXPORTED_FILE_OUTPUT_BY_KEY: Record = { + cpu: "cpu", + hangs: "hangs", + leaks: "leaks", + pftrace: "pftrace", +}; + /** Register each non-null exported file path as a downloadable artifact. */ async function exportedFilesToArtifacts( - store: ArtifactStore, + artifacts: ArtifactRegistrar, files: Record ): Promise> { const out: Record = {}; for (const [key, filePath] of Object.entries(files)) { - out[key] = filePath ? await store.register(filePath) : null; + out[key] = filePath + ? await artifacts.register(EXPORTED_FILE_OUTPUT_BY_KEY[key] ?? "trace", { + hostPath: filePath, + }) + : null; } return out; } @@ -72,11 +104,18 @@ async function exportedFilesToArtifacts( * works even when the path is a directory (iOS `.trace`), and even if it can't * be stat'd at registration (e.g. a recovered session). */ -function registerTrace(store: ArtifactStore, traceFile: string): Promise { - return store.register(traceFile, { archive: "tar.gz" }); +function registerTrace( + artifacts: ArtifactRegistrar, + traceFile: string +): Promise { + return artifacts.register("trace", { hostPath: traceFile, archive: "tar.gz" }); } -export const nativeProfilerStopTool: ToolDefinition, StopResult> = { +export const nativeProfilerStopTool: ToolDefinition< + z.infer, + StopResult, + typeof artifacts +> = { id: "native-profiler-stop", capability, description: `Stop native profiling and export trace data. @@ -87,6 +126,7 @@ Use when the user has finished the interaction to profile and you need to export Returns { traceFile, exportedFiles, exportDiagnostics? }; traceFile is the raw trace bundle and exportedFiles the exports, all downloadable artifacts materialized to local paths. Fails if no active native-profiler-start session exists for the given device_id.`, zodSchema, + artifacts, services: (params) => ({ session: nativeProfilerSessionRef(resolveDevice(params.device_id)), }), diff --git a/packages/tool-server/src/tools/profiler/react/react-profiler-analyze.ts b/packages/tool-server/src/tools/profiler/react/react-profiler-analyze.ts index 703e173a2..92c182d79 100644 --- a/packages/tool-server/src/tools/profiler/react/react-profiler-analyze.ts +++ b/packages/tool-server/src/tools/profiler/react/react-profiler-analyze.ts @@ -1,6 +1,11 @@ import { z } from "zod"; import { promises as fsPromises } from "fs"; -import { FAILURE_CODES, FailureError, type ToolDefinition } from "@argent/registry"; +import { + FAILURE_CODES, + FailureError, + type ToolDefinition, +} from "@argent/registry"; +import type { ArtifactOutputMap } from "@argent/artifacts"; import { RN_ONLY_TOOL_CAPABILITY } from "../../debugger/debugger-service-ref"; import { type ProfilerSessionPaths, @@ -21,7 +26,24 @@ import { } from "../../../utils/react-profiler/debug/dump"; import { serializeCpuSampleIndex } from "../../../utils/react-profiler/pipeline/00-cpu-correlate"; import { requireArtifacts, type ArtifactHandle } from "../../../artifacts"; -import type { ArtifactStore } from "@argent/registry"; +import type { ArtifactRegistrar } from "@argent/registry"; + +const artifacts = { + report: { + kind: "react-profile-report", + mimeTypes: ["text/markdown"], + }, + cpuProfile: { + kind: "react-profile-cpu", + mimeTypes: ["application/json"], + }, + commits: { + kind: "react-profile-commits", + mimeTypes: ["application/json"], + }, +} satisfies ArtifactOutputMap; + +type ArtifactOutputName = Extract; /** * Register a server-side file path as a downloadable artifact (or pass through @@ -31,10 +53,11 @@ import type { ArtifactStore } from "@argent/registry"; * path it can't open. */ async function fileArtifact( - store: ArtifactStore, - p: string | null | undefined + artifacts: ArtifactRegistrar, + p: string | null | undefined, + outputName: ArtifactOutputName ): Promise { - return p ? store.register(p) : null; + return p ? artifacts.register(outputName, { hostPath: p }) : null; } const annotationSchema = z.object({ @@ -69,7 +92,8 @@ const zodSchema = z.object({ export const reactProfilerAnalyzeTool: ToolDefinition< z.infer, - Record + Record, + typeof artifacts > = { id: "react-profiler-analyze", description: `Analyze stored profiling data and return a markdown performance report. @@ -90,6 +114,7 @@ Fails if react-profiler-stop has not been called or no profiling data is stored. // RN-only: operates on profiler trace files captured via the React DevTools // backend's commit recording, which is not present on Chromium. capability: RN_ONLY_TOOL_CAPABILITY, + artifacts, services: () => ({}), async execute(_services, params, ctx) { const sessionPaths: ProfilerSessionPaths | undefined = getCachedProfilerPaths( @@ -227,13 +252,13 @@ Fails if react-profiler-stop has not been called or no profiling data is stored. const artifacts = requireArtifacts(ctx); const result: Record = { report, - reportFile: await fileArtifact(artifacts, reportFile), + reportFile: await fileArtifact(artifacts, reportFile, "report"), hotCommitsTotal, hotCommitsShown, sessionFiles: { sessionId: sessionPaths.sessionId, - cpuProfile: await fileArtifact(artifacts, sessionPaths.cpuProfilePath), - commits: await fileArtifact(artifacts, sessionPaths.commitsPath), + cpuProfile: await fileArtifact(artifacts, sessionPaths.cpuProfilePath, "cpuProfile"), + commits: await fileArtifact(artifacts, sessionPaths.commitsPath, "commits"), }, }; diff --git a/packages/tool-server/src/tools/screenshot-diff/index.ts b/packages/tool-server/src/tools/screenshot-diff/index.ts index 974cbaa5f..7ef297f0f 100644 --- a/packages/tool-server/src/tools/screenshot-diff/index.ts +++ b/packages/tool-server/src/tools/screenshot-diff/index.ts @@ -11,6 +11,7 @@ import type { ToolCapability, ToolDefinition, } from "@argent/registry"; +import type { ArtifactOutputMap } from "@argent/artifacts"; import { simulatorServerRef, type SimulatorServerApi } from "../../blueprints/simulator-server"; import { resolveDevice } from "../../utils/device-info"; import { httpScreenshot } from "../../utils/simulator-client"; @@ -92,7 +93,21 @@ const fileInputs: FileInputSpec[] = [ { target: "outputDir", path: "${outputDir}", kind: "probe", optional: true }, ]; -export const screenshotDiffTool: ToolDefinition = { +const artifacts = { + diff: { + kind: "screenshot-diff", + mimeTypes: ["image/png"], + }, + contextDiff: { + kind: "screenshot-diff-context", + mimeTypes: ["image/png"], + }, +} satisfies ArtifactOutputMap; + +type ArtifactOutputName = Extract; +type ScreenshotDiffToolContext = ToolContext; + +export const screenshotDiffTool: ToolDefinition = { id: "screenshot-diff", description: `Compare two PNG screenshots and return a compact visual-diff summary. Accepts saved baseline/current PNG paths, or one saved PNG plus one live full-resolution capture from a device. Always provide udid so the simulator-server dependency can be resolved. @@ -105,6 +120,7 @@ Fails if the input sources are invalid, PNG files cannot be read, outputDir cann "compare screenshots png diff visual UI changes UI regression visual regression screenshot diff changed regions text ocr live capture", zodSchema, capability, + artifacts, fileInputs, services: (params): Record => { // Only request the SimulatorServer when a live capture is actually needed. @@ -124,7 +140,7 @@ Fails if the input sources are invalid, PNG files cannot be read, outputDir cann export async function executeScreenshotDiffTool( services: Record, params: Params, - options?: Partial, + options?: Partial, captureScreenshot: CaptureScreenshot = httpScreenshot ): Promise { const outputDir = await resolveOutputDir(params, options); @@ -147,11 +163,17 @@ export async function executeScreenshotDiffTool( return { summary: result.summary, ...(result.diffPath - ? { diffPath: await artifacts.register(result.diffPath, { mimeType: "image/png" }) } + ? { + diffPath: await artifacts.register("diff", { + hostPath: result.diffPath, + mimeType: "image/png", + }), + } : {}), ...(result.contextDiffPath ? { - contextDiffPath: await artifacts.register(result.contextDiffPath, { + contextDiffPath: await artifacts.register("contextDiff", { + hostPath: result.contextDiffPath, mimeType: "image/png", }), } @@ -166,7 +188,10 @@ export async function executeScreenshotDiffTool( * Everything else gets a per-call temp dir; the diff images travel back as * artifacts, so the directory's location no longer matters to the agent. */ -async function resolveOutputDir(params: Params, options?: Partial): Promise { +async function resolveOutputDir( + params: Params, + options?: Partial +): Promise { const probe = options?.fileInputs?.outputDir; if (params.outputDir && (probe === undefined || probe.presentOnHost)) { return params.outputDir; @@ -184,7 +209,7 @@ async function resolveInputPaths( services: Record, params: Params, outputDir: string, - options: Partial | undefined, + options: Partial | undefined, captureScreenshot: CaptureScreenshot ): Promise<{ baselinePath: string; currentPath: string }> { validateInputSources(params); diff --git a/packages/tool-server/src/tools/screenshot/index.ts b/packages/tool-server/src/tools/screenshot/index.ts index f95e92391..03e0958b1 100644 --- a/packages/tool-server/src/tools/screenshot/index.ts +++ b/packages/tool-server/src/tools/screenshot/index.ts @@ -3,7 +3,13 @@ import { promisify } from "node:util"; import * as os from "node:os"; import * as path from "node:path"; import { z } from "zod"; -import type { Registry, ToolCapability, ToolDefinition } from "@argent/registry"; +import type { + Registry, + ToolCapability, + ToolContext, + ToolDefinition, +} from "@argent/registry"; +import type { ArtifactOutputMap } from "@argent/artifacts"; import { simulatorServerRef, type SimulatorServerApi } from "../../blueprints/simulator-server"; import { chromiumCdpRef, type ChromiumCdpApi } from "../../blueprints/chromium-cdp"; import { resolveDevice } from "../../utils/device-info"; @@ -70,6 +76,16 @@ const capability: ToolCapability = { vega: { vvd: true }, }; +const artifacts = { + image: { + kind: "screenshot", + mimeTypes: ["image/png"], + }, +} satisfies ArtifactOutputMap; + +type ArtifactOutputName = Extract; +type ScreenshotToolContext = ToolContext; + /** * tvOS screenshot path. The simulator-server backend does not support tvOS, so * capture via `xcrun simctl io screenshot` instead and (optionally) @@ -117,7 +133,9 @@ export async function tvTargetLongSide(file: string, scale: number): Promise { +export function createScreenshotTool( + registry: Registry +): ToolDefinition { return { id: "screenshot", description: `Capture a screenshot of the device screen (iOS simulator, Android emulator, Apple TV simulator, Vega, or Chromium app). Returns { image }; the MCP adapter renders it as a visible image unless the caller passed includeImageInContext: false. @@ -127,12 +145,13 @@ Fails if the simulator-server / emulator backend / Chromium CDP is not reachable searchHint: "device simulator emulator chromium screen image capture baseline tvos apple tv", zodSchema, outputHint: "image", + artifacts, capability, // No eager service: a tvOS udid classifies as iOS by shape, and declaring // simulator-server here would spawn it for the tvOS device (which it cannot // drive) and hang on the ready timeout. Resolve the backend lazily instead. services: () => ({}), - async execute(_services, params, ctx) { + async execute(_services, params, ctx?: ScreenshotToolContext) { const signal = ctx?.signal ?? AbortSignal.timeout(16_000); const scale = params.scale ?? getScreenshotScale(); const device = resolveDevice(params.udid); @@ -146,7 +165,10 @@ Fails if the simulator-server / emulator backend / Chromium CDP is not reachable scale: params.scale, downscaler: params.downscaler, }); - const image = await requireArtifacts(ctx).register(capturedPath, { mimeType: "image/png" }); + const image = await requireArtifacts(ctx).register("image", { + hostPath: capturedPath, + mimeType: "image/png", + }); return { image }; } @@ -154,7 +176,10 @@ Fails if the simulator-server / emulator backend / Chromium CDP is not reachable // tvOS has no simulator-server backend, so capture via xcrun instead. if (device.platform === "ios" && (await isTvOsSimulator(params.udid))) { const pngPath = await tvScreenshot(params.udid, scale, signal); - const image = await requireArtifacts(ctx).register(pngPath, { mimeType: "image/png" }); + const image = await requireArtifacts(ctx).register("image", { + hostPath: pngPath, + mimeType: "image/png", + }); return { image }; } @@ -163,7 +188,10 @@ Fails if the simulator-server / emulator backend / Chromium CDP is not reachable // Vega device would throw), so capture directly here. if (device.platform === "vega") { const pngPath = await captureVegaScreenshotPng({ scale: params.scale }); - const image = await requireArtifacts(ctx).register(pngPath, { mimeType: "image/png" }); + const image = await requireArtifacts(ctx).register("image", { + hostPath: pngPath, + mimeType: "image/png", + }); return { image }; } @@ -175,7 +203,10 @@ Fails if the simulator-server / emulator backend / Chromium CDP is not reachable signal, params.scale ); - const image = await requireArtifacts(ctx).register(capturedPath, { mimeType: "image/png" }); + const image = await requireArtifacts(ctx).register("image", { + hostPath: capturedPath, + mimeType: "image/png", + }); return { image }; }, }; diff --git a/packages/tool-server/src/utils/cross-platform-tool.ts b/packages/tool-server/src/utils/cross-platform-tool.ts index fa8acc201..38898895c 100644 --- a/packages/tool-server/src/utils/cross-platform-tool.ts +++ b/packages/tool-server/src/utils/cross-platform-tool.ts @@ -22,7 +22,12 @@ import { ensureDeps } from "./check-deps"; * *every* invocation regardless of which platform branch fires (rare — usually * only true for analysis / no-device tools). */ -export interface PlatformImpl { +export interface PlatformImpl< + Services, + Params, + Result, + Context extends InvokeToolOptions = InvokeToolOptions, +> { /** Host binaries this branch needs. Probed via `ensureDeps` before `handler` runs. */ requires?: ToolDependency[]; /** Implementation function. Receives typed services, params, the resolved device, and invoke options. */ @@ -30,7 +35,7 @@ export interface PlatformImpl { services: Services, params: Params, device: DeviceInfo, - options?: InvokeToolOptions + options?: Context ) => Promise; } @@ -60,30 +65,31 @@ export function dispatchByPlatform< ChromiumServices = Record, VegaServices = unknown, IosRemoteServices = IosServices, + Context extends InvokeToolOptions = InvokeToolOptions, >(opts: { toolId: string; capability: ToolCapability; - ios: PlatformImpl; - android: PlatformImpl; + ios: PlatformImpl; + android: PlatformImpl; /** * Optional ios-remote branch. When omitted, an ios-remote device will hit * `assertSupported` and fail there if the tool's capability matrix doesn't * include `appleRemote` — so adding ios-remote support is two changes (this * branch + the matrix), and the absence of either is a clean 400. */ - iosRemote?: PlatformImpl; - chromium?: PlatformImpl; + iosRemote?: PlatformImpl; + chromium?: PlatformImpl; /** * Vega (Fire TV) branch. Optional so existing iOS/Android-only tools compile * unchanged. When a tool's capability declares `vega` support but no `vega` * branch is wired here, a Vega device dispatch throws * `NotImplementedOnPlatformError` (501) rather than silently falling through. */ - vega?: PlatformImpl; + vega?: PlatformImpl; }): ( services: Record, params: Params, - options?: InvokeToolOptions + options?: Context ) => Promise { return async (services, params, invokeOptions) => { const device = resolveDevice(params.udid); diff --git a/packages/tool-server/test/android-perfetto/dispatch.test.ts b/packages/tool-server/test/android-perfetto/dispatch.test.ts index 7a474a0a7..49cb29bfc 100644 --- a/packages/tool-server/test/android-perfetto/dispatch.test.ts +++ b/packages/tool-server/test/android-perfetto/dispatch.test.ts @@ -1,9 +1,9 @@ import { describe, it, expect, vi } from "vitest"; -import { ArtifactStore } from "@argent/registry"; import { nativeProfilerSessionBlueprint, type NativeProfilerSessionApi, } from "../../src/blueprints/native-profiler-session"; +import { artifactContext } from "../artifact-context"; // Mock the platform impl modules so we never shell out to xctrace / adb. We // only care that the dispatch picks the correct branch given an api.platform. @@ -96,12 +96,13 @@ describe("native-profiler-* dispatch by session platform", () => { { device_id: "emulator-5554", }, - { artifacts: new ArtifactStore() } + artifactContext(nativeProfilerStopTool) ); // The stop tool wraps the platform's raw path into a downloadable artifact; // the filename still proves it routed through the Android (.pftrace) path. expect(stop.traceFile).toMatchObject({ __argentArtifact: true, + kind: "native-profile-trace", archive: "tar.gz", filename: "android.pftrace", }); diff --git a/packages/tool-server/test/android-perfetto/native-profiler-freshness-e2e.test.ts b/packages/tool-server/test/android-perfetto/native-profiler-freshness-e2e.test.ts index 947c6ecc5..79cd4205f 100644 --- a/packages/tool-server/test/android-perfetto/native-profiler-freshness-e2e.test.ts +++ b/packages/tool-server/test/android-perfetto/native-profiler-freshness-e2e.test.ts @@ -24,10 +24,10 @@ * cases (missing field, NaN, Infinity). */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { ArtifactStore } from "@argent/registry"; import { mkdtemp, rm, writeFile, readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { artifactContext } from "../artifact-context"; // Stub ONLY the Perfetto engine boundary. The pipeline, render, and the entire // freshness chain run for real against the empty .pftrace fixture. @@ -170,7 +170,7 @@ describe("native-profiler freshness flagging — real analyze/render path", () = const result = await nativeProfilerAnalyzeTool.execute( { session: api } as never, { device_id: SERIAL }, - { artifacts: new ArtifactStore() } as never + artifactContext(nativeProfilerAnalyzeTool) as never ); return result.report; } @@ -250,7 +250,7 @@ describe("native-profiler freshness flagging — real analyze/render path", () = const result = await nativeProfilerAnalyzeTool.execute( { session: api } as never, { device_id: SERIAL }, - { artifacts: new ArtifactStore() } as never + artifactContext(nativeProfilerAnalyzeTool) as never ); console.log( `[freshness E2E] wallClockStartMs=NaN → ${staleLine(result.report) ?? "(no stale flag — guarded)"}` @@ -263,7 +263,7 @@ describe("native-profiler freshness flagging — real analyze/render path", () = const result2 = await nativeProfilerAnalyzeTool.execute( { session: api } as never, { device_id: SERIAL }, - { artifacts: new ArtifactStore() } as never + artifactContext(nativeProfilerAnalyzeTool) as never ); console.log( `[freshness E2E] wallClockStartMs=Infinity → ${staleLine(result2.report) ?? "(no stale flag — guarded)"}` diff --git a/packages/tool-server/test/artifact-context.ts b/packages/tool-server/test/artifact-context.ts new file mode 100644 index 000000000..09e01aee8 --- /dev/null +++ b/packages/tool-server/test/artifact-context.ts @@ -0,0 +1,15 @@ +import { + ArtifactStore, + createArtifactRegistrar, + type ToolContext, +} from "@argent/registry"; +import type { ArtifactOutputMap } from "@argent/artifacts"; + +export function artifactContext(tool: { + id: string; + artifacts?: ArtifactOutputMap; +}): ToolContext { + return { + artifacts: createArtifactRegistrar(new ArtifactStore(), tool.artifacts, tool.id), + }; +} diff --git a/packages/tool-server/test/artifacts.test.ts b/packages/tool-server/test/artifacts.test.ts index c8d4dae7a..8e8117b49 100644 --- a/packages/tool-server/test/artifacts.test.ts +++ b/packages/tool-server/test/artifacts.test.ts @@ -51,8 +51,9 @@ describe("ArtifactStore", () => { const filePath = join(dir, "shot.png"); await writeFile(filePath, Buffer.from([1, 2, 3, 4])); - const handle = await new ArtifactStore().register(filePath); + const handle = await new ArtifactStore().register({ hostPath: filePath, kind: "screenshot" }); expect(handle.__argentArtifact).toBe(true); + expect(handle.kind).toBe("screenshot"); expect(handle.filename).toBe("shot.png"); expect(handle.mimeType).toBe("image/png"); expect(handle.size).toBe(4); @@ -70,10 +71,13 @@ describe("ArtifactStore", () => { try { const filePath = join(dir, "raw.bin"); await writeFile(filePath, "hi"); - const handle = await new ArtifactStore().register(filePath, { + const handle = await new ArtifactStore().register({ + hostPath: filePath, + kind: "screenshot", filename: "pretty.png", mimeType: "image/png", }); + expect(handle.kind).toBe("screenshot"); expect(handle.filename).toBe("pretty.png"); expect(handle.mimeType).toBe("image/png"); } finally { @@ -88,7 +92,11 @@ describe("ArtifactStore", () => { await mkdir(bundle, { recursive: true }); await writeFile(join(bundle, "data.bin"), "trace"); - const handle = await new ArtifactStore().register(bundle); + const handle = await new ArtifactStore().register({ + hostPath: bundle, + kind: "native-profile-trace", + }); + expect(handle.kind).toBe("native-profile-trace"); expect(handle.archive).toBe("tar.gz"); expect(handle.filename).toBe("session.trace"); expect(handle.hostPath).toBe(bundle); @@ -98,9 +106,12 @@ describe("ArtifactStore", () => { }); it("honours an explicit archive option when the path can't be stat'd", async () => { - const handle = await new ArtifactStore().register("/tmp/does-not-exist-yet.trace", { + const handle = await new ArtifactStore().register({ + hostPath: "/tmp/does-not-exist-yet.trace", + kind: "native-profile-trace", archive: "tar.gz", }); + expect(handle.kind).toBe("native-profile-trace"); expect(handle.archive).toBe("tar.gz"); }); @@ -110,11 +121,16 @@ describe("ArtifactStore", () => { const filePath = join(dir, "shot.png"); await writeFile(filePath, "png"); const store = new ArtifactStore(); - const handle = await store.register(filePath, { mimeType: "image/png" }); + const handle = await store.register({ + hostPath: filePath, + kind: "screenshot", + mimeType: "image/png", + }); expect(store.list()).toEqual([ { id: handle.id, + kind: "screenshot", filename: "shot.png", mimeType: "image/png", size: 3, @@ -153,7 +169,11 @@ describe("GET /artifacts", () => { const filePath = join(dir, "img.png"); await writeFile(filePath, "image"); const registry = stubRegistry(); - const artifact = await registry.artifacts.register(filePath, { mimeType: "image/png" }); + const artifact = await registry.artifacts.register({ + hostPath: filePath, + kind: "screenshot", + mimeType: "image/png", + }); isFlagEnabledMock.mockReturnValue(true); handle = createHttpApp(registry); @@ -164,6 +184,7 @@ describe("GET /artifacts", () => { artifacts: [ { id: artifact.id, + kind: "screenshot", filename: "img.png", mimeType: "image/png", size: 5, @@ -193,7 +214,11 @@ describe("GET /artifacts/:id", () => { const bytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0xaa, 0xbb]); await writeFile(filePath, bytes); const registry = stubRegistry(); - const artifact = await registry.artifacts.register(filePath, { mimeType: "image/png" }); + const artifact = await registry.artifacts.register({ + hostPath: filePath, + kind: "screenshot", + mimeType: "image/png", + }); handle = createHttpApp(registry); const res = await supertest(handle.app).get(`/artifacts/${artifact.id}`); @@ -215,7 +240,10 @@ describe("GET /artifacts/:id", () => { await writeFile(join(bundle, "sub", "nested.txt"), "nested"); const registry = stubRegistry(); - const artifact = await registry.artifacts.register(bundle); + const artifact = await registry.artifacts.register({ + hostPath: bundle, + kind: "native-profile-trace", + }); handle = createHttpApp(registry); const res = await supertest(handle.app) .get(`/artifacts/${artifact.id}`) @@ -248,7 +276,7 @@ describe("GET /artifacts/:id", () => { const filePath = join(dir, "gone.png"); await writeFile(filePath, "x"); const registry = stubRegistry(); - const artifact = await registry.artifacts.register(filePath); + const artifact = await registry.artifacts.register({ hostPath: filePath, kind: "screenshot" }); await rm(dir, { recursive: true, force: true }); handle = createHttpApp(registry); diff --git a/packages/tool-server/test/flows/flow-remote-recording.test.ts b/packages/tool-server/test/flows/flow-remote-recording.test.ts index 1d0f7ce23..35bf45da8 100644 --- a/packages/tool-server/test/flows/flow-remote-recording.test.ts +++ b/packages/tool-server/test/flows/flow-remote-recording.test.ts @@ -3,7 +3,7 @@ import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import type { Registry, ToolContext } from "@argent/registry"; -import { ArtifactStore, CLIENT_FILE_MARKER } from "@argent/registry"; +import { ArtifactStore, CLIENT_FILE_MARKER, createArtifactRegistrar } from "@argent/registry"; import { flowStartRecordingTool } from "../../src/tools/flows/flow-start-recording"; import { flowInsertEchoTool } from "../../src/tools/flows/flow-insert-echo"; @@ -30,7 +30,7 @@ const CLIENT_FLOW_PATH = path.join(CLIENT_ROOT, ".argent", "flows", "remote-flow function remoteCtx(): ToolContext { return { - artifacts: new ArtifactStore(), + artifacts: createArtifactRegistrar(new ArtifactStore(), {}, "test"), fileInputs: { project_root: { clientPath: CLIENT_ROOT, presentOnHost: false, viaUpload: false }, }, diff --git a/packages/tool-server/test/ios-instruments/stop-recovery.test.ts b/packages/tool-server/test/ios-instruments/stop-recovery.test.ts index f035a19d5..01242be08 100644 --- a/packages/tool-server/test/ios-instruments/stop-recovery.test.ts +++ b/packages/tool-server/test/ios-instruments/stop-recovery.test.ts @@ -25,10 +25,10 @@ import { type IosStopArtifacts, } from "../../src/tools/profiler/native-profiler/native-profiler-stop"; import { handleXctraceExit } from "../../src/tools/profiler/native-profiler/native-profiler-start"; -import { ArtifactStore } from "@argent/registry"; +import { artifactContext } from "../artifact-context"; // Tool context the registry would inject; stop registers its trace/exports here. -const STOP_CTX = { artifacts: new ArtifactStore() }; +const STOP_CTX = artifactContext(nativeProfilerStopTool); const mockedExport = vi.mocked(exportIosTraceData); @@ -137,15 +137,21 @@ describe("native-profiler-stop recovery branch", () => { expect(mockedExport).toHaveBeenCalledWith(FAKE_TRACE); // exportedFiles are now artifact handles (materialized client-side), not raw paths. - expect(result.exportedFiles.cpu).toMatchObject({ __argentArtifact: true, filename: "cpu.xml" }); + expect(result.exportedFiles.cpu).toMatchObject({ + __argentArtifact: true, + kind: "native-profile-cpu", + filename: "cpu.xml", + }); expect(result.exportedFiles.hangs).toMatchObject({ __argentArtifact: true, + kind: "native-profile-hangs", filename: "hangs.xml", }); // The .trace bundle is now a downloadable artifact (delivered as tar.gz // on demand), not a "stays on the host" note. expect(result.traceFile).toMatchObject({ __argentArtifact: true, + kind: "native-profile-trace", archive: "tar.gz", filename: "ios-profiler-20260101-000000.trace", }); @@ -241,7 +247,10 @@ describe("native-profiler-stop recovery branch", () => { const result = await promise; expect(result.warning).toBeUndefined(); - expect(result.exportedFiles.cpu).toMatchObject({ __argentArtifact: true }); + expect(result.exportedFiles.cpu).toMatchObject({ + __argentArtifact: true, + kind: "native-profile-cpu", + }); expect(api.profilingActive).toBe(false); expect(api.captureProcess).toBeNull(); expect(api.recordingExitedUnexpectedly).toBe(false); diff --git a/packages/tool-server/test/native-profiler-analyze-failure.test.ts b/packages/tool-server/test/native-profiler-analyze-failure.test.ts index fd1954f8a..06990c505 100644 --- a/packages/tool-server/test/native-profiler-analyze-failure.test.ts +++ b/packages/tool-server/test/native-profiler-analyze-failure.test.ts @@ -11,10 +11,10 @@ * (empty-but-readable) file so the existence guard passes. */ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { ArtifactStore } from "@argent/registry"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { artifactContext } from "./artifact-context"; // Mocking runTpQuery is enough to control the entire Android pipeline output — // `runAndroidProfilerPipeline` is just `Promise.allSettled` over three calls @@ -145,7 +145,7 @@ describe("native-profiler-analyze: status + exportErrors envelope (Bug 4)", () = const result = await nativeProfilerAnalyzeTool.execute( { session }, { device_id: "emulator-5554" }, - { artifacts: new ArtifactStore() } + artifactContext(nativeProfilerAnalyzeTool) ); expect(result.status).toBe("ok"); @@ -179,7 +179,7 @@ describe("native-profiler-analyze: status + exportErrors envelope (Bug 4)", () = const result = await nativeProfilerAnalyzeTool.execute( { session }, { device_id: "emulator-5554" }, - { artifacts: new ArtifactStore() } + artifactContext(nativeProfilerAnalyzeTool) ); expect(result.status).toBe("analysis_failed"); @@ -230,7 +230,7 @@ describe("native-profiler-analyze: status + exportErrors envelope (Bug 4)", () = const result = await nativeProfilerAnalyzeTool.execute( { session }, { device_id: "emulator-5554" }, - { artifacts: new ArtifactStore() } + artifactContext(nativeProfilerAnalyzeTool) ); expect(result.status).toBe("analysis_failed"); @@ -267,7 +267,7 @@ describe("native-profiler-analyze: status + exportErrors envelope (Bug 4)", () = const result = await nativeProfilerAnalyzeTool.execute( { session }, { device_id: "emulator-5554" }, - { artifacts: new ArtifactStore() } + artifactContext(nativeProfilerAnalyzeTool) ); expect(result.status).toBe("analysis_failed"); diff --git a/packages/tool-server/test/native-profiler-missing-trace.test.ts b/packages/tool-server/test/native-profiler-missing-trace.test.ts index 7da4f7a2a..d8e2a5dc9 100644 --- a/packages/tool-server/test/native-profiler-missing-trace.test.ts +++ b/packages/tool-server/test/native-profiler-missing-trace.test.ts @@ -10,10 +10,10 @@ * has no findings" and surface the export failure via `exportErrors`. */ import { describe, it, expect, vi } from "vitest"; -import { ArtifactStore } from "@argent/registry"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { artifactContext } from "./artifact-context"; // The analyze tool gates the iOS path behind `ensureDeps(["xcrun"])`, but // analyzing an already-exported trace is pure XML parsing — it never shells out @@ -73,7 +73,7 @@ describe("native-profiler-analyze: missing trace file", () => { const result = await nativeProfilerAnalyzeTool.execute( { session }, { device_id: "TEST-DEVICE" }, - { artifacts: new ArtifactStore() } + artifactContext(nativeProfilerAnalyzeTool) ); // Bug: previously this rendered "All clear" with no warning because diff --git a/packages/tool-server/test/screenshot-diff-tool.test.ts b/packages/tool-server/test/screenshot-diff-tool.test.ts index 3d0a8ea82..8f1e01d7f 100644 --- a/packages/tool-server/test/screenshot-diff-tool.test.ts +++ b/packages/tool-server/test/screenshot-diff-tool.test.ts @@ -3,8 +3,8 @@ import os from "os"; import path from "path"; import { PNG } from "pngjs"; import { describe, expect, it, vi } from "vitest"; -import { ArtifactStore } from "@argent/registry"; import { executeScreenshotDiffTool, screenshotDiffTool } from "../src/tools/screenshot-diff"; +import { artifactContext } from "./artifact-context"; describe("screenshotDiffTool", () => { it("rejects public tuning options so defaults stay internal", () => { @@ -80,7 +80,7 @@ describe("screenshotDiffTool", () => { udid: "ABC", outputDir: dir, }, - { artifacts: new ArtifactStore() } + artifactContext(screenshotDiffTool) ); // Diff outputs leave as artifact handles so a remote client can download @@ -88,11 +88,13 @@ describe("screenshotDiffTool", () => { expect(result.summary).toContain("Screenshot diff summary"); expect(result.diffPath).toMatchObject({ __argentArtifact: true, + kind: "screenshot-diff", hostPath: path.join(dir, "current-diff.png"), mimeType: "image/png", }); expect(result.contextDiffPath).toMatchObject({ __argentArtifact: true, + kind: "screenshot-diff-context", hostPath: path.join(dir, "current-context-diff.png"), mimeType: "image/png", }); @@ -120,7 +122,7 @@ describe("screenshotDiffTool", () => { rotation: "LandscapeLeft", outputDir: dir, }, - { signal, artifacts: new ArtifactStore() }, + { ...artifactContext(screenshotDiffTool), signal }, captureScreenshot as never ); @@ -163,7 +165,7 @@ describe("screenshotDiffTool", () => { const result = await executeScreenshotDiffTool( { simulatorServer: { apiUrl: "http://localhost:4949" } }, { baselinePath, captureCurrent: true, udid: "ABC", outputDir: dir }, - { artifacts: new ArtifactStore() }, + artifactContext(screenshotDiffTool), captureScreenshot as never ); @@ -213,13 +215,13 @@ describe("screenshotDiffTool", () => { await executeScreenshotDiffTool( { simulatorServer: { apiUrl: "http://localhost:4949" } }, { baselinePath, captureCurrent: true, udid: "ABC", outputDir: dir }, - { artifacts: new ArtifactStore() }, + artifactContext(screenshotDiffTool), captureScreenshot as never ); await executeScreenshotDiffTool( { simulatorServer: { apiUrl: "http://localhost:4949" } }, { baselinePath, captureCurrent: true, udid: "ABC", outputDir: dir }, - { artifacts: new ArtifactStore() }, + artifactContext(screenshotDiffTool), captureScreenshot as never ); diff --git a/packages/tool-server/test/screenshot-tool.test.ts b/packages/tool-server/test/screenshot-tool.test.ts index be2b90054..9694759dc 100644 --- a/packages/tool-server/test/screenshot-tool.test.ts +++ b/packages/tool-server/test/screenshot-tool.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { ArtifactStore } from "@argent/registry"; import { createScreenshotTool } from "../src/tools/screenshot"; +import { artifactContext } from "./artifact-context"; describe("screenshot tool", () => { afterEach(() => { @@ -35,12 +35,13 @@ describe("screenshot tool", () => { }; screenshotTool.zodSchema!.parse(params); - const result = await screenshotTool.execute({}, params, { artifacts: new ArtifactStore() }); + const result = await screenshotTool.execute({}, params, artifactContext(screenshotTool)); // The PNG is returned as an artifact handle the MCP client materializes — // the unreachable `127.0.0.1` media URL is no longer surfaced. expect(result.image).toMatchObject({ __argentArtifact: true, + kind: "screenshot", filename: "screenshot.png", mimeType: "image/png", hostPath: "/tmp/screenshot.png", diff --git a/tsconfig.json b/tsconfig.json index 6efcef609..48774c790 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "files": [], "references": [ + { "path": "packages/artifacts" }, { "path": "packages/registry" }, { "path": "packages/telemetry" }, { "path": "packages/update-core" },