diff --git a/examples/basics/pull-bundle.ts b/examples/basics/pull-bundle.ts index 70d586f..3df78c9 100644 --- a/examples/basics/pull-bundle.ts +++ b/examples/basics/pull-bundle.ts @@ -10,7 +10,7 @@ import { pull } from "@musher-dev/musher-sdk"; -const bundle = await pull("acme/code-review-kit:1.2.0"); +const bundle = await pull("musher-examples/code-review-kit:1.2.0"); // List every file in the bundle for (const file of bundle.files()) { @@ -18,16 +18,16 @@ for (const file of bundle.files()) { } // Read a prompt by name -const systemPrompt = bundle.prompt("system"); -console.log("\n--- system prompt ---"); -console.log(systemPrompt.content()); +const reviewChecklist = bundle.prompt("review-checklist"); +console.log("\n--- review checklist ---"); +console.log(reviewChecklist.content()); // Access a skill -const skill = bundle.skill("lint-rules"); +const skill = bundle.skill("reviewing-pull-requests"); console.log(`\nSkill "${skill.name}" has ${skill.files().length} file(s)`); // Raw file access by path -const raw = bundle.file("prompts/system.md"); +const raw = bundle.file("prompts/severity-guidelines.md"); if (raw) { console.log(`\nRaw file text length: ${raw.text().length}`); } diff --git a/examples/basics/resolve-bundle.ts b/examples/basics/resolve-bundle.ts index 97ead95..eedcc3b 100644 --- a/examples/basics/resolve-bundle.ts +++ b/examples/basics/resolve-bundle.ts @@ -13,7 +13,7 @@ import { resolve } from "@musher-dev/musher-sdk"; -const meta = await resolve("acme/code-review-kit:1.2.0"); +const meta = await resolve("musher-examples/code-review-kit:1.2.0"); console.log("ref: ", meta.ref); console.log("version:", meta.version); diff --git a/examples/basics/verify-and-lock-bundle.ts b/examples/basics/verify-and-lock-bundle.ts index 463b9ee..0b40b65 100644 --- a/examples/basics/verify-and-lock-bundle.ts +++ b/examples/basics/verify-and-lock-bundle.ts @@ -15,7 +15,7 @@ import { MusherClient, pull } from "@musher-dev/musher-sdk"; // Pull using the top-level convenience function -const bundle = await pull("acme/code-review-kit:1.2.0"); +const bundle = await pull("musher-examples/code-review-kit:1.2.0"); // Verify integrity — all SHA-256 digests must match const result = bundle.verify(); @@ -32,5 +32,5 @@ console.log("Wrote musher-lock.json"); // Use MusherClient directly when you need custom configuration const client = new MusherClient({ cacheDir: "/tmp/musher-cache" }); -const custom = await client.pull("acme/code-review-kit:1.2.0"); +const custom = await client.pull("musher-examples/code-review-kit:1.2.0"); console.log(`Pulled ${custom.ref.toString()} with custom cache dir`); diff --git a/examples/claude/export-plugin.ts b/examples/claude/export-plugin.ts index 5b82d5a..d907b39 100644 --- a/examples/claude/export-plugin.ts +++ b/examples/claude/export-plugin.ts @@ -21,7 +21,7 @@ import { exportClaudePlugin, pull } from "@musher-dev/musher-sdk"; -const bundle = await pull("acme/code-review-kit:1.2.0"); +const bundle = await pull("musher-examples/code-review-kit:1.2.0"); const pluginDir = await exportClaudePlugin(bundle, { targetDir: "./plugins", diff --git a/examples/claude/install-project-skills.ts b/examples/claude/install-project-skills.ts index 3905628..267e67b 100644 --- a/examples/claude/install-project-skills.ts +++ b/examples/claude/install-project-skills.ts @@ -20,7 +20,7 @@ import { installClaudeSkills, pull } from "@musher-dev/musher-sdk"; -const bundle = await pull("acme/code-review-kit:1.2.0"); +const bundle = await pull("musher-examples/code-review-kit:1.2.0"); // Pass the project root — skills are written to /.claude/skills// const paths = await installClaudeSkills(bundle, process.cwd()); diff --git a/examples/ide/install-vscode-skills.ts b/examples/ide/install-vscode-skills.ts index 8d7cdba..9d214c2 100644 --- a/examples/ide/install-vscode-skills.ts +++ b/examples/ide/install-vscode-skills.ts @@ -14,7 +14,7 @@ import { installVSCodeSkills, pull } from "@musher-dev/musher-sdk"; -const bundle = await pull("acme/code-review-kit:1.2.0"); +const bundle = await pull("musher-examples/code-review-kit:1.2.0"); // Explicitly pass the subdir for clarity (this is the default) const paths = await installVSCodeSkills(bundle, process.cwd(), { diff --git a/examples/openai/container-inline-agent.ts b/examples/openai/container-inline-agent.ts index baebbe9..e56e0d2 100644 --- a/examples/openai/container-inline-agent.ts +++ b/examples/openai/container-inline-agent.ts @@ -17,8 +17,8 @@ import { exportOpenAIInlineSkill, pull } from "@musher-dev/musher-sdk"; import { Agent, run, shellTool } from "@openai/agents"; -const bundle = await pull("acme/code-review-kit:1.2.0"); -const inline = exportOpenAIInlineSkill(bundle.skill("lint-rules")); +const bundle = await pull("musher-examples/code-review-kit:1.2.0"); +const inline = exportOpenAIInlineSkill(bundle.skill("reviewing-pull-requests")); console.log(`Exported inline skill "${inline.name}" (${inline.source.data.length} base64 chars)`); diff --git a/examples/openai/container-skill-ref.ts b/examples/openai/container-skill-ref.ts index d5e5a12..0c61f24 100644 --- a/examples/openai/container-skill-ref.ts +++ b/examples/openai/container-skill-ref.ts @@ -29,8 +29,8 @@ const client = new OpenAI() as OpenAI & { }; }; -const bundle = await pull("acme/code-review-kit:1.2.0"); -const inline = exportOpenAIInlineSkill(bundle.skill("lint-rules")); +const bundle = await pull("musher-examples/code-review-kit:1.2.0"); +const inline = exportOpenAIInlineSkill(bundle.skill("reviewing-pull-requests")); // Upload the skill to OpenAI for reuse across agents const uploaded = await client.skills.create({ diff --git a/examples/openai/hosted-inline-skill.ts b/examples/openai/hosted-inline-skill.ts index d7a61b7..9c22956 100644 --- a/examples/openai/hosted-inline-skill.ts +++ b/examples/openai/hosted-inline-skill.ts @@ -13,8 +13,8 @@ import { exportOpenAIInlineSkill, pull } from "@musher-dev/musher-sdk"; -const bundle = await pull("acme/code-review-kit:1.2.0"); -const skill = bundle.skill("lint-rules"); +const bundle = await pull("musher-examples/code-review-kit:1.2.0"); +const skill = bundle.skill("reviewing-pull-requests"); const inline = exportOpenAIInlineSkill(skill); diff --git a/examples/openai/local-shell-agent.ts b/examples/openai/local-shell-agent.ts index 059f722..3fc34ef 100644 --- a/examples/openai/local-shell-agent.ts +++ b/examples/openai/local-shell-agent.ts @@ -17,8 +17,8 @@ import { exportOpenAILocalSkill, pull } from "@musher-dev/musher-sdk"; import { Agent, run, shellTool } from "@openai/agents"; -const bundle = await pull("acme/code-review-kit:1.2.0"); -const skill = bundle.skill("lint-rules"); +const bundle = await pull("musher-examples/code-review-kit:1.2.0"); +const skill = bundle.skill("reviewing-pull-requests"); const exported = await exportOpenAILocalSkill(skill, "./openai-skills"); console.log(`Exported local skill "${exported.name}" → ${exported.path}`); diff --git a/examples/openai/local-shell-skill.ts b/examples/openai/local-shell-skill.ts index e6c5200..278fe27 100644 --- a/examples/openai/local-shell-skill.ts +++ b/examples/openai/local-shell-skill.ts @@ -14,8 +14,8 @@ import { exportOpenAILocalSkill, pull } from "@musher-dev/musher-sdk"; -const bundle = await pull("acme/code-review-kit:1.2.0"); -const skill = bundle.skill("lint-rules"); +const bundle = await pull("musher-examples/code-review-kit:1.2.0"); +const skill = bundle.skill("reviewing-pull-requests"); const exported = await exportOpenAILocalSkill(skill, "./openai-skills"); diff --git a/package.json b/package.json index 9257c44..abc1f9d 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@biomejs/biome": "^1.9.4", + "tsx": "^4.21.0", "typescript": "^5.7.0" }, "packageManager": "pnpm@9.15.0", diff --git a/packages/musher/package.json b/packages/musher/package.json index b61eb69..ede43ca 100644 --- a/packages/musher/package.json +++ b/packages/musher/package.json @@ -19,9 +19,7 @@ "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", - "files": [ - "dist" - ], + "files": ["dist"], "scripts": { "build": "tsup", "check": "pnpm check:format && pnpm check:lint && pnpm check:types && pnpm check:test", @@ -55,11 +53,5 @@ "url": "git+https://github.com/musher-dev/typescript-sdk.git", "directory": "packages/musher" }, - "keywords": [ - "musher", - "bundle", - "sdk", - "ai", - "agent" - ] + "keywords": ["musher", "bundle", "sdk", "ai", "agent"] } diff --git a/packages/musher/src/client.ts b/packages/musher/src/client.ts index 193f89b..05486e8 100644 --- a/packages/musher/src/client.ts +++ b/packages/musher/src/client.ts @@ -9,11 +9,11 @@ import { createHash } from "node:crypto"; import { Bundle } from "./bundle.js"; import { BundleCache } from "./cache.js"; import { type ClientConfig, resolveConfig } from "./config.js"; -import { IntegrityError } from "./errors.js"; +import { ApiError, ForbiddenError, IntegrityError, NotFoundError } from "./errors.js"; import { HttpTransport } from "./http.js"; import { BundleRef } from "./ref.js"; import { BundlesResource } from "./resources/bundles.js"; -import type { BundleResolveOutput } from "./types.js"; +import type { BundleResolveOutput, PullBundleVersionOutput } from "./types.js"; let _loadDeprecationWarned = false; @@ -45,6 +45,8 @@ export class MusherClient { async pull(ref: string, version?: string): Promise { const parsed = BundleRef.parse(ref); const resolvedVersion = version ?? parsed.version; + + // Resolve metadata first (needed for manifest hashes and cache keys) const resolved = await this.bundles.resolve( parsed.namespace, parsed.slug, @@ -52,25 +54,29 @@ export class MusherClient { parsed.digest, ); - // Download asset contents as Buffers, verifying integrity before caching - const assets = new Map(); + // Pull asset content — try :pull endpoint (single request), fall back to + // individual asset fetches if the caller lacks namespace access. + const pulled = await this.pullContent(parsed.namespace, parsed.slug, resolved); + + // Build asset map, verifying integrity against the resolve manifest + const hashByPath = new Map(); if (resolved.manifest?.layers) { for (const layer of resolved.manifest.layers) { - const asset = await this.bundles.getAsset( - parsed.namespace, - parsed.slug, - layer.assetId, - resolved.version, - ); - if (asset.contentText != null) { - const buf = Buffer.from(asset.contentText, "utf-8"); - const hash = createHash("sha256").update(buf).digest("hex"); - if (hash !== layer.contentSha256) { - throw new IntegrityError(layer.contentSha256, hash); - } - assets.set(layer.logicalPath, buf); + hashByPath.set(layer.logicalPath, layer.contentSha256); + } + } + + const assets = new Map(); + for (const asset of pulled.manifest) { + const buf = Buffer.from(asset.contentText, "utf-8"); + const expectedHash = hashByPath.get(asset.logicalPath); + if (expectedHash) { + const hash = createHash("sha256").update(buf).digest("hex"); + if (hash !== expectedHash) { + throw new IntegrityError(expectedHash, hash); } } + assets.set(asset.logicalPath, buf); } await this._cache.write(resolved, assets); @@ -179,6 +185,61 @@ export class MusherClient { return this.pull(ref, version); } + /** + * Pull content via the :pull endpoint with automatic fallback. + * + * 1. Try namespace :pull (works when caller owns the namespace) + * 2. Fall back to hub :pull (works for any public bundle) + * 3. Fall back to individual asset fetches via getAsset + */ + private async pullContent( + namespace: string, + slug: string, + resolved: BundleResolveOutput, + ): Promise { + // Try namespace :pull first + try { + return await this.bundles.pullVersion(namespace, slug, resolved.version); + } catch (error) { + if (!(error instanceof ForbiddenError || error instanceof NotFoundError)) { + throw error; + } + } + + // Fall back to hub :pull (public bundles) + try { + return await this.bundles.pullHubVersion(namespace, slug, resolved.version); + } catch (error) { + if (!(error instanceof ApiError)) { + throw error; + } + } + + // Final fallback: individual asset fetches + if (resolved.manifest?.layers?.length === 0 || !resolved.manifest?.layers) { + return { namespace, slug, version: resolved.version, name: resolved.ref, manifest: [] }; + } + + const manifest = await Promise.all( + resolved.manifest.layers.map(async (layer) => { + const asset = await this.bundles.getAsset( + namespace, + slug, + layer.logicalPath, + resolved.version, + ); + return { + logicalPath: layer.logicalPath, + assetType: layer.assetType, + contentText: asset.contentText ?? "", + mediaType: layer.mediaType ?? null, + }; + }), + ); + + return { namespace, slug, version: resolved.version, name: resolved.ref, manifest }; + } + /** Cache management utilities. */ readonly cache = { /** Remove expired cache entries and garbage-collect unreferenced blobs. */ diff --git a/packages/musher/src/index.ts b/packages/musher/src/index.ts index 95dd4d1..0875cb1 100644 --- a/packages/musher/src/index.ts +++ b/packages/musher/src/index.ts @@ -72,6 +72,8 @@ export { ManifestAssetOutputSchema, ManifestDetailOutputSchema, PaginationMetaSchema, + PullAssetOutputSchema, + PullBundleVersionOutputSchema, paginatedSchema, } from "./schemas/index.js"; @@ -94,6 +96,8 @@ export type { Paginated, PaginateParams, PaginationMeta, + PullAssetOutput, + PullBundleVersionOutput, SelectionFilter, VerifyResult, } from "./types.js"; diff --git a/packages/musher/src/resources/bundles.ts b/packages/musher/src/resources/bundles.ts index 4979542..29e7bec 100644 --- a/packages/musher/src/resources/bundles.ts +++ b/packages/musher/src/resources/bundles.ts @@ -6,7 +6,7 @@ import type { HttpTransport } from "../http.js"; import { AssetDetailOutputSchema, AssetSummaryOutputSchema } from "../schemas/asset.js"; import { BundleDetailOutputSchema, BundleOutputSchema } from "../schemas/bundle.js"; import { paginatedSchema } from "../schemas/common.js"; -import { BundleResolveOutputSchema } from "../schemas/resolve.js"; +import { BundleResolveOutputSchema, PullBundleVersionOutputSchema } from "../schemas/resolve.js"; import { BundleVersionDetailOutputSchema, BundleVersionSummaryOutputSchema, @@ -21,6 +21,7 @@ import type { BundleVersionSummaryOutput, PaginateParams, Paginated, + PullBundleVersionOutput, } from "../types.js"; export class BundlesResource { @@ -122,16 +123,41 @@ export class BundlesResource { async getAsset( namespace: string, bundle: string, - assetId: string, + logicalPath: string, version: string, ): Promise { + const encodedPath = logicalPath.split("/").map(encodeURIComponent).join("/"); return this.http.request( "GET", - `/v1/namespaces/${enc(namespace)}/bundles/${enc(bundle)}/assets/${enc(assetId)}`, + `/v1/namespaces/${enc(namespace)}/bundles/${enc(bundle)}/assets/${encodedPath}`, AssetDetailOutputSchema, { params: { version } }, ); } + + async pullVersion( + namespace: string, + bundle: string, + version: string, + ): Promise { + return this.http.request( + "GET", + `/v1/namespaces/${enc(namespace)}/bundles/${enc(bundle)}/versions/${enc(version)}:pull`, + PullBundleVersionOutputSchema, + ); + } + + async pullHubVersion( + publisherHandle: string, + bundleSlug: string, + version: string, + ): Promise { + return this.http.request( + "GET", + `/v1/hub/bundles/${enc(publisherHandle)}/${enc(bundleSlug)}/versions/${enc(version)}:pull`, + PullBundleVersionOutputSchema, + ); + } } function enc(value: string): string { diff --git a/packages/musher/src/schemas/index.ts b/packages/musher/src/schemas/index.ts index fef185f..fc70c32 100644 --- a/packages/musher/src/schemas/index.ts +++ b/packages/musher/src/schemas/index.ts @@ -15,6 +15,8 @@ export { BundleLayerOutputSchema, BundleManifestOutputSchema, BundleResolveOutputSchema, + PullAssetOutputSchema, + PullBundleVersionOutputSchema, } from "./resolve.js"; export { diff --git a/packages/musher/src/schemas/resolve.ts b/packages/musher/src/schemas/resolve.ts index e5f8196..7458e58 100644 --- a/packages/musher/src/schemas/resolve.ts +++ b/packages/musher/src/schemas/resolve.ts @@ -26,4 +26,25 @@ export const BundleResolveOutputSchema = z.object({ ociDigest: z.string().nullable().optional(), state: BundleVersionState, manifest: BundleManifestOutputSchema.nullable().optional(), + isSigned: z.boolean().optional(), + signerType: z.string().nullable().optional(), + signedAt: z.string().datetime().nullable().optional(), +}); + +// -- Pull endpoint schemas ---------------------------------------------------- + +export const PullAssetOutputSchema = z.object({ + logicalPath: z.string(), + assetType: z.string(), + contentText: z.string(), + mediaType: z.string().nullable().optional(), +}); + +export const PullBundleVersionOutputSchema = z.object({ + namespace: z.string(), + slug: z.string(), + version: z.string(), + name: z.string(), + description: z.string().nullable().optional(), + manifest: z.array(PullAssetOutputSchema), }); diff --git a/packages/musher/src/types.ts b/packages/musher/src/types.ts index 323401a..692bc90 100644 --- a/packages/musher/src/types.ts +++ b/packages/musher/src/types.ts @@ -12,6 +12,8 @@ import type { BundleLayerOutputSchema, BundleManifestOutputSchema, BundleResolveOutputSchema, + PullAssetOutputSchema, + PullBundleVersionOutputSchema, } from "./schemas/resolve.js"; import type { BundleVersionDetailOutputSchema, @@ -56,6 +58,8 @@ export type ManifestDetailOutput = z.infer; export type BundleResolveOutput = z.infer; export type BundleLayerOutput = z.infer; export type BundleManifestOutput = z.infer; +export type PullAssetOutput = z.infer; +export type PullBundleVersionOutput = z.infer; // -- High-level types --------------------------------------------------------- diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0eca41a..4d5a023 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@biomejs/biome': specifier: ^1.9.4 version: 1.9.4 + tsx: + specifier: ^4.21.0 + version: 4.21.0 typescript: specifier: ^5.7.0 version: 5.9.3 @@ -56,7 +59,7 @@ importers: version: 0.3.18 tsup: specifier: ^8.3.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.3) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: ^5.7.0 version: 5.9.3 @@ -1846,6 +1849,11 @@ packages: typescript: optional: true + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} @@ -3401,12 +3409,13 @@ snapshots: mlly: 1.8.1 pathe: 2.0.3 - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(yaml@2.8.3): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 postcss: 8.5.8 + tsx: 4.21.0 yaml: 2.8.3 postcss@8.5.8: @@ -3673,7 +3682,7 @@ snapshots: tslib@2.8.1: optional: true - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.3): + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.4) cac: 6.7.14 @@ -3684,7 +3693,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(yaml@2.8.3) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3) resolve-from: 5.0.0 rollup: 4.59.0 source-map: 0.7.6 @@ -3701,6 +3710,13 @@ snapshots: - tsx - yaml + tsx@4.21.0: + dependencies: + esbuild: 0.27.4 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + type-is@2.0.1: dependencies: content-type: 1.0.5