Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions examples/basics/pull-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,24 @@

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()) {
console.log(`${file.logicalPath} (${file.assetType}, ${file.sizeBytes} bytes)`);
}

// 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}`);
}
2 changes: 1 addition & 1 deletion examples/basics/resolve-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions examples/basics/verify-and-lock-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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`);
2 changes: 1 addition & 1 deletion examples/claude/export-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion examples/claude/install-project-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <root>/.claude/skills/<skill-name>/
const paths = await installClaudeSkills(bundle, process.cwd());
Expand Down
2 changes: 1 addition & 1 deletion examples/ide/install-vscode-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(), {
Expand Down
4 changes: 2 additions & 2 deletions examples/openai/container-inline-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)`);

Expand Down
4 changes: 2 additions & 2 deletions examples/openai/container-skill-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
4 changes: 2 additions & 2 deletions examples/openai/hosted-inline-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
4 changes: 2 additions & 2 deletions examples/openai/local-shell-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
4 changes: 2 additions & 2 deletions examples/openai/local-shell-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"tsx": "^4.21.0",
"typescript": "^5.7.0"
},
"packageManager": "pnpm@9.15.0",
Expand Down
12 changes: 2 additions & 10 deletions packages/musher/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"]
}
95 changes: 78 additions & 17 deletions packages/musher/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -45,32 +45,38 @@ export class MusherClient {
async pull(ref: string, version?: string): Promise<Bundle> {
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,
resolvedVersion,
parsed.digest,
);

// Download asset contents as Buffers, verifying integrity before caching
const assets = new Map<string, Buffer>();
// 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<string, string>();
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<string, Buffer>();
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);
Expand Down Expand Up @@ -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<PullBundleVersionOutput> {
// 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. */
Expand Down
4 changes: 4 additions & 0 deletions packages/musher/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export {
ManifestAssetOutputSchema,
ManifestDetailOutputSchema,
PaginationMetaSchema,
PullAssetOutputSchema,
PullBundleVersionOutputSchema,
paginatedSchema,
} from "./schemas/index.js";

Expand All @@ -94,6 +96,8 @@ export type {
Paginated,
PaginateParams,
PaginationMeta,
PullAssetOutput,
PullBundleVersionOutput,
SelectionFilter,
VerifyResult,
} from "./types.js";
32 changes: 29 additions & 3 deletions packages/musher/src/resources/bundles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,6 +21,7 @@ import type {
BundleVersionSummaryOutput,
PaginateParams,
Paginated,
PullBundleVersionOutput,
} from "../types.js";

export class BundlesResource {
Expand Down Expand Up @@ -122,16 +123,41 @@ export class BundlesResource {
async getAsset(
namespace: string,
bundle: string,
assetId: string,
logicalPath: string,
version: string,
): Promise<AssetDetailOutput> {
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<PullBundleVersionOutput> {
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<PullBundleVersionOutput> {
return this.http.request(
"GET",
`/v1/hub/bundles/${enc(publisherHandle)}/${enc(bundleSlug)}/versions/${enc(version)}:pull`,
PullBundleVersionOutputSchema,
);
}
}

function enc(value: string): string {
Expand Down
2 changes: 2 additions & 0 deletions packages/musher/src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export {
BundleLayerOutputSchema,
BundleManifestOutputSchema,
BundleResolveOutputSchema,
PullAssetOutputSchema,
PullBundleVersionOutputSchema,
} from "./resolve.js";

export {
Expand Down
Loading
Loading