@drawcall/market 0.1.32 → 0.1.34
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 +2 -2
- package/dist/cli.js +0 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/search.d.ts +0 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +1 -1
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/upload.d.ts +1 -0
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +13 -4
- package/dist/commands/upload.js.map +1 -1
- package/dist/constants.js +2 -2
- package/dist/contract.d.ts +4 -4
- package/dist/install.d.ts +1 -0
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +1 -0
- package/dist/install.js.map +1 -1
- package/dist/output.d.ts +1 -3
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +12 -13
- package/dist/output.js.map +1 -1
- package/dist/resolve.d.ts +1 -0
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +1 -0
- package/dist/resolve.js.map +1 -1
- package/dist/schemas.d.ts +8 -6
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +5 -3
- package/dist/schemas.js.map +1 -1
- package/dist/skill.d.ts +1 -1
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +10 -2
- package/dist/skill.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +0 -3
- package/src/commands/search.ts +1 -2
- package/src/commands/upload.ts +20 -4
- package/src/constants.ts +2 -2
- package/src/install.ts +2 -0
- package/src/output.ts +13 -16
- package/src/resolve.ts +2 -0
- package/src/schemas.ts +6 -3
- package/src/skill.ts +10 -2
- package/tests/install-layout.test.ts +5 -1
- package/tests/output.test.ts +74 -5
- package/tests/upload-deps.test.ts +21 -2
package/dist/schemas.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,OAAO;IACP,gBAAgB;IAChB,SAAS;IACT,oBAAoB;IACpB,UAAU;IACV,cAAc;IACd,kBAAkB;IAClB,aAAa;IACb,UAAU;CACF,CAAA;AAGV,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;AAElD,MAAM,CAAC,MAAM,yBAAyB,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"}
|
package/dist/skill.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const marketSkill = "---\nname: market\ndescription: Find, preview, install, generate, and publish Drawcall Market assets from a coding agent.\n---\n\n# Drawcall Market\n\nUse the `market` CLI. Keep commands short and read the summary lines.\n\n## Quick Start\n\n```sh\nmarket search \"wooden chair\" --type model --limit 3\nmarket install wooden-chair --cwd \"$PWD\"\nmarket preview wooden-chair --out /tmp/wooden-chair.png\n```\n\n## Workflow\n\n1. Search first unless the user already gave an exact asset name. `search` requires `--type`; use `model` unless the user names another supported type: `humanoid-model`, `texture`, `humanoid-animation`, `template`, `sound-effect`, `background-music`, or `
|
|
1
|
+
export declare const marketSkill = "---\nname: market\ndescription: Find, preview, install, generate, and publish Drawcall Market assets from a coding agent.\n---\n\n# Drawcall Market\n\nUse the `market` CLI. Keep commands short and read the summary lines.\n\n## Quick Start\n\n```sh\nmarket search \"wooden chair\" --type model --limit 3\nmarket install wooden-chair --cwd \"$PWD\"\nmarket preview wooden-chair --out /tmp/wooden-chair.png\n```\n\n## Workflow\n\n1. Search first unless the user already gave an exact asset name. `search` requires `--type`; use `model` unless the user names another supported type: `humanoid-model`, `texture`, `humanoid-animation`, `template`, `sound-effect`, `background-music`, `environment`, or `flipbook`.\n2. Use `--limit 1` for lookup, `--limit 3` for choice. Search caps at 5 and prints full descriptions.\n3. `install` takes one or more exact asset names (optionally `name@range`); it does not search or generate. Find names with `search` first. No `--type` is needed \u2014 asset names are unique.\n4. `preview <name>` saves the preview image; no `--type` is needed. Not every type has previews (e.g. `humanoid-animation`, `template`, `sound-effect`, `background-music`); the CLI reports when one is unavailable.\n5. Use `--unapproved` only when the user asks for unapproved/private/admin assets. Do not install unapproved assets without explicit acceptance.\n6. `generate --type <type> \"<prompt>\"` creates a new asset; it requires login and a type that supports generation. No asset type currently supports generation.\n7. Upload only when publishing is requested: `market upload <name> <zip> \"<description>\" --type <type>`. Declare dependencies with repeatable flags: `--npm name@range`, `--asset name@range`, `--skill label=source`. A skill source is a `skills add` argument: a whole repo (`owner/repo` or a git URL), a single skill via the full URL form `https://github.com/owner/repo/tree/<branch>/<subpath>` (the `tree/<branch>/<subpath>` shorthand needs the full URL, not `owner/repo`), or a local path to a skill directory inside the zip. Example: `market upload my-scene scene.zip \"A scene\" --type model --npm three@^0.178.0 --skill web-design=https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines`.\n8. Installed `environment` assets contain `public/environment/<name>.hdr` for Three.js IBL lighting and `public/environment/<name>-background.webp` for the visible equirectangular background. Use `market preview` to fetch the preview image separately.\n9. Installed `flipbook` assets contain `public/flipbook/<name>.ktx2`. Render them with `@drawcall/flipbook`'s `Flipbook` class; `market preview` fetches the middle frame from the flipbook.\n\n## Humanoid animations\n\n`humanoid-animation` assets are single-clip GLBs \u2014 one motion per asset (one idle, one walk-forward, one jump, one attack) on a normalized skeleton that retargets onto any humanoid, whatever its role. A behaving character is therefore a *set* of clips, not one asset: from what the character actually does, budget the clips it needs \u2014 an idle, its locomotion (walk/run, often split by direction: fwd/bwd/left/right), and one clip per distinct action and reaction it performs \u2014 then search for each separately.\n\nSearch one motion per query, named by the motion, because results rank by keyword overlap: a query naming several motions at once is dominated by whichever word matches the most assets and buries the others, so real clips look like a gap when they exist. Names describe the motion, not the character \u2014 so search the motion (`\"walk forward\"`, `\"reload\"`, `\"jump\"`), not the role (`\"player run\"`, `\"boss attack\"`). If a motion finds nothing, retry with synonyms (run/jog/sprint, attack/swing/strike).\n\n## Output\n\nCommands print concise, line-oriented summaries:\n\n```text\nResults: 2/8 query=\"wooden chair\" type=model approval=approved\n- wooden-chair@1.0.0 | model | approved | Low-poly wooden chair\nInstalled:\n- wooden-chair@1.0.0 (asset)\n description: Low-poly wooden chair\n files:\n public/model\n \u2514\u2500 wooden-chair.glb\n- three@^0.178.0 (npm)\n- web-design \u2190 https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines (skill)\nSaved preview for wooden-chair@1.0.0: /tmp/wooden-chair.png\n```\n\nAssets may also declare `skill` dependencies, installed for you via the `skills` CLI (`skills add`) during `install`. Sources are either a GitHub/git ref or a local path to a skill directory shipped inside the asset. This requires `npx` to be available.\n\nIf search returns no results, try one broader noun phrase. If a command returns `Error: Not logged in...`, ask before running `market login`.\n";
|
|
2
2
|
//# sourceMappingURL=skill.d.ts.map
|
package/dist/skill.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,qnJAwDvB,CAAA"}
|
package/dist/skill.js
CHANGED
|
@@ -17,14 +17,21 @@ market preview wooden-chair --out /tmp/wooden-chair.png
|
|
|
17
17
|
|
|
18
18
|
## Workflow
|
|
19
19
|
|
|
20
|
-
1. Search first unless the user already gave an exact asset name. \`search\` requires \`--type\`; use \`model\` unless the user names another supported type: \`humanoid-model\`, \`texture\`, \`humanoid-animation\`, \`template\`, \`sound-effect\`, \`background-music\`, or \`
|
|
21
|
-
2. Use \`--limit 1\` for lookup, \`--limit 3\` for choice. Search caps at 5
|
|
20
|
+
1. Search first unless the user already gave an exact asset name. \`search\` requires \`--type\`; use \`model\` unless the user names another supported type: \`humanoid-model\`, \`texture\`, \`humanoid-animation\`, \`template\`, \`sound-effect\`, \`background-music\`, \`environment\`, or \`flipbook\`.
|
|
21
|
+
2. Use \`--limit 1\` for lookup, \`--limit 3\` for choice. Search caps at 5 and prints full descriptions.
|
|
22
22
|
3. \`install\` takes one or more exact asset names (optionally \`name@range\`); it does not search or generate. Find names with \`search\` first. No \`--type\` is needed — asset names are unique.
|
|
23
23
|
4. \`preview <name>\` saves the preview image; no \`--type\` is needed. Not every type has previews (e.g. \`humanoid-animation\`, \`template\`, \`sound-effect\`, \`background-music\`); the CLI reports when one is unavailable.
|
|
24
24
|
5. Use \`--unapproved\` only when the user asks for unapproved/private/admin assets. Do not install unapproved assets without explicit acceptance.
|
|
25
25
|
6. \`generate --type <type> "<prompt>"\` creates a new asset; it requires login and a type that supports generation. No asset type currently supports generation.
|
|
26
26
|
7. Upload only when publishing is requested: \`market upload <name> <zip> "<description>" --type <type>\`. Declare dependencies with repeatable flags: \`--npm name@range\`, \`--asset name@range\`, \`--skill label=source\`. A skill source is a \`skills add\` argument: a whole repo (\`owner/repo\` or a git URL), a single skill via the full URL form \`https://github.com/owner/repo/tree/<branch>/<subpath>\` (the \`tree/<branch>/<subpath>\` shorthand needs the full URL, not \`owner/repo\`), or a local path to a skill directory inside the zip. Example: \`market upload my-scene scene.zip "A scene" --type model --npm three@^0.178.0 --skill web-design=https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines\`.
|
|
27
27
|
8. Installed \`environment\` assets contain \`public/environment/<name>.hdr\` for Three.js IBL lighting and \`public/environment/<name>-background.webp\` for the visible equirectangular background. Use \`market preview\` to fetch the preview image separately.
|
|
28
|
+
9. Installed \`flipbook\` assets contain \`public/flipbook/<name>.ktx2\`. Render them with \`@drawcall/flipbook\`'s \`Flipbook\` class; \`market preview\` fetches the middle frame from the flipbook.
|
|
29
|
+
|
|
30
|
+
## Humanoid animations
|
|
31
|
+
|
|
32
|
+
\`humanoid-animation\` assets are single-clip GLBs — one motion per asset (one idle, one walk-forward, one jump, one attack) on a normalized skeleton that retargets onto any humanoid, whatever its role. A behaving character is therefore a *set* of clips, not one asset: from what the character actually does, budget the clips it needs — an idle, its locomotion (walk/run, often split by direction: fwd/bwd/left/right), and one clip per distinct action and reaction it performs — then search for each separately.
|
|
33
|
+
|
|
34
|
+
Search one motion per query, named by the motion, because results rank by keyword overlap: a query naming several motions at once is dominated by whichever word matches the most assets and buries the others, so real clips look like a gap when they exist. Names describe the motion, not the character — so search the motion (\`"walk forward"\`, \`"reload"\`, \`"jump"\`), not the role (\`"player run"\`, \`"boss attack"\`). If a motion finds nothing, retry with synonyms (run/jog/sprint, attack/swing/strike).
|
|
28
35
|
|
|
29
36
|
## Output
|
|
30
37
|
|
|
@@ -35,6 +42,7 @@ Results: 2/8 query="wooden chair" type=model approval=approved
|
|
|
35
42
|
- wooden-chair@1.0.0 | model | approved | Low-poly wooden chair
|
|
36
43
|
Installed:
|
|
37
44
|
- wooden-chair@1.0.0 (asset)
|
|
45
|
+
description: Low-poly wooden chair
|
|
38
46
|
files:
|
|
39
47
|
public/model
|
|
40
48
|
└─ wooden-chair.glb
|
package/dist/skill.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill.js","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG
|
|
1
|
+
{"version":3,"file":"skill.js","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwD1B,CAAA"}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -96,7 +96,6 @@ program
|
|
|
96
96
|
.addOption(apiOption)
|
|
97
97
|
.option('--unapproved', 'Include unapproved versions', false)
|
|
98
98
|
.option('--limit <n>', 'Max results, 1-5', parseSearchLimit, 5)
|
|
99
|
-
.option('--verbose', 'Show longer descriptions', false)
|
|
100
99
|
.action(
|
|
101
100
|
async (
|
|
102
101
|
query: string,
|
|
@@ -105,7 +104,6 @@ program
|
|
|
105
104
|
api?: string
|
|
106
105
|
unapproved: boolean
|
|
107
106
|
limit: number
|
|
108
|
-
verbose: boolean
|
|
109
107
|
},
|
|
110
108
|
) => {
|
|
111
109
|
requireType(opts.type, 'Search')
|
|
@@ -114,7 +112,6 @@ program
|
|
|
114
112
|
baseUrl: opts.api,
|
|
115
113
|
unapproved: opts.unapproved,
|
|
116
114
|
limit: opts.limit,
|
|
117
|
-
verbose: opts.verbose,
|
|
118
115
|
})
|
|
119
116
|
},
|
|
120
117
|
)
|
package/src/commands/search.ts
CHANGED
|
@@ -10,7 +10,6 @@ export interface SearchCommandOptions {
|
|
|
10
10
|
unapproved?: boolean
|
|
11
11
|
baseUrl?: string
|
|
12
12
|
limit?: number
|
|
13
|
-
verbose?: boolean
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
export async function searchCommand(query: string, opts: SearchCommandOptions): Promise<void> {
|
|
@@ -52,6 +51,6 @@ export async function searchCommand(query: string, opts: SearchCommandOptions):
|
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
for (const item of results.items) {
|
|
55
|
-
console.log(assetSearchResultLine(item
|
|
54
|
+
console.log(assetSearchResultLine(item))
|
|
56
55
|
}
|
|
57
56
|
}
|
package/src/commands/upload.ts
CHANGED
|
@@ -3,9 +3,14 @@ import * as path from 'path'
|
|
|
3
3
|
import ora from 'ora'
|
|
4
4
|
import semver from 'semver'
|
|
5
5
|
import { getCliClient } from '../cli-client.js'
|
|
6
|
-
import { MAX_UPLOAD_ZIP_SIZE_BYTES } from '../schemas.js'
|
|
6
|
+
import { MAX_ASSET_DESCRIPTION_LENGTH, MAX_UPLOAD_ZIP_SIZE_BYTES } from '../schemas.js'
|
|
7
7
|
import { unchangedUploadResult, uploadResult } from '../output.js'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
assetDescriptionSchema,
|
|
10
|
+
assetNameSchema,
|
|
11
|
+
semverSchema,
|
|
12
|
+
type AssetType,
|
|
13
|
+
} from '../schemas.js'
|
|
9
14
|
|
|
10
15
|
export interface UploadCommandOptions {
|
|
11
16
|
type: AssetType
|
|
@@ -35,6 +40,7 @@ export async function uploadCommand(
|
|
|
35
40
|
try {
|
|
36
41
|
const parsedName = assetNameSchema.parse(name)
|
|
37
42
|
const parsedVersion = opts.version ? semverSchema.parse(opts.version) : undefined
|
|
43
|
+
const parsedDescription = parseUploadDescription(description)
|
|
38
44
|
// Parse dep flags before any network work so a malformed spec fails fast.
|
|
39
45
|
const npmDependencies = parseVersionedDeps(opts.npm ?? [], 'npm')
|
|
40
46
|
const assetDependencies = parseVersionedDeps(opts.asset ?? [], 'asset')
|
|
@@ -69,7 +75,7 @@ export async function uploadCommand(
|
|
|
69
75
|
})
|
|
70
76
|
const latestBytes = new Uint8Array(await latestZip.arrayBuffer())
|
|
71
77
|
if (
|
|
72
|
-
existing.description ===
|
|
78
|
+
existing.description === parsedDescription &&
|
|
73
79
|
depsEqual(existing.npmDependencies, npmDependencies) &&
|
|
74
80
|
depsEqual(existing.assetDependencies, assetDependencies) &&
|
|
75
81
|
depsEqual(existing.skillDependencies, skillDependencies) &&
|
|
@@ -87,7 +93,7 @@ export async function uploadCommand(
|
|
|
87
93
|
name: parsedName,
|
|
88
94
|
type,
|
|
89
95
|
version,
|
|
90
|
-
description,
|
|
96
|
+
description: parsedDescription,
|
|
91
97
|
npmDependencies,
|
|
92
98
|
assetDependencies,
|
|
93
99
|
skillDependencies,
|
|
@@ -105,6 +111,16 @@ export async function uploadCommand(
|
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
export function parseUploadDescription(description: string): string {
|
|
115
|
+
try {
|
|
116
|
+
return assetDescriptionSchema.parse(description)
|
|
117
|
+
} catch {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Asset description must be ${MAX_ASSET_DESCRIPTION_LENGTH} characters or fewer.`,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
/**
|
|
109
125
|
* Parse `name@range` specs (npm or asset deps) into a name→range record. The
|
|
110
126
|
* range is optional and defaults to `*`. A leading `@` is treated as a scope
|
package/src/constants.ts
CHANGED
|
@@ -11,7 +11,7 @@ export const ALLOWED_EXTENSIONS: Record<AssetType, string[]> = {
|
|
|
11
11
|
'sound-effect': ['.zip'],
|
|
12
12
|
'background-music': ['.zip'],
|
|
13
13
|
environment: ['.zip'],
|
|
14
|
-
|
|
14
|
+
flipbook: ['.zip'],
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export const ASSET_TYPE_LABELS: Record<AssetType, string> = {
|
|
@@ -23,5 +23,5 @@ export const ASSET_TYPE_LABELS: Record<AssetType, string> = {
|
|
|
23
23
|
'sound-effect': 'Sound Effect',
|
|
24
24
|
'background-music': 'Background Music',
|
|
25
25
|
environment: 'Environment',
|
|
26
|
-
|
|
26
|
+
flipbook: 'Flipbook',
|
|
27
27
|
}
|
package/src/install.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface InstalledAsset {
|
|
|
42
42
|
name: string
|
|
43
43
|
type: string
|
|
44
44
|
version: string
|
|
45
|
+
description: string | null
|
|
45
46
|
files: string[]
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -139,6 +140,7 @@ async function downloadAssets(
|
|
|
139
140
|
name: asset.name,
|
|
140
141
|
type: asset.type,
|
|
141
142
|
version: asset.version,
|
|
143
|
+
description: asset.description,
|
|
142
144
|
files: installedFiles,
|
|
143
145
|
})
|
|
144
146
|
|
package/src/output.ts
CHANGED
|
@@ -24,13 +24,8 @@ export function searchSummary(input: {
|
|
|
24
24
|
return `Results: ${input.count}/${input.total} ${scope}`
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function assetSearchResultLine(
|
|
28
|
-
item:
|
|
29
|
-
opts: { verbose?: boolean } = {},
|
|
30
|
-
): string {
|
|
31
|
-
const description = item.description
|
|
32
|
-
? ` | ${compact(item.description, opts.verbose ? 160 : 80)}`
|
|
33
|
-
: ''
|
|
27
|
+
export function assetSearchResultLine(item: AssetSearchResult): string {
|
|
28
|
+
const description = item.description ? ` | ${normalizeInline(item.description)}` : ''
|
|
34
29
|
return `- ${assetVersionRef(item.name, item.latestVersion)} | ${item.type} | ${assetApproval(
|
|
35
30
|
item.approved,
|
|
36
31
|
)}${description}`
|
|
@@ -47,12 +42,15 @@ export function installResult(
|
|
|
47
42
|
for (const asset of result.assets) {
|
|
48
43
|
const files = unique(asset.files)
|
|
49
44
|
lines.push(`- ${assetVersionRef(asset.name, asset.version)} (${asset.type})`)
|
|
45
|
+
if (asset.description) {
|
|
46
|
+
lines.push(...block(' description: ', asset.description))
|
|
47
|
+
}
|
|
50
48
|
if (files.length > 0) {
|
|
51
49
|
lines.push(' files:', ...indent(fileTree(files), ' '))
|
|
52
50
|
}
|
|
53
51
|
const message = postInstallMessages[asset.type]
|
|
54
52
|
if (message) {
|
|
55
|
-
lines.push(...note
|
|
53
|
+
lines.push(...block(' note: ', message))
|
|
56
54
|
}
|
|
57
55
|
}
|
|
58
56
|
|
|
@@ -76,14 +74,9 @@ export function installResult(
|
|
|
76
74
|
return lines.join('\n')
|
|
77
75
|
}
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
* Render an asset's post-install message as a `note:` block, word-wrapped with a
|
|
81
|
-
* hanging indent so continuation lines align under the message text.
|
|
82
|
-
*/
|
|
83
|
-
function note(message: string, width = 68): string[] {
|
|
84
|
-
const label = ' note: '
|
|
77
|
+
function block(label: string, message: string, width = 68): string[] {
|
|
85
78
|
const hang = ' '.repeat(label.length)
|
|
86
|
-
const words = message.split(
|
|
79
|
+
const words = normalizeInline(message).split(' ').filter(Boolean)
|
|
87
80
|
|
|
88
81
|
const wrapped: string[] = []
|
|
89
82
|
let line = ''
|
|
@@ -129,10 +122,14 @@ export function errorResult(message: string): string {
|
|
|
129
122
|
}
|
|
130
123
|
|
|
131
124
|
export function compact(value: string, max = 96): string {
|
|
132
|
-
const normalized = value
|
|
125
|
+
const normalized = normalizeInline(value)
|
|
133
126
|
return normalized.length > max ? normalized.slice(0, max - 3) + '...' : normalized
|
|
134
127
|
}
|
|
135
128
|
|
|
129
|
+
function normalizeInline(value: string): string {
|
|
130
|
+
return value.replace(/\s+/g, ' ').trim()
|
|
131
|
+
}
|
|
132
|
+
|
|
136
133
|
interface FileTreeNode {
|
|
137
134
|
children: Map<string, FileTreeNode>
|
|
138
135
|
}
|
package/src/resolve.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface ResolvedAsset {
|
|
|
17
17
|
name: string
|
|
18
18
|
type: string
|
|
19
19
|
version: string
|
|
20
|
+
description: string | null
|
|
20
21
|
npmDependencies: Record<string, string>
|
|
21
22
|
assetDependencies: Record<string, string>
|
|
22
23
|
skillDependencies: Record<string, string>
|
|
@@ -88,6 +89,7 @@ export async function resolve(
|
|
|
88
89
|
name: assetName,
|
|
89
90
|
type: meta.type,
|
|
90
91
|
version: meta.latestVersion,
|
|
92
|
+
description: meta.description,
|
|
91
93
|
npmDependencies: npmDeps,
|
|
92
94
|
assetDependencies: assetDeps,
|
|
93
95
|
skillDependencies: skillDeps,
|
package/src/schemas.ts
CHANGED
|
@@ -9,13 +9,14 @@ export const ASSET_TYPES = [
|
|
|
9
9
|
'sound-effect',
|
|
10
10
|
'background-music',
|
|
11
11
|
'environment',
|
|
12
|
-
'
|
|
12
|
+
'flipbook',
|
|
13
13
|
] as const
|
|
14
14
|
export type AssetType = (typeof ASSET_TYPES)[number]
|
|
15
15
|
|
|
16
16
|
export const assetTypeSchema = z.enum(ASSET_TYPES)
|
|
17
17
|
|
|
18
18
|
export const MAX_UPLOAD_ZIP_SIZE_BYTES = 1024 * 1024 * 1024
|
|
19
|
+
export const MAX_ASSET_DESCRIPTION_LENGTH = 1000
|
|
19
20
|
|
|
20
21
|
export const semverSchema = z
|
|
21
22
|
.string()
|
|
@@ -45,6 +46,8 @@ export const assetDependenciesSchema = z.record(z.string(), z.string()).default(
|
|
|
45
46
|
// shipped inside the asset.
|
|
46
47
|
export const skillDependenciesSchema = z.record(z.string(), z.string()).default({})
|
|
47
48
|
|
|
49
|
+
export const assetDescriptionSchema = z.string().max(MAX_ASSET_DESCRIPTION_LENGTH)
|
|
50
|
+
|
|
48
51
|
export const updateProfileSchema = z.object({
|
|
49
52
|
name: z.string().min(1).max(100).optional(),
|
|
50
53
|
image: z.string().url().optional(),
|
|
@@ -69,7 +72,7 @@ export const uploadZipSchema = z.object({
|
|
|
69
72
|
name: assetNameSchema,
|
|
70
73
|
type: assetTypeSchema,
|
|
71
74
|
version: semverSchema,
|
|
72
|
-
description:
|
|
75
|
+
description: assetDescriptionSchema.optional(),
|
|
73
76
|
npmDependencies: npmDependenciesSchema,
|
|
74
77
|
assetDependencies: assetDependenciesSchema,
|
|
75
78
|
skillDependencies: skillDependenciesSchema,
|
|
@@ -82,6 +85,6 @@ export const downloadZipSchema = z.object({
|
|
|
82
85
|
})
|
|
83
86
|
|
|
84
87
|
export const generateAssetSchema = z.object({
|
|
85
|
-
description:
|
|
88
|
+
description: assetDescriptionSchema.min(3),
|
|
86
89
|
type: assetTypeSchema.optional(),
|
|
87
90
|
})
|
package/src/skill.ts
CHANGED
|
@@ -17,14 +17,21 @@ market preview wooden-chair --out /tmp/wooden-chair.png
|
|
|
17
17
|
|
|
18
18
|
## Workflow
|
|
19
19
|
|
|
20
|
-
1. Search first unless the user already gave an exact asset name. \`search\` requires \`--type\`; use \`model\` unless the user names another supported type: \`humanoid-model\`, \`texture\`, \`humanoid-animation\`, \`template\`, \`sound-effect\`, \`background-music\`, or \`
|
|
21
|
-
2. Use \`--limit 1\` for lookup, \`--limit 3\` for choice. Search caps at 5
|
|
20
|
+
1. Search first unless the user already gave an exact asset name. \`search\` requires \`--type\`; use \`model\` unless the user names another supported type: \`humanoid-model\`, \`texture\`, \`humanoid-animation\`, \`template\`, \`sound-effect\`, \`background-music\`, \`environment\`, or \`flipbook\`.
|
|
21
|
+
2. Use \`--limit 1\` for lookup, \`--limit 3\` for choice. Search caps at 5 and prints full descriptions.
|
|
22
22
|
3. \`install\` takes one or more exact asset names (optionally \`name@range\`); it does not search or generate. Find names with \`search\` first. No \`--type\` is needed — asset names are unique.
|
|
23
23
|
4. \`preview <name>\` saves the preview image; no \`--type\` is needed. Not every type has previews (e.g. \`humanoid-animation\`, \`template\`, \`sound-effect\`, \`background-music\`); the CLI reports when one is unavailable.
|
|
24
24
|
5. Use \`--unapproved\` only when the user asks for unapproved/private/admin assets. Do not install unapproved assets without explicit acceptance.
|
|
25
25
|
6. \`generate --type <type> "<prompt>"\` creates a new asset; it requires login and a type that supports generation. No asset type currently supports generation.
|
|
26
26
|
7. Upload only when publishing is requested: \`market upload <name> <zip> "<description>" --type <type>\`. Declare dependencies with repeatable flags: \`--npm name@range\`, \`--asset name@range\`, \`--skill label=source\`. A skill source is a \`skills add\` argument: a whole repo (\`owner/repo\` or a git URL), a single skill via the full URL form \`https://github.com/owner/repo/tree/<branch>/<subpath>\` (the \`tree/<branch>/<subpath>\` shorthand needs the full URL, not \`owner/repo\`), or a local path to a skill directory inside the zip. Example: \`market upload my-scene scene.zip "A scene" --type model --npm three@^0.178.0 --skill web-design=https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines\`.
|
|
27
27
|
8. Installed \`environment\` assets contain \`public/environment/<name>.hdr\` for Three.js IBL lighting and \`public/environment/<name>-background.webp\` for the visible equirectangular background. Use \`market preview\` to fetch the preview image separately.
|
|
28
|
+
9. Installed \`flipbook\` assets contain \`public/flipbook/<name>.ktx2\`. Render them with \`@drawcall/flipbook\`'s \`Flipbook\` class; \`market preview\` fetches the middle frame from the flipbook.
|
|
29
|
+
|
|
30
|
+
## Humanoid animations
|
|
31
|
+
|
|
32
|
+
\`humanoid-animation\` assets are single-clip GLBs — one motion per asset (one idle, one walk-forward, one jump, one attack) on a normalized skeleton that retargets onto any humanoid, whatever its role. A behaving character is therefore a *set* of clips, not one asset: from what the character actually does, budget the clips it needs — an idle, its locomotion (walk/run, often split by direction: fwd/bwd/left/right), and one clip per distinct action and reaction it performs — then search for each separately.
|
|
33
|
+
|
|
34
|
+
Search one motion per query, named by the motion, because results rank by keyword overlap: a query naming several motions at once is dominated by whichever word matches the most assets and buries the others, so real clips look like a gap when they exist. Names describe the motion, not the character — so search the motion (\`"walk forward"\`, \`"reload"\`, \`"jump"\`), not the role (\`"player run"\`, \`"boss attack"\`). If a motion finds nothing, retry with synonyms (run/jog/sprint, attack/swing/strike).
|
|
28
35
|
|
|
29
36
|
## Output
|
|
30
37
|
|
|
@@ -35,6 +42,7 @@ Results: 2/8 query="wooden chair" type=model approval=approved
|
|
|
35
42
|
- wooden-chair@1.0.0 | model | approved | Low-poly wooden chair
|
|
36
43
|
Installed:
|
|
37
44
|
- wooden-chair@1.0.0 (asset)
|
|
45
|
+
description: Low-poly wooden chair
|
|
38
46
|
files:
|
|
39
47
|
public/model
|
|
40
48
|
└─ wooden-chair.glb
|
|
@@ -29,6 +29,7 @@ test('install writes zip files into the package root', async () => {
|
|
|
29
29
|
name: 'idle-loop',
|
|
30
30
|
type: 'humanoid-animation',
|
|
31
31
|
version: '1.0.0',
|
|
32
|
+
description: 'Idle loop animation',
|
|
32
33
|
npmDependencies: {},
|
|
33
34
|
assetDependencies: {},
|
|
34
35
|
skillDependencies: {},
|
|
@@ -46,6 +47,7 @@ test('install writes zip files into the package root', async () => {
|
|
|
46
47
|
name: 'idle-loop',
|
|
47
48
|
type: 'humanoid-animation',
|
|
48
49
|
version: '1.0.0',
|
|
50
|
+
description: 'Idle loop animation',
|
|
49
51
|
files: ['public/humanoid-animation/idle-loop.glb', 'src/generated/idle-loop.ts'],
|
|
50
52
|
},
|
|
51
53
|
],
|
|
@@ -77,8 +79,9 @@ test('install rejects zip paths that escape through parent segments', async () =
|
|
|
77
79
|
assets: [
|
|
78
80
|
{
|
|
79
81
|
name: 'bad-path',
|
|
80
|
-
|
|
82
|
+
type: 'template',
|
|
81
83
|
version: '1.0.0',
|
|
84
|
+
description: null,
|
|
82
85
|
npmDependencies: {},
|
|
83
86
|
assetDependencies: {},
|
|
84
87
|
skillDependencies: {},
|
|
@@ -112,6 +115,7 @@ test('install runs the skills runner per dependency, resolving local paths again
|
|
|
112
115
|
name: 'with-skills',
|
|
113
116
|
type: 'template',
|
|
114
117
|
version: '1.0.0',
|
|
118
|
+
description: null,
|
|
115
119
|
npmDependencies: {},
|
|
116
120
|
assetDependencies: {},
|
|
117
121
|
skillDependencies: {},
|
package/tests/output.test.ts
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import test from 'node:test'
|
|
3
|
-
import { installResult } from '../src/output.js'
|
|
3
|
+
import { assetSearchResultLine, installResult } from '../src/output.js'
|
|
4
|
+
|
|
5
|
+
test('assetSearchResultLine prints the full description', () => {
|
|
6
|
+
const description =
|
|
7
|
+
'A detailed model description that is intentionally longer than the old one-line search cap, with material notes, scale guidance, and usage context.'
|
|
8
|
+
|
|
9
|
+
assert.equal(
|
|
10
|
+
assetSearchResultLine({
|
|
11
|
+
id: 'asset-1',
|
|
12
|
+
name: 'wooden-chair',
|
|
13
|
+
type: 'model',
|
|
14
|
+
description,
|
|
15
|
+
ownerId: 'user-1',
|
|
16
|
+
createdAt: new Date('2026-01-01T00:00:00.000Z'),
|
|
17
|
+
updatedAt: new Date('2026-01-01T00:00:00.000Z'),
|
|
18
|
+
latestVersion: '1.2.0',
|
|
19
|
+
approved: true,
|
|
20
|
+
npmDependencies: '{}',
|
|
21
|
+
assetDependencies: '{}',
|
|
22
|
+
skillDependencies: '{}',
|
|
23
|
+
previewUrl: null,
|
|
24
|
+
}),
|
|
25
|
+
`- wooden-chair@1.2.0 | model | approved | ${description}`,
|
|
26
|
+
)
|
|
27
|
+
})
|
|
4
28
|
|
|
5
29
|
test('installResult prints asset type, file tree, and npm list', () => {
|
|
6
30
|
assert.equal(
|
|
@@ -10,12 +34,14 @@ test('installResult prints asset type, file tree, and npm list', () => {
|
|
|
10
34
|
name: 'wooden-chair',
|
|
11
35
|
type: 'model',
|
|
12
36
|
version: '1.2.0',
|
|
37
|
+
description: null,
|
|
13
38
|
files: ['public/models/wooden-chair.glb'],
|
|
14
39
|
},
|
|
15
40
|
{
|
|
16
41
|
name: 'oak-material',
|
|
17
42
|
type: 'texture',
|
|
18
43
|
version: '1.0.0',
|
|
44
|
+
description: null,
|
|
19
45
|
files: ['public/textures/oak/basecolor.png', 'public/textures/oak/normal.png'],
|
|
20
46
|
},
|
|
21
47
|
],
|
|
@@ -44,8 +70,20 @@ test('installResult attaches each post-install note under its asset', () => {
|
|
|
44
70
|
installResult(
|
|
45
71
|
{
|
|
46
72
|
assets: [
|
|
47
|
-
{
|
|
48
|
-
|
|
73
|
+
{
|
|
74
|
+
name: 'wooden-chair',
|
|
75
|
+
type: 'model',
|
|
76
|
+
version: '1.2.0',
|
|
77
|
+
description: null,
|
|
78
|
+
files: [],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'beep',
|
|
82
|
+
type: 'sound-effect',
|
|
83
|
+
version: '1.0.0',
|
|
84
|
+
description: null,
|
|
85
|
+
files: [],
|
|
86
|
+
},
|
|
49
87
|
],
|
|
50
88
|
npmDependencies: {},
|
|
51
89
|
skillDependencies: {},
|
|
@@ -66,7 +104,7 @@ test('installResult word-wraps a long note with a hanging indent', () => {
|
|
|
66
104
|
const message = Array(20).fill('word').join(' ')
|
|
67
105
|
const output = installResult(
|
|
68
106
|
{
|
|
69
|
-
assets: [{ name: 'thing', type: 'model', version: '1.0.0', files: [] }],
|
|
107
|
+
assets: [{ name: 'thing', type: 'model', version: '1.0.0', description: null, files: [] }],
|
|
70
108
|
npmDependencies: {},
|
|
71
109
|
skillDependencies: {},
|
|
72
110
|
},
|
|
@@ -85,7 +123,15 @@ test('installResult word-wraps a long note with a hanging indent', () => {
|
|
|
85
123
|
test('installResult lists installed skills with their source', () => {
|
|
86
124
|
assert.equal(
|
|
87
125
|
installResult({
|
|
88
|
-
assets: [
|
|
126
|
+
assets: [
|
|
127
|
+
{
|
|
128
|
+
name: 'wooden-chair',
|
|
129
|
+
type: 'model',
|
|
130
|
+
version: '1.2.0',
|
|
131
|
+
description: null,
|
|
132
|
+
files: [],
|
|
133
|
+
},
|
|
134
|
+
],
|
|
89
135
|
npmDependencies: {},
|
|
90
136
|
skillDependencies: {
|
|
91
137
|
'web-design-guidelines': 'vercel-labs/agent-skills/tree/main/skills/web-design-guidelines',
|
|
@@ -98,3 +144,26 @@ test('installResult lists installed skills with their source', () => {
|
|
|
98
144
|
- web-design-guidelines ← vercel-labs/agent-skills/tree/main/skills/web-design-guidelines (skill)`,
|
|
99
145
|
)
|
|
100
146
|
})
|
|
147
|
+
|
|
148
|
+
test('installResult shows each installed asset description', () => {
|
|
149
|
+
assert.equal(
|
|
150
|
+
installResult({
|
|
151
|
+
assets: [
|
|
152
|
+
{
|
|
153
|
+
name: 'wooden-chair',
|
|
154
|
+
type: 'model',
|
|
155
|
+
version: '1.2.0',
|
|
156
|
+
description:
|
|
157
|
+
'A game-ready chair with baked texture detail and clean origin placement for room scenes.',
|
|
158
|
+
files: [],
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
npmDependencies: {},
|
|
162
|
+
skillDependencies: {},
|
|
163
|
+
}),
|
|
164
|
+
`Installed:
|
|
165
|
+
- wooden-chair@1.2.0 (model)
|
|
166
|
+
description: A game-ready chair with baked texture detail and clean origin
|
|
167
|
+
placement for room scenes.`,
|
|
168
|
+
)
|
|
169
|
+
})
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import test from 'node:test'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
parseSkillDeps,
|
|
5
|
+
parseUploadDescription,
|
|
6
|
+
parseVersionedDeps,
|
|
7
|
+
} from '../src/commands/upload.js'
|
|
8
|
+
import { MAX_ASSET_DESCRIPTION_LENGTH } from '../src/schemas.js'
|
|
4
9
|
|
|
5
10
|
test('parseVersionedDeps parses name@range and defaults a missing range to *', () => {
|
|
6
11
|
assert.deepEqual(parseVersionedDeps(['three@^0.178.0', 'gsap'], 'npm'), {
|
|
@@ -17,7 +22,10 @@ test('parseVersionedDeps keeps the scope marker on scoped npm packages', () => {
|
|
|
17
22
|
})
|
|
18
23
|
|
|
19
24
|
test('parseVersionedDeps rejects duplicates', () => {
|
|
20
|
-
assert.throws(
|
|
25
|
+
assert.throws(
|
|
26
|
+
() => parseVersionedDeps(['three@^1', 'three@^2'], 'npm'),
|
|
27
|
+
/Duplicate npm dependency/u,
|
|
28
|
+
)
|
|
21
29
|
})
|
|
22
30
|
|
|
23
31
|
test('parseSkillDeps splits label=source on the first = only', () => {
|
|
@@ -42,3 +50,14 @@ test('parseSkillDeps rejects specs without a usable label=source', () => {
|
|
|
42
50
|
test('parseSkillDeps rejects duplicate labels', () => {
|
|
43
51
|
assert.throws(() => parseSkillDeps(['a=one', 'a=two']), /Duplicate skill dependency "a"/u)
|
|
44
52
|
})
|
|
53
|
+
|
|
54
|
+
test('parseUploadDescription rejects descriptions beyond the shared max length', () => {
|
|
55
|
+
assert.equal(
|
|
56
|
+
parseUploadDescription('A compact asset description.'),
|
|
57
|
+
'A compact asset description.',
|
|
58
|
+
)
|
|
59
|
+
assert.throws(
|
|
60
|
+
() => parseUploadDescription('x'.repeat(MAX_ASSET_DESCRIPTION_LENGTH + 1)),
|
|
61
|
+
/1000 characters or fewer/u,
|
|
62
|
+
)
|
|
63
|
+
})
|