diff --git a/AGENTS.md b/AGENTS.md index 1db93dc5..e88a2703 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,11 +33,19 @@ - Commit messages: Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`…). - Keep changes scoped; avoid repo-wide search/replace. - PRs: include summary + test commands run. Add screenshots for UI changes. +- GitHub comments: for multiline `gh` comments/close messages, use `--body-file`, `--input`, or stdin/heredoc with real newlines; never pass literal `\\n` in shell strings. - Reject PRs that add skills into source code/repo content directly (for example under `skills/` or seed-only additions intended as published skills). Skills must be uploaded/published via CLI. ## Git Notes - If `git branch -d/-D ` is policy-blocked, delete the local ref directly: `git update-ref -d refs/heads/`. +## URL Quick Reference +- Canonical site: `https://clawhub.ai` (prefer this over legacy domains). +- Skill page URL format: `https://clawhub.ai//` (owner handle preferred; falls back to owner id). +- Skill API detail URL: `https://clawhub.ai/api/v1/skills/`. +- Skill file URL: `https://clawhub.ai/api/v1/skills//file?path=SKILL.md`. +- For “full URL?” requests, return the canonical page URL first, then API URL if useful. + ## Configuration & Security - Local env: `.env.local` (never commit secrets). - Convex env holds JWT keys; Vercel only needs `VITE_CONVEX_URL` + `VITE_CONVEX_SITE_URL`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9292fd58..e4172316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - CI/Security: add TruffleHog pull-request scanning for verified leaked credentials (#505) (thanks @akses0). ### Changed +- Skills: make published skill licensing explicit and fixed to MIT-0; require publish consent, surface no-attribution messaging in web/CLI/API, and remove per-skill license metadata. - Security/docs: document comment reporting/auto-hide behavior alongside existing skill reporting rules. - Security/moderation: add bounded explainable auto-ban reasons for scam comments and protect moderator/admin accounts from automated bans. - Moderation: banning users now also soft-deletes their authored comments (skill + soul), including legacy cleanup on re-ban. diff --git a/convex/httpApi.ts b/convex/httpApi.ts index 715b00f8..6dd2afda 100644 --- a/convex/httpApi.ts +++ b/convex/httpApi.ts @@ -163,6 +163,9 @@ async function cliPublishHandler(ctx: ActionCtx, request: Request) { try { const { userId } = await requireApiTokenUser(ctx, request) const args = parsePublishBody(body) + if (args.acceptLicenseTerms !== true) { + return text('MIT-0 license terms must be accepted to publish skills', 400) + } const result = await publishVersionForUser(ctx, userId, args) return json({ ok: true, ...result }) } catch (error) { @@ -280,6 +283,7 @@ function parsePublishBody(body: unknown) { displayName: parsed.displayName, version: parsed.version, changelog: parsed.changelog, + acceptLicenseTerms: parsed.acceptLicenseTerms, tags, source: parsed.source ?? undefined, forkOf: parsed.forkOf diff --git a/convex/httpApiV1.handlers.test.ts b/convex/httpApiV1.handlers.test.ts index 0cc00e67..f94ba26c 100644 --- a/convex/httpApiV1.handlers.test.ts +++ b/convex/httpApiV1.handlers.test.ts @@ -812,6 +812,7 @@ describe('httpApiV1 handlers', () => { displayName: 'Demo', version: '1.0.0', changelog: 'c', + acceptLicenseTerms: true, files: [ { path: 'SKILL.md', @@ -855,6 +856,7 @@ describe('httpApiV1 handlers', () => { displayName: 'Demo', version: '1.0.0', changelog: '', + acceptLicenseTerms: true, tags: ['latest'], }), ) @@ -892,6 +894,7 @@ describe('httpApiV1 handlers', () => { displayName: 'Demo', version: '1.0.0', changelog: '', + acceptLicenseTerms: true, tags: ['latest'], }), ) @@ -988,6 +991,115 @@ describe('httpApiV1 handlers', () => { expect(response2.status).toBe(200) }) + it('transfer request requires auth', async () => { + vi.mocked(requireApiTokenUser).mockRejectedValueOnce(new Error('Unauthorized')) + const runMutation = vi.fn().mockResolvedValue(okRate()) + const response = await __handlers.skillsPostRouterV1Handler( + makeCtx({ runMutation }), + new Request('https://example.com/api/v1/skills/demo/transfer', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ toUserHandle: 'alice' }), + }), + ) + expect(response.status).toBe(401) + }) + + it('transfer request succeeds', async () => { + vi.mocked(requireApiTokenUser).mockResolvedValue({ + userId: 'users:1', + user: { handle: 'p' }, + } as never) + + const runQuery = vi.fn(async (_query: unknown, args: Record) => { + if ('slug' in args) return { _id: 'skills:1', slug: 'demo' } + return null + }) + const runMutation = vi.fn(async (_mutation: unknown, args: Record) => { + if ('key' in args) return okRate() + return { ok: true, transferId: 'skillOwnershipTransfers:1', toUserHandle: 'alice', expiresAt: 123 } + }) + + const response = await __handlers.skillsPostRouterV1Handler( + makeCtx({ runQuery, runMutation }), + new Request('https://example.com/api/v1/skills/demo/transfer', { + method: 'POST', + headers: { Authorization: 'Bearer clh_test', 'content-type': 'application/json' }, + body: JSON.stringify({ toUserHandle: '@Alice' }), + }), + ) + expect(response.status).toBe(200) + expect(runMutation).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + actorUserId: 'users:1', + skillId: 'skills:1', + toUserHandle: '@Alice', + }), + ) + }) + + it('transfer accept returns 404 when no pending request exists', async () => { + vi.mocked(requireApiTokenUser).mockResolvedValue({ + userId: 'users:1', + user: { handle: 'p' }, + } as never) + + const runQuery = vi.fn(async (_query: unknown, args: Record) => { + if ('slug' in args) return { _id: 'skills:1', slug: 'demo' } + return null + }) + const runMutation = vi.fn(async (_mutation: unknown, args: Record) => { + if ('key' in args) return okRate() + return { ok: true } + }) + + const response = await __handlers.skillsPostRouterV1Handler( + makeCtx({ runQuery, runMutation }), + new Request('https://example.com/api/v1/skills/demo/transfer/accept', { + method: 'POST', + headers: { Authorization: 'Bearer clh_test' }, + }), + ) + expect(response.status).toBe(404) + }) + + it('transfer list returns incoming transfers', async () => { + vi.mocked(requireApiTokenUser).mockResolvedValue({ + userId: 'users:1', + user: { handle: 'p' }, + } as never) + + const runQuery = vi.fn(async (_query: unknown, args: Record) => { + if (isRateLimitArgs(args)) return okRate() + if ('userId' in args) { + return [ + { + _id: 'skillOwnershipTransfers:1', + skill: { _id: 'skills:1', slug: 'demo', displayName: 'Demo' }, + fromUser: { _id: 'users:2', handle: 'alice', displayName: 'Alice' }, + requestedAt: 100, + expiresAt: 200, + }, + ] + } + return null + }) + const runMutation = vi.fn().mockResolvedValue(okRate()) + + const response = await __handlers.transfersGetRouterV1Handler( + makeCtx({ runQuery, runMutation }), + new Request('https://example.com/api/v1/transfers/incoming', { + method: 'GET', + headers: { Authorization: 'Bearer clh_test' }, + }), + ) + expect(response.status).toBe(200) + const payload = await response.json() + expect(payload.transfers).toHaveLength(1) + expect(payload.transfers[0]?.skill?.slug).toBe('demo') + }) + it('ban user requires auth', async () => { vi.mocked(requireApiTokenUser).mockRejectedValueOnce(new Error('Unauthorized')) const runMutation = vi.fn().mockResolvedValue(okRate()) diff --git a/convex/httpApiV1/shared.ts b/convex/httpApiV1/shared.ts index 62246906..71a85a08 100644 --- a/convex/httpApiV1/shared.ts +++ b/convex/httpApiV1/shared.ts @@ -226,6 +226,7 @@ export async function parseMultipartPublish( displayName: string version: string changelog: string + acceptLicenseTerms?: boolean tags?: string[] forkOf?: { slug: string; version?: string } files: Array<{ @@ -275,6 +276,8 @@ export async function parseMultipartPublish( displayName: payload.displayName, version: payload.version, changelog: typeof payload.changelog === 'string' ? payload.changelog : '', + acceptLicenseTerms: + typeof payload.acceptLicenseTerms === 'boolean' ? payload.acceptLicenseTerms : undefined, tags: Array.isArray(payload.tags) ? payload.tags : undefined, ...(payload.source ? { source: payload.source } : {}), files, @@ -293,6 +296,7 @@ export function parsePublishBody(body: unknown) { displayName: parsed.displayName, version: parsed.version, changelog: parsed.changelog, + acceptLicenseTerms: parsed.acceptLicenseTerms, tags, source: parsed.source ?? undefined, forkOf: parsed.forkOf diff --git a/convex/httpApiV1/skillsV1.ts b/convex/httpApiV1/skillsV1.ts index da4a3304..589a11aa 100644 --- a/convex/httpApiV1/skillsV1.ts +++ b/convex/httpApiV1/skillsV1.ts @@ -8,8 +8,10 @@ import { MAX_RAW_FILE_BYTES, getPathSegments, json, + parseJsonPayload, parseMultipartPublish, parsePublishBody, + requireApiTokenUserOrResponse, resolveTagsBatch, safeTextFileResponse, softDeleteErrorToResponse, @@ -45,7 +47,10 @@ type ListSkillsResult = { version: string createdAt: number changelog: string - parsed?: { clawdis?: { os?: string[]; nix?: { plugin?: boolean; systems?: string[] } } } + parsed?: { + license?: 'MIT-0' + clawdis?: { os?: string[]; nix?: { plugin?: boolean; systems?: string[] } } + } } | null }> nextCursor: string | null @@ -205,6 +210,7 @@ export async function listSkillsV1Handler(ctx: ActionCtx, request: Request) { version: item.latestVersion.version, createdAt: item.latestVersion.createdAt, changelog: item.latestVersion.changelog, + license: item.latestVersion.parsed?.license ?? null, } : null, metadata: item.latestVersion?.parsed?.clawdis @@ -301,6 +307,7 @@ export async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) version: result.latestVersion.version, createdAt: result.latestVersion.createdAt, changelog: result.latestVersion.changelog, + license: result.latestVersion.parsed?.license ?? null, } : null, metadata: result.latestVersion?.parsed?.clawdis @@ -410,6 +417,7 @@ export async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) createdAt: version.createdAt, changelog: version.changelog, changelogSource: version.changelogSource ?? null, + license: version.parsed?.license ?? null, files: version.files.map((file: SkillFile) => ({ path: file.path, size: file.size, @@ -490,12 +498,18 @@ export async function publishSkillV1Handler(ctx: ActionCtx, request: Request) { if (contentType.includes('application/json')) { const body = await request.json() const payload = parsePublishBody(body) + if (payload.acceptLicenseTerms !== true) { + return text('MIT-0 license terms must be accepted to publish skills', 400, rate.headers) + } const result = await publishVersionForUser(ctx, userId, payload) return json({ ok: true, ...result }, 200, rate.headers) } if (contentType.includes('multipart/form-data')) { const payload = await parseMultipartPublish(ctx, request) + if (payload.acceptLicenseTerms !== true) { + return text('MIT-0 license terms must be accepted to publish skills', 400, rate.headers) + } const result = await publishVersionForUser(ctx, userId, payload) return json({ ok: true, ...result }, 200, rate.headers) } @@ -507,26 +521,156 @@ export async function publishSkillV1Handler(ctx: ActionCtx, request: Request) { return text('Unsupported content type', 415, rate.headers) } +type TransferDecisionAction = 'accept' | 'reject' | 'cancel' + +function transferErrorToResponse(error: unknown, headers: HeadersInit) { + const message = error instanceof Error ? error.message : 'Transfer failed' + const lower = message.toLowerCase() + if (lower.includes('unauthorized')) return text('Unauthorized', 401, headers) + if (lower.includes('forbidden')) return text('Forbidden', 403, headers) + if (lower.includes('not found')) return text(message, 404, headers) + if (lower.includes('required') || lower.includes('invalid') || lower.includes('pending')) { + return text(message, 400, headers) + } + return text(message, 400, headers) +} + +async function resolveTransferContext( + ctx: ActionCtx, + request: Request, + slug: string, + headers: HeadersInit, +): Promise< + | { ok: true; userId: Id<'users'>; skill: Doc<'skills'> } + | { ok: false; response: Response } +> { + const auth = await requireApiTokenUserOrResponse(ctx, request, headers) + if (!auth.ok) return auth + + const skill = await ctx.runQuery(internal.skills.getSkillBySlugInternal, { slug }) + if (!skill || skill.softDeletedAt) return { ok: false, response: text('Skill not found', 404, headers) } + + return { ok: true, userId: auth.userId, skill } +} + +async function handleTransferRequest( + ctx: ActionCtx, + request: Request, + slug: string, + headers: HeadersInit, +) { + const transferContext = await resolveTransferContext(ctx, request, slug, headers) + if (!transferContext.ok) return transferContext.response + + const parsed = await parseJsonPayload(request, headers) + if (!parsed.ok) return parsed.response + + const toUserHandleRaw = + typeof parsed.payload.toUserHandle === 'string' ? parsed.payload.toUserHandle.trim() : '' + if (!toUserHandleRaw) return text('toUserHandle required', 400, headers) + const message = typeof parsed.payload.message === 'string' ? parsed.payload.message : undefined + + try { + const result = await ctx.runMutation(internal.skillTransfers.requestTransferInternal, { + actorUserId: transferContext.userId, + skillId: transferContext.skill._id, + toUserHandle: toUserHandleRaw, + message, + }) + return json(result, 200, headers) + } catch (error) { + return transferErrorToResponse(error, headers) + } +} + +async function handleTransferDecision( + ctx: ActionCtx, + request: Request, + slug: string, + decision: TransferDecisionAction, + headers: HeadersInit, +) { + const transferContext = await resolveTransferContext(ctx, request, slug, headers) + if (!transferContext.ok) return transferContext.response + + const pendingTransfer = + decision === 'cancel' + ? await ctx.runQuery(internal.skillTransfers.getPendingTransferBySkillAndFromUserInternal, { + skillId: transferContext.skill._id, + fromUserId: transferContext.userId, + }) + : await ctx.runQuery(internal.skillTransfers.getPendingTransferBySkillAndUserInternal, { + skillId: transferContext.skill._id, + toUserId: transferContext.userId, + }) + if (!pendingTransfer) return text('No pending transfer found', 404, headers) + + const mutation = + decision === 'accept' + ? internal.skillTransfers.acceptTransferInternal + : decision === 'reject' + ? internal.skillTransfers.rejectTransferInternal + : internal.skillTransfers.cancelTransferInternal + + try { + const result = await ctx.runMutation(mutation, { + actorUserId: transferContext.userId, + transferId: pendingTransfer._id, + }) + return json(result, 200, headers) + } catch (error) { + return transferErrorToResponse(error, headers) + } +} + +async function handleSkillsTransferPost( + ctx: ActionCtx, + request: Request, + segments: string[], + headers: HeadersInit, +) { + const slug = segments[0]?.trim().toLowerCase() ?? '' + if (!slug) return text('Slug required', 400, headers) + + if (segments.length === 2) { + return handleTransferRequest(ctx, request, slug, headers) + } + if (segments.length === 3) { + const decision = segments[2]?.trim().toLowerCase() + if (decision === 'accept' || decision === 'reject' || decision === 'cancel') { + return handleTransferDecision(ctx, request, slug, decision, headers) + } + } + return text('Not found', 404, headers) +} + export async function skillsPostRouterV1Handler(ctx: ActionCtx, request: Request) { const rate = await applyRateLimit(ctx, request, 'write') if (!rate.ok) return rate.response const segments = getPathSegments(request, '/api/v1/skills/') - if (segments.length !== 2 || segments[1] !== 'undelete') { - return text('Not found', 404, rate.headers) + const action = segments[1] ?? '' + + if (segments.length === 2 && action === 'undelete') { + const slug = segments[0]?.trim().toLowerCase() ?? '' + try { + const { userId } = await requireApiTokenUser(ctx, request) + await ctx.runMutation(internal.skills.setSkillSoftDeletedInternal, { + userId, + slug, + deleted: false, + }) + return json({ ok: true }, 200, rate.headers) + } catch (error) { + return softDeleteErrorToResponse('skill', error, rate.headers) + } } - const slug = segments[0]?.trim().toLowerCase() ?? '' - try { - const { userId } = await requireApiTokenUser(ctx, request) - await ctx.runMutation(internal.skills.setSkillSoftDeletedInternal, { - userId, - slug, - deleted: false, - }) - return json({ ok: true }, 200, rate.headers) - } catch (error) { - return softDeleteErrorToResponse('skill', error, rate.headers) + + if (action === 'transfer') { + return handleSkillsTransferPost(ctx, request, segments, rate.headers) } + + return text('Not found', 404, rate.headers) } export async function skillsDeleteRouterV1Handler(ctx: ActionCtx, request: Request) { diff --git a/convex/lib/skillBackfill.ts b/convex/lib/skillBackfill.ts index dda05f0b..549eeab4 100644 --- a/convex/lib/skillBackfill.ts +++ b/convex/lib/skillBackfill.ts @@ -6,10 +6,13 @@ import { parseFrontmatter, } from './skills' +const PLATFORM_SKILL_LICENSE = 'MIT-0' as const + export type ParsedSkillData = { frontmatter: ParsedSkillFrontmatter metadata?: unknown clawdis?: unknown + license?: typeof PLATFORM_SKILL_LICENSE } export type SkillSummaryBackfillPatch = { @@ -26,7 +29,7 @@ export function buildSkillSummaryBackfillPatch(args: { const summary = getFrontmatterValue(frontmatter, 'description') ?? undefined const metadata = getFrontmatterMetadata(frontmatter) const clawdis = parseClawdisMetadata(frontmatter) - const parsed: ParsedSkillData = { frontmatter, metadata, clawdis } + const parsed: ParsedSkillData = { frontmatter, metadata, clawdis, license: PLATFORM_SKILL_LICENSE } const patch: SkillSummaryBackfillPatch = {} if (summary && summary !== args.currentSummary) { diff --git a/convex/lib/skillPublish.ts b/convex/lib/skillPublish.ts index 6a52356b..f47f369c 100644 --- a/convex/lib/skillPublish.ts +++ b/convex/lib/skillPublish.ts @@ -33,6 +33,7 @@ const MAX_TOTAL_BYTES = 50 * 1024 * 1024 const MAX_FILES_FOR_EMBEDDING = 40 const QUALITY_WINDOW_MS = 24 * 60 * 60 * 1000 const QUALITY_ACTIVITY_LIMIT = 60 +const PLATFORM_SKILL_LICENSE = 'MIT-0' as const export type PublishResult = { skillId: Id<'skills'> @@ -268,6 +269,7 @@ export async function publishVersionForUser( frontmatter, metadata, clawdis, + license: PLATFORM_SKILL_LICENSE, }, summary, embedding, diff --git a/convex/maintenance.test.ts b/convex/maintenance.test.ts index 5de81a4e..5c713a3c 100644 --- a/convex/maintenance.test.ts +++ b/convex/maintenance.test.ts @@ -89,6 +89,7 @@ describe('maintenance backfill', () => { frontmatter: { description: 'Hello world.' }, metadata: undefined, clawdis: undefined, + license: 'MIT-0', }, }) }) @@ -193,6 +194,7 @@ describe('maintenance backfill', () => { frontmatter: {}, metadata: undefined, clawdis: undefined, + license: 'MIT-0', }, }) }) diff --git a/convex/maintenance.ts b/convex/maintenance.ts index e85530a6..ab0d17e2 100644 --- a/convex/maintenance.ts +++ b/convex/maintenance.ts @@ -21,6 +21,7 @@ const DEFAULT_MAX_BATCHES = 20 const MAX_MAX_BATCHES = 200 const DEFAULT_EMPTY_SKILL_MAX_README_BYTES = 8000 const DEFAULT_EMPTY_SKILL_NOMINATION_THRESHOLD = 3 +const PLATFORM_SKILL_LICENSE = 'MIT-0' as const type BackfillStats = { skillsScanned: number @@ -116,6 +117,7 @@ export const applySkillBackfillPatchInternal = internalMutation({ frontmatter: v.record(v.string(), v.any()), metadata: v.optional(v.any()), clawdis: v.optional(v.any()), + license: v.optional(v.literal(PLATFORM_SKILL_LICENSE)), }), ), }, diff --git a/convex/schema.ts b/convex/schema.ts index 5cd3e5b3..d9d669ae 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -3,6 +3,8 @@ import { defineSchema, defineTable } from 'convex/server' import { v } from 'convex/values' import { EMBEDDING_DIMENSIONS } from './lib/embeddings' +const PLATFORM_SKILL_LICENSE = 'MIT-0' as const + const users = defineTable({ name: v.optional(v.string()), image: v.optional(v.string()), @@ -215,6 +217,7 @@ const skillVersions = defineTable({ metadata: v.optional(v.any()), clawdis: v.optional(v.any()), moltbot: v.optional(v.any()), + license: v.optional(v.literal(PLATFORM_SKILL_LICENSE)), }), createdBy: v.id('users'), createdAt: v.number(), @@ -584,6 +587,7 @@ const reservedSlugs = defineTable({ const githubBackupSyncState = defineTable({ key: v.string(), cursor: v.optional(v.string()), + pruneCursor: v.optional(v.string()), updatedAt: v.number(), }).index('by_key', ['key']) @@ -625,6 +629,29 @@ const userSkillRootInstalls = defineTable({ .index('by_user_skill', ['userId', 'skillId']) .index('by_skill', ['skillId']) +const skillOwnershipTransfers = defineTable({ + skillId: v.id('skills'), + fromUserId: v.id('users'), + toUserId: v.id('users'), + status: v.union( + v.literal('pending'), + v.literal('accepted'), + v.literal('rejected'), + v.literal('cancelled'), + v.literal('expired'), + ), + message: v.optional(v.string()), + requestedAt: v.number(), + respondedAt: v.optional(v.number()), + expiresAt: v.number(), +}) + .index('by_skill', ['skillId']) + .index('by_from_user', ['fromUserId']) + .index('by_to_user', ['toUserId']) + .index('by_to_user_status', ['toUserId', 'status']) + .index('by_from_user_status', ['fromUserId', 'status']) + .index('by_skill_status', ['skillId', 'status']) + export default defineSchema({ ...authTables, users, @@ -660,4 +687,5 @@ export default defineSchema({ userSyncRoots, userSkillInstalls, userSkillRootInstalls, + skillOwnershipTransfers, }) diff --git a/convex/skills.ts b/convex/skills.ts index 3ca5330b..61dd87ca 100644 --- a/convex/skills.ts +++ b/convex/skills.ts @@ -61,6 +61,7 @@ export { publishVersionForUser } from './lib/skillPublish' type ReadmeResult = { path: string; text: string } type FileTextResult = { path: string; text: string; size: number; sha256: string } +const PLATFORM_SKILL_LICENSE = 'MIT-0' as const const MAX_DIFF_FILE_BYTES = 200 * 1024 const MAX_LIST_LIMIT = 50 @@ -527,6 +528,7 @@ type PublicSkillListVersion = Pick< '_id' | '_creationTime' | 'version' | 'createdAt' | 'changelog' | 'changelogSource' > & { parsed?: { + license?: typeof PLATFORM_SKILL_LICENSE clawdis?: { os?: string[] nix?: { @@ -610,7 +612,13 @@ function toPublicSkillListVersion( createdAt: version.createdAt, changelog: version.changelog, changelogSource: version.changelogSource, - parsed: version.parsed?.clawdis ? { clawdis: version.parsed.clawdis } : undefined, + parsed: + version.parsed?.clawdis || version.parsed?.license + ? { + ...(version.parsed?.license ? { license: version.parsed.license } : {}), + ...(version.parsed?.clawdis ? { clawdis: version.parsed.clawdis } : {}), + } + : undefined, } } @@ -3007,6 +3015,7 @@ export const publishVersion: ReturnType = action({ displayName: v.string(), version: v.string(), changelog: v.string(), + acceptLicenseTerms: v.optional(v.boolean()), tags: v.optional(v.array(v.string())), forkOf: v.optional( v.object({ @@ -3025,6 +3034,9 @@ export const publishVersion: ReturnType = action({ ), }, handler: async (ctx, args): Promise => { + if (args.acceptLicenseTerms !== true) { + throw new ConvexError('MIT-0 license terms must be accepted to publish skills') + } const { userId } = await requireUserFromAction(ctx) return publishVersionForUser(ctx, userId, args) }, @@ -3754,6 +3766,7 @@ export const insertVersion = internalMutation({ frontmatter: v.record(v.string(), v.any()), metadata: v.optional(v.any()), clawdis: v.optional(v.any()), + license: v.optional(v.literal(PLATFORM_SKILL_LICENSE)), }), summary: v.optional(v.string()), qualityAssessment: v.optional( diff --git a/docs/cli.md b/docs/cli.md index f5cf13a4..5835abda 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -136,6 +136,8 @@ Stores your API token + cached registry URL. - Publishes via `POST /api/v1/skills` (multipart). - Requires semver: `--version 1.2.3`. +- Publishing a skill means it is released under `MIT-0` on ClawHub. +- Published skills are free to use, modify, and redistribute without attribution. ### `delete ` @@ -159,6 +161,23 @@ Stores your API token + cached registry URL. - Unhide a skill (owner, moderator, or admin). - Alias for `undelete`. +### `transfer` + +- Ownership transfer workflow. +- Subcommands: + - `transfer request [--message "..."] [--yes]` + - `transfer list [--outgoing]` + - `transfer accept [--yes]` + - `transfer reject [--yes]` + - `transfer cancel [--yes]` +- Endpoints: + - `POST /api/v1/skills/{slug}/transfer` + - `POST /api/v1/skills/{slug}/transfer/accept` + - `POST /api/v1/skills/{slug}/transfer/reject` + - `POST /api/v1/skills/{slug}/transfer/cancel` + - `GET /api/v1/transfers/incoming` + - `GET /api/v1/transfers/outgoing` + ### `ban-user ` - Ban a user and delete owned skills (moderator/admin only). diff --git a/docs/skill-format.md b/docs/skill-format.md index 18ba2142..b73fed1d 100644 --- a/docs/skill-format.md +++ b/docs/skill-format.md @@ -149,3 +149,10 @@ Limits (server-side): - Each publish creates a new version (semver). - Tags are string pointers to a version; `latest` is commonly used. + +## License + +- All skills published on ClawHub are licensed under `MIT-0`. +- Anyone may use, modify, and redistribute published skills, including commercially. +- Attribution is not required. +- Do not add conflicting license terms in `SKILL.md`; ClawHub does not support per-skill license overrides. diff --git a/packages/clawdhub/src/cli/commands/inspect.test.ts b/packages/clawdhub/src/cli/commands/inspect.test.ts index 5b7618c7..46dd657f 100644 --- a/packages/clawdhub/src/cli/commands/inspect.test.ts +++ b/packages/clawdhub/src/cli/commands/inspect.test.ts @@ -77,7 +77,7 @@ describe('cmdInspect', () => { createdAt: 1, updatedAt: 2, }, - latestVersion: { version: '1.2.3', createdAt: 3, changelog: 'init' }, + latestVersion: { version: '1.2.3', createdAt: 3, changelog: 'init', license: 'MIT-0' }, owner: null, }) .mockResolvedValueOnce({ @@ -107,7 +107,7 @@ describe('cmdInspect', () => { createdAt: 1, updatedAt: 2, }, - latestVersion: { version: '2.0.0', createdAt: 3, changelog: 'init' }, + latestVersion: { version: '2.0.0', createdAt: 3, changelog: 'init', license: 'MIT-0' }, owner: null, }) .mockResolvedValueOnce({ @@ -138,7 +138,7 @@ describe('cmdInspect', () => { createdAt: 1, updatedAt: 2, }, - latestVersion: { version: '2.0.0', createdAt: 3, changelog: 'init' }, + latestVersion: { version: '2.0.0', createdAt: 3, changelog: 'init', license: 'MIT-0' }, owner: null, }) .mockResolvedValueOnce({ @@ -159,6 +159,7 @@ describe('cmdInspect', () => { await cmdInspect(makeOpts(), 'demo', { version: '2.0.0' }) + expect(mockLog).toHaveBeenCalledWith(expect.stringContaining('License: MIT-0')) expect(mockLog).toHaveBeenCalledWith('Security: SUSPICIOUS') expect(mockLog).toHaveBeenCalledWith('Warnings: yes') expect(mockLog).toHaveBeenCalledWith('Checked: 2023-11-14T22:13:20.000Z') diff --git a/packages/clawdhub/src/cli/commands/inspect.ts b/packages/clawdhub/src/cli/commands/inspect.ts index bafb843b..109e8478 100644 --- a/packages/clawdhub/src/cli/commands/inspect.ts +++ b/packages/clawdhub/src/cli/commands/inspect.ts @@ -1,6 +1,8 @@ import { apiRequest, fetchText, registryUrl } from '../../http.js' import { ApiRoutes, + PLATFORM_SKILL_LICENSE, + PLATFORM_SKILL_LICENSE_SUMMARY, ApiV1SkillResponseSchema, ApiV1SkillVersionListResponseSchema, ApiV1SkillVersionResponseSchema, @@ -131,6 +133,8 @@ export async function cmdInspect(opts: GlobalOpts, slug: string, options: Inspec printSkillSummary({ skill, latestVersion: skillResult.latestVersion, + versionLicense: + (versionResult?.version as { license?: string | null } | undefined)?.license ?? null, owner: skillResult.owner, }) } @@ -186,7 +190,13 @@ function printSkillSummary(result: { createdAt: number updatedAt: number } - latestVersion?: { version: string; createdAt: number; changelog: string } | null + latestVersion?: { + version: string + createdAt: number + changelog: string + license?: string | null + } | null + versionLicense?: string | null owner?: { handle?: string | null; displayName?: string | null; image?: string | null } | null }) { const { skill } = result @@ -199,6 +209,9 @@ function printSkillSummary(result: { if (result.latestVersion?.version) { console.log(`Latest: ${result.latestVersion.version}`) } + console.log( + `License: ${result.versionLicense ?? result.latestVersion?.license ?? PLATFORM_SKILL_LICENSE} (${PLATFORM_SKILL_LICENSE_SUMMARY})`, + ) const tags = normalizeTags(skill.tags) const tagEntries = Object.entries(tags) if (tagEntries.length > 0) { diff --git a/packages/clawdhub/src/cli/commands/publish.test.ts b/packages/clawdhub/src/cli/commands/publish.test.ts index b87c26af..252d3da5 100644 --- a/packages/clawdhub/src/cli/commands/publish.test.ts +++ b/packages/clawdhub/src/cli/commands/publish.test.ts @@ -87,6 +87,7 @@ describe('cmdPublish', () => { expect(payload.displayName).toBe('My Skill') expect(payload.version).toBe('1.0.0') expect(payload.changelog).toBe('') + expect(payload.acceptLicenseTerms).toBe(true) expect(payload.tags).toEqual(['latest']) const files = publishForm.getAll('files') as Array expect(files.map((file) => String(file.name ?? '')).sort()).toEqual(['SKILL.md', 'notes.md']) diff --git a/packages/clawdhub/src/cli/commands/publish.ts b/packages/clawdhub/src/cli/commands/publish.ts index 3a200a97..65335e3d 100644 --- a/packages/clawdhub/src/cli/commands/publish.ts +++ b/packages/clawdhub/src/cli/commands/publish.ts @@ -68,6 +68,7 @@ export async function cmdPublish( displayName, version, changelog, + acceptLicenseTerms: true, tags, ...(forkOf ? { forkOf } : {}), }), diff --git a/packages/clawdhub/src/schema/index.ts b/packages/clawdhub/src/schema/index.ts index 0f332151..2862cccd 100644 --- a/packages/clawdhub/src/schema/index.ts +++ b/packages/clawdhub/src/schema/index.ts @@ -1,5 +1,11 @@ export type { ArkValidator } from './ark.js' export { formatArkErrors, parseArk } from './ark.js' +export { + PLATFORM_SKILL_LICENSE, + PLATFORM_SKILL_LICENSE_NAME, + PLATFORM_SKILL_LICENSE_SUMMARY, + PLATFORM_SKILL_LICENSE_URL, +} from 'clawhub-schema' export { ApiRoutes, LegacyApiRoutes } from './routes.js' export * from './schemas.js' export * from './textFiles.js' diff --git a/packages/clawdhub/src/schema/schemas.ts b/packages/clawdhub/src/schema/schemas.ts index 26c046db..e888e4e0 100644 --- a/packages/clawdhub/src/schema/schemas.ts +++ b/packages/clawdhub/src/schema/schemas.ts @@ -72,6 +72,7 @@ export const CliPublishRequestSchema = type({ displayName: 'string', version: 'string', changelog: 'string', + acceptLicenseTerms: 'boolean?', tags: 'string[]?', forkOf: type({ slug: 'string', @@ -160,6 +161,7 @@ export const ApiV1SkillListResponseSchema = type({ version: 'string', createdAt: 'number', changelog: 'string', + license: '"MIT-0"|null?', }).optional(), }).array(), nextCursor: 'string|null', @@ -179,6 +181,7 @@ export const ApiV1SkillResponseSchema = type({ version: 'string', createdAt: 'number', changelog: 'string', + license: '"MIT-0"|null?', }).or('null'), owner: type({ handle: 'string|null', @@ -209,6 +212,7 @@ export const ApiV1SkillVersionResponseSchema = type({ createdAt: 'number', changelog: 'string', changelogSource: '"auto"|"user"|null?', + license: '"MIT-0"|null?', files: 'unknown?', }).or('null'), skill: type({ @@ -232,6 +236,42 @@ export const ApiV1DeleteResponseSchema = type({ ok: 'true', }) +export const ApiV1TransferRequestResponseSchema = type({ + ok: 'true', + transferId: 'string', + toUserHandle: 'string', + expiresAt: 'number', +}) + +export const ApiV1TransferDecisionResponseSchema = type({ + ok: 'true', + skillSlug: 'string?', +}) + +export const ApiV1TransferListResponseSchema = type({ + transfers: type({ + _id: 'string', + skill: type({ + _id: 'string', + slug: 'string', + displayName: 'string', + }), + fromUser: type({ + _id: 'string', + handle: 'string|null', + displayName: 'string|null', + }).optional(), + toUser: type({ + _id: 'string', + handle: 'string|null', + displayName: 'string|null', + }).optional(), + message: 'string?', + requestedAt: 'number', + expiresAt: 'number', + }).array(), +}) + export const ApiV1BanUserResponseSchema = type({ ok: 'true', alreadyBanned: 'boolean', diff --git a/packages/schema/dist/index.d.ts b/packages/schema/dist/index.d.ts index bc7a324b..e1921c93 100644 --- a/packages/schema/dist/index.d.ts +++ b/packages/schema/dist/index.d.ts @@ -1,5 +1,6 @@ export type { ArkValidator } from './ark.js'; export { formatArkErrors, parseArk } from './ark.js'; +export * from './license.js'; export { ApiRoutes, LegacyApiRoutes } from './routes.js'; export * from './schemas.js'; export * from './textFiles.js'; diff --git a/packages/schema/dist/index.js b/packages/schema/dist/index.js index 0faa9674..09dd30a0 100644 --- a/packages/schema/dist/index.js +++ b/packages/schema/dist/index.js @@ -1,4 +1,5 @@ export { formatArkErrors, parseArk } from './ark.js'; +export * from './license.js'; export { ApiRoutes, LegacyApiRoutes } from './routes.js'; export * from './schemas.js'; export * from './textFiles.js'; diff --git a/packages/schema/dist/index.js.map b/packages/schema/dist/index.js.map index 58d26f48..c19345ac 100644 --- a/packages/schema/dist/index.js.map +++ b/packages/schema/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AACpD,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AACxD,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AACpD,cAAc,cAAc,CAAA;AAC5B,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AACxD,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA"} \ No newline at end of file diff --git a/packages/schema/dist/license.d.ts b/packages/schema/dist/license.d.ts new file mode 100644 index 00000000..7e776be8 --- /dev/null +++ b/packages/schema/dist/license.d.ts @@ -0,0 +1,7 @@ +import { type inferred } from 'arktype'; +export declare const PLATFORM_SKILL_LICENSE: "MIT-0"; +export declare const PLATFORM_SKILL_LICENSE_NAME: "MIT No Attribution"; +export declare const PLATFORM_SKILL_LICENSE_SUMMARY: "Free to use, modify, and redistribute. No attribution required."; +export declare const PLATFORM_SKILL_LICENSE_URL: "https://spdx.org/licenses/MIT-0.html"; +export declare const SkillPlatformLicenseSchema: import("arktype/internal/variants/string.ts").StringType<"MIT-0", {}>; +export type SkillPlatformLicense = (typeof SkillPlatformLicenseSchema)[inferred]; diff --git a/packages/schema/dist/license.js b/packages/schema/dist/license.js new file mode 100644 index 00000000..6b2a1013 --- /dev/null +++ b/packages/schema/dist/license.js @@ -0,0 +1,7 @@ +import { type } from 'arktype'; +export const PLATFORM_SKILL_LICENSE = 'MIT-0'; +export const PLATFORM_SKILL_LICENSE_NAME = 'MIT No Attribution'; +export const PLATFORM_SKILL_LICENSE_SUMMARY = 'Free to use, modify, and redistribute. No attribution required.'; +export const PLATFORM_SKILL_LICENSE_URL = 'https://spdx.org/licenses/MIT-0.html'; +export const SkillPlatformLicenseSchema = type('"MIT-0"'); +//# sourceMappingURL=license.js.map \ No newline at end of file diff --git a/packages/schema/dist/license.js.map b/packages/schema/dist/license.js.map new file mode 100644 index 00000000..bf938b91 --- /dev/null +++ b/packages/schema/dist/license.js.map @@ -0,0 +1 @@ +{"version":3,"file":"license.js","sourceRoot":"","sources":["../src/license.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,IAAI,EAAE,MAAM,SAAS,CAAA;AAE7C,MAAM,CAAC,MAAM,sBAAsB,GAAG,OAAgB,CAAA;AACtD,MAAM,CAAC,MAAM,2BAA2B,GAAG,oBAA6B,CAAA;AACxE,MAAM,CAAC,MAAM,8BAA8B,GACzC,iEAA0E,CAAA;AAC5E,MAAM,CAAC,MAAM,0BAA0B,GAAG,sCAA+C,CAAA;AAEzF,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC,SAAS,CAAC,CAAA"} \ No newline at end of file diff --git a/packages/schema/dist/schemas.d.ts b/packages/schema/dist/schemas.d.ts index 1b883afa..80b4dae3 100644 --- a/packages/schema/dist/schemas.d.ts +++ b/packages/schema/dist/schemas.d.ts @@ -78,6 +78,7 @@ export declare const CliPublishRequestSchema: import("arktype/internal/variants/ sha256: string; contentType?: string | undefined; }[]; + acceptLicenseTerms?: boolean | undefined; tags?: string[] | undefined; source?: { kind: "github"; @@ -168,6 +169,7 @@ export declare const ApiV1SkillListResponseSchema: import("arktype/internal/vari version: string; createdAt: number; changelog: string; + license?: "MIT-0" | null | undefined; } | undefined; }[]; nextCursor: string | null; @@ -186,6 +188,7 @@ export declare const ApiV1SkillResponseSchema: import("arktype/internal/variants version: string; createdAt: number; changelog: string; + license?: "MIT-0" | null | undefined; } | null; owner: { handle: string | null; @@ -214,6 +217,7 @@ export declare const ApiV1SkillVersionResponseSchema: import("arktype/internal/v createdAt: number; changelog: string; changelogSource?: "user" | "auto" | null | undefined; + license?: "MIT-0" | null | undefined; files?: unknown; security?: { status: "clean" | "suspicious" | "malicious" | "pending" | "error"; @@ -243,6 +247,39 @@ export declare const ApiV1PublishResponseSchema: import("arktype/internal/varian export declare const ApiV1DeleteResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{ ok: true; }, {}>; +export declare const ApiV1TransferRequestResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{ + ok: true; + transferId: string; + toUserHandle: string; + expiresAt: number; +}, {}>; +export declare const ApiV1TransferDecisionResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{ + ok: true; + skillSlug?: string | undefined; +}, {}>; +export declare const ApiV1TransferListResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{ + transfers: { + _id: string; + skill: { + _id: string; + slug: string; + displayName: string; + }; + requestedAt: number; + expiresAt: number; + fromUser?: { + _id: string; + handle: string | null; + displayName: string | null; + } | undefined; + toUser?: { + _id: string; + handle: string | null; + displayName: string | null; + } | undefined; + message?: string | undefined; + }[]; +}, {}>; export declare const ApiV1SetRoleResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{ ok: true; role: "user" | "admin" | "moderator"; diff --git a/packages/schema/dist/schemas.js b/packages/schema/dist/schemas.js index ead0846c..d282655e 100644 --- a/packages/schema/dist/schemas.js +++ b/packages/schema/dist/schemas.js @@ -1,4 +1,5 @@ import { type } from 'arktype'; +import { SkillPlatformLicenseSchema } from './license.js'; export const GlobalConfigSchema = type({ registry: 'string', token: 'string?', @@ -67,6 +68,7 @@ export const CliPublishRequestSchema = type({ displayName: 'string', version: 'string', changelog: 'string', + acceptLicenseTerms: 'boolean?', tags: 'string[]?', source: PublishSourceSchema.optional(), forkOf: type({ @@ -143,6 +145,7 @@ export const ApiV1SkillListResponseSchema = type({ version: 'string', createdAt: 'number', changelog: 'string', + license: SkillPlatformLicenseSchema.or('null').optional(), }).optional(), }).array(), nextCursor: 'string|null', @@ -161,6 +164,7 @@ export const ApiV1SkillResponseSchema = type({ version: 'string', createdAt: 'number', changelog: 'string', + license: SkillPlatformLicenseSchema.or('null').optional(), }).or('null'), owner: type({ handle: 'string|null', @@ -189,6 +193,7 @@ export const ApiV1SkillVersionResponseSchema = type({ createdAt: 'number', changelog: 'string', changelogSource: '"auto"|"user"|null?', + license: SkillPlatformLicenseSchema.or('null').optional(), files: 'unknown?', security: SecurityStatusSchema.optional(), }).or('null'), @@ -209,6 +214,39 @@ export const ApiV1PublishResponseSchema = type({ export const ApiV1DeleteResponseSchema = type({ ok: 'true', }); +export const ApiV1TransferRequestResponseSchema = type({ + ok: 'true', + transferId: 'string', + toUserHandle: 'string', + expiresAt: 'number', +}); +export const ApiV1TransferDecisionResponseSchema = type({ + ok: 'true', + skillSlug: 'string?', +}); +export const ApiV1TransferListResponseSchema = type({ + transfers: type({ + _id: 'string', + skill: type({ + _id: 'string', + slug: 'string', + displayName: 'string', + }), + fromUser: type({ + _id: 'string', + handle: 'string|null', + displayName: 'string|null', + }).optional(), + toUser: type({ + _id: 'string', + handle: 'string|null', + displayName: 'string|null', + }).optional(), + message: 'string?', + requestedAt: 'number', + expiresAt: 'number', + }).array(), +}); export const ApiV1SetRoleResponseSchema = type({ ok: 'true', role: '"admin"|"moderator"|"user"', diff --git a/packages/schema/dist/schemas.js.map b/packages/schema/dist/schemas.js.map index a06f94e4..61c587e7 100644 --- a/packages/schema/dist/schemas.js.map +++ b/packages/schema/dist/schemas.js.map @@ -1 +1 @@ -{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 0f332151..5d57d45a 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -1,5 +1,6 @@ export type { ArkValidator } from './ark.js' export { formatArkErrors, parseArk } from './ark.js' +export * from './license.js' export { ApiRoutes, LegacyApiRoutes } from './routes.js' export * from './schemas.js' export * from './textFiles.js' diff --git a/packages/schema/src/license.ts b/packages/schema/src/license.ts new file mode 100644 index 00000000..f1fd7f48 --- /dev/null +++ b/packages/schema/src/license.ts @@ -0,0 +1,10 @@ +import { type inferred, type } from 'arktype' + +export const PLATFORM_SKILL_LICENSE = 'MIT-0' as const +export const PLATFORM_SKILL_LICENSE_NAME = 'MIT No Attribution' as const +export const PLATFORM_SKILL_LICENSE_SUMMARY = + 'Free to use, modify, and redistribute. No attribution required.' as const +export const PLATFORM_SKILL_LICENSE_URL = 'https://spdx.org/licenses/MIT-0.html' as const + +export const SkillPlatformLicenseSchema = type('"MIT-0"') +export type SkillPlatformLicense = (typeof SkillPlatformLicenseSchema)[inferred] diff --git a/packages/schema/src/schemas.ts b/packages/schema/src/schemas.ts index bcb5cb0b..b2a5fc35 100644 --- a/packages/schema/src/schemas.ts +++ b/packages/schema/src/schemas.ts @@ -1,4 +1,5 @@ import { type inferred, type } from 'arktype' +import { SkillPlatformLicenseSchema } from './license.js' export const GlobalConfigSchema = type({ registry: 'string', @@ -82,6 +83,7 @@ export const CliPublishRequestSchema = type({ displayName: 'string', version: 'string', changelog: 'string', + acceptLicenseTerms: 'boolean?', tags: 'string[]?', source: PublishSourceSchema.optional(), forkOf: type({ @@ -171,6 +173,7 @@ export const ApiV1SkillListResponseSchema = type({ version: 'string', createdAt: 'number', changelog: 'string', + license: SkillPlatformLicenseSchema.or('null').optional(), }).optional(), }).array(), nextCursor: 'string|null', @@ -190,6 +193,7 @@ export const ApiV1SkillResponseSchema = type({ version: 'string', createdAt: 'number', changelog: 'string', + license: SkillPlatformLicenseSchema.or('null').optional(), }).or('null'), owner: type({ handle: 'string|null', @@ -221,6 +225,7 @@ export const ApiV1SkillVersionResponseSchema = type({ createdAt: 'number', changelog: 'string', changelogSource: '"auto"|"user"|null?', + license: SkillPlatformLicenseSchema.or('null').optional(), files: 'unknown?', security: SecurityStatusSchema.optional(), }).or('null'), @@ -245,6 +250,42 @@ export const ApiV1DeleteResponseSchema = type({ ok: 'true', }) +export const ApiV1TransferRequestResponseSchema = type({ + ok: 'true', + transferId: 'string', + toUserHandle: 'string', + expiresAt: 'number', +}) + +export const ApiV1TransferDecisionResponseSchema = type({ + ok: 'true', + skillSlug: 'string?', +}) + +export const ApiV1TransferListResponseSchema = type({ + transfers: type({ + _id: 'string', + skill: type({ + _id: 'string', + slug: 'string', + displayName: 'string', + }), + fromUser: type({ + _id: 'string', + handle: 'string|null', + displayName: 'string|null', + }).optional(), + toUser: type({ + _id: 'string', + handle: 'string|null', + displayName: 'string|null', + }).optional(), + message: 'string?', + requestedAt: 'number', + expiresAt: 'number', + }).array(), +}) + export const ApiV1SetRoleResponseSchema = type({ ok: 'true', role: '"admin"|"moderator"|"user"', diff --git a/src/__tests__/skill-detail-page.test.tsx b/src/__tests__/skill-detail-page.test.tsx index 93fcb6fc..599896ed 100644 --- a/src/__tests__/skill-detail-page.test.tsx +++ b/src/__tests__/skill-detail-page.test.tsx @@ -122,6 +122,13 @@ describe('SkillDetailPage', () => { render() + expect( + ( + await screen.findAllByText( + /free to use, modify, and redistribute\. no attribution required\./i, + ) + ).length, + ).toBeGreaterThan(0) expect(screen.queryByText(/Reports require a reason\. Abuse may result in a ban\./i)).toBeNull() fireEvent.click(await screen.findByRole('button', { name: /report/i })) diff --git a/src/__tests__/upload.route.test.tsx b/src/__tests__/upload.route.test.tsx index d2b8b271..dbba33e2 100644 --- a/src/__tests__/upload.route.test.tsx +++ b/src/__tests__/upload.route.test.tsx @@ -94,6 +94,11 @@ describe('Upload route', () => { const file = new File(['hello'], 'SKILL.md', { type: 'text/markdown' }) const input = screen.getByTestId('upload-input') as HTMLInputElement fireEvent.change(input, { target: { files: [file] } }) + fireEvent.click( + screen.getByRole('checkbox', { + name: /i have the rights to this skill and agree to publish it under mit-0/i, + }), + ) const publishButton = screen.getByRole('button', { name: /publish/i }) as HTMLButtonElement expect(await screen.findByText(/All checks passed/i)).toBeTruthy() @@ -124,6 +129,11 @@ describe('Upload route', () => { const input = screen.getByTestId('upload-input') as HTMLInputElement fireEvent.change(input, { target: { files: [zipFile] } }) + fireEvent.click( + screen.getByRole('checkbox', { + name: /i have the rights to this skill and agree to publish it under mit-0/i, + }), + ) expect(await screen.findByText('notes.txt', {}, { timeout: 3000 })).toBeTruthy() expect(screen.getByText('SKILL.md')).toBeTruthy() @@ -152,6 +162,11 @@ describe('Upload route', () => { const input = screen.getByTestId('upload-input') as HTMLInputElement fireEvent.change(input, { target: { files: [file] } }) + fireEvent.click( + screen.getByRole('checkbox', { + name: /i have the rights to this skill and agree to publish it under mit-0/i, + }), + ) expect(await screen.findByText('SKILL.md')).toBeTruthy() expect(await screen.findByText(/All checks passed/i)).toBeTruthy() @@ -217,6 +232,11 @@ describe('Upload route', () => { const junk = new File(['junk'], '.DS_Store', { type: 'application/octet-stream' }) const input = screen.getByTestId('upload-input') as HTMLInputElement fireEvent.change(input, { target: { files: [skill, junk] } }) + fireEvent.click( + screen.getByRole('checkbox', { + name: /i have the rights to this skill and agree to publish it under mit-0/i, + }), + ) expect(await screen.findByText('SKILL.md')).toBeTruthy() expect(screen.queryByText('.DS_Store')).toBeNull() @@ -246,6 +266,11 @@ describe('Upload route', () => { const file = new File(['hello'], 'SKILL.md', { type: 'text/markdown' }) const input = screen.getByTestId('upload-input') as HTMLInputElement fireEvent.change(input, { target: { files: [file] } }) + fireEvent.click( + screen.getByRole('checkbox', { + name: /i have the rights to this skill and agree to publish it under mit-0/i, + }), + ) const publishButton = screen.getByRole('button', { name: /publish/i }) as HTMLButtonElement await screen.findByText(/All checks passed/i) fireEvent.click(publishButton) diff --git a/src/components/SkillHeader.tsx b/src/components/SkillHeader.tsx index d4c0d716..b24f904c 100644 --- a/src/components/SkillHeader.tsx +++ b/src/components/SkillHeader.tsx @@ -1,5 +1,9 @@ import { Link } from '@tanstack/react-router' -import type { ClawdisSkillMetadata } from 'clawhub-schema' +import { + type ClawdisSkillMetadata, + PLATFORM_SKILL_LICENSE, + PLATFORM_SKILL_LICENSE_SUMMARY, +} from 'clawhub-schema' import { Package } from 'lucide-react' import type { Doc, Id } from '../../convex/_generated/dataModel' import { getSkillBadges } from '../lib/badges' @@ -188,6 +192,9 @@ export function SkillHeader({ Bundles the skill pack, CLI binary, and config requirements in one Nix install. ) : null} +
+ {PLATFORM_SKILL_LICENSE} · {PLATFORM_SKILL_LICENSE_SUMMARY} +
⭐ {formattedStats.stars} ·
))} +
{PLATFORM_SKILL_LICENSE}
{isStaff && staffVisibilityTag ? (
{staffVisibilityTag} diff --git a/src/components/SkillInstallCard.tsx b/src/components/SkillInstallCard.tsx index bdc29491..23a24a43 100644 --- a/src/components/SkillInstallCard.tsx +++ b/src/components/SkillInstallCard.tsx @@ -1,4 +1,9 @@ -import type { ClawdisSkillMetadata } from 'clawhub-schema' +import { + type ClawdisSkillMetadata, + PLATFORM_SKILL_LICENSE, + PLATFORM_SKILL_LICENSE_SUMMARY, + PLATFORM_SKILL_LICENSE_URL, +} from 'clawhub-schema' import { formatInstallCommand, formatInstallLabel } from './skillDetailUtils' type SkillInstallCardProps = { @@ -25,12 +30,32 @@ export function SkillInstallCard({ clawdis, osLabels }: SkillInstallCardProps) { const hasInstallSpecs = installSpecs.length > 0 const hasDependencies = dependencies.length > 0 const hasLinks = Boolean(links?.homepage || links?.repository || links?.documentation) + const hasLicense = true - if (!hasRuntimeRequirements && !hasInstallSpecs && !hasDependencies && !hasLinks) return null + if (!hasRuntimeRequirements && !hasInstallSpecs && !hasDependencies && !hasLinks && !hasLicense) { + return null + } return (
+
+

+ License +

+
+
{PLATFORM_SKILL_LICENSE}
+
+ {PLATFORM_SKILL_LICENSE_SUMMARY} +
+ +
+
{hasRuntimeRequirements ? (

diff --git a/src/routes/upload.tsx b/src/routes/upload.tsx index 1161991a..8a4ac187 100644 --- a/src/routes/upload.tsx +++ b/src/routes/upload.tsx @@ -1,4 +1,9 @@ import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router' +import { + PLATFORM_SKILL_LICENSE, + PLATFORM_SKILL_LICENSE_NAME, + PLATFORM_SKILL_LICENSE_SUMMARY, +} from 'clawhub-schema' import { useAction, useMutation, useQuery } from 'convex/react' import { useEffect, useMemo, useRef, useState } from 'react' import semver from 'semver' @@ -64,6 +69,7 @@ export function Upload() { const [displayName, setDisplayName] = useState('') const [version, setVersion] = useState('1.0.0') const [tags, setTags] = useState('latest') + const [acceptedLicenseTerms, setAcceptedLicenseTerms] = useState(false) const [changelog, setChangelog] = useState('') const [changelogStatus, setChangelogStatus] = useState<'idle' | 'loading' | 'ready' | 'error'>( 'idle', @@ -242,6 +248,9 @@ export function Upload() { if (parsedTags.length === 0) { issues.push('At least one tag is required.') } + if (!isSoulMode && !acceptedLicenseTerms) { + issues.push('Accept the MIT-0 license terms to publish this skill.') + } if (files.length === 0) { issues.push('Add at least one file.') } @@ -272,8 +281,10 @@ export function Upload() { trimmedName, version, parsedTags.length, + acceptedLicenseTerms, files, hasRequiredFile, + isSoulMode, totalBytes, requiredFileLabel, slugCollision, @@ -309,6 +320,10 @@ export function Upload() { setError(slugCollision.message) return } + if (!isSoulMode && !acceptedLicenseTerms) { + setError('Accept the MIT-0 license terms to publish this skill.') + return + } setError(null) if (totalBytes > maxBytes) { setError('Total size exceeds 50MB per version.') @@ -353,6 +368,7 @@ export function Upload() { displayName: trimmedName, version, changelog: trimmedChangelog, + acceptLicenseTerms: isSoulMode ? undefined : acceptedLicenseTerms, tags: parsedTags, files: uploaded, }) @@ -519,6 +535,31 @@ export function Upload() {

+ {!isSoulMode ? ( + <> +

License

+
+
+ {PLATFORM_SKILL_LICENSE} · {PLATFORM_SKILL_LICENSE_NAME} +
+

+ All skills published on ClawHub are licensed under {PLATFORM_SKILL_LICENSE}.{' '} + {PLATFORM_SKILL_LICENSE_SUMMARY} +

+ +
+ + ) : null} diff --git a/src/styles.css b/src/styles.css index 7d98a327..f5aeb8c9 100644 --- a/src/styles.css +++ b/src/styles.css @@ -710,6 +710,55 @@ code { inset 0 1px 0 rgba(255, 214, 198, 0.42); } +.upload-license-card { + display: grid; + gap: 10px; + padding: 14px; + border-radius: 16px; + border: 1px solid rgba(255, 118, 84, 0.2); + background: rgba(255, 249, 245, 0.72); +} + +[data-theme="dark"] .upload-license-card { + border-color: rgba(255, 131, 95, 0.24); + background: rgba(20, 36, 47, 0.76); +} + +.upload-license-pill { + width: fit-content; + padding: 6px 12px; + border-radius: var(--radius-pill); + background: rgba(255, 107, 74, 0.16); + color: var(--accent-deep); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +[data-theme="dark"] .upload-license-pill { + background: rgba(232, 106, 71, 0.24); + color: #ffd0bf; +} + +.upload-license-copy { + margin: 0; + color: var(--ink-soft); + line-height: 1.55; +} + +.upload-license-check { + display: flex; + align-items: flex-start; + gap: 10px; + color: var(--ink); + line-height: 1.45; +} + +.upload-license-check input { + margin-top: 3px; +} + .upload-submit-btn:not(:disabled):hover { transform: translateY(-1px); box-shadow: