From 084e487943af5e5d8b3671662e524155cafb1d38 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Sat, 20 Jun 2026 19:52:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20artifacts=20?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20(#1278)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 新增 cloud-artifacts 包(Cloudflare Worker HTML artifact 托管) POST /upload 鉴权上传 HTML 到 R2 返回 hash URL,GET /<7d|30d>/.html 由 Worker 代理读取并直出 text/html。R2 lifecycle rule 自动 7/30 天删除。 独立服务,不被主 CLI 引用(类似 packages/remote-control-server/ 定位)。 Co-Authored-By: glm-5.2 * docs: 完善 cloud-artifacts 文档并统一出口域名 - CLAUDE.md 加 cloud-artifacts 到 Workspace Packages 表和新增 HTML Artifact Hosting 段落 - docs.json 注册 cloud-artifacts 到运行模式 group - README 加 Quickstart、架构图(含 Deno Deploy 代理层)、Security Considerations、Troubleshooting - 统一出口域名为 https://cloud-artifacts.claude-code-best.win(wrangler.toml PUBLIC_URL、test.sh 默认 WORKER_URL、所有文档示例) - test.sh expect() 加 [via body] fallback:经 Deno Deploy 代理(status 抹平为 200)时按 body 的 error 字段断言 Co-Authored-By: glm-5.2 * docs: 修正 CLAUDE.md cloud-artifacts 引用死链 之前指向不存在的 docs/features/cloud-artifacts.md(用户未注册到 docs.json), 改为指向已存在的 packages/cloud-artifacts/README.md,并补充生产出口域名 与 Deno Deploy status 抹平副作用的说明。 Co-Authored-By: glm-5.2 * chore: 同步 cloud-artifacts 测试默认 TOKEN 到新值 用户已通过 wrangler secret put 把生产 TOKEN 改为 claude-code-best, test.sh 的默认值(之前用旧 token 作 fallback)和注释示例同步更新。 现在直接 bash scripts/test.sh 即可跑通(无需显式传 TOKEN)。 src/index.ts 不依赖具体 token 值(只读 env.TOKEN 做比较), wrangler.toml 不含 secret,README/.dev.vars.example 用 占位符故无需改。 Co-Authored-By: glm-5.2 * docs: add artifacts feature implementation plan * feat(artifact): add cloud-artifacts config with token/URL defaults Co-Authored-By: glm-5.2 * feat(artifact): add HTTP client with body-error parsing Co-Authored-By: glm-5.2 * feat(artifact): add tool name, description, and prompt Co-Authored-By: glm-5.2 * feat(artifact): add buildTool definition with file validation Co-Authored-By: glm-5.2 * test(artifact): add end-to-end tool tests for upload/error paths Co-Authored-By: glm-5.2 * feat(artifact): export ArtifactTool from builtin-tools barrel Co-Authored-By: glm-5.2 * feat(artifact): register ArtifactTool in tools list Co-Authored-By: glm-5.2 * feat(artifact): add /use-artifacts bundled skill Co-Authored-By: glm-5.2 * feat(artifact): add extractArtifacts message scanner Scans Message[] for artifact tool_use/tool_result pairs, parses URL/id/expires from the upload response string, and returns ArtifactInfo[] newest-first. Co-Authored-By: glm-5.2 * fix(artifact): scanner type narrowing and url regex - Use double assertion (`as unknown as Record`) at lines 30 and 90 to fix TS2352 per project convention - Tighten URL_REGEX to avoid capturing trailing punctuation (parens, quotes, commas) when URL is embedded in text - Add test case for array-form tool_result content path Co-Authored-By: glm-5.2 * feat(artifact): add ArtifactsMenu Ink component Co-Authored-By: glm-5.2 * feat(artifact): add /artifacts slash command entry Co-Authored-By: glm-5.2 * feat(artifact): register /artifacts command Co-Authored-By: glm-5.2 * fix(artifact): use setClipboard instead of pbcopy for cross-platform support * fix(artifact): drop userFacingName override so display matches /artifacts * fix(rcs): add resJson helper to resolve strict mode type errors in tests Hono Response.json() returns Promise under strict TypeScript, causing 121 TS errors across middleware and routes test files. Co-Authored-By: glm-5-turbo * fix(cloud-artifacts): add type stubs so tsc passes without worker-configuration.d.ts The wrangler-generated worker-configuration.d.ts is gitignored, causing CI to fail with missing ExportedHandler/Env/R2Bucket types. This file provides minimal stubs for all Cloudflare Workers types used by the artifact upload Worker. Co-Authored-By: glm-5-turbo * fix(query): shallow-copy messages before stripping toolUseResult Previously the per-query cleanup mutated messagesForQuery entries in place via `delete msg.toolUseResult`. Those entries are references shared with mutableMessages (UI state), so the delete stripped the field from the live message object. The next query can start within milliseconds of tool_result creation — before the React UI commit lands — so UserToolSuccessMessage's `!message.toolUseResult` check returned null and tool.renderToolResultMessage was never called, leaving tool-result rows blank. Map to a stripped copy instead so mutableMessages keeps the original for the UI. Downstream API transformations (applyToolResultBudget, snip, microcompact) already build new arrays via .map(), so they compose cleanly with this copy. Co-Authored-By: glm-5.2 * feat(artifact): show uploaded URL inline below ExecuteExtraTool Deferred tools (shouldDefer: true) are invoked via SearchExtraTools → ExecuteExtraTool, so their tool_result rows used to render blank — the UI looked up ExecuteExtraTool, which had no renderToolResultMessage, and returned null. Add a generic delegation in ExecuteTool that forwards renderToolResultMessage to the inner tool when it defines one, unwrapping the { result, tool_name } envelope and the params from the input shape. All 28 deferred tools can now render their own UI by defining renderToolResultMessage. For ArtifactTool specifically, render the uploaded URL as an OSC 8 hyperlink (Link component) in warning color so it's visually prominent, with the expiry timestamp on a second line and a separate error branch. Also add `error: z.string().optional()` to outputSchema — zod's default strip mode was dropping the field, so error states never reached the UI. Co-Authored-By: glm-5.2 * fix(cloud-artifacts): make Env stubs actually take effect in CI The previous stub file (2e29e362) wrapped `interface Env` in `declare global { ... }`, but the file has no top-level import/export so it's a script, not a module. TS2669 forbids `declare global` in scripts, and in .d.ts files that error is silently swallowed — so the Env stubs were never merged into the global scope. Locally typecheck passed only because worker-configuration.d.ts (gitignored) provided Env separately; in CI / fresh clones, `BUCKET`, `MAX_BYTES`, `DEFAULT_TTL_DAYS`, `PUBLIC_URL` were all missing on Env. Drop the wrapper. Top-level `interface Env` in a script .d.ts is already global ambient and merges with worker-configuration.d.ts via interface declaration merging, so both environments typecheck cleanly. Co-Authored-By: glm-5.2 --------- Co-authored-by: glm-5.2 --- CLAUDE.md | 5 + bun.lock | 125 +- .../plans/2026-06-20-artifacts-feature.md | 1325 +++++++++++++++++ packages/builtin-tools/src/index.ts | 1 + .../src/tools/ArtifactTool/ArtifactTool.ts | 177 +++ .../src/tools/ArtifactTool/UI.tsx | 37 + .../__tests__/ArtifactTool.test.ts | 112 ++ .../tools/ArtifactTool/__tests__/UI.test.tsx | 70 + .../ArtifactTool/__tests__/client.test.ts | 109 ++ .../src/tools/ArtifactTool/client.ts | 59 + .../src/tools/ArtifactTool/config.ts | 21 + .../src/tools/ArtifactTool/prompt.ts | 25 + .../src/tools/ExecuteTool/ExecuteTool.ts | 25 + .../__tests__/ExecuteTool.render.test.ts | 167 +++ packages/cloud-artifacts/.dev.vars.example | 1 + packages/cloud-artifacts/.gitignore | 171 +++ packages/cloud-artifacts/README.md | 202 +++ packages/cloud-artifacts/package.json | 19 + packages/cloud-artifacts/scripts/setup.sh | 30 + packages/cloud-artifacts/scripts/test.sh | 162 ++ packages/cloud-artifacts/src/index.ts | 119 ++ packages/cloud-artifacts/src/types.d.ts | 104 ++ packages/cloud-artifacts/tsconfig.json | 17 + packages/cloud-artifacts/wrangler.toml | 16 + .../src/__tests__/middleware.test.ts | 27 +- .../src/__tests__/routes.test.ts | 183 +-- src/commands.ts | 2 + src/commands/artifacts/ArtifactsMenu.tsx | 94 ++ .../artifacts/__tests__/scanner.test.ts | 158 ++ src/commands/artifacts/artifacts.tsx | 11 + src/commands/artifacts/index.ts | 12 + src/commands/artifacts/scanner.ts | 97 ++ src/query.ts | 38 +- src/skills/bundled/index.ts | 2 + src/skills/bundled/useArtifacts.ts | 101 ++ src/tools.ts | 2 + 36 files changed, 3713 insertions(+), 113 deletions(-) create mode 100644 docs/superpowers/plans/2026-06-20-artifacts-feature.md create mode 100644 packages/builtin-tools/src/tools/ArtifactTool/ArtifactTool.ts create mode 100644 packages/builtin-tools/src/tools/ArtifactTool/UI.tsx create mode 100644 packages/builtin-tools/src/tools/ArtifactTool/__tests__/ArtifactTool.test.ts create mode 100644 packages/builtin-tools/src/tools/ArtifactTool/__tests__/UI.test.tsx create mode 100644 packages/builtin-tools/src/tools/ArtifactTool/__tests__/client.test.ts create mode 100644 packages/builtin-tools/src/tools/ArtifactTool/client.ts create mode 100644 packages/builtin-tools/src/tools/ArtifactTool/config.ts create mode 100644 packages/builtin-tools/src/tools/ArtifactTool/prompt.ts create mode 100644 packages/builtin-tools/src/tools/ExecuteTool/__tests__/ExecuteTool.render.test.ts create mode 100644 packages/cloud-artifacts/.dev.vars.example create mode 100644 packages/cloud-artifacts/.gitignore create mode 100644 packages/cloud-artifacts/README.md create mode 100644 packages/cloud-artifacts/package.json create mode 100755 packages/cloud-artifacts/scripts/setup.sh create mode 100755 packages/cloud-artifacts/scripts/test.sh create mode 100644 packages/cloud-artifacts/src/index.ts create mode 100644 packages/cloud-artifacts/src/types.d.ts create mode 100644 packages/cloud-artifacts/tsconfig.json create mode 100644 packages/cloud-artifacts/wrangler.toml create mode 100644 src/commands/artifacts/ArtifactsMenu.tsx create mode 100644 src/commands/artifacts/__tests__/scanner.test.ts create mode 100644 src/commands/artifacts/artifacts.tsx create mode 100644 src/commands/artifacts/index.ts create mode 100644 src/commands/artifacts/scanner.ts create mode 100644 src/skills/bundled/useArtifacts.ts diff --git a/CLAUDE.md b/CLAUDE.md index 834711a9d..44ed38bb9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -172,6 +172,7 @@ bun run docs:dev | `packages/acp-link/` | ACP 代理服务器(WebSocket → ACP agent 桥接) | | `packages/mcp-client/` | MCP 客户端库 | | `packages/remote-control-server/` | 自托管 Remote Control Server(Docker 部署,含 Web UI)— Web UI 已重构为 React + Vite + Radix UI,支持 ACP agent 接入 | +| `packages/cloud-artifacts/` | 独立 Cloudflare Worker + R2 服务:POST `/upload` HTML 上传返回 hash URL,GET `/<7d\|30d>/.html` 由 Worker 代理读取;R2 lifecycle rule 自动 7/30 天过期 | | `packages/audio-capture-napi/` | 原生音频捕获(已恢复) | | `packages/color-diff-napi/` | 颜色差异计算(完整实现,11 tests) | | `packages/image-processor-napi/` | 图像处理(已恢复) | @@ -188,6 +189,10 @@ bun run docs:dev - CLI 快速路径: `claude remote-control` / `claude rc` / `claude bridge`。 - 详见 `docs/features/remote-control-self-hosting.md`。 +### HTML Artifact Hosting + +- **`packages/cloud-artifacts/`** — 独立 Cloudflare Worker + R2 服务,类似 `remote-control-server/` 的"独立部署服务"定位,**不被主 CLI import**。Worker 处理 `POST /upload`(Bearer token 鉴权 + text/html 校验 + 10MB 上限 + ttl∈{7,30})和 `GET /<7d|30d>/.html`(从 R2 读 + Cache-Control: max-age=86400)。R2 用 prefix + lifecycle rule 实现 TTL(`7d/` 删 7 天、`30d/` 删 30 天),Worker 不参与过期处理。ID 默认 `nanoid(21)`(126 bit 熵),可指定 `?hash=` 自定义 ID(覆盖语义:先删 7d/30d prefix 旧 key 再写新 key)。Worker 用 `wrangler types` 生成的全局 `Env` 类型(`worker-configuration.d.ts`,已 gitignore),不依赖 `@cloudflare/workers-types`。部署用 `npm create cloudflare@latest` 初始化 + `bun run setup`(创建 bucket + lifecycle + secret)+ `bun run deploy`。生产出口经 Deno Deploy 边缘代理(`https://cloud-artifacts.claude-code-best.win`),副作用是 HTTP status code 被抹平为 200(body 的 `{error}` 字段仍保留)。详见 `packages/cloud-artifacts/README.md`。 + ### ACP Protocol (Agent Client Protocol) - **`src/services/acp/`** — ACP agent 实现,包含 `agent.ts`(AcpAgent 类)、`bridge.ts`(Claude Code ↔ ACP 桥接)、`permissions.ts`(权限处理)、`entry.ts`(入口)。 diff --git a/bun.lock b/bun.lock index 6f87914cb..09ba84e2a 100644 --- a/bun.lock +++ b/bun.lock @@ -239,6 +239,17 @@ "@claude-code-best/agent-tools": "workspace:*", }, }, + "packages/cloud-artifacts": { + "name": "cloud-artifacts", + "version": "0.0.0", + "dependencies": { + "nanoid": "^5.0.0", + }, + "devDependencies": { + "typescript": "^6.0.0", + "wrangler": "^4.0.0", + }, + }, "packages/color-diff-napi": { "name": "color-diff-napi", "version": "1.0.0", @@ -599,8 +610,26 @@ "@claude-code-best/workflow-engine": ["@claude-code-best/workflow-engine@workspace:packages/workflow-engine"], + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], + + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], + + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260617.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-jWwmgEVVWbsHNrLSNXzwjJaH90VzRxq1cWkQFUidxyeUPnMxemeNE8I9qFAfrpzGgE11e9sKDcE3ettJW08swQ=="], + + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260617.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LHH7b565g9znfCUOkwbec6FG2rmRbsgCy6aJiU9KN662mNheWl5sw/iKleiFSiljPKQQP3HkjnC/NSkdgi/aSA=="], + + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260617.1", "", { "os": "linux", "cpu": "x64" }, "sha512-FMnaAKXe4Cfd8TQurCVd9fs2XQVBFRCsP+Id/SRdUv89MlwYu9zXfoyx6BxM+brPTIUK38SHbo8iaxiwzLi9JQ=="], + + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260617.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MRoifFYcqbxxIIQy7PqO5tFY/qPFSnjXzakWl0sO93l+HLyG35jRAgOi6jfqa4kBxc7gKKtH861DcewjxUfkjA=="], + + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260617.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rgBV9wQrv0OSKgCTTbhFUFY3sLGNANZ88aqaLvtmEn2gmbFVb1J4PDGochVUdB7NSEp4D/ghHva6/8SZmbONpw=="], + + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260620.1", "", {}, "sha512-WB81w9u1bAS7KcekpC7/nYhLpIXAEtgybso7XgGJV8CQKNkNPYcyjvICLdghOlDBi/9Ivk+f7NRckV2Bkq1bDg=="], + "@commander-js/extra-typings": ["@commander-js/extra-typings@14.0.0", "https://registry.npmmirror.com/@commander-js/extra-typings/-/extra-typings-14.0.0.tgz", { "peerDependencies": { "commander": "~14.0.0" } }, "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg=="], + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + "@emnapi/core": ["@emnapi/core@1.9.2", "https://registry.npmmirror.com/@emnapi/core/-/core-1.9.2.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.9.2.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], @@ -1001,6 +1030,12 @@ "@pondwader/socks5-server": ["@pondwader/socks5-server@1.0.10", "https://registry.npmmirror.com/@pondwader/socks5-server/-/socks5-server-1.0.10.tgz", {}, "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg=="], + "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], + + "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], + + "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], + "@prisma/instrumentation": ["@prisma/instrumentation@7.6.0", "https://registry.npmmirror.com/@prisma/instrumentation/-/instrumentation-7.6.0.tgz", { "dependencies": { "@opentelemetry/instrumentation": "^0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ=="], "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], @@ -1249,6 +1284,8 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "https://registry.npmmirror.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "https://registry.npmmirror.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], "@smithy/abort-controller": ["@smithy/abort-controller@2.2.0", "https://registry.npmmirror.com/@smithy/abort-controller/-/abort-controller-2.2.0.tgz", { "dependencies": { "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw=="], @@ -1341,6 +1378,8 @@ "@smithy/uuid": ["@smithy/uuid@1.1.2", "https://registry.npmmirror.com/@smithy/uuid/-/uuid-1.1.2.tgz", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], + "@speed-highlight/core": ["@speed-highlight/core@1.2.17", "", {}, "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@stricli/auto-complete": ["@stricli/auto-complete@1.2.6", "https://registry.npmmirror.com/@stricli/auto-complete/-/auto-complete-1.2.6.tgz", { "dependencies": { "@stricli/core": "^1.2.6" }, "bin": { "auto-complete": "dist/bin/cli.js" } }, "sha512-H7dectwnLBoyDrp4Vek1gTNdUWzqkEDt5X6oFoOPxPVbca5FA9ttBZ/OlfNvt14aeiZUsg1rC7GEHjIh3tjn2A=="], @@ -1589,6 +1628,8 @@ "bignumber.js": ["bignumber.js@9.3.1", "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.3.1.tgz", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + "body-parser": ["body-parser@2.2.2", "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.2.tgz", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "bowser": ["bowser@2.14.1", "https://registry.npmmirror.com/bowser/-/bowser-2.14.1.tgz", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], @@ -1659,6 +1700,8 @@ "cliui": ["cliui@7.0.4", "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], + "cloud-artifacts": ["cloud-artifacts@workspace:packages/cloud-artifacts"], + "clsx": ["clsx@2.1.1", "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "cmdk": ["cmdk@1.1.1", "https://registry.npmmirror.com/cmdk/-/cmdk-1.1.1.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], @@ -1843,6 +1886,8 @@ "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], + "es-define-property": ["es-define-property@1.0.1", "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -2137,6 +2182,8 @@ "khroma": ["khroma@2.1.0", "https://registry.npmmirror.com/khroma/-/khroma-2.1.0.tgz", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "knip": ["knip@6.4.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "get-tsconfig": "4.13.7", "jiti": "^2.6.0", "minimist": "^1.2.8", "oxc-parser": "^0.121.0", "oxc-resolver": "^11.19.1", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.6.1", "strip-json-comments": "5.0.3", "unbash": "^2.2.0", "yaml": "^2.8.2", "zod": "^4.1.11" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-Ry+ywmDFSZvKp/jx7LxMgsZWRTs931alV84e60lh0Stf6kSRYqSIUTkviyyDFRcSO3yY1Kpbi83OirN+4lA2Xw=="], "langium": ["langium@4.2.2", "https://registry.npmmirror.com/langium/-/langium-4.2.2.tgz", { "dependencies": { "@chevrotain/regexp-to-ast": "~12.0.0", "chevrotain": "~12.0.0", "chevrotain-allstar": "~0.4.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ=="], @@ -2327,6 +2374,8 @@ "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "miniflare": ["miniflare@4.20260617.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260617.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-Go3/gzStm99QHptsSgU+q1S+xDfLoRgwjJNY80kaTVi0ENhTyqKq+sc4xZiWBSbM7uUcJwmzm8+QFKtcYLJ9nw=="], + "minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "minimist": ["minimist@1.2.8", "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -2427,7 +2476,7 @@ "path-scurry": ["path-scurry@2.0.2", "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.2.tgz", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - "path-to-regexp": ["path-to-regexp@8.4.2", "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.4.2.tgz", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], "pathe": ["pathe@2.0.3", "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -2747,6 +2796,8 @@ "undici-types": ["undici-types@7.19.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + "unicorn-magic": ["unicorn-magic@0.3.0", "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], "unified": ["unified@11.0.5", "https://registry.npmmirror.com/unified/-/unified-11.0.5.tgz", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], @@ -2823,6 +2874,10 @@ "which-module": ["which-module@2.0.1", "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], + "workerd": ["workerd@1.20260617.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260617.1", "@cloudflare/workerd-darwin-arm64": "1.20260617.1", "@cloudflare/workerd-linux-64": "1.20260617.1", "@cloudflare/workerd-linux-arm64": "1.20260617.1", "@cloudflare/workerd-windows-64": "1.20260617.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Re5pl6pdowt3ZmWUzGlOuB7jbRIIPetgKalmo4cYmucQnVhpo7/3e4MfpekbhLi2EhZZz5EY9NWRu8zFzuEZew=="], + + "wrangler": ["wrangler@4.103.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260617.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260617.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260617.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "cf-wrangler": "bin/cf-wrangler.js", "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-3Lv1P5t2xcSEkSTKtG+Lz+3JFryuU7YPLkaCUj7gNe+CJsjZJLtUwqsh1x595QBxkIbCE0GAvDx2DCJUU4+oqw=="], + "wrap-ansi": ["wrap-ansi@10.0.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-10.0.0.tgz", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], "wrappy": ["wrappy@1.0.2", "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -2849,6 +2904,10 @@ "yoctocolors": ["yoctocolors@2.1.2", "https://registry.npmmirror.com/yoctocolors/-/yoctocolors-2.1.2.tgz", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], + + "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], + "zod": ["zod@4.3.6", "https://registry.npmmirror.com/zod/-/zod-4.3.6.tgz", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], @@ -3083,6 +3142,8 @@ "@claude-code-best/mcp-client/zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@fastify/otel/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "https://registry.npmmirror.com/@opentelemetry/core/-/core-2.6.1.tgz", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="], "@fastify/otel/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.212.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation/-/instrumentation-0.212.0.tgz", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "import-in-the-middle": "^2.0.6", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg=="], @@ -3337,6 +3398,10 @@ "micromatch/picomatch": ["picomatch@2.3.2", "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "miniflare/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], + + "miniflare/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + "minipass-flush/minipass": ["minipass@3.3.6", "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-pipeline/minipass": ["minipass@3.3.6", "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -3367,6 +3432,8 @@ "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="], + "router/path-to-regexp": ["path-to-regexp@8.4.2", "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.4.2.tgz", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + "streamdown/lucide-react": ["lucide-react@0.542.0", "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.542.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw=="], "streamdown/marked": ["marked@16.4.2", "https://registry.npmmirror.com/marked/-/marked-16.4.2.tgz", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], @@ -3375,10 +3442,14 @@ "vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], + "wrangler/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + "xss/commander": ["commander@2.20.3", "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "yargs/string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "youch/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "@anthropic-ai/vertex-sdk/google-auth-library/gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="], "@anthropic-ai/vertex-sdk/google-auth-library/gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="], @@ -3629,6 +3700,58 @@ "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], diff --git a/docs/superpowers/plans/2026-06-20-artifacts-feature.md b/docs/superpowers/plans/2026-06-20-artifacts-feature.md new file mode 100644 index 000000000..8264a99f0 --- /dev/null +++ b/docs/superpowers/plans/2026-06-20-artifacts-feature.md @@ -0,0 +1,1325 @@ +# Artifacts Feature Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add an `ArtifactTool` (deferred) that uploads local HTML to `cloud-artifacts` service and returns a public URL + hash, a `/artifacts` panel command to browse uploaded files in the current session, and a `/use-artifacts` bundled skill that teaches the agent when/how to use artifacts. + +**Architecture:** Tool is a deferred `@claude-code-best/builtin-tools` entry that wraps a `fetch`-based HTTP client; the client reads token/URL from hardcoded defaults with env var override, parses the `{error}` field in body for failure detection (Deno Deploy proxy flattens HTTP status to 200). The panel command is a `local-jsx` slash command that scans `context.messages` for `artifact` tool_use + tool_result pairs. The skill is a bundled skill that injects guidance on artifact types, cadence, and the two-step `SearchExtraTools` + `ExecuteExtraTool` invocation flow. + +**Tech Stack:** Bun, TypeScript strict, Zod v4 (`zod/v4`), React (Ink via `packages/@ant/ink`), `bun:test`. + +--- + +## File Structure + +| File | Responsibility | +|------|----------------| +| `packages/builtin-tools/src/tools/ArtifactTool/config.ts` | Token/URL constants + env var override helpers | +| `packages/builtin-tools/src/tools/ArtifactTool/client.ts` | `uploadArtifact()` — HTTP POST to cloud-artifacts, body error parsing | +| `packages/builtin-tools/src/tools/ArtifactTool/prompt.ts` | `ARTIFACT_TOOL_NAME`, async `description()` / `prompt()` | +| `packages/builtin-tools/src/tools/ArtifactTool/ArtifactTool.ts` | `buildTool()` definition: schema, `call`, render, map | +| `packages/builtin-tools/src/tools/ArtifactTool/__tests__/client.test.ts` | client unit tests (mock fetch) | +| `packages/builtin-tools/src/tools/ArtifactTool/__tests__/ArtifactTool.test.ts` | Tool end-to-end tests (real temp file + mock fetch) | +| `packages/builtin-tools/src/index.ts` | Barrel export for `ArtifactTool` (modify) | +| `src/tools.ts` | Register `ArtifactTool` in the tools array (modify) | +| `src/skills/bundled/useArtifacts.ts` | Bundled skill body + `registerUseArtifactsSkill()` | +| `src/skills/bundled/index.ts` | Call `registerUseArtifactsSkill()` in `initBundledSkills()` (modify) | +| `src/commands/artifacts/scanner.ts` | Pure `extractArtifacts(messages)` — scan tool_use/tool_result pairs | +| `src/commands/artifacts/__tests__/scanner.test.ts` | scanner unit tests | +| `src/commands/artifacts/ArtifactsMenu.tsx` | React/Ink list component with Enter/c/Esc | +| `src/commands/artifacts/artifacts.tsx` | `call(onDone, context)` entry — calls scanner, renders menu | +| `src/commands/artifacts/index.ts` | `satisfies Command` definition with lazy `load` | +| `src/commands.ts` | Register `artifacts` command (modify) | + +--- + +## Task 1: ArtifactTool config (token/URL defaults) + +**Files:** +- Create: `packages/builtin-tools/src/tools/ArtifactTool/config.ts` + +- [ ] **Step 1: Write config file** + +```typescript +// packages/builtin-tools/src/tools/ArtifactTool/config.ts +/** + * Cloud Artifacts service configuration. + * Token/URL have hardcoded production defaults; env vars override for self-hosted deployments. + */ +export const ARTIFACTS_DEFAULT_TOKEN = 'claude-code-best' +export const ARTIFACTS_DEFAULT_URL = 'https://cloud-artifacts.claude-code-best.win' + +export function getArtifactsToken(): string { + return process.env.CLAUDE_ARTIFACTS_TOKEN ?? ARTIFACTS_DEFAULT_TOKEN +} + +export function getArtifactsBaseUrl(): string { + return process.env.CLAUDE_ARTIFACTS_URL ?? ARTIFACTS_DEFAULT_URL +} + +/** Strip trailing slash so `${base}/upload` is well-formed. */ +export function getUploadUrl(): string { + const base = getArtifactsBaseUrl() + return base.endsWith('/') ? `${base}upload` : `${base}/upload` +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add packages/builtin-tools/src/tools/ArtifactTool/config.ts +git commit -m "feat(artifact): add cloud-artifacts config with token/URL defaults" +``` + +--- + +## Task 2: ArtifactTool client (TDD — uploadArtifact) + +**Files:** +- Create: `packages/builtin-tools/src/tools/ArtifactTool/client.ts` +- Test: `packages/builtin-tools/src/tools/ArtifactTool/__tests__/client.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// packages/builtin-tools/src/tools/ArtifactTool/__tests__/client.test.ts +import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { uploadArtifact } from '../client.js' + +const originalFetch = globalThis.fetch + +function mockFetch(body: object, status = 200): typeof fetch { + return mock((_url: string | URL | Request, _init?: RequestInit) => + Promise.resolve( + new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }), + ), + ) as unknown as typeof fetch +} + +describe('uploadArtifact', () => { + afterEach(() => { + globalThis.fetch = originalFetch + }) + + test('returns id/url/expiresAt on successful upload', async () => { + globalThis.fetch = mockFetch({ + id: 'V1StGXR8_Z5jdHi6B', + url: 'https://cloud-artifacts.claude-code-best.win/7d/V1StGXR8_Z5jdHi6B.html', + expiresAt: '2026-06-27T10:00:00.000Z', + }) + + const result = await uploadArtifact({ + html: '

hello

', + token: 'test-token', + uploadUrl: 'https://example.test/upload', + }) + + expect(result).toEqual({ + id: 'V1StGXR8_Z5jdHi6B', + url: 'https://cloud-artifacts.claude-code-best.win/7d/V1StGXR8_Z5jdHi6B.html', + expiresAt: '2026-06-27T10:00:00.000Z', + }) + }) + + test('passes hash as query param when provided', async () => { + const fetchMock = mockFetch({ id: 'my-id', url: 'https://x/y.html', expiresAt: '2026-06-27T00:00:00.000Z' }) + globalThis.fetch = fetchMock + + await uploadArtifact({ + html: '

x

', + token: 't', + uploadUrl: 'https://example.test/upload', + hash: 'my-id', + }) + + const calledUrl = (fetchMock as unknown as { mock: { calls: [string | URL | Request][] } }).mock.calls[0][0] + expect(calledUrl.toString()).toContain('hash=my-id') + }) + + test('passes ttl=30 query param when provided', async () => { + const fetchMock = mockFetch({ id: 'x', url: 'https://x', expiresAt: '2026-07-20T00:00:00.000Z' }) + globalThis.fetch = fetchMock + + await uploadArtifact({ + html: '

x

', + token: 't', + uploadUrl: 'https://example.test/upload', + ttl: 30, + }) + + const calledUrl = (fetchMock as unknown as { mock: { calls: [string | URL | Request][] } }).mock.calls[0][0] + expect(calledUrl.toString()).toContain('ttl=30') + }) + + test('throws with error code when body contains {error} (Deno Deploy flattens status)', async () => { + globalThis.fetch = mockFetch({ error: 'payload_too_large' }, 200) + + await expect( + uploadArtifact({ + html: 'x'.repeat(100), + token: 't', + uploadUrl: 'https://example.test/upload', + }), + ).rejects.toThrow(/payload_too_large/) + }) + + test('throws on non-JSON body', async () => { + globalThis.fetch = mock((_u: string | URL | Request) => + Promise.resolve(new Response('Internal Server Error', { status: 500 })), + ) as unknown as typeof fetch + + await expect( + uploadArtifact({ html: '

', token: 't', uploadUrl: 'https://example.test/upload' }), + ).rejects.toThrow() + }) +}) +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +bun test packages/builtin-tools/src/tools/ArtifactTool/__tests__/client.test.ts +``` + +Expected: FAIL with `Cannot find module '../client.js'` or similar. + +- [ ] **Step 3: Implement the client** + +```typescript +// packages/builtin-tools/src/tools/ArtifactTool/client.ts +export type UploadResult = { + id: string + url: string + expiresAt: string +} + +export type UploadParams = { + html: string + token: string + uploadUrl: string + hash?: string + ttl?: 7 | 30 +} + +export async function uploadArtifact(params: UploadParams): Promise { + const url = new URL(params.uploadUrl) + if (params.hash) url.searchParams.set('hash', params.hash) + if (params.ttl) url.searchParams.set('ttl', String(params.ttl)) + + const response = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${params.token}`, + 'Content-Type': 'text/html', + }, + body: params.html, + }) + + // Deno Deploy proxy flattens upstream status to 200; the Worker embeds the + // real error in the body as `{ "error": "" }`. Always parse body first. + const text = await response.text() + let parsed: unknown + try { + parsed = JSON.parse(text) + } catch { + throw new Error(`Artifact upload failed: HTTP ${response.status} (non-JSON body)`) + } + + if (parsed && typeof parsed === 'object' && 'error' in parsed) { + const code = (parsed as { error: unknown }).error + throw new Error(`Artifact upload failed: ${String(code)}`) + } + + const data = parsed as Partial + if (typeof data.id !== 'string' || typeof data.url !== 'string' || typeof data.expiresAt !== 'string') { + throw new Error(`Artifact upload returned malformed body: ${text.slice(0, 200)}`) + } + return { id: data.id, url: data.url, expiresAt: data.expiresAt } +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +```bash +bun test packages/builtin-tools/src/tools/ArtifactTool/__tests__/client.test.ts +``` + +Expected: PASS (5 tests). + +- [ ] **Step 5: Commit** + +```bash +git add packages/builtin-tools/src/tools/ArtifactTool/client.ts packages/builtin-tools/src/tools/ArtifactTool/__tests__/client.test.ts +git commit -m "feat(artifact): add HTTP client with body-error parsing" +``` + +--- + +## Task 3: ArtifactTool prompt (name + description) + +**Files:** +- Create: `packages/builtin-tools/src/tools/ArtifactTool/prompt.ts` + +- [ ] **Step 1: Write prompt file** + +```typescript +// packages/builtin-tools/src/tools/ArtifactTool/prompt.ts +export const ARTIFACT_TOOL_NAME = 'artifact' + +export async function describeArtifactTool(): Promise { + return 'Upload an HTML file to the cloud-artifacts hosting service and get back a public URL. Pass `hash` to overwrite a previously-uploaded artifact (keeps URL stable).' +} + +export async function getArtifactToolPrompt(): Promise { + return `Upload an HTML file to a public hosting service and return a shareable URL plus an internal \`id\` (the "hash"). + +## Inputs +- \`file_path\` (required): absolute path to a local HTML file. +- \`hash\` (optional): if provided, overwrites the artifact with the same hash (URL stays the same). If omitted, a new random id is generated. +- \`ttl\` (optional, default \`7\`): artifact lifetime in days. Must be \`7\` or \`30\`. + +## Output +\`{ id, url, expiresAt }\` — \`id\` is the hash (save it for future overwrite calls), \`url\` is publicly accessible. + +## Workflow +1. Use the Write tool to create a local HTML file. +2. Call this tool with its \`file_path\`. +3. If iterating on the same artifact, pass back the \`id\` returned from the first call as \`hash\` so the URL stays stable. + +## Errors +The tool surfaces backend error codes verbatim (e.g. \`payload_too_large\`, \`unauthorized\`). If the file does not exist or is not a regular file, the tool returns an \`error\` field without making an HTTP request.` +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add packages/builtin-tools/src/tools/ArtifactTool/prompt.ts +git commit -m "feat(artifact): add tool name, description, and prompt" +``` + +--- + +## Task 4: ArtifactTool definition (schema + call + render + map) + +**Files:** +- Create: `packages/builtin-tools/src/tools/ArtifactTool/ArtifactTool.ts` + +- [ ] **Step 1: Write the tool definition** + +```typescript +// packages/builtin-tools/src/tools/ArtifactTool/ArtifactTool.ts +import { stat, readFile } from 'fs/promises' +import { z } from 'zod/v4' +import type { ToolResultBlockParam } from 'src/Tool.js' +import { buildTool } from 'src/Tool.js' +import { lazySchema } from 'src/utils/lazySchema.js' +import { ARTIFACT_TOOL_NAME, describeArtifactTool, getArtifactToolPrompt } from './prompt.js' +import { getArtifactsToken, getUploadUrl } from './config.js' +import { uploadArtifact } from './client.js' + +const inputSchema = lazySchema(() => + z.strictObject({ + file_path: z.string().describe('Absolute path to a local HTML file to upload.'), + hash: z + .string() + .regex(/^[A-Za-z0-9_-]{1,128}$/, 'must match ^[A-Za-z0-9_-]{1,128}$') + .optional() + .describe('If provided, overwrites the existing artifact with this hash (URL stays stable). If omitted, a new random id is generated.'), + ttl: z.union([z.literal(7), z.literal(30)]).default(7).describe('Lifetime in days. Must be 7 or 30. Default 7.'), + }), +) +type InputSchema = ReturnType +type ArtifactInput = z.infer + +const outputSchema = lazySchema(() => + z.object({ + id: z.string(), + url: z.string(), + expiresAt: z.string(), + }), +) +type OutputSchema = ReturnType +type ArtifactOutput = z.infer +type ArtifactErrorOutput = ArtifactOutput & { error?: string } + +export const ArtifactTool = buildTool({ + name: ARTIFACT_TOOL_NAME, + searchHint: 'upload html artifact share url cloud publish progress report public link', + maxResultSizeChars: 2_000, + shouldDefer: true, + strict: true, + + get inputSchema(): InputSchema { + return inputSchema() + }, + get outputSchema(): OutputSchema { + return outputSchema() + }, + + async description() { + return describeArtifactTool() + }, + async prompt() { + return getArtifactToolPrompt() + }, + + isEnabled() { + return true + }, + isConcurrencySafe() { + return false + }, + isReadOnly() { + return false + }, + requiresUserInteraction() { + return true + }, + userFacingName() { + return 'Artifact' + }, + + renderToolUseMessage(input: Partial) { + const hashPart = input.hash ? ` (hash=${input.hash})` : '' + return `Upload artifact: ${input.file_path ?? '...'}${hashPart}` + }, + + mapToolResultToToolResultBlockParam(content: ArtifactErrorOutput, toolUseID: string): ToolResultBlockParam { + if (content.error) { + return { + tool_use_id: toolUseID, + type: 'tool_result', + is_error: true, + content: content.error, + } + } + return { + tool_use_id: toolUseID, + type: 'tool_result', + content: `Artifact uploaded: ${content.url} (id: ${content.id}, expires: ${content.expiresAt})`, + } + }, + + async call(input: ArtifactInput) { + const { file_path, hash, ttl } = input + + let size: number + try { + const fileStat = await stat(file_path) + if (!fileStat.isFile()) { + return { data: { id: '', url: '', expiresAt: '', error: `Path is not a regular file: ${file_path}` } } + } + size = fileStat.size + } catch { + return { data: { id: '', url: '', expiresAt: '', error: `File does not exist or is not readable: ${file_path}` } } + } + + if (size > 10 * 1024 * 1024) { + return { data: { id: '', url: '', expiresAt: '', error: `File is ${size} bytes; backend limit is 10MB.` } } + } + + let html: string + try { + html = await readFile(file_path, 'utf8') + } catch { + return { data: { id: '', url: '', expiresAt: '', error: `Failed to read file: ${file_path}` } } + } + + try { + const result = await uploadArtifact({ + html, + token: getArtifactsToken(), + uploadUrl: getUploadUrl(), + hash, + ttl, + }) + return { data: result } + } catch (e) { + const message = e instanceof Error ? e.message : String(e) + return { data: { id: '', url: '', expiresAt: '', error: message } } + } + }, +}) +``` + +- [ ] **Step 2: Commit** + +```bash +git add packages/builtin-tools/src/tools/ArtifactTool/ArtifactTool.ts +git commit -m "feat(artifact): add buildTool definition with file validation" +``` + +--- + +## Task 5: Tool end-to-end tests + +**Files:** +- Test: `packages/builtin-tools/src/tools/ArtifactTool/__tests__/ArtifactTool.test.ts` + +- [ ] **Step 1: Write the e2e tool test** + +```typescript +// packages/builtin-tools/src/tools/ArtifactTool/__tests__/ArtifactTool.test.ts +import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' +import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { ArtifactTool } from '../ArtifactTool.js' + +const TEST_DIR = join(tmpdir(), 'artifact-tool-test') +const TEST_FILE = join(TEST_DIR, 'report.html') +const MISSING_FILE = join(TEST_DIR, 'does-not-exist.html') +const DIR_AS_FILE = TEST_DIR + +const originalFetch = globalThis.fetch + +function mockFetchSuccess(body: object): typeof fetch { + return mock(() => + Promise.resolve( + new Response(JSON.stringify(body), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ), + ) as unknown as typeof fetch +} + +describe('ArtifactTool.call', () => { + beforeEach(() => { + mkdirSync(TEST_DIR, { recursive: true }) + writeFileSync(TEST_FILE, '

test report

', 'utf8') + process.env.CLAUDE_ARTIFACTS_TOKEN = 'test-token' + process.env.CLAUDE_ARTIFACTS_URL = 'https://example.test' + }) + + afterEach(() => { + if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true, force: true }) + delete process.env.CLAUDE_ARTIFACTS_TOKEN + delete process.env.CLAUDE_ARTIFACTS_URL + globalThis.fetch = originalFetch + }) + + test('uploads existing HTML file and returns id/url/expiresAt', async () => { + globalThis.fetch = mockFetchSuccess({ + id: 'abc123', + url: 'https://example.test/7d/abc123.html', + expiresAt: '2026-06-27T10:00:00.000Z', + }) + + const result = await ArtifactTool.call({ file_path: TEST_FILE, ttl: 7 }, {} as never, {} as never, {} as never) + + expect(result.data).toMatchObject({ + id: 'abc123', + url: 'https://example.test/7d/abc123.html', + expiresAt: '2026-06-27T10:00:00.000Z', + }) + expect((result.data as { error?: string }).error).toBeUndefined() + }) + + test('passes hash through when overwriting', async () => { + const fetchMock = mockFetchSuccess({ + id: 'stable-id', + url: 'https://example.test/7d/stable-id.html', + expiresAt: '2026-06-27T10:00:00.000Z', + }) + globalThis.fetch = fetchMock + + await ArtifactTool.call({ file_path: TEST_FILE, hash: 'stable-id', ttl: 7 }, {} as never, {} as never, {} as never) + + const calledUrl = (fetchMock as unknown as { mock: { calls: [string | URL | Request][] } }).mock.calls[0][0] + expect(calledUrl.toString()).toContain('hash=stable-id') + }) + + test('returns error when file does not exist (no HTTP call)', async () => { + let fetchCalled = false + globalThis.fetch = mock(() => { + fetchCalled = true + return Promise.resolve(new Response('{}')) + }) as unknown as typeof fetch + + const result = await ArtifactTool.call({ file_path: MISSING_FILE, ttl: 7 }, {} as never, {} as never, {} as never) + + expect(fetchCalled).toBe(false) + expect((result.data as { error?: string }).error).toContain('does not exist') + }) + + test('returns error when path is a directory', async () => { + const result = await ArtifactTool.call({ file_path: DIR_AS_FILE, ttl: 7 }, {} as never, {} as never, {} as never) + + expect((result.data as { error?: string }).error).toContain('not a regular file') + }) + + test('returns error verbatim when backend rejects', async () => { + globalThis.fetch = mock(() => + Promise.resolve(new Response(JSON.stringify({ error: 'payload_too_large' }), { status: 200 })), + ) as unknown as typeof fetch + + // Force the size guard to pass by writing a small file but having backend complain. + const result = await ArtifactTool.call({ file_path: TEST_FILE, ttl: 7 }, {} as never, {} as never, {} as never) + + expect((result.data as { error?: string }).error).toContain('payload_too_large') + }) +}) +``` + +- [ ] **Step 2: Run the e2e test** + +```bash +bun test packages/builtin-tools/src/tools/ArtifactTool/__tests__/ArtifactTool.test.ts +``` + +Expected: PASS (5 tests). + +- [ ] **Step 3: Commit** + +```bash +git add packages/builtin-tools/src/tools/ArtifactTool/__tests__/ArtifactTool.test.ts +git commit -m "test(artifact): add end-to-end tool tests for upload/error paths" +``` + +--- + +## Task 6: Export ArtifactTool from builtin-tools barrel + +**Files:** +- Modify: `packages/builtin-tools/src/index.ts` + +- [ ] **Step 1: Read barrel to find an insertion point** + +```bash +grep -n "SendUserFile" packages/builtin-tools/src/index.ts +``` + +- [ ] **Step 2: Add the export (insert after the SendUserFileTool line, keep alphabetical/grouped ordering)** + +Add this single line next to the other tool exports: + +```typescript +export { ArtifactTool } from './tools/ArtifactTool/ArtifactTool.js' +``` + +- [ ] **Step 3: Verify export works** + +```bash +bun -e "import('@claude-code-best/builtin-tools').then(m => console.log(typeof m.ArtifactTool))" +``` + +Expected output: `object` (the built tool). + +- [ ] **Step 4: Commit** + +```bash +git add packages/builtin-tools/src/index.ts +git commit -m "feat(artifact): export ArtifactTool from builtin-tools barrel" +``` + +--- + +## Task 7: Register ArtifactTool in src/tools.ts + +**Files:** +- Modify: `src/tools.ts` + +- [ ] **Step 1: Add the require import (place near other non-feature-gated tools)** + +Find a clean spot in the top section (near other `const X = require(...)` declarations) and add: + +```typescript +const ArtifactTool = require('@claude-code-best/builtin-tools/tools/ArtifactTool/ArtifactTool.js').ArtifactTool +``` + +- [ ] **Step 2: Spread into the tools array (find the main returned array and add ArtifactTool unconditionally)** + +Add `ArtifactTool,` to the array literal that returns the assembled tool list (e.g. next to `BriefTool,`). + +- [ ] **Step 3: Verify by importing** + +```bash +bun -e "import('./src/tools.js').then(m => { const t = (m.getTools ?? m.tools); const arr = typeof t === 'function' ? t({mode:'default',additionalWorkingDirectories:new Set(),alwaysAllowRules:{deny:[],allow:[]},alwaysDenyRules:{deny:[],allow:[]},alwaysAskRules:{deny:[],allow:[]},isBypassPermissionsModeAvailable:false}) : t; console.log(arr.map(x=>x.name).includes('artifact')) })" +``` + +If the dynamic shape is hard to invoke, instead just typecheck: + +```bash +bunx tsc --noEmit -p tsconfig.json 2>&1 | head -50 +``` + +Expected: no new errors mentioning ArtifactTool. + +- [ ] **Step 4: Commit** + +```bash +git add src/tools.ts +git commit -m "feat(artifact): register ArtifactTool in tools list" +``` + +--- + +## Task 8: /use-artifacts bundled skill + +**Files:** +- Create: `src/skills/bundled/useArtifacts.ts` +- Modify: `src/skills/bundled/index.ts` + +- [ ] **Step 1: Write the skill file** + +```typescript +// src/skills/bundled/useArtifacts.ts +import { registerBundledSkill } from '../bundledSkills.js' + +const USE_ARTIFACTS_PROMPT = `# Using Artifacts + +Artifacts are public HTML pages you upload to a hosting service. They have stable URLs that you can share with the user or open in a browser. Use them to surface work-in-progress, summaries, and reports. + +## When to use artifacts + +**Good artifact content:** +- Progress panels / kanbans (task list with status) +- Research reports and analysis (data + findings + recommendations) +- Design docs / decision records (with context and rationale) +- Data visualizations (tables, SVG charts, flow diagrams) +- Final deliverables (the "thing the user asked for" rendered as HTML) + +**Do NOT use artifacts for:** +- Code snippets — use files directly +- One-line answers — keep them in chat +- Internal debug logs — keep them in chat +- Large data dumps — link to source files instead + +## Cadence — when to upload + +- **Task start**: if the task is complex (multi-step, research, deliverable), upload a skeleton artifact first as scaffolding (placeholder sections). +- **Milestones**: when you complete a phase (research done / implementation done / tests pass), update the artifact. +- **User asks**: upload immediately. +- **Task end**: ship the final artifact as the deliverable. + +**Do NOT upload:** +- After every tool call (noise) +- Mid-step with no meaningful change (e.g. fixed a typo) + +## How to invoke (deferred tool) + +\`artifact\` is a deferred tool. The first call requires two steps; subsequent calls one step. + +**First upload (creates a new artifact):** +\`\`\` +1. Use the Write tool to write HTML to a local file (location is your choice). +2. SearchExtraTools({ query: "select:artifact" }) // loads the tool schema +3. ExecuteExtraTool({ tool_name: "artifact", params: { file_path: ".html" } }) +4. Save the returned \`id\` from the tool result — this is the hash. +\`\`\` + +**Subsequent updates (overwrites in place, URL stays stable):** +\`\`\` +1. Update the local HTML file. +2. ExecuteExtraTool({ tool_name: "artifact", params: { file_path: ".html", hash: "" } }) +\`\`\` + +The URL returned on every call is the same when you pass the same \`hash\`. The user can open it at any time to see the latest version. + +## Minimal HTML skeleton + +\`\`\`html + + + + + Artifact Title + + + +

Artifact Title

+ + + +\`\`\` + +The hosting service serves the HTML verbatim (including any \`