@vibe-agent-toolkit/cli 0.1.42 → 0.2.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/dist/bin/vat +95 -28
- package/dist/bin/vat.js +95 -28
- package/dist/bin/vat.js.map +1 -1
- package/dist/bin.js +137 -76
- package/dist/bin.js.map +1 -1
- package/dist/command-loaders.d.ts +44 -0
- package/dist/command-loaders.d.ts.map +1 -0
- package/dist/command-loaders.js +60 -0
- package/dist/command-loaders.js.map +1 -0
- package/dist/commands/agent/build.d.ts.map +1 -1
- package/dist/commands/agent/build.js +2 -1
- package/dist/commands/agent/build.js.map +1 -1
- package/dist/commands/agent/import.d.ts.map +1 -1
- package/dist/commands/agent/import.js +3 -2
- package/dist/commands/agent/import.js.map +1 -1
- package/dist/commands/agent/index.js +1 -1
- package/dist/commands/agent/install-path.d.ts +36 -0
- package/dist/commands/agent/install-path.d.ts.map +1 -0
- package/dist/commands/agent/install-path.js +44 -0
- package/dist/commands/agent/install-path.js.map +1 -0
- package/dist/commands/agent/install.d.ts.map +1 -1
- package/dist/commands/agent/install.js +66 -13
- package/dist/commands/agent/install.js.map +1 -1
- package/dist/commands/agent/installed.d.ts.map +1 -1
- package/dist/commands/agent/installed.js +22 -12
- package/dist/commands/agent/installed.js.map +1 -1
- package/dist/commands/agent/list.d.ts.map +1 -1
- package/dist/commands/agent/list.js +2 -1
- package/dist/commands/agent/list.js.map +1 -1
- package/dist/commands/agent/run.d.ts.map +1 -1
- package/dist/commands/agent/run.js +2 -1
- package/dist/commands/agent/run.js.map +1 -1
- package/dist/commands/agent/uninstall.d.ts.map +1 -1
- package/dist/commands/agent/uninstall.js +13 -6
- package/dist/commands/agent/uninstall.js.map +1 -1
- package/dist/commands/agent/validate.d.ts.map +1 -1
- package/dist/commands/agent/validate.js +3 -2
- package/dist/commands/agent/validate.js.map +1 -1
- package/dist/commands/ard/emit.d.ts +153 -0
- package/dist/commands/ard/emit.d.ts.map +1 -0
- package/dist/commands/ard/emit.js +276 -0
- package/dist/commands/ard/emit.js.map +1 -0
- package/dist/commands/ard/index.d.ts +12 -0
- package/dist/commands/ard/index.d.ts.map +1 -0
- package/dist/commands/ard/index.js +80 -0
- package/dist/commands/ard/index.js.map +1 -0
- package/dist/commands/ard/surfaces.d.ts +56 -0
- package/dist/commands/ard/surfaces.d.ts.map +1 -0
- package/dist/commands/ard/surfaces.js +150 -0
- package/dist/commands/ard/surfaces.js.map +1 -0
- package/dist/commands/audit/distributed-tree.d.ts +60 -2
- package/dist/commands/audit/distributed-tree.d.ts.map +1 -1
- package/dist/commands/audit/distributed-tree.js +73 -1
- package/dist/commands/audit/distributed-tree.js.map +1 -1
- package/dist/commands/audit/git-url-clone.d.ts +1 -1
- package/dist/commands/audit/git-url-clone.d.ts.map +1 -1
- package/dist/commands/audit/git-url-clone.js.map +1 -1
- package/dist/commands/audit/hierarchical-output.d.ts +1 -1
- package/dist/commands/audit/hierarchical-output.d.ts.map +1 -1
- package/dist/commands/audit/hierarchical-output.js +1 -1
- package/dist/commands/audit/hierarchical-output.js.map +1 -1
- package/dist/commands/audit/provenance.d.ts.map +1 -1
- package/dist/commands/audit/provenance.js +4 -3
- package/dist/commands/audit/provenance.js.map +1 -1
- package/dist/commands/audit/scan-population.d.ts +158 -0
- package/dist/commands/audit/scan-population.d.ts.map +1 -0
- package/dist/commands/audit/scan-population.js +292 -0
- package/dist/commands/audit/scan-population.js.map +1 -0
- package/dist/commands/audit-settings.d.ts.map +1 -1
- package/dist/commands/audit-settings.js +6 -5
- package/dist/commands/audit-settings.js.map +1 -1
- package/dist/commands/audit.d.ts +98 -8
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +1133 -395
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/build.d.ts +1 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +40 -36
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/cache/cache-control.d.ts +82 -0
- package/dist/commands/cache/cache-control.d.ts.map +1 -0
- package/dist/commands/cache/cache-control.js +86 -0
- package/dist/commands/cache/cache-control.js.map +1 -0
- package/dist/commands/cache/clear.d.ts +105 -0
- package/dist/commands/cache/clear.d.ts.map +1 -0
- package/dist/commands/cache/clear.js +248 -0
- package/dist/commands/cache/clear.js.map +1 -0
- package/dist/commands/cache/index.d.ts +18 -0
- package/dist/commands/cache/index.d.ts.map +1 -0
- package/dist/commands/cache/index.js +94 -0
- package/dist/commands/cache/index.js.map +1 -0
- package/dist/commands/claude/claude-config.d.ts.map +1 -1
- package/dist/commands/claude/claude-config.js +6 -0
- package/dist/commands/claude/claude-config.js.map +1 -1
- package/dist/commands/claude/context.d.ts +340 -0
- package/dist/commands/claude/context.d.ts.map +1 -0
- package/dist/commands/claude/context.js +1080 -0
- package/dist/commands/claude/context.js.map +1 -0
- package/dist/commands/claude/index.d.ts +14 -0
- package/dist/commands/claude/index.d.ts.map +1 -1
- package/dist/commands/claude/index.js +22 -2
- package/dist/commands/claude/index.js.map +1 -1
- package/dist/commands/claude/marketplace/changelog-utils.d.ts +1 -1
- package/dist/commands/claude/marketplace/changelog-utils.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/changelog-utils.js +1 -2
- package/dist/commands/claude/marketplace/changelog-utils.js.map +1 -1
- package/dist/commands/claude/marketplace/git-publish.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/git-publish.js +40 -31
- package/dist/commands/claude/marketplace/git-publish.js.map +1 -1
- package/dist/commands/claude/marketplace/license-utils.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/license-utils.js +0 -1
- package/dist/commands/claude/marketplace/license-utils.js.map +1 -1
- package/dist/commands/claude/marketplace/publish-tree.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/publish-tree.js +0 -6
- package/dist/commands/claude/marketplace/publish-tree.js.map +1 -1
- package/dist/commands/claude/marketplace/publish.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/publish.js +2 -1
- package/dist/commands/claude/marketplace/publish.js.map +1 -1
- package/dist/commands/claude/marketplace/validate.d.ts +73 -22
- package/dist/commands/claude/marketplace/validate.d.ts.map +1 -1
- package/dist/commands/claude/marketplace/validate.js +407 -72
- package/dist/commands/claude/marketplace/validate.js.map +1 -1
- package/dist/commands/claude/org/api-keys.d.ts +0 -3
- package/dist/commands/claude/org/api-keys.d.ts.map +1 -1
- package/dist/commands/claude/org/api-keys.js +2 -1
- package/dist/commands/claude/org/api-keys.js.map +1 -1
- package/dist/commands/claude/org/helpers.d.ts +54 -1
- package/dist/commands/claude/org/helpers.d.ts.map +1 -1
- package/dist/commands/claude/org/helpers.js +42 -7
- package/dist/commands/claude/org/helpers.js.map +1 -1
- package/dist/commands/claude/org/index.d.ts.map +1 -1
- package/dist/commands/claude/org/index.js +24 -16
- package/dist/commands/claude/org/index.js.map +1 -1
- package/dist/commands/claude/org/invites.d.ts +0 -3
- package/dist/commands/claude/org/invites.d.ts.map +1 -1
- package/dist/commands/claude/org/invites.js +3 -2
- package/dist/commands/claude/org/invites.js.map +1 -1
- package/dist/commands/claude/org/skills.d.ts +527 -0
- package/dist/commands/claude/org/skills.d.ts.map +1 -1
- package/dist/commands/claude/org/skills.js +1414 -108
- package/dist/commands/claude/org/skills.js.map +1 -1
- package/dist/commands/claude/org/users.d.ts +0 -3
- package/dist/commands/claude/org/users.d.ts.map +1 -1
- package/dist/commands/claude/org/users.js +6 -3
- package/dist/commands/claude/org/users.js.map +1 -1
- package/dist/commands/claude/org/workspaces.d.ts +0 -3
- package/dist/commands/claude/org/workspaces.d.ts.map +1 -1
- package/dist/commands/claude/org/workspaces.js +11 -7
- package/dist/commands/claude/org/workspaces.js.map +1 -1
- package/dist/commands/claude/plugin/build.d.ts +41 -3
- package/dist/commands/claude/plugin/build.d.ts.map +1 -1
- package/dist/commands/claude/plugin/build.js +147 -80
- package/dist/commands/claude/plugin/build.js.map +1 -1
- package/dist/commands/claude/plugin/helpers.d.ts.map +1 -1
- package/dist/commands/claude/plugin/helpers.js +2 -7
- package/dist/commands/claude/plugin/helpers.js.map +1 -1
- package/dist/commands/claude/plugin/install.d.ts +12 -0
- package/dist/commands/claude/plugin/install.d.ts.map +1 -1
- package/dist/commands/claude/plugin/install.js +93 -62
- package/dist/commands/claude/plugin/install.js.map +1 -1
- package/dist/commands/claude/plugin/list.d.ts.map +1 -1
- package/dist/commands/claude/plugin/list.js +3 -2
- package/dist/commands/claude/plugin/list.js.map +1 -1
- package/dist/commands/claude/plugin/marketplace-json.d.ts +24 -9
- package/dist/commands/claude/plugin/marketplace-json.d.ts.map +1 -1
- package/dist/commands/claude/plugin/marketplace-json.js +10 -9
- package/dist/commands/claude/plugin/marketplace-json.js.map +1 -1
- package/dist/commands/claude/plugin/plugin-changelog.d.ts.map +1 -1
- package/dist/commands/claude/plugin/plugin-changelog.js +0 -2
- package/dist/commands/claude/plugin/plugin-changelog.js.map +1 -1
- package/dist/commands/claude/plugin/plugin-files.d.ts.map +1 -1
- package/dist/commands/claude/plugin/plugin-files.js +3 -6
- package/dist/commands/claude/plugin/plugin-files.js.map +1 -1
- package/dist/commands/claude/plugin/plugin-validators.d.ts.map +1 -1
- package/dist/commands/claude/plugin/plugin-validators.js +3 -6
- package/dist/commands/claude/plugin/plugin-validators.js.map +1 -1
- package/dist/commands/claude/plugin/tree-copy.d.ts +73 -0
- package/dist/commands/claude/plugin/tree-copy.d.ts.map +1 -1
- package/dist/commands/claude/plugin/tree-copy.js +221 -14
- package/dist/commands/claude/plugin/tree-copy.js.map +1 -1
- package/dist/commands/claude/plugin/uninstall.d.ts.map +1 -1
- package/dist/commands/claude/plugin/uninstall.js +4 -4
- package/dist/commands/claude/plugin/uninstall.js.map +1 -1
- package/dist/commands/consistency-check.d.ts +17 -15
- package/dist/commands/consistency-check.d.ts.map +1 -1
- package/dist/commands/consistency-check.js +152 -106
- package/dist/commands/consistency-check.js.map +1 -1
- package/dist/commands/corpus/index.d.ts.map +1 -1
- package/dist/commands/corpus/index.js +3 -0
- package/dist/commands/corpus/index.js.map +1 -1
- package/dist/commands/corpus/report.d.ts +1 -2
- package/dist/commands/corpus/report.d.ts.map +1 -1
- package/dist/commands/corpus/report.js +1 -3
- package/dist/commands/corpus/report.js.map +1 -1
- package/dist/commands/corpus/runner.d.ts +58 -1
- package/dist/commands/corpus/runner.d.ts.map +1 -1
- package/dist/commands/corpus/runner.js +95 -41
- package/dist/commands/corpus/runner.js.map +1 -1
- package/dist/commands/corpus/scan.d.ts.map +1 -1
- package/dist/commands/corpus/scan.js +20 -12
- package/dist/commands/corpus/scan.js.map +1 -1
- package/dist/commands/corpus/seed.d.ts +4 -4
- package/dist/commands/corpus/seed.d.ts.map +1 -1
- package/dist/commands/corpus/seed.js +1 -3
- package/dist/commands/corpus/seed.js.map +1 -1
- package/dist/commands/doctor.d.ts +50 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +245 -52
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/inventory.d.ts +13 -1
- package/dist/commands/inventory.d.ts.map +1 -1
- package/dist/commands/inventory.js +128 -17
- package/dist/commands/inventory.js.map +1 -1
- package/dist/commands/mcp/collections.d.ts +1 -1
- package/dist/commands/mcp/collections.d.ts.map +1 -1
- package/dist/commands/mcp/list-collections.d.ts.map +1 -1
- package/dist/commands/mcp/list-collections.js +2 -1
- package/dist/commands/mcp/list-collections.js.map +1 -1
- package/dist/commands/mcp/serve.d.ts.map +1 -1
- package/dist/commands/mcp/serve.js +3 -2
- package/dist/commands/mcp/serve.js.map +1 -1
- package/dist/commands/okf/index.d.ts +11 -0
- package/dist/commands/okf/index.d.ts.map +1 -0
- package/dist/commands/okf/index.js +114 -0
- package/dist/commands/okf/index.js.map +1 -0
- package/dist/commands/okf/validate.d.ts +191 -0
- package/dist/commands/okf/validate.d.ts.map +1 -0
- package/dist/commands/okf/validate.js +190 -0
- package/dist/commands/okf/validate.js.map +1 -0
- package/dist/commands/phase-utils.d.ts +182 -114
- package/dist/commands/phase-utils.d.ts.map +1 -1
- package/dist/commands/phase-utils.js +154 -163
- package/dist/commands/phase-utils.js.map +1 -1
- package/dist/commands/positional-args.d.ts.map +1 -1
- package/dist/commands/positional-args.js +3 -2
- package/dist/commands/positional-args.js.map +1 -1
- package/dist/commands/rag/clear-command.d.ts.map +1 -1
- package/dist/commands/rag/clear-command.js +2 -1
- package/dist/commands/rag/clear-command.js.map +1 -1
- package/dist/commands/rag/index-command.d.ts +64 -0
- package/dist/commands/rag/index-command.d.ts.map +1 -1
- package/dist/commands/rag/index-command.js +72 -6
- package/dist/commands/rag/index-command.js.map +1 -1
- package/dist/commands/rag/index.d.ts.map +1 -1
- package/dist/commands/rag/index.js +23 -10
- package/dist/commands/rag/index.js.map +1 -1
- package/dist/commands/rag/query-command.d.ts.map +1 -1
- package/dist/commands/rag/query-command.js +2 -1
- package/dist/commands/rag/query-command.js.map +1 -1
- package/dist/commands/rag/stats-command.d.ts.map +1 -1
- package/dist/commands/rag/stats-command.js +2 -1
- package/dist/commands/rag/stats-command.js.map +1 -1
- package/dist/commands/resources/builtin-help.d.ts +27 -0
- package/dist/commands/resources/builtin-help.d.ts.map +1 -0
- package/dist/commands/resources/builtin-help.js +46 -0
- package/dist/commands/resources/builtin-help.js.map +1 -0
- package/dist/commands/resources/check-progress.d.ts +154 -0
- package/dist/commands/resources/check-progress.d.ts.map +1 -0
- package/dist/commands/resources/check-progress.js +222 -0
- package/dist/commands/resources/check-progress.js.map +1 -0
- package/dist/commands/resources/check-supervisor.d.ts +465 -0
- package/dist/commands/resources/check-supervisor.d.ts.map +1 -0
- package/dist/commands/resources/check-supervisor.js +573 -0
- package/dist/commands/resources/check-supervisor.js.map +1 -0
- package/dist/commands/resources/check.d.ts +593 -0
- package/dist/commands/resources/check.d.ts.map +1 -0
- package/dist/commands/resources/check.js +1365 -0
- package/dist/commands/resources/check.js.map +1 -0
- package/dist/commands/resources/index.d.ts.map +1 -1
- package/dist/commands/resources/index.js +425 -6
- package/dist/commands/resources/index.js.map +1 -1
- package/dist/commands/resources/query.d.ts +106 -0
- package/dist/commands/resources/query.d.ts.map +1 -0
- package/dist/commands/resources/query.js +177 -0
- package/dist/commands/resources/query.js.map +1 -0
- package/dist/commands/resources/scan.d.ts +24 -0
- package/dist/commands/resources/scan.d.ts.map +1 -1
- package/dist/commands/resources/scan.js +17 -8
- package/dist/commands/resources/scan.js.map +1 -1
- package/dist/commands/resources/validate.d.ts +57 -4
- package/dist/commands/resources/validate.d.ts.map +1 -1
- package/dist/commands/resources/validate.js +224 -56
- package/dist/commands/resources/validate.js.map +1 -1
- package/dist/commands/skill/review-checklist.d.ts +3 -2
- package/dist/commands/skill/review-checklist.d.ts.map +1 -1
- package/dist/commands/skill/review-checklist.js +40 -2
- package/dist/commands/skill/review-checklist.js.map +1 -1
- package/dist/commands/skill/review.d.ts +247 -0
- package/dist/commands/skill/review.d.ts.map +1 -1
- package/dist/commands/skill/review.js +99 -62
- package/dist/commands/skill/review.js.map +1 -1
- package/dist/commands/skill/test/configure.d.ts +56 -0
- package/dist/commands/skill/test/configure.d.ts.map +1 -1
- package/dist/commands/skill/test/configure.js +75 -16
- package/dist/commands/skill/test/configure.js.map +1 -1
- package/dist/commands/skill/test/run.d.ts +63 -10
- package/dist/commands/skill/test/run.d.ts.map +1 -1
- package/dist/commands/skill/test/run.js +258 -77
- package/dist/commands/skill/test/run.js.map +1 -1
- package/dist/commands/skills/build.d.ts +94 -4
- package/dist/commands/skills/build.d.ts.map +1 -1
- package/dist/commands/skills/build.js +325 -97
- package/dist/commands/skills/build.js.map +1 -1
- package/dist/commands/skills/install.d.ts +2 -1
- package/dist/commands/skills/install.d.ts.map +1 -1
- package/dist/commands/skills/install.js +20 -31
- package/dist/commands/skills/install.js.map +1 -1
- package/dist/commands/skills/list.d.ts +2 -1
- package/dist/commands/skills/list.d.ts.map +1 -1
- package/dist/commands/skills/list.js +44 -26
- package/dist/commands/skills/list.js.map +1 -1
- package/dist/commands/skills/package.d.ts +24 -3
- package/dist/commands/skills/package.d.ts.map +1 -1
- package/dist/commands/skills/package.js +42 -17
- package/dist/commands/skills/package.js.map +1 -1
- package/dist/commands/skills/scope-guard.d.ts.map +1 -1
- package/dist/commands/skills/scope-guard.js +3 -5
- package/dist/commands/skills/scope-guard.js.map +1 -1
- package/dist/commands/skills/shared.d.ts.map +1 -1
- package/dist/commands/skills/shared.js +2 -4
- package/dist/commands/skills/shared.js.map +1 -1
- package/dist/commands/skills/skill-discovery.d.ts +47 -6
- package/dist/commands/skills/skill-discovery.d.ts.map +1 -1
- package/dist/commands/skills/skill-discovery.js +66 -16
- package/dist/commands/skills/skill-discovery.js.map +1 -1
- package/dist/commands/skills/source-resolvers.d.ts +15 -0
- package/dist/commands/skills/source-resolvers.d.ts.map +1 -1
- package/dist/commands/skills/source-resolvers.js +25 -4
- package/dist/commands/skills/source-resolvers.js.map +1 -1
- package/dist/commands/skills/validate-command.d.ts.map +1 -1
- package/dist/commands/skills/validate-command.js +11 -3
- package/dist/commands/skills/validate-command.js.map +1 -1
- package/dist/commands/skills/validate.d.ts +140 -4
- package/dist/commands/skills/validate.d.ts.map +1 -1
- package/dist/commands/skills/validate.js +224 -67
- package/dist/commands/skills/validate.js.map +1 -1
- package/dist/commands/validate.d.ts +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +63 -27
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/verify.d.ts +119 -15
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +357 -142
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/report-schemas.d.ts +45 -0
- package/dist/report-schemas.d.ts.map +1 -0
- package/dist/report-schemas.js +100 -0
- package/dist/report-schemas.js.map +1 -0
- package/dist/skill-resolution/classify.js +2 -2
- package/dist/skill-resolution/classify.js.map +1 -1
- package/dist/skill-resolution/packaging-config.d.ts +49 -5
- package/dist/skill-resolution/packaging-config.d.ts.map +1 -1
- package/dist/skill-resolution/packaging-config.js +59 -19
- package/dist/skill-resolution/packaging-config.js.map +1 -1
- package/dist/skill-resolution/resolve-skill-reference.d.ts.map +1 -1
- package/dist/skill-resolution/resolve-skill-reference.js +55 -57
- package/dist/skill-resolution/resolve-skill-reference.js.map +1 -1
- package/dist/utils/agent-discovery.d.ts.map +1 -1
- package/dist/utils/agent-discovery.js +22 -11
- package/dist/utils/agent-discovery.js.map +1 -1
- package/dist/utils/agent-runner.d.ts.map +1 -1
- package/dist/utils/agent-runner.js +7 -4
- package/dist/utils/agent-runner.js.map +1 -1
- package/dist/utils/claude-context-lens.d.ts +34 -0
- package/dist/utils/claude-context-lens.d.ts.map +1 -0
- package/dist/utils/claude-context-lens.js +39 -0
- package/dist/utils/claude-context-lens.js.map +1 -0
- package/dist/utils/command-error.d.ts +144 -7
- package/dist/utils/command-error.d.ts.map +1 -1
- package/dist/utils/command-error.js +179 -16
- package/dist/utils/command-error.js.map +1 -1
- package/dist/utils/config-loader.d.ts +2 -1
- package/dist/utils/config-loader.d.ts.map +1 -1
- package/dist/utils/config-loader.js +40 -14
- package/dist/utils/config-loader.js.map +1 -1
- package/dist/utils/corpus-target.d.ts +26 -0
- package/dist/utils/corpus-target.d.ts.map +1 -0
- package/dist/utils/corpus-target.js +35 -0
- package/dist/utils/corpus-target.js.map +1 -0
- package/dist/utils/edge-lens-evaluation.d.ts +83 -0
- package/dist/utils/edge-lens-evaluation.d.ts.map +1 -0
- package/dist/utils/edge-lens-evaluation.js +110 -0
- package/dist/utils/edge-lens-evaluation.js.map +1 -0
- package/dist/utils/help-loader.d.ts.map +1 -1
- package/dist/utils/help-loader.js +0 -1
- package/dist/utils/help-loader.js.map +1 -1
- package/dist/utils/issue-anchor.d.ts +2 -2
- package/dist/utils/issue-anchor.d.ts.map +1 -1
- package/dist/utils/issue-anchor.js +1 -1
- package/dist/utils/issue-rendering.d.ts +3 -3
- package/dist/utils/issue-rendering.d.ts.map +1 -1
- package/dist/utils/issue-rendering.js +3 -3
- package/dist/utils/issue-rendering.js.map +1 -1
- package/dist/utils/issue-severity.d.ts +12 -5
- package/dist/utils/issue-severity.d.ts.map +1 -1
- package/dist/utils/issue-severity.js +1 -1
- package/dist/utils/issue-severity.js.map +1 -1
- package/dist/utils/optional-backend.d.ts +78 -0
- package/dist/utils/optional-backend.d.ts.map +1 -0
- package/dist/utils/optional-backend.js +117 -0
- package/dist/utils/optional-backend.js.map +1 -0
- package/dist/utils/output.d.ts +26 -0
- package/dist/utils/output.d.ts.map +1 -1
- package/dist/utils/output.js +35 -0
- package/dist/utils/output.js.map +1 -1
- package/dist/utils/package-json.d.ts +24 -0
- package/dist/utils/package-json.d.ts.map +1 -0
- package/dist/utils/package-json.js +49 -0
- package/dist/utils/package-json.js.map +1 -0
- package/dist/utils/population-wiring.d.ts +100 -0
- package/dist/utils/population-wiring.d.ts.map +1 -0
- package/dist/utils/population-wiring.js +98 -0
- package/dist/utils/population-wiring.js.map +1 -0
- package/dist/utils/project-root-policy.d.ts +20 -0
- package/dist/utils/project-root-policy.d.ts.map +1 -1
- package/dist/utils/project-root-policy.js +30 -0
- package/dist/utils/project-root-policy.js.map +1 -1
- package/dist/utils/projection-lenses.d.ts +98 -0
- package/dist/utils/projection-lenses.d.ts.map +1 -0
- package/dist/utils/projection-lenses.js +203 -0
- package/dist/utils/projection-lenses.js.map +1 -0
- package/dist/utils/projection-query.d.ts +324 -0
- package/dist/utils/projection-query.d.ts.map +1 -0
- package/dist/utils/projection-query.js +357 -0
- package/dist/utils/projection-query.js.map +1 -0
- package/dist/utils/projection-store.d.ts +335 -0
- package/dist/utils/projection-store.d.ts.map +1 -0
- package/dist/utils/projection-store.js +522 -0
- package/dist/utils/projection-store.js.map +1 -0
- package/dist/utils/relation-limits.d.ts +50 -0
- package/dist/utils/relation-limits.d.ts.map +1 -0
- package/dist/utils/relation-limits.js +50 -0
- package/dist/utils/relation-limits.js.map +1 -0
- package/dist/utils/repeatable-option.d.ts +37 -0
- package/dist/utils/repeatable-option.d.ts.map +1 -0
- package/dist/utils/repeatable-option.js +39 -0
- package/dist/utils/repeatable-option.js.map +1 -0
- package/dist/utils/resource-loader.d.ts +132 -2
- package/dist/utils/resource-loader.d.ts.map +1 -1
- package/dist/utils/resource-loader.js +288 -27
- package/dist/utils/resource-loader.js.map +1 -1
- package/dist/utils/root-argv.d.ts +53 -0
- package/dist/utils/root-argv.d.ts.map +1 -0
- package/dist/utils/root-argv.js +215 -0
- package/dist/utils/root-argv.js.map +1 -0
- package/dist/utils/run-integrity.d.ts +95 -0
- package/dist/utils/run-integrity.d.ts.map +1 -0
- package/dist/utils/run-integrity.js +102 -0
- package/dist/utils/run-integrity.js.map +1 -0
- package/dist/utils/skill-discovery.d.ts.map +1 -1
- package/dist/utils/skill-discovery.js +2 -1
- package/dist/utils/skill-discovery.js.map +1 -1
- package/dist/utils/skill-packaging-config.d.ts +63 -1
- package/dist/utils/skill-packaging-config.d.ts.map +1 -1
- package/dist/utils/skill-packaging-config.js +56 -1
- package/dist/utils/skill-packaging-config.js.map +1 -1
- package/dist/utils/sqlite-experimental-warning.d.ts +69 -0
- package/dist/utils/sqlite-experimental-warning.d.ts.map +1 -0
- package/dist/utils/sqlite-experimental-warning.js +144 -0
- package/dist/utils/sqlite-experimental-warning.js.map +1 -0
- package/dist/utils/url-redact.d.ts.map +1 -1
- package/dist/utils/url-redact.js +4 -6
- package/dist/utils/url-redact.js.map +1 -1
- package/dist/utils/user-context-scanner.d.ts +8 -0
- package/dist/utils/user-context-scanner.d.ts.map +1 -1
- package/dist/utils/user-context-scanner.js +4 -2
- package/dist/utils/user-context-scanner.js.map +1 -1
- package/dist/utils/validate-help-files.d.ts.map +1 -1
- package/dist/utils/validate-help-files.js +12 -6
- package/dist/utils/validate-help-files.js.map +1 -1
- package/dist/utils/vat-bin-path.d.ts +36 -0
- package/dist/utils/vat-bin-path.d.ts.map +1 -0
- package/dist/utils/vat-bin-path.js +48 -0
- package/dist/utils/vat-bin-path.js.map +1 -0
- package/dist/utils/verdict-helpers.d.ts +1 -1
- package/dist/utils/verdict-helpers.d.ts.map +1 -1
- package/dist/utils/verdict-helpers.js +1 -1
- package/dist/utils/verdict-helpers.js.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -3
- package/dist/version.js.map +1 -1
- package/docs/agent.md +2 -2
- package/docs/audit.md +141 -37
- package/docs/doctor.md +7 -6
- package/docs/index.md +40 -12
- package/docs/mcp.md +2 -2
- package/docs/rag.md +3 -2
- package/docs/resources.md +13 -3
- package/docs/skill-test.md +10 -9
- package/docs/skills.md +53 -69
- package/package.json +38 -27
- package/schemas/ard-emit.json +149 -0
- package/schemas/okf-validate.json +170 -0
- package/schemas/resources-check.json +229 -0
- package/schemas/skill-review.json +230 -0
- package/dist/commands/resources/command-helpers.d.ts +0 -5
- package/dist/commands/resources/command-helpers.d.ts.map +0 -1
- package/dist/commands/resources/command-helpers.js +0 -6
- package/dist/commands/resources/command-helpers.js.map +0 -1
|
@@ -3,29 +3,708 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
5
5
|
import { basename } from 'node:path';
|
|
6
|
-
import { evalSuiteUnitPath, readDeclaredSkillName } from '@vibe-agent-toolkit/agent-skills';
|
|
7
|
-
import { buildMultipartFormData } from '@vibe-agent-toolkit/claude-marketplace';
|
|
8
|
-
import {
|
|
6
|
+
import { API_SKILL_MAX_UPLOAD_BYTES, collectNonPortableAssetReferenceIssues, collectNonPortableCommandIssues, collectUnqualifiedMcpToolIssues, declaredSkillNameIn, describeOversizeBundle, evalSuiteUnitPath, formatBytes, NEVER_UPLOADED_DIR_NAMES, readDeclaredSkillName, } from '@vibe-agent-toolkit/agent-skills';
|
|
7
|
+
import { ApiRequestError, ApiTransportError, buildMultipartFormData, skillVersionsPath, } from '@vibe-agent-toolkit/claude-marketplace';
|
|
8
|
+
import { createAllowUsageLedger, runValidationFramework } from '@vibe-agent-toolkit/schema';
|
|
9
|
+
import { direntKindFollowingSync, isAbsoluteAnyPlatform, isFilesystemAccessError, normalizedTmpdir, safePath, toForwardSlashAnyPlatform, } from '@vibe-agent-toolkit/utils';
|
|
9
10
|
import { Command } from 'commander';
|
|
10
11
|
import { resolveSkillPackagingConfig } from '../../../skill-resolution/packaging-config.js';
|
|
11
12
|
import { downloadNpmPackage } from '../plugin/helpers.js';
|
|
12
|
-
import { autopaginateSkills, executeOrgCommand } from './helpers.js';
|
|
13
|
+
import { autopaginateSkills, executeOrgCommand, orgCommandFailure } from './helpers.js';
|
|
13
14
|
const SKILL_ID_ARG = '<skill-id>';
|
|
14
15
|
const SKILL_ID_DESC = 'Skill ID (slug)';
|
|
15
16
|
const DEBUG_OPT_DESC = 'Enable debug logging';
|
|
16
17
|
/**
|
|
17
|
-
*
|
|
18
|
+
* Read an upload response into the result this command prints, refusing a body
|
|
19
|
+
* that does not carry the fields the printed document promises.
|
|
20
|
+
*
|
|
21
|
+
* 🔑 The client's `<T>` is a type ASSERTION, not a check: any 2xx with a JSON
|
|
22
|
+
* body satisfies it. Without this, a response whose keys differ from the ones
|
|
23
|
+
* named here resolves happily and the operator reads `status: success` beside
|
|
24
|
+
* `version: null` — and `version` is precisely the value a later
|
|
25
|
+
* `skills versions delete` takes, so the run that "succeeded" leaves them unable
|
|
26
|
+
* to address what it created. The create endpoint's shape was measured against
|
|
27
|
+
* the live API; `POST /v1/skills/{id}/versions` was not, which is exactly why it
|
|
28
|
+
* must fail loudly rather than print nulls if it differs.
|
|
29
|
+
*
|
|
30
|
+
* The message names both the fields that were missing and the keys the body did
|
|
31
|
+
* carry, because those two lists together are the whole diagnosis when a shape
|
|
32
|
+
* drifts.
|
|
33
|
+
*/
|
|
34
|
+
function readSkillUploadResponse(endpoint, raw, fields) {
|
|
35
|
+
const body = typeof raw === 'object' && raw !== null ? raw : {};
|
|
36
|
+
const read = (key) => {
|
|
37
|
+
const value = body[key];
|
|
38
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
39
|
+
};
|
|
40
|
+
const id = read(fields.id);
|
|
41
|
+
const displayTitle = read(fields.displayTitle);
|
|
42
|
+
const version = read(fields.version);
|
|
43
|
+
const createdAt = read(fields.createdAt);
|
|
44
|
+
if (id === undefined || displayTitle === undefined || version === undefined || createdAt === undefined) {
|
|
45
|
+
const missing = [
|
|
46
|
+
[fields.id, id], [fields.displayTitle, displayTitle],
|
|
47
|
+
[fields.version, version], [fields.createdAt, createdAt],
|
|
48
|
+
].filter(([, value]) => value === undefined).map(([key]) => String(key));
|
|
49
|
+
const present = Object.keys(body).join(', ') || '(none)';
|
|
50
|
+
throw new Error(`${endpoint} returned a body with no usable ${missing.join(', ')}. Keys present: ${present}. `
|
|
51
|
+
+ 'Refusing to report success for an upload whose identifiers cannot be read.');
|
|
52
|
+
}
|
|
53
|
+
return { id, displayTitle, version, createdAt };
|
|
54
|
+
}
|
|
55
|
+
/** Read `POST /v1/skills` — the shape measured against the live API. */
|
|
56
|
+
export function readCreateSkillResponse(raw) {
|
|
57
|
+
return readSkillUploadResponse('POST /v1/skills', raw, {
|
|
58
|
+
id: 'id', displayTitle: 'display_title', version: 'latest_version', createdAt: 'created_at',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read a DELETE response into the result a delete command prints.
|
|
63
|
+
*
|
|
64
|
+
* 🔑 Same class of problem as {@link readSkillUploadResponse} — the client's
|
|
65
|
+
* `<T>` is an assertion, not a check — but the OPPOSITE verdict on an empty
|
|
66
|
+
* body, and deliberately so. The client used to reject a 2xx carrying no body
|
|
67
|
+
* with a parse error, which reported a 204 DELETE that SUCCEEDED as a failure;
|
|
68
|
+
* that is fixed, and the value now handed to this reader for such a response is
|
|
69
|
+
* `undefined`. Refusing it here, the way an upload response is refused, would
|
|
70
|
+
* reinstate the same lie one layer up — and reading `.id` off it, which is what
|
|
71
|
+
* the two call sites did, is a `TypeError` in place of a report.
|
|
72
|
+
*
|
|
73
|
+
* So an empty body is read as the success it is: the status already said the
|
|
74
|
+
* resource is gone, the id is the one this process asked for, and there is
|
|
75
|
+
* nothing else to learn. Measured live behaviour today is a JSON body carrying
|
|
76
|
+
* `type: skill_deleted`, so the empty case is LATENT rather than a live
|
|
77
|
+
* regression — but it is one 204 away, and a latent `TypeError` is not a
|
|
78
|
+
* contract.
|
|
79
|
+
*
|
|
80
|
+
* `deleted` is false only when the body affirmatively names a DIFFERENT
|
|
81
|
+
* outcome, which is the one case where the API is saying something this command
|
|
82
|
+
* must not paper over — and {@link reportDelete} is what makes the run END that
|
|
83
|
+
* way rather than merely say so in a field nothing reads.
|
|
84
|
+
*
|
|
85
|
+
* ⚠️ `expectedTypes` is a LIST because only one member of it is measured.
|
|
86
|
+
* `skill_deleted` came back from a live `DELETE /v1/skills/{id}`;
|
|
87
|
+
* `skill_version_deleted` is the plausible spelling for the version endpoint and
|
|
88
|
+
* has never been seen. A single guessed string would make every version delete
|
|
89
|
+
* report `deleted: false` — and now exit 1 — if the vendor spells it differently
|
|
90
|
+
* by one character. Accepting either keeps the check honest about what it knows:
|
|
91
|
+
* a body naming something that is neither is a real divergence worth failing on.
|
|
92
|
+
*/
|
|
93
|
+
export function readDeleteResponse(raw, requestedId, expectedTypes) {
|
|
94
|
+
const body = typeof raw === 'object' && raw !== null ? raw : {};
|
|
95
|
+
const echoedId = body['id'];
|
|
96
|
+
const type = body['type'];
|
|
97
|
+
return {
|
|
98
|
+
id: typeof echoedId === 'string' && echoedId.length > 0 ? echoedId : requestedId,
|
|
99
|
+
deleted: typeof type === 'string' ? expectedTypes.includes(type) : true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** The success types measured against the live `DELETE /v1/skills/{id}`. */
|
|
103
|
+
export const SKILL_DELETED_TYPES = ['skill_deleted'];
|
|
104
|
+
/**
|
|
105
|
+
* The success types a version delete may answer with — see
|
|
106
|
+
* {@link readDeleteResponse} for why this is two and not one.
|
|
107
|
+
*/
|
|
108
|
+
export const SKILL_VERSION_DELETED_TYPES = ['skill_version_deleted', 'skill_deleted'];
|
|
109
|
+
/**
|
|
110
|
+
* A delete's document, tagged as a FAILED run when the API said the thing was
|
|
111
|
+
* not deleted.
|
|
112
|
+
*
|
|
113
|
+
* 🔑 Without this, `deleted: false` was computed, printed under
|
|
114
|
+
* `status: success`, and exited 0 — the exact "the run completed and its outcome
|
|
115
|
+
* is wrong" shape that {@link orgCommandFailure} was added in this same change to
|
|
116
|
+
* fix for `--from-npm`, in the one branch that never got wired to it. A CI
|
|
117
|
+
* wrapper spelled `vat claude org skills delete X || fail` reported green while
|
|
118
|
+
* the skill was still there.
|
|
119
|
+
*
|
|
120
|
+
* The document is still PUBLISHED — the id and the verdict are what the operator
|
|
121
|
+
* needs — and only the exit code changes. Throwing instead would end non-zero and
|
|
122
|
+
* discard the report.
|
|
123
|
+
*/
|
|
124
|
+
export function reportDelete(raw, requestedId, expectedTypes) {
|
|
125
|
+
const result = readDeleteResponse(raw, requestedId, expectedTypes);
|
|
126
|
+
return result.deleted ? result : orgCommandFailure(result);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* A version delete's document — the same verdict as {@link reportDelete}, about
|
|
130
|
+
* a subject that can actually be identified.
|
|
131
|
+
*
|
|
132
|
+
* 🚨 **The version-delete document used to name no version at all, and its `id`
|
|
133
|
+
* meant two different things depending on the response.** It went through
|
|
134
|
+
* `reportDelete(raw, skillId, …)`, so `readDeleteResponse` returned the API's
|
|
135
|
+
* ECHOED id when there was a body — the VERSION identifier — and fell back to
|
|
136
|
+
* the requested id — the SKILL identifier — on a 204. Two runs of the same
|
|
137
|
+
* command therefore published `id` fields from two different namespaces, and
|
|
138
|
+
* neither run's document said which of a skill's versions had been destroyed.
|
|
139
|
+
* An audit trail of an irreversible operation that cannot name what it destroyed
|
|
140
|
+
* is not an audit trail.
|
|
141
|
+
*
|
|
142
|
+
* Both fields are now local knowledge, which is the whole point: this process
|
|
143
|
+
* asked to delete `version` of `skillId` and the API confirmed a deletion, so
|
|
144
|
+
* nothing about the subject's identity has to be recovered from the response
|
|
145
|
+
* shape. The response is still read — for the VERDICT, which is the one thing
|
|
146
|
+
* only the API knows.
|
|
147
|
+
*/
|
|
148
|
+
export function reportVersionDelete(raw, skillId, version) {
|
|
149
|
+
const { deleted } = readDeleteResponse(raw, skillId, SKILL_VERSION_DELETED_TYPES);
|
|
150
|
+
const document = { id: skillId, version, deleted };
|
|
151
|
+
return deleted ? document : orgCommandFailure(document);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Add the versions a `--all` run destroyed to its report, preserving whatever
|
|
155
|
+
* verdict {@link reportDelete} reached.
|
|
156
|
+
*
|
|
157
|
+
* Unwrapping and re-wrapping rather than spreading into the tagged value: the
|
|
158
|
+
* failure tag sits BESIDE the document, so a naive `{ ...result, deletedVersions }`
|
|
159
|
+
* on a failure would produce an object carrying `orgCommandFailed` next to the
|
|
160
|
+
* skill's fields and publish the tag as part of the operator's document.
|
|
161
|
+
*/
|
|
162
|
+
export function mergeDeleteReport(result, deletedVersions) {
|
|
163
|
+
const failed = 'orgCommandFailed' in result;
|
|
164
|
+
const document = { ...(failed ? result.document : result), deletedVersions };
|
|
165
|
+
return failed ? orgCommandFailure(document) : document;
|
|
166
|
+
}
|
|
167
|
+
/** Read `POST /v1/skills/{id}/versions`. */
|
|
168
|
+
export function readSkillVersionResponse(raw) {
|
|
169
|
+
return readSkillUploadResponse('POST /v1/skills/{id}/versions', raw, {
|
|
170
|
+
id: 'skill_id', displayTitle: 'name', version: 'version', createdAt: 'created_at',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Build the multipart request body for an upload, refusing it before anything is
|
|
175
|
+
* sent when THAT BODY is OVER the API's upload ceiling.
|
|
176
|
+
*
|
|
177
|
+
* 🔑 **The ceiling is INCLUSIVE, and that is measured rather than reasoned.** A
|
|
178
|
+
* request body of exactly {@link API_SKILL_MAX_UPLOAD_BYTES} bytes is ACCEPTED;
|
|
179
|
+
* one byte more is refused `413`:
|
|
180
|
+
*
|
|
181
|
+
* | raw file bytes | + framing | = body | live API |
|
|
182
|
+
* |---|---|---|---|
|
|
183
|
+
* | 31,456,735 | 545 | **31,457,280** | `status: success` |
|
|
184
|
+
* | 31,456,736 | 545 | 31,457,281 | `413` |
|
|
185
|
+
*
|
|
186
|
+
* So the comparison here is `>`, not `>=`. An earlier version of this code used
|
|
187
|
+
* `>=` on the argument that firing AT the ceiling was the conservative reading —
|
|
188
|
+
* an argument that was never measured, and the measurement above refutes it. It
|
|
189
|
+
* is not conservative: it refuses a body the API takes, and the operator has no
|
|
190
|
+
* way to tell that VAT and not Anthropic said no.
|
|
191
|
+
*
|
|
192
|
+
* ⚠️ **The build-time lane deliberately differs, and must not be "harmonised"
|
|
193
|
+
* with this one.** `checkPackagedSizeLimit` fires when FILE BYTES reach the same
|
|
194
|
+
* number — `>=` — because framing is never zero, so files totalling exactly the
|
|
195
|
+
* limit always produce a request LARGER than the limit and therefore a real 413.
|
|
196
|
+
* Two lanes, two measures, two rules: **file bytes `>=` refuse; request bytes `>`
|
|
197
|
+
* refuse.** Both are right about the quantity each can see.
|
|
198
|
+
*
|
|
199
|
+
* THE one gate every upload passes through, whichever shape it started as. The
|
|
200
|
+
* check used to live inside the directory-packaging step alone, so
|
|
201
|
+
* `skills install big-skill.zip` — the one input that is by construction a
|
|
202
|
+
* single large binary, and the shape the check was written for — reached the
|
|
203
|
+
* wire unmeasured.
|
|
204
|
+
*
|
|
205
|
+
* 🔑 **It weighs the BODY, not the sum of the file bytes.** The API measures the
|
|
206
|
+
* request, and the request is this buffer. Summing `file.content.length` — what
|
|
207
|
+
* this did — ignores the per-part framing `buildMultipartFormData` adds: a
|
|
208
|
+
* 51-byte boundary line, a `Content-Disposition` of 61 bytes plus the filename, a
|
|
209
|
+
* 42-byte content-type-and-blank-line and a 2-byte trailing CRLF, so 156 bytes
|
|
210
|
+
* plus the filename PER FILE, plus a 53-byte terminator. A 1,000-file bundle
|
|
211
|
+
* therefore carries ~180 KiB that the old measure could not see, and a bundle
|
|
212
|
+
* whose content sat just under the ceiling passed the pre-flight and then earned
|
|
213
|
+
* the 413 this check exists to prevent — 11 seconds for 30 MB, measured.
|
|
214
|
+
*
|
|
215
|
+
* No headroom constant closes that gap, and none is added: the exact number is
|
|
216
|
+
* available for free by building the body first and asking it how long it is.
|
|
217
|
+
* The body is then RETURNED, so the bytes that were weighed are the bytes that go
|
|
218
|
+
* out; a gate that only inspected its inputs would be one refactor away from
|
|
219
|
+
* measuring something the caller no longer sends.
|
|
220
|
+
*
|
|
221
|
+
* The build-time `PACKAGED_SIZE_EXCEEDS_API_LIMIT` cannot do this — there is no
|
|
222
|
+
* request at build time — so it weighs files on disk and says so. The two
|
|
223
|
+
* messages name what each measured rather than pretending to be the same number.
|
|
224
|
+
*/
|
|
225
|
+
export function buildUploadBodyOrRefuse(fields, files, bundleRoot) {
|
|
226
|
+
const multipart = buildMultipartFormData(fields, [...files]);
|
|
227
|
+
// `>`, not `>=`: a body of exactly the ceiling was ACCEPTED by the live API and
|
|
228
|
+
// one byte more was refused 413. The two measured rows, and why the build-time
|
|
229
|
+
// lane keeps `>=` on a different quantity, are in this function's doc comment.
|
|
230
|
+
if (multipart.body.length > API_SKILL_MAX_UPLOAD_BYTES) {
|
|
231
|
+
const sized = files.map(f => ({ path: bundleRelativeName(f.filename, bundleRoot), bytes: f.content.length }));
|
|
232
|
+
const measure = { of: 'upload-request', bytes: multipart.body.length };
|
|
233
|
+
throw new Error(`${describeOversizeBundle(sized, measure)}. The API will refuse this upload.`);
|
|
234
|
+
}
|
|
235
|
+
return multipart;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* An uploaded part's filename in the SAME namespace the build-time finding uses.
|
|
239
|
+
*
|
|
240
|
+
* A directory's parts are keyed `<declared-name>/<bundle-relative-path>`, because
|
|
241
|
+
* that is how the API roots them; `PACKAGED_SIZE_EXCEEDS_API_LIMIT` names files
|
|
242
|
+
* relative to the bundle root. Printing the keyed spelling here meant the path an
|
|
243
|
+
* author copied out of an upload refusal never matched the `link` their build had
|
|
244
|
+
* reported, so a `validation.allow` entry written from one message did not waive
|
|
245
|
+
* the other. The prefix is stripped when it is there and left alone when it is
|
|
246
|
+
* not — a ZIP is one part named for the archive itself, with no root to strip.
|
|
247
|
+
*/
|
|
248
|
+
function bundleRelativeName(filename, bundleRoot) {
|
|
249
|
+
if (bundleRoot === undefined)
|
|
250
|
+
return filename;
|
|
251
|
+
// Both sides normalized before comparing. The parts are minted from
|
|
252
|
+
// `safePath.relative`, so they are already forward-slashed — but a prefix test
|
|
253
|
+
// that only holds because of where its inputs came from is one refactor from
|
|
254
|
+
// silently never matching on Windows.
|
|
255
|
+
const name = toForwardSlashAnyPlatform(filename);
|
|
256
|
+
const prefix = `${toForwardSlashAnyPlatform(bundleRoot)}/`;
|
|
257
|
+
return name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* A failed request, re-thrown with the command that answers it when — and only
|
|
261
|
+
* when — the API said one of the specific things listed by the caller.
|
|
262
|
+
*
|
|
263
|
+
* **What is matched, exactly:** an {@link ApiRequestError} whose `statusCode` is
|
|
264
|
+
* 400 and whose message satisfies every pattern of one {@link
|
|
265
|
+
* VendorRefusalRemedy}. Both live wordings are measured, not guessed:
|
|
266
|
+
* `400 Skill cannot reuse an existing display_title` and
|
|
267
|
+
* `400 Cannot delete skill with existing versions. Delete all versions first.`
|
|
268
|
+
*
|
|
269
|
+
* **How it fails safe:** anything that does not match is returned UNTOUCHED, so
|
|
270
|
+
* an unrelated 400 keeps the API's exact words and gets no misleading remedy. If
|
|
271
|
+
* the vendor rewords a refusal, the operator loses a hint — they never gain a
|
|
272
|
+
* wrong one. The remedy is appended, so the vendor's sentence survives in full.
|
|
273
|
+
*
|
|
274
|
+
* One mechanism, several refusals, and each call site passes only the refusals
|
|
275
|
+
* that are REACHABLE from it — a second copy of this matcher per case is how the
|
|
276
|
+
* two would come to disagree about what "fail safe" means.
|
|
277
|
+
*/
|
|
278
|
+
export function withRemedy(error, candidates) {
|
|
279
|
+
if (!(error instanceof ApiRequestError) || error.statusCode !== 400)
|
|
280
|
+
return error;
|
|
281
|
+
const message = error.message;
|
|
282
|
+
const matched = candidates.find(c => c.matches.every(pattern => pattern.test(message)));
|
|
283
|
+
if (matched === undefined)
|
|
284
|
+
return error;
|
|
285
|
+
return new ApiRequestError(`${message}\n${matched.remedy}`, error.statusCode, error.retryAfterHeader);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The display title is taken.
|
|
289
|
+
*
|
|
290
|
+
* It tells the operator how to FIND the id and does not offer to find it for
|
|
291
|
+
* them: `display_title` is unique only when the field is sent explicitly, so a
|
|
292
|
+
* workspace can hold several skills of one title and a title→id lookup matches
|
|
293
|
+
* none, one, or several. Appending a version to the wrong match is silent and
|
|
294
|
+
* destroys somebody else's skill, so the id is always the operator's to supply.
|
|
295
|
+
*/
|
|
296
|
+
const DUPLICATE_TITLE_REFUSAL = {
|
|
297
|
+
matches: [/display_title/i, /reuse|already|exist|duplicat|unique/i],
|
|
298
|
+
remedy: 'This workspace already has a skill with that display title, and `install` only ever CREATES. '
|
|
299
|
+
+ 'To ship a change to that skill, add a version to it: find its id with '
|
|
300
|
+
+ '`vat claude org skills list`, then run '
|
|
301
|
+
+ '`vat claude org skills versions add <skill-id> <source>`. '
|
|
302
|
+
+ 'VAT will not turn the title into an id for you — display_title is not unique in general '
|
|
303
|
+
+ '(the API enforces it only when the field is sent), so a title can match none, one, or '
|
|
304
|
+
+ 'several skills. To create a genuinely separate skill instead, pass a different --title.',
|
|
305
|
+
};
|
|
306
|
+
/**
|
|
307
|
+
* The skill still has versions, so it cannot be deleted.
|
|
308
|
+
*
|
|
309
|
+
* `--all` leads, because it IS the one-step path: the `--all` branch of this
|
|
310
|
+
* command fetches every version and deletes them before deleting the skill. The
|
|
311
|
+
* by-hand sequence follows for an operator who wants to see what is there first.
|
|
312
|
+
* Naming only `versions delete` would send them round a loop they can already
|
|
313
|
+
* ask the CLI to run.
|
|
314
|
+
*/
|
|
315
|
+
export const EXISTING_VERSIONS_REFUSAL = {
|
|
316
|
+
matches: [/versions?/i, /cannot delete|delete all version|existing version/i],
|
|
317
|
+
remedy: 'A skill cannot be deleted while it still has versions. '
|
|
318
|
+
+ 'Re-run with `--all` — `vat claude org skills delete <skill-id> --all` deletes every '
|
|
319
|
+
+ 'version and then the skill, in one command. To do it by hand instead: list them with '
|
|
320
|
+
+ '`vat claude org skills versions list <skill-id>`, delete each with '
|
|
321
|
+
+ '`vat claude org skills versions delete <skill-id> <version>`, then delete the skill.',
|
|
322
|
+
};
|
|
323
|
+
/** Only the CREATE path can earn a duplicate-title refusal — see {@link uploadSkillDir}. */
|
|
324
|
+
export function withDuplicateTitleRemedy(error) {
|
|
325
|
+
return withRemedy(error, [DUPLICATE_TITLE_REFUSAL]);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* The version's SKILL.md declares a different `name` than the skill it is being
|
|
329
|
+
* added to.
|
|
330
|
+
*
|
|
331
|
+
* The server enforces name consistency per `skill_id` (measured), which is why
|
|
332
|
+
* `versions add` cannot silently re-root a skill's file tree and why this is the
|
|
333
|
+
* one refusal that endpoint can earn. The remedy names both fixes, because which
|
|
334
|
+
* one is right depends on what the operator meant: keep the skill and put its
|
|
335
|
+
* name back, or publish the renamed tree as the separate skill it now is.
|
|
336
|
+
*
|
|
337
|
+
* ⚠️ The vendor's exact wording here has NOT been measured, only the fact of the
|
|
338
|
+
* refusal. `withRemedy` fails safe on no match — the operator loses a hint, never
|
|
339
|
+
* gains a wrong one — so a guessed pattern is the conservative thing to ship.
|
|
340
|
+
*/
|
|
341
|
+
export const VERSION_NAME_MISMATCH_REFUSAL = {
|
|
342
|
+
matches: [/\bname\b/i, /match|consistent|differ|mismatch|same skill|must be/i],
|
|
343
|
+
remedy: "This skill's files are rooted at the `name` its SKILL.md frontmatter declared when it was "
|
|
344
|
+
+ 'created, and the API refuses a version that declares a different one. Either restore the '
|
|
345
|
+
+ 'original name in this tree\'s SKILL.md and re-run, or — if the rename was deliberate — '
|
|
346
|
+
+ 'publish the renamed tree as a NEW skill with '
|
|
347
|
+
+ '`vat claude org skills install <source>`, which always creates.',
|
|
348
|
+
};
|
|
349
|
+
/** Only the VERSION path can earn a name-consistency refusal — see {@link uploadSkillVersionDir}. */
|
|
350
|
+
export function withVersionRootRemedy(error) {
|
|
351
|
+
return withRemedy(error, [VERSION_NAME_MISMATCH_REFUSAL]);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* The refusals a `skills delete` run WITHOUT `--all` can earn.
|
|
355
|
+
*
|
|
356
|
+
* The empty counterpart is not a missing case, it is the answer for a run that
|
|
357
|
+
* already used `--all`: this command's `--all` branch deletes every version
|
|
358
|
+
* before the skill, so an operator who just watched it do that does not need to
|
|
359
|
+
* be told to run `--all` — that is exactly the loop the remedy exists to break.
|
|
360
|
+
* A refusal after `--all` means something the version listing did not show, and
|
|
361
|
+
* the vendor's own sentence is then the honest whole of what VAT knows.
|
|
18
362
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
363
|
+
const DELETE_REMEDIES = [EXISTING_VERSIONS_REFUSAL];
|
|
364
|
+
const DELETE_REMEDIES_AFTER_ALL = [];
|
|
365
|
+
/** DELETE one skill, explaining the refusals this invocation could earn. */
|
|
366
|
+
async function deleteSkillOrExplain(client, skillId, remedies) {
|
|
367
|
+
try {
|
|
368
|
+
return await client.deleteSkill(skillId);
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
throw withRemedy(error, remedies);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* A failure that got NO answer from the server, re-thrown saying what was
|
|
376
|
+
* actually sent and what a retry would risk.
|
|
377
|
+
*
|
|
378
|
+
* ⛔ **The trigger is a RECORDED FACT, never the error's class.** This used to
|
|
379
|
+
* read `if (error instanceof ApiRequestError) return error;` — "a completed
|
|
380
|
+
* exchange always arrives as an `ApiRequestError`, so anything else must be a
|
|
381
|
+
* dropped connection". The first half is true and the converse is not, and the
|
|
382
|
+
* converse is what shipped: `buildSkillsHeaders()` throws a plain `Error` before
|
|
383
|
+
* a socket is ever opened, so
|
|
384
|
+
* `ANTHROPIC_API_KEY= vat claude org skills install <dir>` told a first-time
|
|
385
|
+
* operator, on top of the real "no key" message, that a connection had closed,
|
|
386
|
+
* that VAT had sent 6.8 KiB (the length of a buffer that never left the process),
|
|
387
|
+
* and that the outcome was unknown — then sent them to a recovery command that
|
|
388
|
+
* fails with the identical missing-key error. Three false statements and a loop.
|
|
389
|
+
*
|
|
390
|
+
* So the gate is POSITIVE and evidential: annotate only an
|
|
391
|
+
* {@link ApiTransportError}, which exists only once the transport has been
|
|
392
|
+
* reached and carries `bytesSent` read off the socket at failure time. Anything
|
|
393
|
+
* else — a missing credential, a bad URL, a response VAT refused to read —
|
|
394
|
+
* passes through with its own message untouched.
|
|
395
|
+
*
|
|
396
|
+
* Two outcomes, because the byte count decides which is TRUE:
|
|
397
|
+
*
|
|
398
|
+
* - **Nothing left the machine** (`bytesSent === 0`: DNS never resolved, the
|
|
399
|
+
* connection was refused, the handshake failed). The outcome is not unknown,
|
|
400
|
+
* it is known: nothing happened. Saying "check what exists" here would send
|
|
401
|
+
* an operator to look for a skill that was never sent.
|
|
402
|
+
* - **Some bytes went out and no status came back.** Now the outcome genuinely
|
|
403
|
+
* is unknown, and the part the operator cannot see is how far it got. The
|
|
404
|
+
* client never replays a POST — one that failed with no status may still have
|
|
405
|
+
* taken effect — so the retry is theirs, and it can leave two skills behind.
|
|
406
|
+
*
|
|
407
|
+
* **No threshold.** There is no constant here for "close to the ceiling" and
|
|
408
|
+
* there must not be: an invented margin is a number nobody can re-derive. The
|
|
409
|
+
* bytes sent, the body size and the ceiling are printed and the reader draws
|
|
410
|
+
* their own conclusion.
|
|
411
|
+
*/
|
|
412
|
+
export function explainAnsweredNothing(error, bodyBytes) {
|
|
413
|
+
if (!(error instanceof ApiTransportError))
|
|
414
|
+
return error;
|
|
415
|
+
if (error.bytesSent === 0) {
|
|
416
|
+
return new Error(`${error.message}\nNo byte of the request left this machine, so nothing was created and `
|
|
417
|
+
+ 'there is nothing to clean up. Fix the connection and re-run the same command.', { cause: error });
|
|
418
|
+
}
|
|
419
|
+
return new Error(`${error.message}\nThe connection closed before the API answered, so this is not a verdict on `
|
|
420
|
+
+ `the upload. VAT had sent ${formatBytes(error.bytesSent)} of a ${formatBytes(bodyBytes)} `
|
|
421
|
+
+ `request body against a ${formatBytes(API_SKILL_MAX_UPLOAD_BYTES)} ceiling; the API has `
|
|
422
|
+
+ 'been observed to drop a connection near that ceiling instead of returning 413. Whether '
|
|
423
|
+
+ 'anything was created is unknown — VAT never replays a POST, because one that failed with '
|
|
424
|
+
+ 'no status may still have taken effect. Check what exists '
|
|
425
|
+
+ '(`vat claude org skills list`, or `vat claude org skills versions list <skill-id>`) '
|
|
426
|
+
+ 'before re-running this command.', { cause: error });
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* THE one call that crosses the wire, with both post-hoc explanations applied:
|
|
430
|
+
* the transport annotation (which describes the REQUEST) first, then whichever
|
|
431
|
+
* vendor refusals this endpoint can actually earn.
|
|
432
|
+
*
|
|
433
|
+
* Shared so the two upload endpoints cannot drift on either. Reading the
|
|
434
|
+
* response is deliberately OUTSIDE the try — a response-shape refusal is VAT's
|
|
435
|
+
* own verdict on a completed exchange and must never be dressed up as a
|
|
436
|
+
* transport failure.
|
|
437
|
+
*/
|
|
438
|
+
async function sendUpload(send, bodyBytes, remedy) {
|
|
439
|
+
try {
|
|
440
|
+
return await send();
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
throw remedy(explainAnsweredNothing(error, bodyBytes));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Resolve a `<source>` CLI argument to an absolute path.
|
|
448
|
+
*
|
|
449
|
+
* 🪤 The test used to be `source.startsWith('/')`, which is false for
|
|
450
|
+
* `D:\builds\skill` — so a Windows operator's absolute path was joined onto the
|
|
451
|
+
* working directory and reported back as `Source not found: <cwd>/D:/builds/skill`.
|
|
452
|
+
* {@link isAbsoluteAnyPlatform} answers for POSIX roots, drive letters and UNC
|
|
453
|
+
* paths on EVERY host, so the behaviour is the same wherever it runs and a
|
|
454
|
+
* POSIX-only CI can see the drive-letter case at all.
|
|
455
|
+
*/
|
|
456
|
+
export function resolveSourceArgument(source) {
|
|
457
|
+
return isAbsoluteAnyPlatform(source)
|
|
458
|
+
? toForwardSlashAnyPlatform(source)
|
|
459
|
+
: safePath.resolve(process.cwd(), source);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* The largest single markdown member this will decompress to read.
|
|
463
|
+
*
|
|
464
|
+
* Not a limit on what may be PUBLISHED — the API's own ceiling decides that, and
|
|
465
|
+
* a bigger document simply goes unread rather than refused. It bounds the
|
|
466
|
+
* allocations this module performs on attacker-supplied metadata: `getData()`
|
|
467
|
+
* inflates whatever `header.size` claims, and a header can claim anything. A
|
|
468
|
+
* megabyte is two orders of magnitude above any real skill document.
|
|
469
|
+
*
|
|
470
|
+
* ⚠️ It is a PER-ENTRY bound. The total is bounded separately, and more
|
|
471
|
+
* tightly: nothing is inflated at all until the archive's declared uncompressed
|
|
472
|
+
* total is known to be within {@link API_SKILL_MAX_UPLOAD_BYTES}.
|
|
473
|
+
*/
|
|
474
|
+
const MAX_INSPECTED_DOCUMENT_BYTES = 1024 * 1024;
|
|
475
|
+
/**
|
|
476
|
+
* What a ZIP will weigh AFTER the API expands it, read from the archive's own
|
|
477
|
+
* central directory — plus the name its inner SKILL.md declares, any answer key
|
|
478
|
+
* it carries, and the markdown members the portability checks read.
|
|
479
|
+
*
|
|
480
|
+
* 🔑 **The API expands a ZIP and applies the ceiling to the UNCOMPRESSED total,
|
|
481
|
+
* not to the archive.** Measured against the live API by an adopter: a
|
|
482
|
+
* 10,707,463-byte archive of a 47.85 MiB tree — comfortably inside the request
|
|
483
|
+
* ceiling as bytes on the wire — came back
|
|
484
|
+
* `400: Zip file uncompressed size exceeds 30MB`. So the body check every other
|
|
485
|
+
* shape passes through is structurally blind here: compression is exactly the
|
|
486
|
+
* thing that makes the measured quantity stop predicting the enforced one. A
|
|
487
|
+
* ZIP is therefore the one input that could pass every local gate and still be
|
|
488
|
+
* refused remotely, which is the same defect class as the ZIP path once
|
|
489
|
+
* skipping the size check altogether.
|
|
490
|
+
*
|
|
491
|
+
* ⚠️ **The boundary itself is UNMEASURED on this lane.** One refusal at 47.85
|
|
492
|
+
* MiB is all the evidence there is; nobody has bracketed where the uncompressed
|
|
493
|
+
* rule actually cuts, or confirmed it is the same constant as the request lane.
|
|
494
|
+
* So this refuses only on `>` — strictly the looser choice — and a bundle near
|
|
495
|
+
* the line is still sent for the API to judge. Do not tighten this to `>=`, or
|
|
496
|
+
* quote it as a measured ceiling, without bracketing it the way
|
|
497
|
+
* {@link API_SKILL_MAX_UPLOAD_BYTES} was bracketed.
|
|
498
|
+
*
|
|
499
|
+
* ⛔ Sizes come from entry HEADERS, which an archive can lie about. That is
|
|
500
|
+
* fine: a lie can only change whether VAT refuses early, never whether the API
|
|
501
|
+
* does.
|
|
502
|
+
*
|
|
503
|
+
* ⚠️ **Reading an entry is not free, and the docstring here used to claim it
|
|
504
|
+
* was** — "no zip-slip or zip-bomb surface is opened — the entries are read, not
|
|
505
|
+
* written" was true of the enumeration half and FALSE of the one `getData()`
|
|
506
|
+
* call, which inflates `header.size` bytes into memory. Worse, it ran BEFORE the
|
|
507
|
+
* caller's uncompressed-total gate, so the ceiling this function exists to
|
|
508
|
+
* enforce could not protect the inflation this function performed. Nothing is
|
|
509
|
+
* written to disk — that part stands — but the decompression is now ordered
|
|
510
|
+
* after the total is known and bounded by {@link MAX_INSPECTED_DOCUMENT_BYTES},
|
|
511
|
+
* so an archive declaring a 4 GiB SKILL.md is enumerated and never inflated.
|
|
512
|
+
*/
|
|
513
|
+
export async function inspectZipArchive(zipPath) {
|
|
514
|
+
// `adm-zip` is already a runtime dependency of this package (the packager and
|
|
515
|
+
// the url skill-source both use it). An earlier comment here reasoned that
|
|
516
|
+
// reading the archive would mean "adding a dependency … for a log line" and
|
|
517
|
+
// declined on that basis; the premise was false, and the payoff is not a log
|
|
518
|
+
// line but a refusal that saves a doomed upload of up to 30 MiB.
|
|
519
|
+
const AdmZip = (await import('adm-zip')).default;
|
|
520
|
+
let entries;
|
|
521
|
+
try {
|
|
522
|
+
entries = new AdmZip(zipPath).getEntries();
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
// An archive VAT cannot PARSE is not an archive VAT should block. The API
|
|
526
|
+
// is the authority on the upload either way, and refusing here would turn
|
|
527
|
+
// "our reader disagrees with your zip tool" into a failed publish. The
|
|
528
|
+
// caller is told why, so "checks skipped" is never silent.
|
|
529
|
+
//
|
|
530
|
+
// An archive the FILESYSTEM refuses is a different thing: the path was
|
|
531
|
+
// readable a moment ago (the caller holds its bytes), so a refusal here
|
|
532
|
+
// is not a parse disagreement and stays loud.
|
|
533
|
+
if (isFilesystemAccessError(error))
|
|
534
|
+
throw error;
|
|
535
|
+
return { archiveUnreadable: describeZipFailure(error) };
|
|
536
|
+
}
|
|
537
|
+
// PASS 1 — headers only. Nothing is decompressed while this runs, so both
|
|
538
|
+
// refusals the caller can reach are decided on metadata alone.
|
|
539
|
+
const files = entries.filter(entry => !entry.isDirectory);
|
|
540
|
+
const uncompressedBytes = files.reduce((sum, entry) => sum + entry.header.size, 0);
|
|
541
|
+
const neverUploaded = files
|
|
542
|
+
.map(entry => toForwardSlashAnyPlatform(entry.entryName))
|
|
543
|
+
.filter(name => name.split('/').some(segment => NEVER_UPLOADED_DIR_NAMES.has(segment)));
|
|
544
|
+
// PASS 2 — the entries worth inflating, and only once the total says this
|
|
545
|
+
// archive is not already refused. An over-ceiling archive is about to be
|
|
546
|
+
// rejected by the caller, so inflating anything out of it would be work spent
|
|
547
|
+
// on an upload that cannot happen.
|
|
548
|
+
if (uncompressedBytes > API_SKILL_MAX_UPLOAD_BYTES) {
|
|
549
|
+
return {
|
|
550
|
+
uncompressedBytes, declaredName: undefined, neverUploaded, documents: [], bundleRoot: undefined, unreadable: [],
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
const elected = electShallowestSkillMd(files);
|
|
554
|
+
const unreadable = [];
|
|
22
555
|
return {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
556
|
+
uncompressedBytes,
|
|
557
|
+
declaredName: declaredNameOf(elected, unreadable),
|
|
558
|
+
neverUploaded,
|
|
559
|
+
documents: inflateMarkdownMembers(files, unreadable),
|
|
560
|
+
bundleRoot: archiveRootOf(elected?.entryName),
|
|
561
|
+
unreadable,
|
|
27
562
|
};
|
|
28
563
|
}
|
|
564
|
+
/** adm-zip throws plain `Error`s; the message is all it has to say. */
|
|
565
|
+
function describeZipFailure(error) {
|
|
566
|
+
return error instanceof Error ? error.message : String(error);
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Every markdown member of the archive, as an upload part.
|
|
570
|
+
*
|
|
571
|
+
* 🔑 This exists so the ZIP lane can run the portability family through the ONE
|
|
572
|
+
* function the directory lane runs it through. Until it did, `install <zip>`
|
|
573
|
+
* performed none of the checks `install --help` promises of "every markdown
|
|
574
|
+
* document in the bundle": the archive went to `sendSkillUpload` as one opaque
|
|
575
|
+
* part, and an operator following the documented contract got silence where they
|
|
576
|
+
* were promised warnings. Routing the parts back through
|
|
577
|
+
* {@link warnUnportableReferences} keeps ONE implementation of the checks — a
|
|
578
|
+
* second copy of the three collector calls is how the two lanes start disagreeing.
|
|
579
|
+
*
|
|
580
|
+
* An entry that will not inflate is SKIPPED, never fatal: this module's standing
|
|
581
|
+
* position is that an archive VAT cannot read is not an archive VAT should block,
|
|
582
|
+
* and the checks it feeds only ever warn.
|
|
583
|
+
*/
|
|
584
|
+
function inflateMarkdownMembers(files, unreadable) {
|
|
585
|
+
const documents = [];
|
|
586
|
+
for (const entry of files) {
|
|
587
|
+
const filename = toForwardSlashAnyPlatform(entry.entryName);
|
|
588
|
+
if (!filename.toLowerCase().endsWith('.md'))
|
|
589
|
+
continue;
|
|
590
|
+
if (entry.header.size > MAX_INSPECTED_DOCUMENT_BYTES)
|
|
591
|
+
continue;
|
|
592
|
+
const content = inflateOrRecord(entry, unreadable);
|
|
593
|
+
if (content !== undefined)
|
|
594
|
+
documents.push({ fieldName: 'files[]', filename, content });
|
|
595
|
+
}
|
|
596
|
+
return documents;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* One member's bytes, or `undefined` with the refusal RECORDED.
|
|
600
|
+
*
|
|
601
|
+
* `getData()` is the only decompression in this module, so it is the only thing
|
|
602
|
+
* that can throw here: adm-zip raises on BAD_CRC, on any compression method but
|
|
603
|
+
* store/deflate, and on an encrypted entry. Those used to escape and abort the
|
|
604
|
+
* publish on exit 2 — contradicting this module's own stance that an archive VAT
|
|
605
|
+
* cannot parse is not an archive VAT should block — and then, once caught, were
|
|
606
|
+
* dropped without a word. The member is skipped AND named.
|
|
607
|
+
*/
|
|
608
|
+
function inflateOrRecord(entry, unreadable) {
|
|
609
|
+
try {
|
|
610
|
+
return entry.getData();
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
// Once per member: the elected SKILL.md is asked for twice (its name, then
|
|
614
|
+
// its text), and one refusal is one fact.
|
|
615
|
+
const name = toForwardSlashAnyPlatform(entry.entryName);
|
|
616
|
+
if (!unreadable.some((failure) => failure.entry === name)) {
|
|
617
|
+
unreadable.push({ entry: name, reason: describeZipFailure(error) });
|
|
618
|
+
}
|
|
619
|
+
return undefined;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* The directory the elected SKILL.md sits in, which is the archive's bundle root.
|
|
624
|
+
*
|
|
625
|
+
* `my-skill/SKILL.md` roots the bundle at `my-skill`; a SKILL.md at the archive
|
|
626
|
+
* root has no prefix, and neither does an archive with no SKILL.md at all. ZIP
|
|
627
|
+
* fixes `/` as the entry separator, so this reads the format rather than handling
|
|
628
|
+
* a host path.
|
|
629
|
+
*/
|
|
630
|
+
function archiveRootOf(entryName) {
|
|
631
|
+
if (entryName === undefined)
|
|
632
|
+
return undefined;
|
|
633
|
+
const cut = toForwardSlashAnyPlatform(entryName).lastIndexOf('/');
|
|
634
|
+
return cut === -1 ? undefined : entryName.slice(0, cut);
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* The SKILL.md nearest the archive root, which is the one whose `name` the API
|
|
638
|
+
* will key the skill by.
|
|
639
|
+
*
|
|
640
|
+
* 🪤 Compared on DEPTH, with name length only as a tiebreak. It used to compare
|
|
641
|
+
* `entryName.length` alone, on the reasoning that "a shortest-path comparison
|
|
642
|
+
* needs no separator at all" — but shortest string is not shallowest path:
|
|
643
|
+
* `a/b/SKILL.md` (12 chars, depth 2) beats `my-skill-bundle/SKILL.md` (24
|
|
644
|
+
* chars, depth 1), so a bundled FIXTURE supplied the name and the operator was
|
|
645
|
+
* advised to `--title` it. ZIP fixes `/` as the entry separator on every
|
|
646
|
+
* platform, so splitting on it is not path handling — it is reading the format.
|
|
647
|
+
*/
|
|
648
|
+
function electShallowestSkillMd(files) {
|
|
649
|
+
let elected;
|
|
650
|
+
for (const entry of files) {
|
|
651
|
+
if (basename(entry.entryName) !== 'SKILL.md')
|
|
652
|
+
continue;
|
|
653
|
+
if (elected !== undefined && !isShallower(entry.entryName, elected.entryName))
|
|
654
|
+
continue;
|
|
655
|
+
elected = entry;
|
|
656
|
+
}
|
|
657
|
+
return elected;
|
|
658
|
+
}
|
|
659
|
+
/** Fewer path segments wins; equal depth falls back to the shorter name. */
|
|
660
|
+
function isShallower(candidate, incumbent) {
|
|
661
|
+
const candidateDepth = zipEntryDepth(candidate);
|
|
662
|
+
const incumbentDepth = zipEntryDepth(incumbent);
|
|
663
|
+
return candidateDepth === incumbentDepth
|
|
664
|
+
? candidate.length < incumbent.length
|
|
665
|
+
: candidateDepth < incumbentDepth;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* How many segments a ZIP entry name has.
|
|
669
|
+
*
|
|
670
|
+
* Counted rather than `split('/')`-ed because the linter — correctly, for
|
|
671
|
+
* FILESYSTEM paths — refuses that idiom: a host path's separator depends on the
|
|
672
|
+
* platform. A ZIP entry name's does not; the format fixes it as `/`. Counting
|
|
673
|
+
* the separator states that, and needs no exemption.
|
|
674
|
+
*/
|
|
675
|
+
function zipEntryDepth(entryName) {
|
|
676
|
+
let depth = 1;
|
|
677
|
+
for (const character of entryName) {
|
|
678
|
+
if (character === '/')
|
|
679
|
+
depth += 1;
|
|
680
|
+
}
|
|
681
|
+
return depth;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* The frontmatter `name` inside the ELECTED entry — or `undefined`, including
|
|
685
|
+
* when that entry declares none.
|
|
686
|
+
*
|
|
687
|
+
* 🪤 It used to be `declaredSkillNameIn(…) ?? declaredName`, which KEPT the
|
|
688
|
+
* previous (deeper) candidate's name when a newly-elected shallower SKILL.md had
|
|
689
|
+
* no `name`. The elected file and the reported name then came from two different
|
|
690
|
+
* documents, and the divergence warning named a name no operator could find at
|
|
691
|
+
* the root of their archive. The election decides; whatever it holds is the
|
|
692
|
+
* answer, absence included.
|
|
693
|
+
*
|
|
694
|
+
* An elected SKILL.md that will not inflate costs only the name (the size total
|
|
695
|
+
* is already computed from headers) — see {@link inflateOrRecord}, which records
|
|
696
|
+
* the refusal so the operator learns why no name was read.
|
|
697
|
+
*/
|
|
698
|
+
function declaredNameOf(entry, unreadable) {
|
|
699
|
+
if (entry === undefined || entry.header.size > MAX_INSPECTED_DOCUMENT_BYTES)
|
|
700
|
+
return undefined;
|
|
701
|
+
const content = inflateOrRecord(entry, unreadable);
|
|
702
|
+
return content === undefined ? undefined : declaredSkillNameIn(content.toString('utf8'));
|
|
703
|
+
}
|
|
704
|
+
async function sendSkillUpload(client, displayTitle, files, bundleRoot) {
|
|
705
|
+
const multipart = buildUploadBodyOrRefuse({ display_title: displayTitle }, files, bundleRoot);
|
|
706
|
+
return readCreateSkillResponse(await sendUpload(() => client.uploadSkill(multipart), multipart.body.length, withDuplicateTitleRemedy));
|
|
707
|
+
}
|
|
29
708
|
/**
|
|
30
709
|
* The name a SKILL.md declares, which is both the uploaded skill's display
|
|
31
710
|
* title and the top-level directory the API keys it by.
|
|
@@ -37,35 +716,6 @@ function requireDeclaredName(skillMdPath) {
|
|
|
37
716
|
}
|
|
38
717
|
return declared;
|
|
39
718
|
}
|
|
40
|
-
/**
|
|
41
|
-
* Directories never uploaded to the organization, at any depth, by NAME.
|
|
42
|
-
*
|
|
43
|
-
* `evals/` is the conventional home of a skill's eval suite — its answer key.
|
|
44
|
-
* A correctly built skill directory (what this command documents as its input)
|
|
45
|
-
* never contains one, because the packager excludes declared test input; this
|
|
46
|
-
* is the backstop for the easy mistake of pointing the uploader at the *source*
|
|
47
|
-
* tree instead, where the suite does live. The invariant is "a published skill
|
|
48
|
-
* carries no answer key", not "…none when the operator remembered to build".
|
|
49
|
-
*
|
|
50
|
-
* The name match alone cannot uphold that invariant, because the suite's
|
|
51
|
-
* location is the ADOPTER's to declare (`skills.config.<name>.test.evals`;
|
|
52
|
-
* `evals/evals.json` is only the default). It stays as the unconditional
|
|
53
|
-
* fail-safe for the case where no config is discoverable at all — a fetched
|
|
54
|
-
* artifact, an extracted tarball, a tree outside any VAT project — where there
|
|
55
|
-
* is no declaration to read and the convention is the only thing left to honor.
|
|
56
|
-
* The declared location is resolved separately, in
|
|
57
|
-
* {@link declaredTestInputPaths}, and the two are unioned.
|
|
58
|
-
*
|
|
59
|
-
* This lane is deliberately BROADER than the packager, which excludes exactly
|
|
60
|
-
* `<skill-root>/evals` and never guesses from a name (see test-input.ts). Here
|
|
61
|
-
* a mistaken input is the entire scenario and the blast radius is an org-wide
|
|
62
|
-
* publish, so over-withholding a directory literally named `evals` is the cheap
|
|
63
|
-
* error and it is reported rather than silent.
|
|
64
|
-
*
|
|
65
|
-
* `node_modules`/`.git` are development detritus that has no meaning inside a
|
|
66
|
-
* published skill and would silently bloat the multipart payload.
|
|
67
|
-
*/
|
|
68
|
-
const NEVER_UPLOADED_DIR_NAMES = new Set(['evals', 'node_modules', '.git']);
|
|
69
719
|
/**
|
|
70
720
|
* The absolute path of this skill's DECLARED eval suite, when its governing VAT
|
|
71
721
|
* config declares one, as a set of paths to withhold.
|
|
@@ -87,31 +737,97 @@ const NEVER_UPLOADED_DIR_NAMES = new Set(['evals', 'node_modules', '.git']);
|
|
|
87
737
|
* skill dir — nothing inside the tree to withhold.
|
|
88
738
|
*/
|
|
89
739
|
async function declaredTestInputPaths(skillDir) {
|
|
90
|
-
|
|
740
|
+
// `'refuse'`: an upload that cannot tell which paths are test input might
|
|
741
|
+
// publish an answer key. The throw is reported per skill by the install
|
|
742
|
+
// loop's catch (logged, and counted in its failures) — never swallowed.
|
|
743
|
+
const config = await resolveSkillPackagingConfig(safePath.join(skillDir, 'SKILL.md'), 'refuse');
|
|
91
744
|
const declared = config?.test?.evals;
|
|
92
745
|
if (declared === undefined)
|
|
93
746
|
return new Set();
|
|
94
747
|
const unit = evalSuiteUnitPath(safePath.resolve(skillDir), declared);
|
|
95
748
|
return unit === undefined ? new Set() : new Set([unit]);
|
|
96
749
|
}
|
|
750
|
+
/**
|
|
751
|
+
* Whether a directory entry is a symbolic link resolving to a DIRECTORY —
|
|
752
|
+
* throwing, naming the path, when the link cannot be followed at all.
|
|
753
|
+
*
|
|
754
|
+
* `Dirent.isDirectory()` is lstat-based, so it answers `false` for a link to a
|
|
755
|
+
* directory. That entry therefore used to fall into the FILE branch below and
|
|
756
|
+
* `readFileSync` threw a raw `EISDIR`: the upload died on a Node error that
|
|
757
|
+
* named no path and said nothing about what to do. A dangling link produced the
|
|
758
|
+
* same shape of failure with `ENOENT`.
|
|
759
|
+
*
|
|
760
|
+
* `statSync` follows the link, which is the same thing the build-time size walk
|
|
761
|
+
* does to classify one — so both lanes reach the same verdict about the same
|
|
762
|
+
* entry.
|
|
763
|
+
*/
|
|
764
|
+
function resolvesToDirectory(entry, fullPath, relativePath) {
|
|
765
|
+
if (!entry.isSymbolicLink())
|
|
766
|
+
return false;
|
|
767
|
+
try {
|
|
768
|
+
return statSync(fullPath).isDirectory();
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
772
|
+
throw new Error(`Cannot upload ${relativePath}: it is a symbolic link whose target could not be read `
|
|
773
|
+
+ `(${cause}). Replace it with the file or directory it should point at, or remove it.`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
97
776
|
/**
|
|
98
777
|
* Collect the files under a skill directory that should be uploaded,
|
|
99
778
|
* recursively, returning relative paths alongside what was deliberately left
|
|
100
779
|
* out.
|
|
780
|
+
*
|
|
781
|
+
* 🚨 **A symlink is REFUSED, whatever it resolves to.** A multipart body has no
|
|
782
|
+
* way to express a link, so there are only three things this could do with one
|
|
783
|
+
* and two of them are wrong. Skipping it changes what gets published without the
|
|
784
|
+
* skill breaking until someone opens it — and the one thing this collector
|
|
785
|
+
* guarantees is that every withholding is reported. FOLLOWING it is data egress:
|
|
786
|
+
* an earlier version refused only a link resolving to a DIRECTORY, so
|
|
787
|
+
* `notes.md -> /etc/passwd` fell through both branches, `readFileSync` returned
|
|
788
|
+
* the target's bytes, and they were posted under the in-bundle name `notes.md`
|
|
789
|
+
* into a workspace every org member can read. Nothing in the run said a link had
|
|
790
|
+
* been followed: the "every withholding is reported" guarantee covers
|
|
791
|
+
* EXCLUSIONS, not DEREFERENCES. Any file the operator can read left the machine.
|
|
792
|
+
*
|
|
793
|
+
* The vector is not a registry tarball — node-tar 7 de-roots an absolute
|
|
794
|
+
* linkpath and refuses an escaping relative one, measured — but a directory
|
|
795
|
+
* extracted with system `tar`, which does recreate `-> /etc/passwd`, or cloned
|
|
796
|
+
* from an untrusted repo and handed to `install <dir>`.
|
|
797
|
+
*
|
|
798
|
+
* Refusing and naming the path is the only answer that is both complete and
|
|
799
|
+
* honest, and it keeps this lane on the same payload as the build-time size
|
|
800
|
+
* walk, which does not weigh a linked entry either.
|
|
801
|
+
*
|
|
802
|
+
* The never-uploaded NAMES are matched on a linked directory too. Those are
|
|
803
|
+
* never published whatever their type, and the size walk weighs a linked
|
|
804
|
+
* directory as zero bytes either way — so excluding one keeps both lanes on the
|
|
805
|
+
* same payload instead of blocking a publish over a directory neither lane
|
|
806
|
+
* would have sent.
|
|
101
807
|
*/
|
|
102
808
|
function collectFiles(dir, base, testInput, collected) {
|
|
103
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- dir from CLI arg
|
|
104
809
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
105
810
|
const fullPath = safePath.join(dir, entry.name);
|
|
106
811
|
const relativePath = safePath.relative(base, fullPath);
|
|
812
|
+
const linkedDirectory = resolvesToDirectory(entry, fullPath, relativePath);
|
|
813
|
+
const directoryLike = entry.isDirectory() || linkedDirectory;
|
|
107
814
|
if (testInput.has(safePath.resolve(fullPath))
|
|
108
|
-
|| (
|
|
815
|
+
|| (directoryLike && NEVER_UPLOADED_DIR_NAMES.has(entry.name))) {
|
|
109
816
|
collected.excluded.push(relativePath);
|
|
110
817
|
continue;
|
|
111
818
|
}
|
|
112
819
|
if (entry.isDirectory()) {
|
|
113
820
|
collectFiles(fullPath, base, testInput, collected);
|
|
114
821
|
}
|
|
822
|
+
else if (entry.isSymbolicLink()) {
|
|
823
|
+
// Whatever it points at. A link to a FILE used to fall through to the
|
|
824
|
+
// branch below and be read through — see this function's doc comment.
|
|
825
|
+
throw new Error(`Cannot upload ${relativePath}: it is a symbolic link${linkedDirectory ? ' to a directory' : ''}, `
|
|
826
|
+
+ 'which a multipart upload cannot express. VAT will not publish the bytes it points '
|
|
827
|
+
+ 'at under this name: a link out of the skill directory would send whatever the '
|
|
828
|
+
+ 'target holds into a workspace every org member can read. Replace it with a real '
|
|
829
|
+
+ 'copy of the file or directory it points at, or remove it.');
|
|
830
|
+
}
|
|
115
831
|
else {
|
|
116
832
|
collected.files.push({ relativePath, absolutePath: fullPath });
|
|
117
833
|
}
|
|
@@ -129,11 +845,16 @@ export async function collectSkillUploadFiles(skillDir) {
|
|
|
129
845
|
return collected;
|
|
130
846
|
}
|
|
131
847
|
/**
|
|
132
|
-
*
|
|
848
|
+
* Package a skill directory into the multipart file set the API takes.
|
|
849
|
+
*
|
|
850
|
+
* Deliberately separate from SENDING it. Creating a skill and adding a version to
|
|
851
|
+
* one differ only in the endpoint; the bundle, the exclusions and the size ceiling
|
|
852
|
+
* are identical, and a skill that packages one way when created and another way
|
|
853
|
+
* when updated would be the bug this split exists to prevent. Which endpoint gets
|
|
854
|
+
* the bundle is the caller's command, never something inferred from the workspace.
|
|
133
855
|
*/
|
|
134
|
-
async function
|
|
856
|
+
async function prepareSkillUpload(skillDir, titleOverride, logger) {
|
|
135
857
|
const skillMdPath = safePath.join(skillDir, 'SKILL.md');
|
|
136
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- derived from CLI arg
|
|
137
858
|
if (!existsSync(skillMdPath)) {
|
|
138
859
|
throw new Error(`SKILL.md not found in ${skillDir}. Is this a built skill directory?`);
|
|
139
860
|
}
|
|
@@ -146,7 +867,6 @@ async function uploadSkillDir(client, skillDir, titleOverride, logger) {
|
|
|
146
867
|
const collected = await collectSkillUploadFiles(skillDir);
|
|
147
868
|
const files = [];
|
|
148
869
|
for (const file of collected.files) {
|
|
149
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- collected from dir walk
|
|
150
870
|
const content = readFileSync(file.absolutePath);
|
|
151
871
|
files.push({
|
|
152
872
|
fieldName: 'files[]',
|
|
@@ -154,30 +874,164 @@ async function uploadSkillDir(client, skillDir, titleOverride, logger) {
|
|
|
154
874
|
content,
|
|
155
875
|
});
|
|
156
876
|
}
|
|
157
|
-
|
|
158
|
-
|
|
877
|
+
// Reported on the collected files, so the exclusions listed below are already
|
|
878
|
+
// accounted for — this is the bundle actually being sent. It is FILE BYTES and
|
|
879
|
+
// says so: the ceiling is enforced on the multipart body, which adds ~156 bytes
|
|
880
|
+
// plus the filename per part, and a reader who mistook this line for the
|
|
881
|
+
// measured quantity would not understand a refusal that names a larger number.
|
|
882
|
+
const contentBytes = files.reduce((sum, f) => sum + f.content.length, 0);
|
|
883
|
+
logger.info(` ${dirName}: ${files.length} ${files.length === 1 ? 'file' : 'files'}, `
|
|
884
|
+
+ `${formatBytes(contentBytes)} of file content, title="${displayTitle}"`);
|
|
159
885
|
for (const excluded of collected.excluded) {
|
|
160
886
|
logger.info(` Excluded from upload: ${excluded} (never published with a skill)`);
|
|
161
887
|
}
|
|
162
|
-
|
|
888
|
+
// The SAME merge `declaredTestInputPaths` read to find the eval suite — both
|
|
889
|
+
// resolutions are served from `loadConfigCached` and the per-root discovery
|
|
890
|
+
// cache, so asking twice costs one map lookup rather than a second walk-up.
|
|
891
|
+
// Resolved HERE rather than plumbed out of `collectSkillUploadFiles`, which is
|
|
892
|
+
// exported and deliberately resolves its own so no caller can hand it a config
|
|
893
|
+
// with the exclusion skipped.
|
|
894
|
+
const packaging = await resolveSkillPackagingConfig(skillMdPath, 'refuse');
|
|
895
|
+
warnUnportableReferences(files, dirName, packaging?.validation, logger);
|
|
896
|
+
return { displayTitle, files, dirName };
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Warn about references in the bundle that cannot resolve once it is published.
|
|
900
|
+
*
|
|
901
|
+
* 🚨 **Until this existed, `install` validated NOTHING.** It weighed the request
|
|
902
|
+
* and sent it; every check VAT owns — the non-portable reference family, the
|
|
903
|
+
* unqualified MCP tool names, the non-portable command family — ran in
|
|
904
|
+
* `vat audit` and `vat skills build` and never on the one path where content
|
|
905
|
+
* leaves the machine. An adopter found the consequence directly: of 54 built
|
|
906
|
+
* skills, 10 referenced paths outside their own directory, one of them exec'ing
|
|
907
|
+
* a script under a SIBLING skill. Under the Skills API each skill is its own
|
|
908
|
+
* top-level tree with no siblings, so those uploaded with a green tick and could
|
|
909
|
+
* not possibly run. "It uploaded" is not "it works", and the publish command was
|
|
910
|
+
* the only lane in a position to say so before the bytes left.
|
|
911
|
+
*
|
|
912
|
+
* ⚠️ **Warns, never refuses.** These are `warning`-severity codes and the
|
|
913
|
+
* operator may have reasons VAT cannot see; turning a publish into an exit 2 on
|
|
914
|
+
* a heuristic is a worse failure than the one being reported. Blocking belongs
|
|
915
|
+
* to `vat skills build` and `vat audit`, which run before this and can gate CI.
|
|
916
|
+
*
|
|
917
|
+
* Costs nothing extra: it reads the buffers already collected for the upload, so
|
|
918
|
+
* no file is opened twice and nothing is walked again.
|
|
919
|
+
*
|
|
920
|
+
* 🚨 **The three collectors are RAW issue producers, and raw issues are not what
|
|
921
|
+
* an adopter agreed to see.** Everywhere else in VAT they pass through
|
|
922
|
+
* {@link runValidationFramework}, which applies `validation.allow` waivers and
|
|
923
|
+
* `validation.severity` overrides. Printed unfiltered, this lane contradicted
|
|
924
|
+
* VAT's own documented remedy: `MCP_TOOL_NAME_UNQUALIFIED` tells authors to
|
|
925
|
+
* "waive that one identifier with a `validation.allow` entry", and an author who
|
|
926
|
+
* did exactly that got a green `vat skills build` and then the same warning on
|
|
927
|
+
* every publish with nothing left to silence it with.
|
|
928
|
+
*
|
|
929
|
+
* ⚠️ The `location` is the BUNDLE-relative path, not the API-keyed
|
|
930
|
+
* `<declared-name>/<path>` spelling. An allow glob matches `ValidationIssue.location`,
|
|
931
|
+
* which is contractually project/bundle-relative, so the keyed spelling could not
|
|
932
|
+
* have matched an entry even once allows were applied — the same divergence
|
|
933
|
+
* {@link bundleRelativeName} exists to strip out of the oversize message.
|
|
934
|
+
*
|
|
935
|
+
* ⛔ The run-level ALLOW_UNUSED sweep is deliberately NOT drained here. This lane
|
|
936
|
+
* runs three validator families out of the whole registry, so every allow entry
|
|
937
|
+
* for any other code would be reported as dead — advising the author to delete
|
|
938
|
+
* the entry that makes their build green.
|
|
939
|
+
*/
|
|
940
|
+
export function warnUnportableReferences(files, bundleRoot, validation, logger) {
|
|
941
|
+
const rawIssues = [];
|
|
942
|
+
for (const file of files) {
|
|
943
|
+
// Skill DOCUMENTS only. These detectors read prose and code spans; running
|
|
944
|
+
// them over a `.png` or a bundled `.mjs` would report on bytes no agent
|
|
945
|
+
// reads as instructions.
|
|
946
|
+
if (!file.filename.toLowerCase().endsWith('.md'))
|
|
947
|
+
continue;
|
|
948
|
+
const location = bundleRelativeName(file.filename, bundleRoot);
|
|
949
|
+
const content = file.content.toString('utf8');
|
|
950
|
+
collectNonPortableAssetReferenceIssues(content, location, rawIssues);
|
|
951
|
+
collectNonPortableCommandIssues(content, location, rawIssues);
|
|
952
|
+
collectUnqualifiedMcpToolIssues(content, location, rawIssues);
|
|
953
|
+
}
|
|
954
|
+
const { emitted: issues } = runValidationFramework(rawIssues, validation ?? {}, createAllowUsageLedger());
|
|
955
|
+
if (issues.length === 0)
|
|
956
|
+
return;
|
|
957
|
+
logger.warn(` ${issues.length} portability ${issues.length === 1 ? 'warning' : 'warnings'} in this bundle `
|
|
958
|
+
+ '(uploading anyway; run `vat audit <dir>` for the full report):');
|
|
959
|
+
for (const issue of issues) {
|
|
960
|
+
logger.warn(` ${issue.location ?? ''}: ${issue.message}`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Upload a skill directory as a NEW skill.
|
|
965
|
+
*
|
|
966
|
+
* Always creates. It does not look for an existing skill of the same title and
|
|
967
|
+
* quietly switch to adding a version: `display_title` is not unique in a workspace
|
|
968
|
+
* (the API enforces it only when the field is sent explicitly, and derives a title
|
|
969
|
+
* from frontmatter otherwise), so such a lookup returns 0, 1 or N matches and the
|
|
970
|
+
* command's effect would depend on which. Updating an existing skill is
|
|
971
|
+
* `skills versions add`, which takes the id outright.
|
|
972
|
+
*/
|
|
973
|
+
async function uploadSkillDir(client, skillDir, titleOverride, logger) {
|
|
974
|
+
const { displayTitle, files, dirName } = await prepareSkillUpload(skillDir, titleOverride, logger);
|
|
975
|
+
return sendSkillUpload(client, displayTitle, files, dirName);
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Upload a skill directory as a new VERSION of an existing skill.
|
|
979
|
+
*
|
|
980
|
+
* The server assigns the version identifier and promotes it to `latest_version`;
|
|
981
|
+
* nothing here numbers a version.
|
|
982
|
+
*
|
|
983
|
+
* ⚠️ The uploaded files are keyed under the SOURCE tree's declared name, which
|
|
984
|
+
* this command cannot check against the roots earlier versions used: reading
|
|
985
|
+
* them back would mean a second endpoint whose response shape has not been
|
|
986
|
+
* measured. So the root is REPORTED rather than checked here, and the log line
|
|
987
|
+
* below is what lets an operator see which root their files landed under.
|
|
988
|
+
*
|
|
989
|
+
* ⛔ It is NOT true that a changed name "silently re-roots" the version — this
|
|
990
|
+
* comment and the command's help both said so, and the measurement says
|
|
991
|
+
* otherwise: the SERVER enforces SKILL.md name consistency per `skill_id`, so
|
|
992
|
+
* publishing from a renamed tree earns a loud 400. That is a better outcome than
|
|
993
|
+
* the one the warning described, and it is the one refusal this endpoint can
|
|
994
|
+
* earn, so it gets a remedy ({@link VERSION_NAME_MISMATCH_REFUSAL}) rather than
|
|
995
|
+
* the bare vendor sentence it used to get.
|
|
996
|
+
*/
|
|
997
|
+
async function uploadSkillVersionDir(client, skillId, skillDir, logger) {
|
|
998
|
+
const { files, dirName } = await prepareSkillUpload(skillDir, undefined, logger);
|
|
999
|
+
logger.info(` Files are keyed under "${dirName}/", taken from this tree's SKILL.md name. `
|
|
1000
|
+
+ 'The API refuses a version whose name differs from the one this skill already has.');
|
|
1001
|
+
// No `display_title` field: this version belongs to a skill that already has a
|
|
1002
|
+
// title, and sending one here would be an attempt to rename by side effect.
|
|
1003
|
+
// Its absence also changes the body's length, which is why the ceiling is
|
|
1004
|
+
// weighed on the body this endpoint sends rather than on the one `install` builds.
|
|
1005
|
+
const multipart = buildUploadBodyOrRefuse({}, files, dirName);
|
|
1006
|
+
// The duplicate-title 400 is unreachable here — `versions add` sends no
|
|
1007
|
+
// `display_title` — but the NAME refusal is not: the server enforces SKILL.md
|
|
1008
|
+
// name consistency per skill_id, so publishing from a tree whose frontmatter
|
|
1009
|
+
// `name` has changed is a 400, and it is the one refusal this endpoint can
|
|
1010
|
+
// earn. It got no remedy at all until the reviewer who read the log line above
|
|
1011
|
+
// pointed out that the line describes silent behaviour the server forbids.
|
|
1012
|
+
return readSkillVersionResponse(await sendUpload(() => client.uploadSkillVersion(skillId, multipart), multipart.body.length, withVersionRootRemedy));
|
|
163
1013
|
}
|
|
164
1014
|
/**
|
|
165
1015
|
* List candidate package directories in node_modules (scoped + unscoped).
|
|
1016
|
+
*
|
|
1017
|
+
* Exported for testing: this and {@link findSkillsDir} decide WHICH tree
|
|
1018
|
+
* `--from-npm` publishes to an org, and the whole `--from-npm` path was
|
|
1019
|
+
* untested — `packages/cli/src/commands/**` is coverage-excluded, so nothing in
|
|
1020
|
+
* any report said so.
|
|
166
1021
|
*/
|
|
167
|
-
function listNodeModulePackages(nodeModulesDir) {
|
|
168
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- constructed from temp dir
|
|
1022
|
+
export function listNodeModulePackages(nodeModulesDir) {
|
|
169
1023
|
if (!existsSync(nodeModulesDir))
|
|
170
1024
|
return [];
|
|
171
1025
|
const results = [];
|
|
172
|
-
//
|
|
1026
|
+
// Followed: a workspace package under node_modules IS a symlink, and a walk
|
|
1027
|
+
// that tested `isDirectory()` on the link skipped every one of them.
|
|
173
1028
|
for (const entry of readdirSync(nodeModulesDir, { withFileTypes: true })) {
|
|
174
|
-
if (
|
|
1029
|
+
if (direntKindFollowingSync(nodeModulesDir, entry) !== 'directory')
|
|
175
1030
|
continue;
|
|
176
1031
|
if (entry.name.startsWith('@')) {
|
|
177
1032
|
const scopeDir = safePath.join(nodeModulesDir, entry.name);
|
|
178
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- constructed from temp dir
|
|
179
1033
|
for (const scopedEntry of readdirSync(scopeDir, { withFileTypes: true })) {
|
|
180
|
-
if (scopedEntry
|
|
1034
|
+
if (direntKindFollowingSync(scopeDir, scopedEntry) === 'directory')
|
|
181
1035
|
results.push(safePath.join(scopeDir, scopedEntry.name));
|
|
182
1036
|
}
|
|
183
1037
|
}
|
|
@@ -191,20 +1045,43 @@ function listNodeModulePackages(nodeModulesDir) {
|
|
|
191
1045
|
* Find the dist/skills/ directory in a package. Checks the package itself
|
|
192
1046
|
* first, then scans node_modules for sub-packages that contain built skills.
|
|
193
1047
|
*/
|
|
194
|
-
function findSkillsDir(packageDir) {
|
|
1048
|
+
export function findSkillsDir(packageDir) {
|
|
195
1049
|
const direct = safePath.join(packageDir, 'dist', 'skills');
|
|
196
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- constructed from temp dir
|
|
197
1050
|
if (existsSync(direct))
|
|
198
1051
|
return direct;
|
|
199
1052
|
const candidates = listNodeModulePackages(safePath.join(packageDir, 'node_modules'));
|
|
200
1053
|
for (const pkgDir of candidates) {
|
|
201
1054
|
const candidate = safePath.join(pkgDir, 'dist', 'skills');
|
|
202
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- constructed from temp dir
|
|
203
1055
|
if (existsSync(candidate))
|
|
204
1056
|
return candidate;
|
|
205
1057
|
}
|
|
206
1058
|
return undefined;
|
|
207
1059
|
}
|
|
1060
|
+
/**
|
|
1061
|
+
* The batch's report, tagged as a failure when ANY skill did not publish.
|
|
1062
|
+
*
|
|
1063
|
+
* 🔑 Partial success is a failure. The old code returned this document plainly,
|
|
1064
|
+
* so `executeOrgCommand` stamped `status: success` on it and exited 0 — a run in
|
|
1065
|
+
* which all three skills were rejected printed `skillsUploaded: 0 /
|
|
1066
|
+
* skillsFailed: 3` under `status: success`, and a CI wrapper written as
|
|
1067
|
+
* `vat claude org skills install --from-npm … || fail` published nothing and
|
|
1068
|
+
* reported green.
|
|
1069
|
+
*
|
|
1070
|
+
* Some-succeeded is tagged the same way as none-succeeded, deliberately: the
|
|
1071
|
+
* workspace is now in a MIXED state that nobody asked for, which is exactly the
|
|
1072
|
+
* case a human has to look at. Calling it green because two of three landed is
|
|
1073
|
+
* the same lie, only smaller. What did land stays in the document, so the reader
|
|
1074
|
+
* can see how far the run got.
|
|
1075
|
+
*/
|
|
1076
|
+
export function summarizeNpmInstall(source, results, errors) {
|
|
1077
|
+
const summary = {
|
|
1078
|
+
source,
|
|
1079
|
+
skillsUploaded: results.length,
|
|
1080
|
+
...(errors.length > 0 ? { skillsFailed: errors.length, errors } : {}),
|
|
1081
|
+
skills: results,
|
|
1082
|
+
};
|
|
1083
|
+
return errors.length > 0 ? orgCommandFailure(summary) : summary;
|
|
1084
|
+
}
|
|
208
1085
|
/**
|
|
209
1086
|
* Upload skills from an npm package.
|
|
210
1087
|
*/
|
|
@@ -218,9 +1095,9 @@ async function installFromNpm(npmPackage, skillFilter, client, logger) {
|
|
|
218
1095
|
throw new Error(`No dist/skills/ directory found in ${npmPackage}. Was the package built with vat skills build?`);
|
|
219
1096
|
}
|
|
220
1097
|
logger.info(`Found skills at: ${safePath.relative(packageDir, skillsDir) || 'dist/skills/'}`);
|
|
221
|
-
//
|
|
1098
|
+
// Followed: a built skill directory that is a link is still a skill to upload.
|
|
222
1099
|
const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
|
|
223
|
-
.filter(e => e
|
|
1100
|
+
.filter(e => direntKindFollowingSync(skillsDir, e) === 'directory')
|
|
224
1101
|
.map(e => e.name);
|
|
225
1102
|
if (skillDirs.length === 0) {
|
|
226
1103
|
throw new Error(`No skills found in dist/skills/ of ${npmPackage}`);
|
|
@@ -246,47 +1123,247 @@ async function installFromNpm(npmPackage, skillFilter, client, logger) {
|
|
|
246
1123
|
errors.push({ skill: skillName, error: msg });
|
|
247
1124
|
}
|
|
248
1125
|
}
|
|
249
|
-
return
|
|
250
|
-
source: npmPackage,
|
|
251
|
-
skillsUploaded: results.length,
|
|
252
|
-
...(errors.length > 0 ? { skillsFailed: errors.length, errors } : {}),
|
|
253
|
-
skills: results,
|
|
254
|
-
};
|
|
1126
|
+
return summarizeNpmInstall(npmPackage, results, errors);
|
|
255
1127
|
}
|
|
256
1128
|
finally {
|
|
257
1129
|
rmSync(tempDir, { recursive: true, force: true });
|
|
258
1130
|
}
|
|
259
1131
|
}
|
|
260
1132
|
/**
|
|
261
|
-
* Upload a local skill directory or ZIP file.
|
|
1133
|
+
* Upload a local skill directory or ZIP file. Exported for testing.
|
|
262
1134
|
*/
|
|
263
|
-
async function installFromLocal(source, titleOverride, client, logger) {
|
|
264
|
-
const sourcePath =
|
|
265
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- path from CLI arg
|
|
1135
|
+
export async function installFromLocal(source, titleOverride, client, logger) {
|
|
1136
|
+
const sourcePath = resolveSourceArgument(source);
|
|
266
1137
|
if (!existsSync(sourcePath)) {
|
|
267
1138
|
throw new Error(`Source not found: ${sourcePath}`);
|
|
268
1139
|
}
|
|
269
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- path from CLI arg
|
|
270
1140
|
const stat = statSync(sourcePath);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
fieldName: 'files[]',
|
|
277
|
-
filename: basename(sourcePath),
|
|
278
|
-
content: zipContent,
|
|
279
|
-
}];
|
|
280
|
-
logger.info(`Uploading ZIP: ${sourcePath} (${(zipContent.length / 1024).toFixed(1)}KB)`);
|
|
281
|
-
logger.info(`Display title: ${displayTitle}`);
|
|
282
|
-
return sendSkillUpload(client, displayTitle, files);
|
|
1141
|
+
// Case-INSENSITIVE. `endsWith('.zip')` refused `MySkill.ZIP` as "not a directory
|
|
1142
|
+
// or .zip file" — a file Windows and macOS both consider a zip archive, and one
|
|
1143
|
+
// an operator has no way to read that refusal as being about capitalisation.
|
|
1144
|
+
if (!stat.isDirectory() && sourcePath.toLowerCase().endsWith('.zip')) {
|
|
1145
|
+
return installZipArchive(sourcePath, titleOverride, client, logger);
|
|
283
1146
|
}
|
|
284
1147
|
if (!stat.isDirectory()) {
|
|
285
1148
|
throw new Error(`Source must be a directory or .zip file: ${sourcePath}`);
|
|
286
1149
|
}
|
|
287
|
-
|
|
1150
|
+
// "Packaging", not "Uploading". Everything that follows this line — reading the
|
|
1151
|
+
// config, collecting the file set, weighing the request — happens locally and
|
|
1152
|
+
// can refuse locally, and an operator who read "Uploading skill directory: X"
|
|
1153
|
+
// above a local size refusal was told about an upload that never started. The
|
|
1154
|
+
// line stays HERE rather than moving after the point of no return, because
|
|
1155
|
+
// naming the source before the work is what makes a failure attributable at
|
|
1156
|
+
// all — `--from-npm` packages several skills in a loop, and a packaging error
|
|
1157
|
+
// with no path above it names nothing.
|
|
1158
|
+
logger.info(`Packaging skill directory: ${sourcePath}`);
|
|
288
1159
|
return uploadSkillDir(client, sourcePath, titleOverride, logger);
|
|
289
1160
|
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Publish a ZIP archive as a NEW skill.
|
|
1163
|
+
*
|
|
1164
|
+
* Split out of {@link installFromLocal} because the archive lane now has three
|
|
1165
|
+
* decisions of its own — an answer-key refusal, an expanded-size refusal, and a
|
|
1166
|
+
* title/name divergence warning — and inlining them made the dispatcher a
|
|
1167
|
+
* cognitively complex function whose real job (directory or archive?) was two
|
|
1168
|
+
* lines of it.
|
|
1169
|
+
*/
|
|
1170
|
+
async function installZipArchive(sourcePath, titleOverride, client, logger) {
|
|
1171
|
+
// `basename(path, ext)` matches the extension exactly, so the literal '.zip'
|
|
1172
|
+
// leaves `.ZIP` on the title. Slice the length instead.
|
|
1173
|
+
const displayTitle = titleOverride ?? basename(sourcePath).slice(0, -'.zip'.length);
|
|
1174
|
+
const zipContent = readFileSync(sourcePath);
|
|
1175
|
+
const files = [{
|
|
1176
|
+
fieldName: 'files[]',
|
|
1177
|
+
filename: basename(sourcePath),
|
|
1178
|
+
content: zipContent,
|
|
1179
|
+
}];
|
|
1180
|
+
// A ZIP is by construction a single large binary, so this is the shape most
|
|
1181
|
+
// likely to be over the ceiling — and the one that reached the wire unmeasured
|
|
1182
|
+
// while the check lived only in the directory-packaging path. It is gated in
|
|
1183
|
+
// `sendSkillUpload` below, on the same measure (the multipart body) as every
|
|
1184
|
+
// other shape; this line reports the file's own bytes, which are smaller.
|
|
1185
|
+
//
|
|
1186
|
+
// "Preparing", not "Uploading": nothing has been sent at this point, and the
|
|
1187
|
+
// very next step can refuse locally. A line that claims an upload started is
|
|
1188
|
+
// the log telling the operator about work that never happened.
|
|
1189
|
+
logger.info(`Preparing ZIP: ${sourcePath} (${formatBytes(zipContent.length)} of file content)`);
|
|
1190
|
+
// Where the title came FROM, not just what it is. A ZIP's title is the
|
|
1191
|
+
// filename — `wiki-lint-v2.zip` publishes a skill called `wiki-lint-v2`,
|
|
1192
|
+
// which is a DIFFERENT skill from `wiki-lint` and is not refused, because
|
|
1193
|
+
// display_title uniqueness is enforced only when the field is sent. A
|
|
1194
|
+
// directory takes its title from SKILL.md instead, so the same tree zipped
|
|
1195
|
+
// and unzipped can publish under two names.
|
|
1196
|
+
//
|
|
1197
|
+
// ⛔ This comment used to end "VAT cannot read the SKILL.md inside the
|
|
1198
|
+
// archive: Node ships no ZIP reader, and adding a dependency to parse one
|
|
1199
|
+
// here would mean owning central-directory, zip64 and zip-slip handling for
|
|
1200
|
+
// a log line." The PREMISE was false — `adm-zip` is already a runtime
|
|
1201
|
+
// dependency of this package — and the cost/benefit was weighed against the
|
|
1202
|
+
// wrong benefit: the same read also answers whether the archive EXPANDS
|
|
1203
|
+
// past the ceiling, which is a refusal, not a log line. So the archive is
|
|
1204
|
+
// now read (see {@link inspectZipArchive}) and the divergence is reported
|
|
1205
|
+
// as a fact below, in addition to this provenance line.
|
|
1206
|
+
logger.info(`Display title: "${displayTitle}" (${titleOverride === undefined
|
|
1207
|
+
? 'from the ZIP filename — NOT from the SKILL.md inside it; pass --title to set it'
|
|
1208
|
+
: 'from --title'})`);
|
|
1209
|
+
const inspected = await inspectZipArchive(sourcePath);
|
|
1210
|
+
if ('archiveUnreadable' in inspected) {
|
|
1211
|
+
// Degrade, and SAY so. Every check below is skipped, and a skipped check
|
|
1212
|
+
// that is not announced is indistinguishable from one that passed.
|
|
1213
|
+
logger.warn(`Could not read the archive (${inspected.archiveUnreadable}); the answer-key, expanded-size, `
|
|
1214
|
+
+ `name and portability checks were not run on it. The API decides the upload.`);
|
|
1215
|
+
}
|
|
1216
|
+
else {
|
|
1217
|
+
refuseOrWarnOnArchive(inspected, displayTitle, zipContent.length, logger);
|
|
1218
|
+
}
|
|
1219
|
+
return sendSkillUpload(client, displayTitle, files);
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* The archive lane's own decisions, over what {@link inspectZipArchive} read:
|
|
1223
|
+
* two refusals (answer key, expanded size), and the warnings the inspection
|
|
1224
|
+
* earns (title/name divergence, portability, members it could not inflate).
|
|
1225
|
+
*/
|
|
1226
|
+
function refuseOrWarnOnArchive(inspected, displayTitle, wireBytes, logger) {
|
|
1227
|
+
// 🚨 REFUSED, not warned. The directory lane refuses an answer key
|
|
1228
|
+
// unconditionally (`NEVER_UPLOADED_DIR_NAMES`, plus whatever the config
|
|
1229
|
+
// declares), and until this existed the ZIP lane bypassed `collectFiles`
|
|
1230
|
+
// entirely and posted the archive as one opaque part — so
|
|
1231
|
+
// `zip -r my-skill.zip my-skill/` over a tree holding
|
|
1232
|
+
// `my-skill/evals/evals.json` published the answer key org-wide with a
|
|
1233
|
+
// green tick, while `install my-skill/` on the very same tree refused.
|
|
1234
|
+
// Two lanes of one command must not disagree about whether a skill's
|
|
1235
|
+
// answer key may be published, and a warning is a disagreement.
|
|
1236
|
+
//
|
|
1237
|
+
// This lane can only see the CONVENTIONAL names: there is no skill
|
|
1238
|
+
// directory to walk up from, so a config-declared suite location is
|
|
1239
|
+
// unreadable here. That is a narrower check than the directory lane's,
|
|
1240
|
+
// not a looser verdict on what it does see.
|
|
1241
|
+
if (inspected.neverUploaded.length > 0) {
|
|
1242
|
+
throw new Error(`This ZIP contains ${String(inspected.neverUploaded.length)} entr`
|
|
1243
|
+
+ `${inspected.neverUploaded.length === 1 ? 'y' : 'ies'} that are never published with a `
|
|
1244
|
+
+ `skill: ${inspected.neverUploaded.slice(0, 10).join(', ')}`
|
|
1245
|
+
+ `${inspected.neverUploaded.length > 10 ? ', …' : ''}. An eval suite is the skill's ANSWER `
|
|
1246
|
+
+ `KEY and this would publish it to everyone in the organization. Rebuild the archive from `
|
|
1247
|
+
+ `the BUILT skill directory (\`vat skills build\`), or upload that directory directly — `
|
|
1248
|
+
+ `\`install <dir>\` withholds these and reports each one.`);
|
|
1249
|
+
}
|
|
1250
|
+
if (inspected.uncompressedBytes > API_SKILL_MAX_UPLOAD_BYTES) {
|
|
1251
|
+
throw new Error(`ZIP expands to ${formatBytes(inspected.uncompressedBytes)}, over the `
|
|
1252
|
+
+ `${formatBytes(API_SKILL_MAX_UPLOAD_BYTES)} ceiling. The API expands the archive and `
|
|
1253
|
+
+ `weighs the UNCOMPRESSED total, so ${formatBytes(wireBytes)} on the wire does not `
|
|
1254
|
+
+ `get this under the limit — it will refuse with "Zip file uncompressed size exceeds 30MB". `
|
|
1255
|
+
+ `Remove files from the bundle; compressing harder cannot help.`);
|
|
1256
|
+
}
|
|
1257
|
+
// The title/name divergence this branch discloses above, now stated as a
|
|
1258
|
+
// FACT rather than as a caveat, because the archive can be read after
|
|
1259
|
+
// all. A ZIP whose inner SKILL.md declares a different name publishes a
|
|
1260
|
+
// skill whose title and versions disagree — and that divergence is what
|
|
1261
|
+
// makes a title-keyed lookup unsafe, so it is worth naming at the moment
|
|
1262
|
+
// it is minted rather than diagnosing later.
|
|
1263
|
+
if (inspected.declaredName !== undefined && inspected.declaredName !== displayTitle) {
|
|
1264
|
+
logger.warn(`Title/name divergence: this uploads with display title "${displayTitle}", but the `
|
|
1265
|
+
+ `SKILL.md inside declares name "${inspected.declaredName}". Every version will carry `
|
|
1266
|
+
+ `the declared name. Pass --title "${inspected.declaredName}" to make them agree.`);
|
|
1267
|
+
}
|
|
1268
|
+
// 🚨 The SAME function the directory lane calls, over the archive's own
|
|
1269
|
+
// markdown. Until this line existed, `install <zip>` ran NONE of the
|
|
1270
|
+
// portability checks its `--help` promises of "every markdown document in
|
|
1271
|
+
// the bundle" — the archive went straight to `sendSkillUpload`, so the
|
|
1272
|
+
// adopter finding that produced the family (10 of 54 skills referencing a
|
|
1273
|
+
// path outside their own tree, published green and unable to run) was
|
|
1274
|
+
// invisible on the lane most likely to carry a hand-built bundle.
|
|
1275
|
+
//
|
|
1276
|
+
// ⚠️ NO validation config, and that is a real narrowing, stated in the
|
|
1277
|
+
// help text. `validation.allow` / `validation.severity` are resolved by
|
|
1278
|
+
// walking up from a skill's SKILL.md to its governing project — and an
|
|
1279
|
+
// archive member has no path on disk to walk up from, the same reason a
|
|
1280
|
+
// config-declared eval-suite location is unreadable here. So a waiver that
|
|
1281
|
+
// silences `vat skills build` does NOT silence this lane. The checks only
|
|
1282
|
+
// warn, so the cost of the narrowing is noise, never a blocked publish;
|
|
1283
|
+
// the cost of skipping them was silence on a documented promise.
|
|
1284
|
+
warnUnportableReferences(inspected.documents, inspected.bundleRoot, undefined, logger);
|
|
1285
|
+
if (inspected.unreadable.length > 0) {
|
|
1286
|
+
const named = inspected.unreadable.map(({ entry, reason }) => `${entry} (${reason})`).join(', ');
|
|
1287
|
+
logger.warn(`${String(inspected.unreadable.length)} archive member(s) could not be inflated and were `
|
|
1288
|
+
+ `not checked: ${named}`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Delete every version handed in, and keep going when one refuses.
|
|
1293
|
+
*
|
|
1294
|
+
* 🚨 **One refusal is a fact about ONE version.** This loop used to return on the
|
|
1295
|
+
* first one, so every version after it was never ATTEMPTED — not failed, not
|
|
1296
|
+
* reported, unexamined. Deleting a workspace skill requires deleting every
|
|
1297
|
+
* version first, so the operator was left with a skill that could not be deleted,
|
|
1298
|
+
* a report that named one problem out of an unknown number, and no way to tell
|
|
1299
|
+
* from the output which versions were still there. A transient refusal on version
|
|
1300
|
+
* 2 of 30 hid the state of the other 28.
|
|
1301
|
+
*
|
|
1302
|
+
* The sweep therefore ATTEMPTS all of them and reports both lists. The caller
|
|
1303
|
+
* decides what the outcome means: some deleted plus some failed is a run that
|
|
1304
|
+
* happened and went wrong (exit 1, with the record); nothing deleted at all is
|
|
1305
|
+
* the ending the command's help documents as exit 2.
|
|
1306
|
+
*
|
|
1307
|
+
* ⛔ It deliberately does NOT stop early on a repeated failure. A "the API is
|
|
1308
|
+
* clearly down, give up" heuristic is how the abandoned-versions defect reads
|
|
1309
|
+
* from inside: the whole value here is that every version's state is KNOWN when
|
|
1310
|
+
* the command ends.
|
|
1311
|
+
*/
|
|
1312
|
+
export async function deleteEveryVersion(client, skillId, versions, logger) {
|
|
1313
|
+
const deleted = [];
|
|
1314
|
+
const failures = [];
|
|
1315
|
+
for (const version of versions) {
|
|
1316
|
+
try {
|
|
1317
|
+
await client.deleteSkillVersion(skillId, version);
|
|
1318
|
+
}
|
|
1319
|
+
catch (error) {
|
|
1320
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1321
|
+
failures.push({ version, reason });
|
|
1322
|
+
logger.info(` ⚠ version ${version} was not deleted: ${reason}`);
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
deleted.push(version);
|
|
1326
|
+
logger.info(` Deleted version ${version}`);
|
|
1327
|
+
}
|
|
1328
|
+
return { deleted, failures };
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Every version the sweep could not delete, and why — the one sentence both the
|
|
1332
|
+
* published report and the exit-2 throw are built from, so the two endings never
|
|
1333
|
+
* describe the same run differently.
|
|
1334
|
+
*/
|
|
1335
|
+
export function describeVersionSweepFailures(failures) {
|
|
1336
|
+
return failures.map(failure => `${failure.version}: ${failure.reason}`).join('; ');
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* The document a `--all` run publishes when it destroyed something and then did
|
|
1340
|
+
* not finish.
|
|
1341
|
+
*
|
|
1342
|
+
* 🔑 The deleted list is a RECORD, not a progress counter. Version deletion is
|
|
1343
|
+
* irreversible, so a failure part-way through leaves a workspace nobody can
|
|
1344
|
+
* reconstruct — and throwing here would end on `handleCommandError`'s exit 2
|
|
1345
|
+
* ("the run could not happen") and DISCARD the document. It happened, and it
|
|
1346
|
+
* destroyed things the operator now has no other list of. So the lists travel
|
|
1347
|
+
* with the failure and the run ends 1: it happened, and its outcome is wrong.
|
|
1348
|
+
*
|
|
1349
|
+
* `failedVersions` is the other half, and the half a re-run needs: what is still
|
|
1350
|
+
* there. Without it the operator knows what they lost and not what they still
|
|
1351
|
+
* have to deal with.
|
|
1352
|
+
*/
|
|
1353
|
+
export function reportHalfDeleted(report) {
|
|
1354
|
+
const { skillId, deletedVersions, failedVersions } = report;
|
|
1355
|
+
return orgCommandFailure({
|
|
1356
|
+
id: skillId,
|
|
1357
|
+
deleted: false,
|
|
1358
|
+
deletedVersions,
|
|
1359
|
+
failedVersions,
|
|
1360
|
+
error: report.error,
|
|
1361
|
+
note: `${String(deletedVersions.length)} version(s) of ${skillId} were deleted and cannot be `
|
|
1362
|
+
+ `restored; ${String(failedVersions.length)} could not be deleted and are still there. `
|
|
1363
|
+
+ 'The skill itself still exists — the API refuses a skill that still has versions. '
|
|
1364
|
+
+ 'Re-run `--all` once the cause is cleared: it will attempt only what is left.',
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
290
1367
|
// ── Commands ───────────────────────────────────────────────────────────
|
|
291
1368
|
export function createOrgSkillsCommand() {
|
|
292
1369
|
const command = new Command('skills');
|
|
@@ -322,14 +1399,32 @@ Example:
|
|
|
322
1399
|
.option('--title <title>', 'Display title override (single skill only)')
|
|
323
1400
|
.option('--debug', DEBUG_OPT_DESC)
|
|
324
1401
|
.action(async (source, options) => {
|
|
325
|
-
if (!source && !options.fromNpm) {
|
|
326
|
-
throw new Error('Provide a <source> path or use --from-npm <package>');
|
|
327
|
-
}
|
|
328
|
-
if (source && options.fromNpm) {
|
|
329
|
-
throw new Error('Provide either <source> or --from-npm, not both');
|
|
330
|
-
}
|
|
331
1402
|
const commandName = options.fromNpm ? 'OrgSkillsInstallNpm' : 'OrgSkillsInstall';
|
|
332
1403
|
await executeOrgCommand(commandName, options.debug, async ({ client, logger }) => {
|
|
1404
|
+
// INSIDE the action, like `versions add`'s own guards. Thrown from the
|
|
1405
|
+
// Commander handler instead, these were a floating rejection that
|
|
1406
|
+
// reached no catch: Node printed a raw stack trace with absolute $HOME
|
|
1407
|
+
// paths, wrote nothing to the stdout this command's help promises, and
|
|
1408
|
+
// exited 1 — which the documented contract reads as "at least one
|
|
1409
|
+
// error-severity finding" for a run in which nothing executed.
|
|
1410
|
+
if (!source && !options.fromNpm) {
|
|
1411
|
+
throw new Error('Provide a <source> path or use --from-npm <package>');
|
|
1412
|
+
}
|
|
1413
|
+
if (source && options.fromNpm) {
|
|
1414
|
+
throw new Error('Provide either <source> or --from-npm, not both');
|
|
1415
|
+
}
|
|
1416
|
+
// The other two illegal combinations, refused rather than dropped. Both
|
|
1417
|
+
// flags used to be accepted and silently ignored on the lane that cannot
|
|
1418
|
+
// honour them, which is how an operator gets a skill published under the
|
|
1419
|
+
// wrong title — or every skill in a package published when they named one.
|
|
1420
|
+
if (options.title !== undefined && options.fromNpm) {
|
|
1421
|
+
throw new Error('--title applies to a single skill and --from-npm can publish several, so it is '
|
|
1422
|
+
+ 'refused here rather than silently ignored. Publish the one skill with '
|
|
1423
|
+
+ '--skill and set its title in its SKILL.md, or upload its directory directly.');
|
|
1424
|
+
}
|
|
1425
|
+
if (options.skill !== undefined && !options.fromNpm) {
|
|
1426
|
+
throw new Error('--skill selects one skill inside an npm package; it applies only with --from-npm');
|
|
1427
|
+
}
|
|
333
1428
|
if (options.fromNpm) {
|
|
334
1429
|
return installFromNpm(options.fromNpm, options.skill, client, logger);
|
|
335
1430
|
}
|
|
@@ -342,14 +1437,66 @@ Description:
|
|
|
342
1437
|
Accepts a built skill directory, a ZIP file, or an npm package.
|
|
343
1438
|
Requires ANTHROPIC_API_KEY (regular key, not admin key).
|
|
344
1439
|
|
|
345
|
-
|
|
1440
|
+
A DIRECTORY uploads under the "name" its SKILL.md frontmatter declares, which
|
|
346
1441
|
is also the default display_title.
|
|
347
1442
|
|
|
1443
|
+
A ZIP takes its display_title from the ZIP FILENAME, so my-skill-v2.zip
|
|
1444
|
+
publishes a skill titled "my-skill-v2", which is a separate skill from
|
|
1445
|
+
"my-skill" and is not refused as a duplicate. VAT reads the SKILL.md nearest
|
|
1446
|
+
the root of the archive and WARNS when the name it declares differs from that
|
|
1447
|
+
title, because every version the skill ever gets will carry the declared name.
|
|
1448
|
+
Pass --title to set the title explicitly.
|
|
1449
|
+
|
|
348
1450
|
A skill's eval suite is its answer key and is never uploaded: whatever the
|
|
349
1451
|
governing vibe-agent-toolkit.config.yaml declares as this skill's test input
|
|
350
|
-
(skills.config.<name>.test.evals) is withheld, and so is
|
|
351
|
-
|
|
352
|
-
either. Each exclusion is reported in the output.
|
|
1452
|
+
(skills.config.<name>.test.evals) is withheld, and so is ANY directory named
|
|
1453
|
+
evals/ — unconditionally, whether or not a config was found. node_modules/ and
|
|
1454
|
+
.git/ are never uploaded either. Each exclusion is reported in the output.
|
|
1455
|
+
|
|
1456
|
+
A ZIP is REFUSED outright when it contains any of those three directory names,
|
|
1457
|
+
rather than having them withheld: the archive is posted as one part, so there
|
|
1458
|
+
is nothing to strip out of it. Rebuild it from the built skill directory, or
|
|
1459
|
+
upload that directory. (A config-declared suite in some other location is
|
|
1460
|
+
invisible to this lane — there is no directory to resolve a config from.)
|
|
1461
|
+
|
|
1462
|
+
Those same three directory names are also left out of the bytes weighed against
|
|
1463
|
+
the ceiling below, and out of the build-time PACKAGED_SIZE_EXCEEDS_API_LIMIT
|
|
1464
|
+
total, so \`du -sb\` on the directory will report more than VAT does.
|
|
1465
|
+
|
|
1466
|
+
A symbolic link is refused rather than followed, whatever it points at: a
|
|
1467
|
+
multipart body cannot express one, and reading through it would publish the
|
|
1468
|
+
target's bytes under an in-bundle name.
|
|
1469
|
+
|
|
1470
|
+
Before anything is sent, every markdown document in the bundle is run through
|
|
1471
|
+
the non-portable reference, non-portable command, and unqualified MCP tool-name
|
|
1472
|
+
checks — the ones that catch a skill referencing a path outside its own tree,
|
|
1473
|
+
which cannot resolve once the API publishes it alone. These WARN and never
|
|
1474
|
+
block. Run \`vat audit <dir>\` for the full report.
|
|
1475
|
+
|
|
1476
|
+
A ZIP faces the same three checks: its markdown members are read out of the
|
|
1477
|
+
archive and run through them, so a hand-built bundle is not the quiet lane.
|
|
1478
|
+
The one difference is the config. On a DIRECTORY, \`validation.allow\` /
|
|
1479
|
+
\`validation.severity\` from the governing vibe-agent-toolkit.config.yaml are
|
|
1480
|
+
honoured, so a waiver that makes \`vat skills build\` green makes this quiet
|
|
1481
|
+
too. On a ZIP there is no directory to resolve that config from — the same
|
|
1482
|
+
reason a config-declared eval-suite location is invisible here — so the checks
|
|
1483
|
+
run UNWAIVED and a warning you have already waived elsewhere will still print.
|
|
1484
|
+
|
|
1485
|
+
There are TWO size gates and a ZIP faces both. The REQUEST gate weighs the
|
|
1486
|
+
multipart body — file bytes plus about 156 bytes of framing per file — against
|
|
1487
|
+
the API's ${formatBytes(API_SKILL_MAX_UPLOAD_BYTES)} ceiling; it is inclusive,
|
|
1488
|
+
measured: a body of exactly ${API_SKILL_MAX_UPLOAD_BYTES.toLocaleString('en-US')}
|
|
1489
|
+
bytes is accepted and one byte more is refused 413. A directory and a ZIP both
|
|
1490
|
+
go through it. The API then EXPANDS a ZIP and applies the same ceiling to the
|
|
1491
|
+
uncompressed total, so an archive far under the request ceiling can still be
|
|
1492
|
+
refused — VAT reads the archive's own headers and refuses first, naming the
|
|
1493
|
+
expanded size, because compressing harder cannot get it under the limit.
|
|
1494
|
+
|
|
1495
|
+
Exit Codes:
|
|
1496
|
+
0 - Every skill uploaded
|
|
1497
|
+
1 - The run completed and at least one skill failed to upload (--from-npm
|
|
1498
|
+
uploads several; the ones that landed are listed under skills)
|
|
1499
|
+
2 - The run could not happen: no API key, no such source, unusable input
|
|
353
1500
|
|
|
354
1501
|
Examples:
|
|
355
1502
|
$ vat claude org skills install dist/skills/org-admin
|
|
@@ -366,27 +1513,103 @@ Examples:
|
|
|
366
1513
|
.option('--debug', DEBUG_OPT_DESC)
|
|
367
1514
|
.action(async (skillId, options) => {
|
|
368
1515
|
await executeOrgCommand('OrgSkillsDelete', options.debug, async ({ client, logger }) => {
|
|
1516
|
+
// Every version this run actually destroyed, accumulated as it goes.
|
|
1517
|
+
//
|
|
1518
|
+
// 🔑 It is a RECORD, not a progress counter. Version deletion is
|
|
1519
|
+
// irreversible and `--all` does it in a loop, so a failure part-way
|
|
1520
|
+
// through leaves a workspace nobody can reconstruct — and the old code
|
|
1521
|
+
// threw at that point, which ends on `handleCommandError`'s exit 2
|
|
1522
|
+
// ("the run could not happen") and DISCARDS the document. It happened,
|
|
1523
|
+
// and it destroyed things the operator now has no list of. So the list
|
|
1524
|
+
// travels with the failure, and the run ends 1: it happened, and its
|
|
1525
|
+
// outcome is wrong.
|
|
1526
|
+
const deletedVersions = [];
|
|
1527
|
+
const halfDeleted = (error, failedVersions) => reportHalfDeleted({
|
|
1528
|
+
skillId,
|
|
1529
|
+
deletedVersions,
|
|
1530
|
+
failedVersions,
|
|
1531
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1532
|
+
});
|
|
369
1533
|
if (options.all) {
|
|
370
|
-
|
|
371
|
-
const versions = await autopaginateSkills(client, `/v1/skills/${encodeURIComponent(skillId)}/versions`);
|
|
1534
|
+
const versions = await autopaginateSkills(client, skillVersionsPath(skillId));
|
|
372
1535
|
const versionData = versions.data;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
1536
|
+
// "Found …", not "Deleting …": nothing has been deleted at this point
|
|
1537
|
+
// and the very next call can be refused. Cf. `installFromLocal`.
|
|
1538
|
+
logger.info(`Found ${String(versionData.length)} version(s) of ${skillId} to delete first`);
|
|
1539
|
+
// ATTEMPTS every version, whatever any one of them answers. See
|
|
1540
|
+
// `deleteEveryVersion`: returning on the first refusal left the rest
|
|
1541
|
+
// unexamined, and the skill undeletable with no record of what remained.
|
|
1542
|
+
const sweep = await deleteEveryVersion(client, skillId, versionData.map(v => v.version), logger);
|
|
1543
|
+
deletedVersions.push(...sweep.deleted);
|
|
1544
|
+
if (sweep.failures.length > 0) {
|
|
1545
|
+
const detail = describeVersionSweepFailures(sweep.failures);
|
|
1546
|
+
// Nothing was destroyed, so there is no record worth publishing and
|
|
1547
|
+
// "could not happen" (exit 2, via the throw) is the honest ending —
|
|
1548
|
+
// the same rule as before, now decided on the WHOLE sweep rather than
|
|
1549
|
+
// on its first refusal.
|
|
1550
|
+
if (deletedVersions.length === 0) {
|
|
1551
|
+
throw new Error(`No version of ${skillId} could be deleted, so the skill was left alone. `
|
|
1552
|
+
+ `${String(sweep.failures.length)} attempt(s) failed — ${detail}`);
|
|
1553
|
+
}
|
|
1554
|
+
// The skill delete is not attempted: the API refuses a skill that still
|
|
1555
|
+
// has versions, so it would spend a round trip to be told what is
|
|
1556
|
+
// already known here.
|
|
1557
|
+
return halfDeleted(new Error(detail), sweep.failures.map(failure => failure.version));
|
|
377
1558
|
}
|
|
378
1559
|
}
|
|
379
1560
|
logger.info(`Deleting skill: ${skillId}`);
|
|
380
|
-
|
|
381
|
-
|
|
1561
|
+
// The vendor's refusal here names no VAT command — measured live as
|
|
1562
|
+
// `400 Cannot delete skill with existing versions. Delete all versions
|
|
1563
|
+
// first.` — so the one that answers it is appended, unless this run
|
|
1564
|
+
// already deleted them.
|
|
1565
|
+
let raw;
|
|
1566
|
+
try {
|
|
1567
|
+
raw = await deleteSkillOrExplain(client, skillId, options.all === true ? DELETE_REMEDIES_AFTER_ALL : DELETE_REMEDIES);
|
|
1568
|
+
}
|
|
1569
|
+
catch (error) {
|
|
1570
|
+
if (deletedVersions.length === 0)
|
|
1571
|
+
throw error;
|
|
1572
|
+
// The sweep completed — every version is gone — and the SKILL delete is
|
|
1573
|
+
// what failed, so nothing is left in `failedVersions`.
|
|
1574
|
+
return halfDeleted(error, []);
|
|
1575
|
+
}
|
|
1576
|
+
const deleted = reportDelete(raw, skillId, SKILL_DELETED_TYPES);
|
|
1577
|
+
return deletedVersions.length === 0
|
|
1578
|
+
? deleted
|
|
1579
|
+
// The versions are part of what this run DID, so they belong in the
|
|
1580
|
+
// document either way — and the failure tag has to survive being
|
|
1581
|
+
// merged with them.
|
|
1582
|
+
: mergeDeleteReport(deleted, deletedVersions);
|
|
382
1583
|
});
|
|
383
1584
|
})
|
|
384
1585
|
.addHelpText('after', `
|
|
385
1586
|
Description:
|
|
386
1587
|
Deletes a skill from the organization. Uses the Skills API (beta).
|
|
387
|
-
Use --all to auto-delete all versions before the skill.
|
|
388
1588
|
Requires ANTHROPIC_API_KEY (regular key, not admin key).
|
|
389
1589
|
|
|
1590
|
+
The API refuses to delete a skill that still has versions (400). Use --all to
|
|
1591
|
+
delete every version and then the skill in one command; without it, delete the
|
|
1592
|
+
versions yourself with \`vat claude org skills versions delete\` first.
|
|
1593
|
+
|
|
1594
|
+
--all deletes irreversibly and in a loop, and it ATTEMPTS EVERY VERSION even
|
|
1595
|
+
when one refuses — one refusal is a fact about one version, not about the ones
|
|
1596
|
+
after it. When the sweep ends with anything left, the versions it destroyed are
|
|
1597
|
+
listed under deletedVersions, the ones still there under failedVersions, and
|
|
1598
|
+
the run exits 1 — the run happened, and its outcome is wrong. The skill itself
|
|
1599
|
+
is then left alone, because the API refuses a skill that still has versions.
|
|
1600
|
+
It exits 2 only when nothing was deleted at all.
|
|
1601
|
+
|
|
1602
|
+
A version delete whose response is lost is replayed, and the replay is answered
|
|
1603
|
+
404 because the first attempt already removed it. That 404 is read as the
|
|
1604
|
+
delete it is, not as a failure — the version appears under deletedVersions.
|
|
1605
|
+
|
|
1606
|
+
Exit Codes:
|
|
1607
|
+
0 - The skill was deleted
|
|
1608
|
+
1 - The run completed and the skill was NOT deleted: the API named a different
|
|
1609
|
+
outcome, or --all destroyed some versions and could not destroy the rest
|
|
1610
|
+
2 - The run could not happen: no API key, the first request was refused, or
|
|
1611
|
+
--all deleted no version at all
|
|
1612
|
+
|
|
390
1613
|
Example:
|
|
391
1614
|
$ vat claude org skills delete skill_abc123 --all
|
|
392
1615
|
`);
|
|
@@ -400,7 +1623,7 @@ Example:
|
|
|
400
1623
|
.option('--debug', DEBUG_OPT_DESC)
|
|
401
1624
|
.action(async (skillId, options) => {
|
|
402
1625
|
await executeOrgCommand('OrgSkillsVersionsList', options.debug, async ({ client }) => {
|
|
403
|
-
return autopaginateSkills(client,
|
|
1626
|
+
return autopaginateSkills(client, skillVersionsPath(skillId));
|
|
404
1627
|
});
|
|
405
1628
|
})
|
|
406
1629
|
.addHelpText('after', `
|
|
@@ -420,8 +1643,7 @@ Example:
|
|
|
420
1643
|
.action(async (skillId, version, options) => {
|
|
421
1644
|
await executeOrgCommand('OrgSkillsVersionsDelete', options.debug, async ({ client, logger }) => {
|
|
422
1645
|
logger.info(`Deleting version ${version} of skill ${skillId}`);
|
|
423
|
-
|
|
424
|
-
return { id: result.id, deleted: result.type === 'skill_version_deleted' };
|
|
1646
|
+
return reportVersionDelete(await client.deleteSkillVersion(skillId, version), skillId, version);
|
|
425
1647
|
});
|
|
426
1648
|
})
|
|
427
1649
|
.addHelpText('after', `
|
|
@@ -430,10 +1652,94 @@ Description:
|
|
|
430
1652
|
All versions must be deleted before a skill can be deleted.
|
|
431
1653
|
Requires ANTHROPIC_API_KEY.
|
|
432
1654
|
|
|
1655
|
+
Output:
|
|
1656
|
+
- id: the SKILL this version belonged to (always the id you passed)
|
|
1657
|
+
- version: the version that was destroyed (always the one you passed)
|
|
1658
|
+
- deleted: whether the API confirmed the deletion
|
|
1659
|
+
|
|
1660
|
+
Exit Codes:
|
|
1661
|
+
0 - The version was deleted
|
|
1662
|
+
1 - The run completed and the API named an outcome other than a deletion
|
|
1663
|
+
2 - The run could not happen: no API key, or the request was refused
|
|
1664
|
+
|
|
433
1665
|
Example:
|
|
434
1666
|
$ vat claude org skills versions delete skill_abc123 1775007400733130
|
|
1667
|
+
`);
|
|
1668
|
+
const versionsAddCmd = new Command('add');
|
|
1669
|
+
versionsAddCmd
|
|
1670
|
+
.description('Publish a new version of an existing skill')
|
|
1671
|
+
.argument(SKILL_ID_ARG, SKILL_ID_DESC)
|
|
1672
|
+
// NOT "or ZIP file", which is what this said when it was copied from
|
|
1673
|
+
// `install`: the guard below refuses anything that is not a directory, so the
|
|
1674
|
+
// help promised an input the command rejects. Narrowing the promise rather
|
|
1675
|
+
// than widening the code is the honest fix — a ZIP posted to
|
|
1676
|
+
// POST /v1/skills/{id}/versions has never been tried against the live API, and
|
|
1677
|
+
// this is not the place to find out by guessing.
|
|
1678
|
+
.argument('<source>', 'Path to a built skill directory')
|
|
1679
|
+
.option('--debug', DEBUG_OPT_DESC)
|
|
1680
|
+
.action(async (skillId, source, options) => {
|
|
1681
|
+
await executeOrgCommand('OrgSkillsVersionsAdd', options.debug, async ({ client, logger }) => {
|
|
1682
|
+
const resolved = resolveSourceArgument(source);
|
|
1683
|
+
if (!existsSync(resolved))
|
|
1684
|
+
throw new Error(`Source not found: ${resolved}`);
|
|
1685
|
+
if (!statSync(resolved).isDirectory()) {
|
|
1686
|
+
// Name the asymmetry IN the refusal, not only in --help. `install`
|
|
1687
|
+
// accepts a ZIP and this verb does not, so the operator most likely to
|
|
1688
|
+
// hit this is the one who just read that `install` takes one — and a
|
|
1689
|
+
// refusal that says only "must be a directory" reads as a bug in their
|
|
1690
|
+
// path, not as a difference between two verbs.
|
|
1691
|
+
const zipHint = resolved.toLowerCase().endsWith('.zip')
|
|
1692
|
+
? ' A ZIP is accepted by `vat claude org skills install`, not here:'
|
|
1693
|
+
+ ' publish a new version from the built skill directory instead.'
|
|
1694
|
+
: '';
|
|
1695
|
+
throw new Error(`Source must be a skill directory: ${resolved}.${zipHint}`);
|
|
1696
|
+
}
|
|
1697
|
+
// "Packaging", not "Publishing" — see `installFromLocal`. Measured: this
|
|
1698
|
+
// line printed, then `Failed to load config: …`, and nothing had been
|
|
1699
|
+
// sent. The announcement now describes the step it actually precedes.
|
|
1700
|
+
logger.info(`Packaging new version of ${skillId} from: ${resolved}`);
|
|
1701
|
+
return uploadSkillVersionDir(client, skillId, resolved, logger);
|
|
1702
|
+
});
|
|
1703
|
+
})
|
|
1704
|
+
.addHelpText('after', `
|
|
1705
|
+
Description:
|
|
1706
|
+
Publishes the contents of a skill directory as a NEW VERSION of an existing
|
|
1707
|
+
skill. Uses the Skills API (beta). Requires ANTHROPIC_API_KEY.
|
|
1708
|
+
|
|
1709
|
+
This is how you ship a change to a skill you have already published. It is a
|
|
1710
|
+
separate command from \`install\` on purpose: \`install\` always creates a new
|
|
1711
|
+
skill, this always adds a version to the skill you name, and neither inspects
|
|
1712
|
+
the workspace to decide which it "should" do. A display title is NOT unique in
|
|
1713
|
+
a workspace, so resolving one to a skill can match none, one, or several — and
|
|
1714
|
+
a wrong match would append your version to somebody else's skill.
|
|
1715
|
+
|
|
1716
|
+
Find the id with \`vat claude org skills list\`. The API assigns the version
|
|
1717
|
+
identifier and makes it the skill's latest; nothing is numbered locally.
|
|
1718
|
+
|
|
1719
|
+
Takes a built skill DIRECTORY. A ZIP is accepted by \`install\`, not here.
|
|
1720
|
+
|
|
1721
|
+
The same exclusions and the same ${formatBytes(API_SKILL_MAX_UPLOAD_BYTES)} request
|
|
1722
|
+
ceiling as \`install\` apply: the eval suite, node_modules/ and .git/ are never
|
|
1723
|
+
uploaded, and a symbolic link is refused rather than followed.
|
|
1724
|
+
|
|
1725
|
+
The uploaded files are keyed under the top-level directory named by this
|
|
1726
|
+
tree's SKILL.md \`name\`, and that root is printed as the upload runs. The API
|
|
1727
|
+
enforces that name against the one this skill already has, so publishing from a
|
|
1728
|
+
renamed tree is refused with a 400 rather than silently re-rooting the version's
|
|
1729
|
+
files. VAT does not check it first — it reports the root it used and lets the
|
|
1730
|
+
server be the authority.
|
|
1731
|
+
|
|
1732
|
+
Exit Codes:
|
|
1733
|
+
0 - The version was published
|
|
1734
|
+
2 - The run could not happen: no API key, no such source, not a directory,
|
|
1735
|
+
over the upload ceiling, or a response the version identifier
|
|
1736
|
+
cannot be read from
|
|
1737
|
+
|
|
1738
|
+
Example:
|
|
1739
|
+
$ vat claude org skills versions add skill_abc123 dist/skills/org-admin
|
|
435
1740
|
`);
|
|
436
1741
|
versionsCmd.addCommand(versionsListCmd);
|
|
1742
|
+
versionsCmd.addCommand(versionsAddCmd);
|
|
437
1743
|
versionsCmd.addCommand(versionsDeleteCmd);
|
|
438
1744
|
command.addCommand(listCmd);
|
|
439
1745
|
command.addCommand(installCmd);
|