Compare commits

...
Author SHA1 Message Date
Peter Steinberger f2434dcb95 Revert "chore: rename molthub branding"
This reverts commit 8d68b55333.
2026-01-29 18:20:36 +01:00
127 changed files with 694 additions and 688 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- name: Typecheck packages
run: |
bunx tsc -p packages/schema/tsconfig.json --noEmit
bunx tsc -p packages/molthub/tsconfig.json --noEmit
bunx tsc -p packages/clawdhub/tsconfig.json --noEmit
- name: Build
run: bun run build
+7 -7
View File
@@ -26,7 +26,7 @@
### Added
- Web: dynamic OG image cards for skills (name, description, version).
- CLI: auto-scan Moltbot skill roots (per-agent workspaces, shared skills, extraDirs).
- CLI: auto-scan Clawdbot skill roots (per-agent workspaces, shared skills, extraDirs).
- Web: import skills from public GitHub URLs (auto-detect `SKILL.md`, smart file selection, provenance).
- Web/API: SoulHub (SOUL.md registry) with v1 endpoints and first-run auto-seed.
@@ -36,7 +36,7 @@
- Registry: make SoulHub auto-seed idempotent and non-user-owned.
- Registry: keep GitHub backup state + publish backups intact (thanks @joshp123, #1).
- CLI/Registry: restore fork lineage on sync + clamp bulk list queries (thanks @joshp123, #1).
- CLI: default workdir falls back to Moltbot workspace (override with `--workdir` / `MOLTHUB_WORKDIR`).
- CLI: default workdir falls back to Clawdbot workspace (override with `--workdir` / `CLAWDHUB_WORKDIR`).
## 0.0.6 - 2026-01-07
@@ -52,7 +52,7 @@
## 0.0.5 - 2026-01-06
### Added
- Telemetry: track installs via `molthub sync` (logged-in only), per root, with 120-day staleness.
- Telemetry: track installs via `clawdhub sync` (logged-in only), per root, with 120-day staleness.
- Skills: show current + all-time installs; sort by installs.
- Profile: private "Installed" tab with JSON export + delete telemetry controls.
- Docs: add `docs/telemetry.md` (what we track + how to opt out).
@@ -60,7 +60,7 @@
- Web: dashboard for managing your published skills (thanks @dbhurley!).
### Changed
- CLI: telemetry opt-out via `MOLTHUB_DISABLE_TELEMETRY=1`.
- CLI: telemetry opt-out via `CLAWDHUB_DISABLE_TELEMETRY=1`.
- Web: move theme picker into mobile menu.
### Fixed
@@ -89,7 +89,7 @@
- Web: improved mobile responsiveness (nav menu, skill detail layout, install command overflow).
- Web: upload now unwraps folder picks so `SKILL.md` can be at the bundle root.
- Registry: cap embedding payload size to avoid model context errors.
- CLI: ignore legacy `auth.molthub.com` registry and prefer site discovery.
- CLI: ignore legacy `auth.clawdhub.com` registry and prefer site discovery.
### Changed
- Web: homepage search now expands into full search mode with live results + highlighted toggle.
@@ -108,8 +108,8 @@
### Fixed
- CLI sync: wrap note output to avoid terminal overflow; cap list lengths.
- CLI sync: label fallback scans as fallback locations.
- CLI package: bundle schema internally (no external `molthub-schema` publish).
- Repo: mark `molthub-schema` as private to prevent publishing.
- CLI package: bundle schema internally (no external `clawdhub-schema` publish).
- Repo: mark `clawdhub-schema` as private to prevent publishing.
## 0.0.2 - 2026-01-04
+17 -17
View File
@@ -1,17 +1,17 @@
# MoltHub
# ClawdHub
<p align="center">
<a href="https://github.com/moltbot/molthub/actions/workflows/ci.yml?branch=main"><img src="https://img.shields.io/github/actions/workflow/status/moltbot/molthub/ci.yml?branch=main&style=for-the-badge" alt="CI status"></a>
<a href="https://discord.gg/molt"><img src="https://img.shields.io/discord/1456350064065904867?label=Discord&logo=discord&logoColor=white&color=5865F2&style=for-the-badge" alt="Discord"></a>
<a href="https://github.com/clawdbot/clawdhub/actions/workflows/ci.yml?branch=main"><img src="https://img.shields.io/github/actions/workflow/status/clawdbot/clawdhub/ci.yml?branch=main&style=for-the-badge" alt="CI status"></a>
<a href="https://discord.gg/clawd"><img src="https://img.shields.io/discord/1456350064065904867?label=Discord&logo=discord&logoColor=white&color=5865F2&style=for-the-badge" alt="Discord"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
</p>
MoltHub is the **public skill registry for Moltbot**: publish, version, and search text-based agent skills (a `SKILL.md` plus supporting files).
ClawdHub is the **public skill registry for Clawdbot**: publish, version, and search text-based agent skills (a `SKILL.md` plus supporting files).
Its designed for fast browsing + a CLI-friendly API, with moderation hooks and vector search.
onlycrabs.ai is the **SOUL.md registry**: publish and share system lore the same way you publish skills.
Live: `https://molthub.com`
Live: `https://clawdhub.com`
onlycrabs.ai: `https://onlycrabs.ai`
## What you can do
@@ -27,7 +27,7 @@ onlycrabs.ai: `https://onlycrabs.ai`
- Entry point is host-based: `onlycrabs.ai`.
- On the onlycrabs.ai host, the home page and nav default to souls.
- On MoltHub, souls live under `/souls`.
- On ClawdHub, souls live under `/souls`.
- Soul bundles only accept `SOUL.md` for now (no extra files).
## How it works (high level)
@@ -35,15 +35,15 @@ onlycrabs.ai: `https://onlycrabs.ai`
- Web app: TanStack Start (React, Vite/Nitro).
- Backend: Convex (DB + file storage + HTTP actions) + Convex Auth (GitHub OAuth).
- Search: OpenAI embeddings (`text-embedding-3-small`) + Convex vector search.
- API schema + routes: `packages/schema` (`molthub-schema`).
- API schema + routes: `packages/schema` (`clawdhub-schema`).
## Telemetry
MoltHub tracks minimal **install telemetry** (to compute install counts) when you run `molthub sync` while logged in.
ClawdHub tracks minimal **install telemetry** (to compute install counts) when you run `clawdhub sync` while logged in.
Disable via:
```bash
export MOLTHUB_DISABLE_TELEMETRY=1
export CLAWDHUB_DISABLE_TELEMETRY=1
```
Details: `docs/telemetry.md`.
@@ -95,7 +95,7 @@ This writes `JWT_PRIVATE_KEY` + `JWKS` to the deployment and prints values for y
## Nix plugins (nixmode skills)
MoltHub can store a nix-moltbot plugin pointer in SKILL frontmatter so the registry knows which
ClawdHub can store a nix-clawdbot plugin pointer in SKILL frontmatter so the registry knows which
Nix package bundle to install. A nix plugin is different from a regular skill pack: it bundles the
skill pack, the CLI binary, and its config flags/requirements together.
@@ -105,15 +105,15 @@ Add this to `SKILL.md`:
---
name: peekaboo
description: Capture and automate macOS UI with the Peekaboo CLI.
metadata: {"moltbot":{"nix":{"plugin":"github:moltbot/nix-steipete-tools?dir=tools/peekaboo","systems":["aarch64-darwin"]}}}
metadata: {"clawdbot":{"nix":{"plugin":"github:clawdbot/nix-steipete-tools?dir=tools/peekaboo","systems":["aarch64-darwin"]}}}
---
```
Install via nix-moltbot:
Install via nix-clawdbot:
```nix
programs.moltbot.plugins = [
{ source = "github:moltbot/nix-steipete-tools?dir=tools/peekaboo"; }
programs.clawdbot.plugins = [
{ source = "github:clawdbot/nix-steipete-tools?dir=tools/peekaboo"; }
];
```
@@ -123,7 +123,7 @@ You can also declare config requirements + an example snippet:
---
name: padel
description: Check padel court availability and manage bookings via Playtomic.
metadata: {"moltbot":{"config":{"requiredEnv":["PADEL_AUTH_FILE"],"stateDirs":[".config/padel"],"example":"config = { env = { PADEL_AUTH_FILE = \\\"/run/agenix/padel-auth\\\"; }; };"}}}
metadata: {"clawdbot":{"config":{"requiredEnv":["PADEL_AUTH_FILE"],"stateDirs":[".config/padel"],"example":"config = { env = { PADEL_AUTH_FILE = \\\"/run/agenix/padel-auth\\\"; }; };"}}}
---
```
@@ -133,11 +133,11 @@ To show CLI help (recommended for nix plugins), include the `cli --help` output:
---
name: padel
description: Check padel court availability and manage bookings via Playtomic.
metadata: {"moltbot":{"cliHelp":"padel --help\\nUsage: padel [command]\\n"}}
metadata: {"clawdbot":{"cliHelp":"padel --help\\nUsage: padel [command]\\n"}}
---
```
`metadata.moltbot` is preferred, but `metadata.moltbot` is accepted as an alias for compatibility.
`metadata.clawdbot` is preferred, but `metadata.clawdis` is accepted as an alias for compatibility.
## Scripts
+12 -8
View File
@@ -3,7 +3,7 @@
"configVersion": 0,
"workspaces": {
"": {
"name": "molthub",
"name": "clawdhub",
"dependencies": {
"@auth/core": "^0.37.4",
"@convex-dev/auth": "^0.0.90",
@@ -21,13 +21,13 @@
"@tanstack/react-start": "^1.152.0",
"@tanstack/router-plugin": "^1.151.6",
"@vercel/analytics": "^1.6.1",
"clawdhub-schema": "workspace:*",
"clsx": "^2.1.1",
"convex": "^1.31.6",
"convex-helpers": "^0.1.111",
"fflate": "^0.8.2",
"h3": "2.0.1-rc.8",
"lucide-react": "^0.562.0",
"molthub-schema": "workspace:*",
"monaco-editor": "^0.55.1",
"nitro": "^3.0.1-alpha.1",
"react": "^19.2.3",
@@ -61,11 +61,11 @@
"vitest": "^4.0.17",
},
},
"packages/molthub": {
"name": "molthub",
"packages/clawdhub": {
"name": "clawdhub",
"version": "0.3.0",
"bin": {
"molthub": "bin/molthub.js",
"clawdhub": "bin/clawdhub.js",
},
"dependencies": {
"@clack/prompts": "^0.11.0",
@@ -86,7 +86,7 @@
},
},
"packages/schema": {
"name": "molthub-schema",
"name": "clawdhub-schema",
"version": "0.0.2",
"dependencies": {
"arktype": "^2.1.29",
@@ -723,6 +723,10 @@
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"clawdhub": ["clawdhub@workspace:packages/clawdhub"],
"clawdhub-schema": ["clawdhub-schema@workspace:packages/schema"],
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
"cli-spinners": ["cli-spinners@3.3.0", "", {}, "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ=="],
@@ -1059,9 +1063,9 @@
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
"molthub": ["molthub@workspace:packages/molthub"],
"clawdhub": ["clawdhub@workspace:packages/clawdhub"],
"molthub-schema": ["molthub-schema@workspace:packages/schema"],
"clawdhub-schema": ["clawdhub-schema@workspace:packages/schema"],
"monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="],
+3 -3
View File
@@ -3,9 +3,9 @@ import { existsSync } from 'node:fs'
import { stat } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
const distCliUrl = new URL('./packages/molthub/dist/cli.js', import.meta.url)
const distCliUrl = new URL('./packages/clawdhub/dist/cli.js', import.meta.url)
const distCliPath = fileURLToPath(distCliUrl)
const srcRootPath = fileURLToPath(new URL('./packages/molthub/src/', import.meta.url))
const srcRootPath = fileURLToPath(new URL('./packages/clawdhub/src/', import.meta.url))
const shouldBuild = await (async () => {
if (!existsSync(distCliPath)) return true
@@ -19,7 +19,7 @@ const shouldBuild = await (async () => {
})()
if (shouldBuild) {
const proc = Bun.spawn(['bunx', 'tsc', '-p', 'packages/molthub/tsconfig.json'], {
const proc = Bun.spawn(['bunx', 'tsc', '-p', 'packages/clawdhub/tsconfig.json'], {
stdin: 'inherit',
stdout: 'inherit',
stderr: 'inherit',
+12 -12
View File
@@ -3,7 +3,7 @@ import { internal } from './_generated/api'
import type { ActionCtx } from './_generated/server'
import { internalAction, internalMutation } from './_generated/server'
import { EMBEDDING_DIMENSIONS } from './lib/embeddings'
import { parseFrontmatter, parseMoltbotMetadata } from './lib/skills'
import { parseClawdisMetadata, parseFrontmatter } from './lib/skills'
type SeedSkillSpec = {
slug: string
@@ -32,7 +32,7 @@ const SEED_SKILLS: SeedSkillSpec[] = [
summary: 'Check padel court availability and manage bookings via Playtomic.',
version: '0.1.0',
metadata: {
moltbot: {
clawdbot: {
nix: {
plugin: 'github:joshp123/padel-cli',
systems: ['aarch64-darwin', 'x86_64-linux'],
@@ -74,7 +74,7 @@ description: Check padel court availability and manage bookings via the padel CL
## CLI
\`\`\`bash
padel # On PATH (moltbot plugin bundle)
padel # On PATH (clawdbot plugin bundle)
\`\`\`
## Venues
@@ -110,7 +110,7 @@ Only the authorized booker can confirm bookings. If the requester is not authori
summary: 'Operate GoHome via gRPC discovery, metrics, and Grafana dashboards.',
version: '0.1.0',
metadata: {
moltbot: {
clawdbot: {
nix: {
plugin: 'github:joshp123/gohome',
systems: ['x86_64-linux', 'aarch64-linux'],
@@ -141,7 +141,7 @@ Flags:
},
rawSkillMd: `---
name: gohome
description: Use when Moltbot needs to test or operate GoHome via gRPC discovery, metrics, and Grafana.
description: Use when Clawdbot needs to test or operate GoHome via gRPC discovery, metrics, and Grafana.
---
# GoHome Skill
@@ -183,7 +183,7 @@ Only call write RPCs after explicit user approval.
summary: 'Teach Mandarin with the xuezh engine for review, speaking, and audits.',
version: '0.1.0',
metadata: {
moltbot: {
clawdbot: {
nix: {
plugin: 'github:joshp123/xuezh',
systems: ['aarch64-darwin', 'x86_64-linux'],
@@ -258,7 +258,7 @@ async function seedNixSkillsHandler(
for (const spec of SEED_SKILLS) {
const skillMd = injectMetadata(spec.rawSkillMd, spec.metadata)
const frontmatter = parseFrontmatter(skillMd)
const moltbot = parseMoltbotMetadata(frontmatter)
const clawdis = parseClawdisMetadata(frontmatter)
const storageId = await ctx.storage.store(new Blob([skillMd], { type: 'text/markdown' }))
const result: SeedMutationResult = await ctx.runMutation(internal.devSeed.seedSkillMutation, {
@@ -266,7 +266,7 @@ async function seedNixSkillsHandler(
storageId,
metadata: spec.metadata,
frontmatter,
moltbot,
clawdis,
skillMd,
slug: spec.slug,
displayName: spec.displayName,
@@ -296,7 +296,7 @@ async function seedPadelSkillHandler(
const skillMd = injectMetadata(spec.rawSkillMd, spec.metadata)
const frontmatter = parseFrontmatter(skillMd)
const moltbot = parseMoltbotMetadata(frontmatter)
const clawdis = parseClawdisMetadata(frontmatter)
const storageId = await ctx.storage.store(new Blob([skillMd], { type: 'text/markdown' }))
return (await ctx.runMutation(internal.devSeed.seedSkillMutation, {
@@ -304,7 +304,7 @@ async function seedPadelSkillHandler(
storageId,
metadata: spec.metadata,
frontmatter,
moltbot,
clawdis,
skillMd,
slug: spec.slug,
displayName: spec.displayName,
@@ -326,7 +326,7 @@ export const seedSkillMutation = internalMutation({
storageId: v.id('_storage'),
metadata: v.any(),
frontmatter: v.any(),
moltbot: v.any(),
clawdis: v.any(),
skillMd: v.string(),
slug: v.string(),
displayName: v.string(),
@@ -418,7 +418,7 @@ export const seedSkillMutation = internalMutation({
parsed: {
frontmatter: args.frontmatter,
metadata: args.metadata,
moltbot: args.moltbot,
clawdis: args.clawdis,
},
createdBy: userId,
createdAt: now,
+4 -4
View File
@@ -11,7 +11,7 @@ import { internal } from './_generated/api'
import type { Id } from './_generated/dataModel'
import type { ActionCtx } from './_generated/server'
import { internalAction, internalMutation } from './_generated/server'
import { parseFrontmatter, parseMoltbotMetadata } from './lib/skills'
import { parseClawdisMetadata, parseFrontmatter } from './lib/skills'
type SeedSkillSpec = {
slug: string
@@ -66,7 +66,7 @@ Use this skill to ${summary.toLowerCase()}.
summary,
version: '0.1.0',
metadata: {
moltbot: {
clawdbot: {
nix: {
plugin: `github:example/${slug}`,
systems: ['aarch64-darwin', 'x86_64-linux'],
@@ -505,7 +505,7 @@ export const seedExtraSkillsInternal = internalAction({
for (const spec of EXTRA_SEED_SKILLS) {
const skillMd = injectMetadata(spec.rawSkillMd, spec.metadata)
const frontmatter = parseFrontmatter(skillMd)
const moltbot = parseMoltbotMetadata(frontmatter)
const clawdis = parseClawdisMetadata(frontmatter)
const storageId = await ctx.storage.store(new Blob([skillMd], { type: 'text/markdown' }))
const result = (await ctx.runMutation(internal.devSeed.seedSkillMutation, {
@@ -513,7 +513,7 @@ export const seedExtraSkillsInternal = internalAction({
storageId,
metadata: spec.metadata,
frontmatter,
moltbot,
clawdis,
skillMd,
slug: spec.slug,
displayName: spec.displayName,
+1 -1
View File
@@ -1,5 +1,5 @@
import { ApiRoutes, LegacyApiRoutes } from 'clawdhub-schema'
import { httpRouter } from 'convex/server'
import { ApiRoutes, LegacyApiRoutes } from 'molthub-schema'
import { auth } from './auth'
import { downloadZip } from './downloads'
import {
+1 -1
View File
@@ -5,7 +5,7 @@ import {
CliSkillDeleteRequestSchema,
CliTelemetrySyncRequestSchema,
parseArk,
} from 'molthub-schema'
} from 'clawdhub-schema'
import { api, internal } from './_generated/api'
import type { Id } from './_generated/dataModel'
import type { ActionCtx } from './_generated/server'
+1 -1
View File
@@ -1,4 +1,4 @@
import { CliPublishRequestSchema, parseArk } from 'molthub-schema'
import { CliPublishRequestSchema, parseArk } from 'clawdhub-schema'
import { api, internal } from './_generated/api'
import type { Doc, Id } from './_generated/dataModel'
import type { ActionCtx } from './_generated/server'
+2 -2
View File
@@ -5,10 +5,10 @@ import type { Id } from '../_generated/dataModel'
import type { ActionCtx } from '../_generated/server'
const GITHUB_API = 'https://api.github.com'
const DEFAULT_REPO = 'moltbot/skills'
const DEFAULT_REPO = 'clawdbot/skills'
const DEFAULT_ROOT = 'skills'
const META_FILENAME = '_meta.json'
const USER_AGENT = 'molthub/skills-backup'
const USER_AGENT = 'clawdhub/skills-backup'
type BackupFile = {
path: string
+3 -3
View File
@@ -1,5 +1,5 @@
import { TEXT_FILE_EXTENSION_SET } from 'clawdhub-schema'
import { zipSync } from 'fflate'
import { TEXT_FILE_EXTENSION_SET } from 'molthub-schema'
import semver from 'semver'
import { parseFrontmatter } from './skills'
@@ -118,7 +118,7 @@ async function resolveRefCommit(parsed: GitHubImportUrl, ref: string, fetcher: t
const response = await fetcher(apiUrl, {
headers: {
Accept: 'application/vnd.github+json',
'User-Agent': 'molthub/github-import',
'User-Agent': 'clawdhub/github-import',
},
})
if (!response.ok) throw new Error('GitHub ref not found')
@@ -156,7 +156,7 @@ export async function fetchGitHubZipBytes(
const maxZipBytes = limits?.maxZipBytes ?? 25 * 1024 * 1024
const url = `https://${CODELOAD_HOST}/${resolved.owner}/${resolved.repo}/zip/${resolved.commit}`
const response = await fetcher(url, {
headers: { 'User-Agent': 'molthub/github-import' },
headers: { 'User-Agent': 'clawdhub/github-import' },
})
if (!response.ok) throw new Error('GitHub archive download failed')
+2 -2
View File
@@ -5,10 +5,10 @@ import type { Id } from '../_generated/dataModel'
import type { ActionCtx } from '../_generated/server'
const GITHUB_API = 'https://api.github.com'
const DEFAULT_REPO = 'moltbot/souls'
const DEFAULT_REPO = 'clawdbot/souls'
const DEFAULT_ROOT = 'souls'
const META_FILENAME = '_meta.json'
const USER_AGENT = 'molthub/souls-backup'
const USER_AGENT = 'clawdhub/souls-backup'
type BackupFile = {
path: string
+4 -4
View File
@@ -2,14 +2,14 @@ import {
getFrontmatterMetadata,
getFrontmatterValue,
type ParsedSkillFrontmatter,
parseClawdisMetadata,
parseFrontmatter,
parseMoltbotMetadata,
} from './skills'
export type ParsedSkillData = {
frontmatter: ParsedSkillFrontmatter
metadata?: unknown
moltbot?: unknown
clawdis?: unknown
}
export type SkillSummaryBackfillPatch = {
@@ -25,8 +25,8 @@ export function buildSkillSummaryBackfillPatch(args: {
const frontmatter = parseFrontmatter(args.readmeText)
const summary = getFrontmatterValue(frontmatter, 'description') ?? undefined
const metadata = getFrontmatterMetadata(frontmatter)
const moltbot = parseMoltbotMetadata(frontmatter)
const parsed: ParsedSkillData = { frontmatter, metadata, moltbot }
const clawdis = parseClawdisMetadata(frontmatter)
const parsed: ParsedSkillData = { frontmatter, metadata, clawdis }
const patch: SkillSummaryBackfillPatch = {}
if (summary && summary !== args.currentSummary) {
+2 -2
View File
@@ -4,7 +4,7 @@ import { __test } from './skillPublish'
describe('skillPublish', () => {
it('merges github source into metadata', () => {
const merged = __test.mergeSourceIntoMetadata(
{ moltbot: { emoji: 'x' } },
{ clawdis: { emoji: 'x' } },
{
kind: 'github',
url: 'https://github.com/a/b',
@@ -15,7 +15,7 @@ describe('skillPublish', () => {
importedAt: 123,
},
)
expect((merged as Record<string, unknown>).moltbot).toEqual({ emoji: 'x' })
expect((merged as Record<string, unknown>).clawdis).toEqual({ emoji: 'x' })
const source = (merged as Record<string, unknown>).source
expect(source).toEqual(
expect.objectContaining({
+3 -3
View File
@@ -12,8 +12,8 @@ import {
getFrontmatterMetadata,
hashSkillFiles,
isTextFile,
parseClawdisMetadata,
parseFrontmatter,
parseMoltbotMetadata,
sanitizePath,
} from './skills'
import type { WebhookSkillPayload } from './webhooks'
@@ -97,7 +97,7 @@ export async function publishVersionForUser(
const readmeText = await fetchText(ctx, readmeFile.storageId)
const frontmatter = parseFrontmatter(readmeText)
const moltbot = parseMoltbotMetadata(frontmatter)
const clawdis = parseClawdisMetadata(frontmatter)
const metadata = mergeSourceIntoMetadata(getFrontmatterMetadata(frontmatter), args.source)
const otherFiles = [] as Array<{ path: string; content: string }>
@@ -161,7 +161,7 @@ export async function publishVersionForUser(
parsed: {
frontmatter,
metadata,
moltbot,
clawdis,
},
embedding,
})) as PublishResult
+43 -43
View File
@@ -5,8 +5,8 @@ import {
getFrontmatterValue,
hashSkillFiles,
isTextFile,
parseClawdisMetadata,
parseFrontmatter,
parseMoltbotMetadata,
sanitizePath,
} from './skills'
@@ -51,89 +51,89 @@ describe('skills utils', () => {
expect(getFrontmatterValue(frontmatter, 'count')).toBeUndefined()
})
it('parses moltbot metadata', () => {
it('parses clawdis metadata', () => {
const frontmatter = parseFrontmatter(
`---\nmetadata: {"moltbot":{"requires":{"bins":["rg"]},"emoji":"🦞"}}\n---\nBody`,
`---\nmetadata: {"clawdis":{"requires":{"bins":["rg"]},"emoji":"🦞"}}\n---\nBody`,
)
const moltbot = parseMoltbotMetadata(frontmatter)
expect(moltbot?.emoji).toBe('🦞')
expect(moltbot?.requires?.bins).toEqual(['rg'])
const clawdis = parseClawdisMetadata(frontmatter)
expect(clawdis?.emoji).toBe('🦞')
expect(clawdis?.requires?.bins).toEqual(['rg'])
})
it('ignores invalid moltbot metadata', () => {
it('ignores invalid clawdis metadata', () => {
const frontmatter = parseFrontmatter(`---\nmetadata: not-json\n---\nBody`)
expect(parseMoltbotMetadata(frontmatter)).toBeUndefined()
expect(parseClawdisMetadata(frontmatter)).toBeUndefined()
})
it('accepts metadata as YAML object (no JSON string)', () => {
const frontmatter = parseFrontmatter(
`---\nmetadata:\n moltbot:\n emoji: "🦞"\n requires:\n bins:\n - rg\n---\nBody`,
`---\nmetadata:\n clawdis:\n emoji: "🦞"\n requires:\n bins:\n - rg\n---\nBody`,
)
expect(getFrontmatterMetadata(frontmatter)).toEqual({
moltbot: { emoji: '🦞', requires: { bins: ['rg'] } },
clawdis: { emoji: '🦞', requires: { bins: ['rg'] } },
})
const moltbot = parseMoltbotMetadata(frontmatter)
expect(moltbot?.emoji).toBe('🦞')
expect(moltbot?.requires?.bins).toEqual(['rg'])
const clawdis = parseClawdisMetadata(frontmatter)
expect(clawdis?.emoji).toBe('🦞')
expect(clawdis?.requires?.bins).toEqual(['rg'])
})
it('accepts moltbot as top-level YAML key', () => {
it('accepts clawdis as top-level YAML key', () => {
const frontmatter = parseFrontmatter(
`---\nmoltbot:\n emoji: "🦞"\n requires:\n anyBins: [rg, fd]\n---\nBody`,
`---\nclawdis:\n emoji: "🦞"\n requires:\n anyBins: [rg, fd]\n---\nBody`,
)
const moltbot = parseMoltbotMetadata(frontmatter)
expect(moltbot?.emoji).toBe('🦞')
expect(moltbot?.requires?.anyBins).toEqual(['rg', 'fd'])
const clawdis = parseClawdisMetadata(frontmatter)
expect(clawdis?.emoji).toBe('🦞')
expect(clawdis?.requires?.anyBins).toEqual(['rg', 'fd'])
})
it('accepts legacy metadata JSON string (quoted)', () => {
const frontmatter = parseFrontmatter(
`---\nmetadata: '{"moltbot":{"emoji":"🦞","requires":{"bins":["rg"]}}}'\n---\nBody`,
`---\nmetadata: '{"clawdis":{"emoji":"🦞","requires":{"bins":["rg"]}}}'\n---\nBody`,
)
const metadata = getFrontmatterMetadata(frontmatter)
expect(metadata).toEqual({ moltbot: { emoji: '🦞', requires: { bins: ['rg'] } } })
const moltbot = parseMoltbotMetadata(frontmatter)
expect(moltbot?.emoji).toBe('🦞')
expect(moltbot?.requires?.bins).toEqual(['rg'])
expect(metadata).toEqual({ clawdis: { emoji: '🦞', requires: { bins: ['rg'] } } })
const clawdis = parseClawdisMetadata(frontmatter)
expect(clawdis?.emoji).toBe('🦞')
expect(clawdis?.requires?.bins).toEqual(['rg'])
})
it('parses moltbot install specs and os', () => {
it('parses clawdis install specs and os', () => {
const frontmatter = parseFrontmatter(
`---\nmetadata: {"moltbot":{"install":[{"kind":"brew","formula":"rg"},{"kind":"nope"},{"kind":"node","package":"x"}],"os":"macos,linux","requires":{"anyBins":["rg","fd"]}}}\n---\nBody`,
`---\nmetadata: {"clawdis":{"install":[{"kind":"brew","formula":"rg"},{"kind":"nope"},{"kind":"node","package":"x"}],"os":"macos,linux","requires":{"anyBins":["rg","fd"]}}}\n---\nBody`,
)
const moltbot = parseMoltbotMetadata(frontmatter)
expect(moltbot?.install?.map((entry) => entry.kind)).toEqual(['brew', 'node'])
expect(moltbot?.os).toEqual(['macos', 'linux'])
expect(moltbot?.requires?.anyBins).toEqual(['rg', 'fd'])
const clawdis = parseClawdisMetadata(frontmatter)
expect(clawdis?.install?.map((entry) => entry.kind)).toEqual(['brew', 'node'])
expect(clawdis?.os).toEqual(['macos', 'linux'])
expect(clawdis?.requires?.anyBins).toEqual(['rg', 'fd'])
})
it('parses moltbot metadata with nix plugin pointer', () => {
it('parses clawdbot metadata with nix plugin pointer', () => {
const frontmatter = parseFrontmatter(
`---\nmetadata: {"moltbot":{"nix":{"plugin":"github:moltbot/nix-steipete-tools?dir=tools/peekaboo","systems":["aarch64-darwin"]}}}\n---\nBody`,
`---\nmetadata: {"clawdbot":{"nix":{"plugin":"github:clawdbot/nix-steipete-tools?dir=tools/peekaboo","systems":["aarch64-darwin"]}}}\n---\nBody`,
)
const moltbot = parseMoltbotMetadata(frontmatter)
expect(moltbot?.nix?.plugin).toBe('github:moltbot/nix-steipete-tools?dir=tools/peekaboo')
expect(moltbot?.nix?.systems).toEqual(['aarch64-darwin'])
const clawdis = parseClawdisMetadata(frontmatter)
expect(clawdis?.nix?.plugin).toBe('github:clawdbot/nix-steipete-tools?dir=tools/peekaboo')
expect(clawdis?.nix?.systems).toEqual(['aarch64-darwin'])
})
it('parses moltbot config requirements with example', () => {
it('parses clawdbot config requirements with example', () => {
const frontmatter = parseFrontmatter(
`---\nmetadata: {"moltbot":{"config":{"requiredEnv":["PADEL_AUTH_FILE"],"stateDirs":[".config/padel"],"example":"config = { env = { PADEL_AUTH_FILE = \\"/run/agenix/padel-auth\\"; }; };"}}}\n---\nBody`,
`---\nmetadata: {"clawdbot":{"config":{"requiredEnv":["PADEL_AUTH_FILE"],"stateDirs":[".config/padel"],"example":"config = { env = { PADEL_AUTH_FILE = \\"/run/agenix/padel-auth\\"; }; };"}}}\n---\nBody`,
)
const moltbot = parseMoltbotMetadata(frontmatter)
expect(moltbot?.config?.requiredEnv).toEqual(['PADEL_AUTH_FILE'])
expect(moltbot?.config?.stateDirs).toEqual(['.config/padel'])
expect(moltbot?.config?.example).toBe(
const clawdis = parseClawdisMetadata(frontmatter)
expect(clawdis?.config?.requiredEnv).toEqual(['PADEL_AUTH_FILE'])
expect(clawdis?.config?.stateDirs).toEqual(['.config/padel'])
expect(clawdis?.config?.example).toBe(
'config = { env = { PADEL_AUTH_FILE = "/run/agenix/padel-auth"; }; };',
)
})
it('parses cli help output', () => {
const frontmatter = parseFrontmatter(
`---\nmetadata: {"moltbot":{"cliHelp":"padel --help\\nUsage: padel [command]\\n"}}\n---\nBody`,
`---\nmetadata: {"clawdbot":{"cliHelp":"padel --help\\nUsage: padel [command]\\n"}}\n---\nBody`,
)
const moltbot = parseMoltbotMetadata(frontmatter)
expect(moltbot?.cliHelp).toBe('padel --help\nUsage: padel [command]')
const clawdis = parseClawdisMetadata(frontmatter)
expect(clawdis?.cliHelp).toBe('padel --help\nUsage: padel [command]')
})
it('sanitizes file paths', () => {
+32 -29
View File
@@ -1,17 +1,17 @@
import {
type ClawdbotConfigSpec,
type ClawdisSkillMetadata,
ClawdisSkillMetadataSchema,
isTextContentType,
type MoltbotConfigSpec,
type MoltbotSkillMetadata,
MoltbotSkillMetadataSchema,
type NixPluginSpec,
parseArk,
type SkillInstallSpec,
TEXT_FILE_EXTENSION_SET,
} from 'molthub-schema'
} from 'clawdhub-schema'
import { parse as parseYaml } from 'yaml'
export type ParsedSkillFrontmatter = Record<string, unknown>
export type { MoltbotSkillMetadata, SkillInstallSpec }
export type { ClawdisSkillMetadata, SkillInstallSpec }
const FRONTMATTER_START = '---'
const DEFAULT_EMBEDDING_MAX_CHARS = 12_000
@@ -59,39 +59,42 @@ export function getFrontmatterMetadata(frontmatter: ParsedSkillFrontmatter) {
return undefined
}
export function parseMoltbotMetadata(frontmatter: ParsedSkillFrontmatter) {
export function parseClawdisMetadata(frontmatter: ParsedSkillFrontmatter) {
const metadata = getFrontmatterMetadata(frontmatter)
const metadataRecord =
metadata && typeof metadata === 'object' && !Array.isArray(metadata)
? (metadata as Record<string, unknown>)
: undefined
const moltbotMeta = metadataRecord?.moltbot
const clawdbotMeta = metadataRecord?.clawdbot
const clawdisMeta = metadataRecord?.clawdis
const metadataSource =
moltbotMeta && typeof moltbotMeta === 'object' && !Array.isArray(moltbotMeta)
? (moltbotMeta as Record<string, unknown>)
: undefined
const moltbotRaw = metadataSource ?? frontmatter.moltbot
if (!moltbotRaw || typeof moltbotRaw !== 'object' || Array.isArray(moltbotRaw)) return undefined
clawdbotMeta && typeof clawdbotMeta === 'object' && !Array.isArray(clawdbotMeta)
? (clawdbotMeta as Record<string, unknown>)
: clawdisMeta && typeof clawdisMeta === 'object' && !Array.isArray(clawdisMeta)
? (clawdisMeta as Record<string, unknown>)
: undefined
const clawdisRaw = metadataSource ?? frontmatter.clawdis
if (!clawdisRaw || typeof clawdisRaw !== 'object' || Array.isArray(clawdisRaw)) return undefined
try {
const moltbotObj = moltbotRaw as Record<string, unknown>
const clawdisObj = clawdisRaw as Record<string, unknown>
const requiresRaw =
typeof moltbotObj.requires === 'object' && moltbotObj.requires !== null
? (moltbotObj.requires as Record<string, unknown>)
typeof clawdisObj.requires === 'object' && clawdisObj.requires !== null
? (clawdisObj.requires as Record<string, unknown>)
: undefined
const installRaw = Array.isArray(moltbotObj.install) ? (moltbotObj.install as unknown[]) : []
const installRaw = Array.isArray(clawdisObj.install) ? (clawdisObj.install as unknown[]) : []
const install = installRaw
.map((entry) => parseInstallSpec(entry))
.filter((entry): entry is SkillInstallSpec => Boolean(entry))
const osRaw = normalizeStringList(moltbotObj.os)
const osRaw = normalizeStringList(clawdisObj.os)
const metadata: MoltbotSkillMetadata = {}
if (typeof moltbotObj.always === 'boolean') metadata.always = moltbotObj.always
if (typeof moltbotObj.emoji === 'string') metadata.emoji = moltbotObj.emoji
if (typeof moltbotObj.homepage === 'string') metadata.homepage = moltbotObj.homepage
if (typeof moltbotObj.skillKey === 'string') metadata.skillKey = moltbotObj.skillKey
if (typeof moltbotObj.primaryEnv === 'string') metadata.primaryEnv = moltbotObj.primaryEnv
if (typeof moltbotObj.cliHelp === 'string') metadata.cliHelp = moltbotObj.cliHelp
const metadata: ClawdisSkillMetadata = {}
if (typeof clawdisObj.always === 'boolean') metadata.always = clawdisObj.always
if (typeof clawdisObj.emoji === 'string') metadata.emoji = clawdisObj.emoji
if (typeof clawdisObj.homepage === 'string') metadata.homepage = clawdisObj.homepage
if (typeof clawdisObj.skillKey === 'string') metadata.skillKey = clawdisObj.skillKey
if (typeof clawdisObj.primaryEnv === 'string') metadata.primaryEnv = clawdisObj.primaryEnv
if (typeof clawdisObj.cliHelp === 'string') metadata.cliHelp = clawdisObj.cliHelp
if (osRaw.length > 0) metadata.os = osRaw
if (requiresRaw) {
@@ -109,12 +112,12 @@ export function parseMoltbotMetadata(frontmatter: ParsedSkillFrontmatter) {
}
if (install.length > 0) metadata.install = install
const nix = parseNixPluginSpec(moltbotObj.nix)
const nix = parseNixPluginSpec(clawdisObj.nix)
if (nix) metadata.nix = nix
const config = parseMoltbotConfigSpec(moltbotObj.config)
const config = parseClawdbotConfigSpec(clawdisObj.config)
if (config) metadata.config = config
return parseArk(MoltbotSkillMetadataSchema, metadata, 'Moltbot metadata')
return parseArk(ClawdisSkillMetadataSchema, metadata, 'Clawdis metadata')
} catch {
return undefined
}
@@ -247,13 +250,13 @@ function parseNixPluginSpec(input: unknown): NixPluginSpec | undefined {
return spec
}
function parseMoltbotConfigSpec(input: unknown): MoltbotConfigSpec | undefined {
function parseClawdbotConfigSpec(input: unknown): ClawdbotConfigSpec | undefined {
if (!input || typeof input !== 'object') return undefined
const raw = input as Record<string, unknown>
const requiredEnv = normalizeStringList(raw.requiredEnv)
const stateDirs = normalizeStringList(raw.stateDirs)
const example = typeof raw.example === 'string' ? raw.example.trim() : ''
const spec: MoltbotConfigSpec = {}
const spec: ClawdbotConfigSpec = {}
if (requiredEnv.length > 0) spec.requiredEnv = requiredEnv
if (stateDirs.length > 0) spec.stateDirs = stateDirs
if (example) spec.example = example
+6 -6
View File
@@ -20,7 +20,7 @@ describe('webhook config', () => {
delete process.env.SITE_URL
process.env.DISCORD_WEBHOOK_URL = 'https://example.com'
const config = getWebhookConfig()
expect(config.siteUrl).toBe('https://molthub.com')
expect(config.siteUrl).toBe('https://clawdhub.com')
})
})
@@ -36,7 +36,7 @@ describe('webhook filtering', () => {
const config = {
url: 'https://example.com',
highlightedOnly: true,
siteUrl: 'https://molthub.com',
siteUrl: 'https://clawdhub.com',
}
const allowed = shouldSendWebhook(
'skill.publish',
@@ -50,7 +50,7 @@ describe('webhook filtering', () => {
const config = {
url: 'https://example.com',
highlightedOnly: true,
siteUrl: 'https://molthub.com',
siteUrl: 'https://clawdhub.com',
}
const allowed = shouldSendWebhook(
'skill.highlighted',
@@ -65,9 +65,9 @@ describe('payload building', () => {
it('builds canonical url with owner', () => {
const url = buildSkillUrl(
{ slug: 'beeper', displayName: 'Beeper', ownerHandle: 'KrauseFx' },
'https://molthub.com',
'https://clawdhub.com',
)
expect(url).toBe('https://molthub.com/KrauseFx/beeper')
expect(url).toBe('https://clawdhub.com/KrauseFx/beeper')
})
it('builds a publish embed', () => {
@@ -81,7 +81,7 @@ describe('payload building', () => {
ownerHandle: 'steipete',
tags: ['latest', 'discord'],
},
{ url: 'https://example.com', highlightedOnly: false, siteUrl: 'https://molthub.com' },
{ url: 'https://example.com', highlightedOnly: false, siteUrl: 'https://clawdhub.com' },
)
const embed = payload.embeds[0]
expect(embed.title).toBe('Demo Skill')
+5 -5
View File
@@ -16,7 +16,7 @@ export type WebhookConfig = {
siteUrl: string
}
const DEFAULT_SITE_URL = 'https://molthub.com'
const DEFAULT_SITE_URL = 'https://clawdhub.com'
export function getWebhookConfig(env: NodeJS.ProcessEnv = process.env): WebhookConfig {
const url = env.DISCORD_WEBHOOK_URL?.trim() || null
@@ -72,7 +72,7 @@ export function buildDiscordPayload(
},
],
footer: {
text: 'MoltHub',
text: 'ClawdHub',
},
timestamp: new Date().toISOString(),
},
@@ -89,9 +89,9 @@ export function buildSkillUrl(skill: WebhookSkillPayload, siteUrl: string) {
function buildDescription(event: WebhookEvent, skill: WebhookSkillPayload) {
const summary = (skill.summary ?? '').trim()
if (summary) return truncate(summary, 200)
if (event === 'skill.highlighted') return 'Newly highlighted skill on MoltHub.'
if (skill.version) return `New version v${skill.version} published on MoltHub.`
return 'New skill published on MoltHub.'
if (event === 'skill.highlighted') return 'Newly highlighted skill on ClawdHub.'
if (skill.version) return `New version v${skill.version} published on ClawdHub.`
return 'New skill published on ClawdHub.'
}
function parseBoolean(value?: string) {
+1 -1
View File
@@ -62,7 +62,7 @@ describe('maintenance backfill', () => {
parsed: {
frontmatter: { description: 'Hello world.' },
metadata: undefined,
moltbot: undefined,
clawdis: undefined,
},
})
})
+1 -1
View File
@@ -100,7 +100,7 @@ export const applySkillBackfillPatchInternal = internalMutation({
v.object({
frontmatter: v.record(v.string(), v.any()),
metadata: v.optional(v.any()),
moltbot: v.optional(v.any()),
clawdis: v.optional(v.any()),
}),
),
},
-1
View File
@@ -144,7 +144,6 @@ const skillVersions = defineTable({
parsed: v.object({
frontmatter: v.record(v.string(), v.any()),
metadata: v.optional(v.any()),
moltbot: v.optional(v.any()),
clawdis: v.optional(v.any()),
}),
createdBy: v.id('users'),
+11 -11
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1324,7 +1324,7 @@ export const insertVersion = internalMutation({
parsed: v.object({
frontmatter: v.record(v.string(), v.any()),
metadata: v.optional(v.any()),
moltbot: v.optional(v.any()),
clawdis: v.optional(v.any()),
}),
embedding: v.array(v.number()),
},
+1 -1
View File
@@ -22,7 +22,7 @@ Reading order (new contributor):
Feature/ops docs (already present):
- `docs/spec.md`: product + implementation spec (data model + flows).
- `docs/telemetry.md`: what `molthub sync` reports; opt-out.
- `docs/telemetry.md`: what `clawdhub sync` reports; opt-out.
- `docs/webhook.md`: Discord webhook events/payload.
- `docs/diffing.md`: version-to-version diff UI spec.
- `docs/manual-testing.md`: CLI smoke scripts.
+1 -1
View File
@@ -7,7 +7,7 @@ read_when:
# API v1
Base: `https://molthub.com`
Base: `https://clawdhub.com`
OpenAPI: `/api/v1/openapi.json`
+4 -4
View File
@@ -11,8 +11,8 @@ read_when:
- Web app: TanStack Start (React) under `src/`.
- Backend: Convex under `convex/` (DB, storage, actions, HTTP routes).
- CLI: `packages/molthub/` (published as `molthub`).
- Shared schemas/routes: `packages/schema/` (`molthub-schema`).
- CLI: `packages/clawdhub/` (published as `clawdhub`).
- Shared schemas/routes: `packages/schema/` (`clawdhub-schema`).
## Data + storage
@@ -38,8 +38,8 @@ read_when:
- Download zip via `/api/v1/download?slug=...&version=...`.
- Extract into `./skills/<slug>` (default).
- Persist install state:
- `./.molthub/lock.json` (per workdir)
- `./skills/<slug>/.molthub/origin.json` (per skill folder)
- `./.clawdhub/lock.json` (per workdir)
- `./skills/<slug>/.clawdhub/origin.json` (per skill folder)
### Update (CLI)
+4 -4
View File
@@ -23,7 +23,7 @@ The CLI uses a long-lived API token (Bearer token) for publish/sync/delete.
### Browser flow (default)
`molthub login` does:
`clawdhub login` does:
1. Starts a loopback HTTP server on `127.0.0.1` (random port).
2. Opens `<site>/cli/auth?redirect_uri=http://127.0.0.1:<port>/callback&state=...`.
@@ -35,18 +35,18 @@ The CLI uses a long-lived API token (Bearer token) for publish/sync/delete.
Create a token in the web UI (Settings → API tokens) and paste it:
```bash
molthub login --token clh_...
clawdhub login --token clh_...
```
### Token storage
Default global config path:
- macOS: `~/Library/Application Support/molthub/config.json`
- macOS: `~/Library/Application Support/clawdhub/config.json`
Override:
- `MOLTHUB_CONFIG_PATH=/path/to/config.json`
- `CLAWDHUB_CONFIG_PATH=/path/to/config.json`
### Revocation
+18 -18
View File
@@ -7,41 +7,41 @@ read_when:
# CLI
CLI package: `packages/molthub/` (bin: `molthub`).
CLI package: `packages/clawdhub/` (bin: `clawdhub`).
From this repo you can run it via the wrapper script:
```bash
bun molthub --help
bun clawdhub --help
```
## Global flags
- `--workdir <dir>`: working directory (default: cwd; falls back to Moltbot workspace if configured)
- `--workdir <dir>`: working directory (default: cwd; falls back to Clawdbot workspace if configured)
- `--dir <dir>`: install dir under workdir (default: `skills`)
- `--site <url>`: base URL for browser login (default: `https://molthub.com`)
- `--registry <url>`: API base URL (default: discovered, else `https://molthub.com`)
- `--site <url>`: base URL for browser login (default: `https://clawdhub.com`)
- `--registry <url>`: API base URL (default: discovered, else `https://clawdhub.com`)
- `--no-input`: disable prompts
Env equivalents:
- `MOLTHUB_SITE`
- `MOLTHUB_REGISTRY`
- `MOLTHUB_WORKDIR`
- `CLAWDHUB_SITE`
- `CLAWDHUB_REGISTRY`
- `CLAWDHUB_WORKDIR`
## Config file
Stores your API token + cached registry URL.
- macOS: `~/Library/Application Support/molthub/config.json`
- override: `MOLTHUB_CONFIG_PATH`
- macOS: `~/Library/Application Support/clawdhub/config.json`
- override: `CLAWDHUB_CONFIG_PATH`
## Commands
### `login` / `auth login`
- Default: opens browser to `<site>/cli/auth` and completes via loopback callback.
- Headless: `molthub login --token clh_...`
- Headless: `clawdhub login --token clh_...`
### `whoami`
@@ -72,12 +72,12 @@ Stores your API token + cached registry URL.
- Downloads zip via `/api/v1/download`.
- Extracts into `<workdir>/<dir>/<slug>`.
- Writes:
- `<workdir>/.molthub/lock.json`
- `<skill>/.molthub/origin.json`
- `<workdir>/.clawdhub/lock.json`
- `<skill>/.clawdhub/origin.json`
### `list`
- Reads `<workdir>/.molthub/lock.json`.
- Reads `<workdir>/.clawdhub/lock.json`.
### `update [slug]` / `update --all`
@@ -96,12 +96,12 @@ Stores your API token + cached registry URL.
- Scans for local skill folders and publishes new/changed ones.
- Roots can be any folder: a skills directory or a single skill folder with `SKILL.md`.
- Auto-adds Moltbot skill roots when `~/.moltbot/moltbot.json` is present:
- Auto-adds Clawdbot skill roots when `~/.clawdbot/clawdbot.json` is present:
- `agent.workspace/skills` (main agent)
- `routing.agents.*.workspace/skills` (per-agent)
- `~/.moltbot/skills` (shared)
- `~/.clawdbot/skills` (shared)
- `skills.load.extraDirs` (shared packs)
- Respects `MOLTBOT_CONFIG_PATH` and `MOLTBOT_STATE_DIR`.
- Respects `CLAWDBOT_CONFIG_PATH` and `CLAWDBOT_STATE_DIR`.
- Flags:
- `--root <dir...>` extra scan roots
- `--all` upload without prompting
@@ -113,5 +113,5 @@ Stores your API token + cached registry URL.
Telemetry:
- Sent during `sync` when logged in, unless `MOLTHUB_DISABLE_TELEMETRY=1`.
- Sent during `sync` when logged in, unless `CLAWDHUB_DISABLE_TELEMETRY=1`.
- Details: `docs/telemetry.md`.
+5 -5
View File
@@ -7,7 +7,7 @@ read_when:
# Deploy
MoltHub is two deployables:
ClawdHub is two deployables:
- Web app (TanStack Start) → typically Vercel.
- Convex backend → Convex deployment (serves `/api/...` routes).
@@ -55,12 +55,12 @@ For self-host:
The CLI can discover the API base from:
- `/.well-known/molthub.json`
- `/.well-known/clawdhub.json`
If you dont serve that file, users must set:
```bash
export MOLTHUB_REGISTRY=https://your-site.example
export CLAWDHUB_REGISTRY=https://your-site.example
```
## 5) Post-deploy checks
@@ -73,6 +73,6 @@ curl -i "https://<site>/api/v1/skills/gifgrep"
Then:
```bash
molthub login --site https://<site>
molthub whoami
clawdhub login --site https://<site>
clawdhub whoami
```
+2 -2
View File
@@ -10,7 +10,7 @@ read_when:
## Goals
- Compare any file between two versions.
- Default compare: `latest` vs `previous` (SemVer precedence).
- UX feels native to MoltHub (theme + typography + motion).
- UX feels native to ClawdHub (theme + typography + motion).
- Inline or side-by-side toggle.
- Public access.
@@ -57,7 +57,7 @@ Optional helper action:
- Feed into Monaco diff editor.
## Monaco theming
- Define `molthub-light` / `molthub-dark` via `monaco.editor.defineTheme`.
- Define `clawdhub-light` / `clawdhub-dark` via `monaco.editor.defineTheme`.
- Derive colors from CSS variables on `document.documentElement`:
- `--surface`, `--surface-muted`, `--ink`, `--ink-soft`, `--line`, `--accent`.
- Apply theme on load + when theme changes (`data-theme`).
+5 -5
View File
@@ -7,7 +7,7 @@ read_when:
# HTTP API
Base URL: `https://molthub.com` (default).
Base URL: `https://clawdhub.com` (default).
All v1 paths are under `/api/v1/...` and implemented by Convex HTTP routes (`convex/http.ts`).
Legacy `/api/...` and `/api/cli/...` remain for compatibility (see `DEPRECATIONS.md`).
@@ -172,16 +172,16 @@ Still supported for older CLI versions:
See `DEPRECATIONS.md` for removal plan.
## Registry discovery (`/.well-known/molthub.json`)
## Registry discovery (`/.well-known/clawdhub.json`)
The CLI can discover registry/auth settings from the site:
- `/.well-known/molthub.json` (JSON)
- `/.well-known/clawdhub.json` (JSON)
Schema:
```json
{ "apiBase": "https://molthub.com", "authBase": "https://molthub.com", "minCliVersion": "0.0.5" }
{ "apiBase": "https://clawdhub.com", "authBase": "https://clawdhub.com", "minCliVersion": "0.0.5" }
```
If you self-host, serve this file (or set `MOLTHUB_REGISTRY` explicitly).
If you self-host, serve this file (or set `CLAWDHUB_REGISTRY` explicitly).
+20 -20
View File
@@ -8,53 +8,53 @@ read_when:
# Manual testing (CLI)
## Setup
- Ensure logged in: `bun molthub whoami` (or `bun molthub login`).
- Ensure logged in: `bun clawdhub whoami` (or `bun clawdhub login`).
- Optional: set env
- `MOLTHUB_SITE=https://molthub.com`
- `MOLTHUB_REGISTRY=https://molthub.com`
- `CLAWDHUB_SITE=https://clawdhub.com`
- `CLAWDHUB_REGISTRY=https://clawdhub.com`
## Smoke
- `bun molthub --help`
- `bun molthub --cli-version`
- `bun molthub whoami`
- `bun clawdhub --help`
- `bun clawdhub --cli-version`
- `bun clawdhub whoami`
## Search
- `bun molthub search gif --limit 5`
- `bun clawdhub search gif --limit 5`
## Install / list / update
- `mkdir -p /tmp/molthub-manual && cd /tmp/molthub-manual`
- `bunx molthub@beta install gifgrep --force`
- `bunx molthub@beta list`
- `bunx molthub@beta update gifgrep --force`
- `mkdir -p /tmp/clawdhub-manual && cd /tmp/clawdhub-manual`
- `bunx clawdhub@beta install gifgrep --force`
- `bunx clawdhub@beta list`
- `bunx clawdhub@beta update gifgrep --force`
## Publish (changelog optional)
- `mkdir -p /tmp/molthub-skill-demo/SKILL && cd /tmp/molthub-skill-demo`
- `mkdir -p /tmp/clawdhub-skill-demo/SKILL && cd /tmp/clawdhub-skill-demo`
- Create files:
- `SKILL.md`
- `notes.md`
- Publish:
- `bun molthub publish . --slug molthub-manual-<ts> --name "Manual <ts>" --version 1.0.0 --tags latest`
- `bun clawdhub publish . --slug clawdhub-manual-<ts> --name "Manual <ts>" --version 1.0.0 --tags latest`
- Publish update with empty changelog:
- `bun molthub publish . --slug molthub-manual-<ts> --name "Manual <ts>" --version 1.0.1 --tags latest`
- `bun clawdhub publish . --slug clawdhub-manual-<ts> --name "Manual <ts>" --version 1.0.1 --tags latest`
## Delete / undelete (owner/admin)
- `bun molthub delete molthub-manual-<ts> --yes`
- `bun clawdhub delete clawdhub-manual-<ts> --yes`
- Verify hidden:
- `curl -i "https://molthub.com/api/v1/skills/molthub-manual-<ts>"`
- `curl -i "https://clawdhub.com/api/v1/skills/clawdhub-manual-<ts>"`
- Restore:
- `bun molthub undelete molthub-manual-<ts> --yes`
- `bun clawdhub undelete clawdhub-manual-<ts> --yes`
- Cleanup:
- `bun molthub delete molthub-manual-<ts> --yes`
- `bun clawdhub delete clawdhub-manual-<ts> --yes`
## Sync
- `bun molthub sync --dry-run --all`
- `bun clawdhub sync --dry-run --all`
## Playwright (menu smoke)
Run against prod:
```
PLAYWRIGHT_BASE_URL=https://molthub.com bun run test:pw
PLAYWRIGHT_BASE_URL=https://clawdhub.com bun run test:pw
```
Run against a local preview server:
+1 -1
View File
@@ -20,7 +20,7 @@ Example (starter):
```json
{
"name": "MoltHub",
"name": "ClawdHub",
"logo": "public/logo.svg",
"navigation": [
{ "group": "Start", "pages": ["docs/README", "docs/quickstart"] },
+14 -14
View File
@@ -51,29 +51,29 @@ Then paste the printed `JWT_PRIVATE_KEY` + `JWKS` into `.env.local` (and ensure
From this repo:
```bash
bun molthub --help
bun molthub login
bun molthub whoami
bun molthub search gif --limit 5
bun clawdhub --help
bun clawdhub login
bun clawdhub whoami
bun clawdhub search gif --limit 5
```
Install a skill into `./skills/<slug>` (if Moltbot is configured, installs into that workspace instead):
Install a skill into `./skills/<slug>` (if Clawdbot is configured, installs into that workspace instead):
```bash
bun molthub install <slug>
bun molthub list
bun clawdhub install <slug>
bun clawdhub list
```
You can also install into any folder:
```bash
bun molthub install <slug> --workdir /tmp/molthub-demo --dir skills
bun clawdhub install <slug> --workdir /tmp/clawdhub-demo --dir skills
```
Update:
```bash
bun molthub update --all
bun clawdhub update --all
```
## 4) Publish a skill
@@ -81,7 +81,7 @@ bun molthub update --all
Create a folder containing `SKILL.md` (required) plus any supporting text files:
```bash
mkdir -p /tmp/molthub-skill-demo && cd /tmp/molthub-skill-demo
mkdir -p /tmp/clawdhub-skill-demo && cd /tmp/clawdhub-skill-demo
cat > SKILL.md <<'EOF'
---
name: Demo Skill
@@ -97,8 +97,8 @@ EOF
Publish:
```bash
bun molthub publish . \
--slug molthub-demo-$(date +%s) \
bun clawdhub publish . \
--slug clawdhub-demo-$(date +%s) \
--name "Demo $(date +%s)" \
--version 1.0.0 \
--tags latest \
@@ -110,11 +110,11 @@ bun molthub publish . \
`sync` scans for local skill folders and publishes the ones that arent “synced” yet.
```bash
bun molthub sync
bun clawdhub sync
```
Dry run + non-interactive:
```bash
bun molthub sync --all --dry-run --no-input
bun clawdhub sync --all --dry-run --no-input
```
+3 -3
View File
@@ -18,16 +18,16 @@ Required:
Optional:
- any supporting *text-based* files (see “Allowed files”)
- `.molthubignore` (ignore patterns for publish/sync)
- `.clawdhubignore` (ignore patterns for publish/sync)
- `.gitignore` (also honored)
Local install metadata (written by the CLI):
- `<skill>/.molthub/origin.json`
- `<skill>/.clawdhub/origin.json`
Workdir install state (written by the CLI):
- `<workdir>/.molthub/lock.json`
- `<workdir>/.clawdhub/lock.json`
## `SKILL.md`
+7 -7
View File
@@ -1,18 +1,18 @@
---
summary: "MoltHub spec: skills registry, versioning, vector search, moderation"
summary: "ClawdHub spec: skills registry, versioning, vector search, moderation"
read_when:
- Bootstrapping MoltHub
- Bootstrapping ClawdHub
- Implementing schema/auth/search/versioning
- Reviewing API and upload/download flows
---
# MoltHub — product + implementation spec (v1)
# ClawdHub — product + implementation spec (v1)
## Goals
- onlycrabs.ai mode for sharing `SOUL.md` bundles (host-based entry point).
- Minimal, fast SPA for browsing and publishing agent skills.
- Skills stored in Convex (files + metadata + versions + stats).
- GitHub OAuth login; GitHub App backs up skills to `moltbot/skills`.
- GitHub OAuth login; GitHub App backs up skills to `clawdbot/skills`.
- Vector-based search over skill text + metadata.
- Versioning, tags (`latest` + user tags), changelog, rollback (tag movement).
- Public read access; upload requires auth.
@@ -63,12 +63,12 @@ read_when:
- `softDeletedAt` (nullable)
### Parsed Skill Metadata
From SKILL.md frontmatter + AgentSkills + Moltbot extensions:
From SKILL.md frontmatter + AgentSkills + Clawdis extensions:
- `name`, `description`, `homepage`, `website`, `url`, `emoji`
- `metadata.moltbot`: `always`, `skillKey`, `primaryEnv`, `emoji`, `homepage`, `os`,
- `metadata.clawdis`: `always`, `skillKey`, `primaryEnv`, `emoji`, `homepage`, `os`,
`requires` (`bins`, `anyBins`, `env`, `config`), `install[]`, `nix` (`plugin`, `systems`),
`config` (`requiredEnv`, `stateDirs`, `example`), `cliHelp` (string; `cli --help` output)
- `metadata.moltbot`: alias of `metadata.moltbot` (preferred for nix-moltbot plugin pointers)
- `metadata.clawdbot`: alias of `metadata.clawdis` (preferred for nix-clawdbot plugin pointers)
- Nix plugins are different from regular skills; they bundle the skill pack, the CLI binary, and config flags/requirements together.
- `metadata` in frontmatter is YAML (object) preferred; legacy JSON-string accepted.
+8 -8
View File
@@ -1,5 +1,5 @@
---
summary: 'Install telemetry collected via `molthub sync` + opt-out.'
summary: 'Install telemetry collected via `clawdhub sync` + opt-out.'
read_when:
- Working on telemetry / privacy controls
- Questions about what data is collected
@@ -7,22 +7,22 @@ read_when:
# Telemetry
MoltHub uses **minimal telemetry** to compute **install counts** (whats actually in use) and to power better sorting/filtering.
This is based on the CLI `molthub sync` command.
ClawdHub uses **minimal telemetry** to compute **install counts** (whats actually in use) and to power better sorting/filtering.
This is based on the CLI `clawdhub sync` command.
## When telemetry is collected
Telemetry is only sent when:
- You are **logged in** in the CLI (we already require auth for sync/publish flows).
- You run `molthub sync`.
- You run `clawdhub sync`.
- Telemetry is **not disabled** (see “How to disable” below).
If you are not logged in, nothing is reported.
## What we collect
On each `molthub sync`, the CLI reports a **full snapshot** of what it found, grouped by scan root (“folder/root”).
On each `clawdhub sync`, the CLI reports a **full snapshot** of what it found, grouped by scan root (“folder/root”).
For each root we store:
@@ -70,7 +70,7 @@ This is evaluated lazily (on the next telemetry report) to avoid background jobs
## Transparency + user controls
MoltHub provides a private “Installed” tab on your own profile:
ClawdHub provides a private “Installed” tab on your own profile:
- Shows the exact roots + installed skills we store.
- Includes a **JSON export** view.
@@ -85,7 +85,7 @@ Deleting your account also deletes your telemetry data.
Set the environment variable:
```bash
export MOLTHUB_DISABLE_TELEMETRY=1
export CLAWDHUB_DISABLE_TELEMETRY=1
```
With this set, the CLI will not send telemetry during `molthub sync`.
With this set, the CLI will not send telemetry during `clawdhub sync`.
+7 -7
View File
@@ -6,16 +6,16 @@ read_when:
# Troubleshooting
## `molthub login` opens browser but never completes
## `clawdhub login` opens browser but never completes
- Ensure your browser can reach `http://127.0.0.1:<port>/callback` (local firewalls/VPNs can interfere).
- Use headless mode:
- create a token in the web UI (Settings → API tokens)
- `molthub login --token clh_...`
- `clawdhub login --token clh_...`
## `whoami` / `publish` returns `Unauthorized` (401)
- Token missing or revoked: check your config file (`MOLTHUB_CONFIG_PATH` override?).
- Token missing or revoked: check your config file (`CLAWDHUB_CONFIG_PATH` override?).
- Ensure requests include `Authorization: Bearer ...` (CLI does this automatically).
## `publish` fails with `OPENAI_API_KEY is not configured`
@@ -28,11 +28,11 @@ read_when:
- `sync` looks for folders containing `SKILL.md` (or `skill.md`).
- It scans:
- workdir first
- then fallback roots (legacy `~/moltbot/skills`, `~/moltbot/skills`, etc.)
- then fallback roots (legacy `~/clawdis/skills`, `~/clawdbot/skills`, etc.)
- Provide explicit roots:
```bash
molthub sync --root /path/to/skills
clawdhub sync --root /path/to/skills
```
## `update` refuses due to “local changes (no match)”
@@ -40,8 +40,8 @@ molthub sync --root /path/to/skills
- Your local files dont match any published fingerprint.
- Options:
- keep local edits; skip updating
- overwrite: `molthub update <slug> --force`
- publish as fork: copy to new folder/slug then `molthub publish ... --fork-of upstream@version`
- overwrite: `clawdhub update <slug> --force`
- publish as fork: copy to new folder/slug then `clawdhub publish ... --fork-of upstream@version`
## `GET /api/*` works locally but not on Vercel
+4 -4
View File
@@ -6,7 +6,7 @@ read_when:
# Webhooks (Discord)
MoltHub can post Discord embeds when skills are published or highlighted.
ClawdHub can post Discord embeds when skills are published or highlighted.
## Setup
@@ -14,7 +14,7 @@ Set the webhook URL in the Convex environment:
- `DISCORD_WEBHOOK_URL` (required): Discord webhook URL.
- `DISCORD_WEBHOOK_HIGHLIGHTED_ONLY` (optional): `true` to only send for highlighted skills.
- `SITE_URL` (optional): Base site URL for links (default `https://molthub.com`).
- `SITE_URL` (optional): Base site URL for links (default `https://clawdhub.com`).
## Events
@@ -38,13 +38,13 @@ Discord receives a JSON payload with a single embed:
{
"title": "Demo Skill",
"description": "Nice skill",
"url": "https://molthub.com/owner/demo-skill",
"url": "https://clawdhub.com/owner/demo-skill",
"fields": [
{ "name": "Version", "value": "v1.2.3", "inline": true },
{ "name": "Owner", "value": "@owner", "inline": true },
{ "name": "Tags", "value": "latest, discord", "inline": false }
],
"footer": { "text": "MoltHub" }
"footer": { "text": "ClawdHub" }
}
]
}
@@ -10,10 +10,10 @@ import {
ApiV1SearchResponseSchema,
ApiV1WhoamiResponseSchema,
parseArk,
} from 'molthub-schema'
} from 'clawdhub-schema'
import { Agent, setGlobalDispatcher } from 'undici'
import { describe, expect, it } from 'vitest'
import { readGlobalConfig } from '../packages/molthub/src/config'
import { readGlobalConfig } from '../packages/clawdhub/src/config'
const REQUEST_TIMEOUT_MS = 15_000
@@ -29,13 +29,13 @@ try {
}
function mustGetToken() {
const fromEnv = process.env.MOLTHUB_E2E_TOKEN?.trim()
const fromEnv = process.env.CLAWDHUB_E2E_TOKEN?.trim()
if (fromEnv) return fromEnv
return null
}
async function makeTempConfig(registry: string, token: string | null) {
const dir = await mkdtemp(join(tmpdir(), 'molthub-e2e-'))
const dir = await mkdtemp(join(tmpdir(), 'clawdhub-e2e-'))
const path = join(dir, 'config.json')
await writeFile(
path,
@@ -55,9 +55,9 @@ async function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit) {
}
}
describe('molthub e2e', () => {
describe('clawdhub e2e', () => {
it('prints CLI version via --cli-version', async () => {
const result = spawnSync('bun', ['molthub', '--cli-version'], {
const result = spawnSync('bun', ['clawdhub', '--cli-version'], {
cwd: process.cwd(),
encoding: 'utf8',
})
@@ -66,7 +66,7 @@ describe('molthub e2e', () => {
})
it('search endpoint returns a results array (schema parse)', async () => {
const registry = process.env.MOLTHUB_REGISTRY?.trim() || 'https://molthub.com'
const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com'
const url = new URL(ApiRoutes.search, registry)
url.searchParams.set('q', 'gif')
url.searchParams.set('limit', '5')
@@ -81,17 +81,17 @@ describe('molthub e2e', () => {
})
it('cli search does not error on multi-result responses', async () => {
const registry = process.env.MOLTHUB_REGISTRY?.trim() || 'https://molthub.com'
const site = process.env.MOLTHUB_SITE?.trim() || 'https://molthub.com'
const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com'
const site = process.env.CLAWDHUB_SITE?.trim() || 'https://clawdhub.com'
const token = mustGetToken() ?? (await readGlobalConfig())?.token ?? null
const cfg = await makeTempConfig(registry, token)
try {
const workdir = await mkdtemp(join(tmpdir(), 'molthub-e2e-workdir-'))
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-e2e-workdir-'))
const result = spawnSync(
'bun',
[
'molthub',
'clawdhub',
'search',
'gif',
'--limit',
@@ -105,7 +105,7 @@ describe('molthub e2e', () => {
],
{
cwd: process.cwd(),
env: { ...process.env, MOLTHUB_CONFIG_PATH: cfg.path, MOLTHUB_DISABLE_TELEMETRY: '1' },
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
encoding: 'utf8',
},
)
@@ -119,11 +119,11 @@ describe('molthub e2e', () => {
})
it('assumes a logged-in user (whoami succeeds)', async () => {
const registry = process.env.MOLTHUB_REGISTRY?.trim() || 'https://molthub.com'
const site = process.env.MOLTHUB_SITE?.trim() || 'https://molthub.com'
const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com'
const site = process.env.CLAWDHUB_SITE?.trim() || 'https://clawdhub.com'
const token = mustGetToken() ?? (await readGlobalConfig())?.token ?? null
if (!token) {
throw new Error('Missing token. Set MOLTHUB_E2E_TOKEN or run: bun molthub auth login')
throw new Error('Missing token. Set CLAWDHUB_E2E_TOKEN or run: bun clawdhub auth login')
}
const cfg = await makeTempConfig(registry, token)
@@ -142,10 +142,10 @@ describe('molthub e2e', () => {
const result = spawnSync(
'bun',
['molthub', 'whoami', '--site', site, '--registry', registry],
['clawdhub', 'whoami', '--site', site, '--registry', registry],
{
cwd: process.cwd(),
env: { ...process.env, MOLTHUB_CONFIG_PATH: cfg.path, MOLTHUB_DISABLE_TELEMETRY: '1' },
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
encoding: 'utf8',
},
)
@@ -157,15 +157,15 @@ describe('molthub e2e', () => {
})
it('sync dry-run finds skills from an explicit root', async () => {
const registry = process.env.MOLTHUB_REGISTRY?.trim() || 'https://molthub.com'
const site = process.env.MOLTHUB_SITE?.trim() || 'https://molthub.com'
const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com'
const site = process.env.CLAWDHUB_SITE?.trim() || 'https://clawdhub.com'
const token = mustGetToken() ?? (await readGlobalConfig())?.token ?? null
if (!token) {
throw new Error('Missing token. Set MOLTHUB_E2E_TOKEN or run: bun molthub auth login')
throw new Error('Missing token. Set CLAWDHUB_E2E_TOKEN or run: bun clawdhub auth login')
}
const cfg = await makeTempConfig(registry, token)
const root = await mkdtemp(join(tmpdir(), 'molthub-e2e-sync-'))
const root = await mkdtemp(join(tmpdir(), 'clawdhub-e2e-sync-'))
try {
const skillDir = join(root, 'cool-skill')
await mkdir(skillDir, { recursive: true })
@@ -174,7 +174,7 @@ describe('molthub e2e', () => {
const result = spawnSync(
'bun',
[
'molthub',
'clawdhub',
'sync',
'--dry-run',
'--all',
@@ -187,7 +187,7 @@ describe('molthub e2e', () => {
],
{
cwd: process.cwd(),
env: { ...process.env, MOLTHUB_CONFIG_PATH: cfg.path, MOLTHUB_DISABLE_TELEMETRY: '1' },
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
encoding: 'utf8',
},
)
@@ -200,19 +200,19 @@ describe('molthub e2e', () => {
}
})
it('sync dry-run finds skills from moltbot.json roots', async () => {
const registry = process.env.MOLTHUB_REGISTRY?.trim() || 'https://molthub.com'
const site = process.env.MOLTHUB_SITE?.trim() || 'https://molthub.com'
it('sync dry-run finds skills from clawdbot.json roots', async () => {
const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com'
const site = process.env.CLAWDHUB_SITE?.trim() || 'https://clawdhub.com'
const token = mustGetToken() ?? (await readGlobalConfig())?.token ?? null
if (!token) {
throw new Error('Missing token. Set MOLTHUB_E2E_TOKEN or run: bun molthub auth login')
throw new Error('Missing token. Set CLAWDHUB_E2E_TOKEN or run: bun clawdhub auth login')
}
const cfg = await makeTempConfig(registry, token)
const root = await mkdtemp(join(tmpdir(), 'molthub-e2e-moltbot-'))
const root = await mkdtemp(join(tmpdir(), 'clawdhub-e2e-clawdbot-'))
const stateDir = join(root, 'state')
const configPath = join(root, 'moltbot.json')
const workspace = join(root, 'molt-work')
const configPath = join(root, 'clawdbot.json')
const workspace = join(root, 'clawd-work')
const skillsRoot = join(workspace, 'skills')
const skillDir = join(skillsRoot, 'auto-skill')
@@ -232,15 +232,15 @@ describe('molthub e2e', () => {
const result = spawnSync(
'bun',
['molthub', 'sync', '--dry-run', '--all', '--site', site, '--registry', registry],
['clawdhub', 'sync', '--dry-run', '--all', '--site', site, '--registry', registry],
{
cwd: process.cwd(),
env: {
...process.env,
MOLTHUB_CONFIG_PATH: cfg.path,
MOLTHUB_DISABLE_TELEMETRY: '1',
MOLTBOT_CONFIG_PATH: configPath,
MOLTBOT_STATE_DIR: stateDir,
CLAWDHUB_CONFIG_PATH: cfg.path,
CLAWDHUB_DISABLE_TELEMETRY: '1',
CLAWDBOT_CONFIG_PATH: configPath,
CLAWDBOT_STATE_DIR: stateDir,
},
encoding: 'utf8',
},
@@ -256,16 +256,16 @@ describe('molthub e2e', () => {
})
it('publishes, deletes, and undeletes a skill (logged-in)', async () => {
const registry = process.env.MOLTHUB_REGISTRY?.trim() || 'https://molthub.com'
const site = process.env.MOLTHUB_SITE?.trim() || 'https://molthub.com'
const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com'
const site = process.env.CLAWDHUB_SITE?.trim() || 'https://clawdhub.com'
const token = mustGetToken() ?? (await readGlobalConfig())?.token ?? null
if (!token) {
throw new Error('Missing token. Set MOLTHUB_E2E_TOKEN or run: bun molthub auth login')
throw new Error('Missing token. Set CLAWDHUB_E2E_TOKEN or run: bun clawdhub auth login')
}
const cfg = await makeTempConfig(registry, token)
const workdir = await mkdtemp(join(tmpdir(), 'molthub-e2e-publish-'))
const installWorkdir = await mkdtemp(join(tmpdir(), 'molthub-e2e-install-'))
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-e2e-publish-'))
const installWorkdir = await mkdtemp(join(tmpdir(), 'clawdhub-e2e-install-'))
const slug = `e2e-${Date.now()}`
const skillDir = join(workdir, slug)
@@ -276,7 +276,7 @@ describe('molthub e2e', () => {
const publish1 = spawnSync(
'bun',
[
'molthub',
'clawdhub',
'publish',
skillDir,
'--slug',
@@ -296,7 +296,7 @@ describe('molthub e2e', () => {
],
{
cwd: process.cwd(),
env: { ...process.env, MOLTHUB_CONFIG_PATH: cfg.path, MOLTHUB_DISABLE_TELEMETRY: '1' },
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
encoding: 'utf8',
},
)
@@ -306,7 +306,7 @@ describe('molthub e2e', () => {
const publish2 = spawnSync(
'bun',
[
'molthub',
'clawdhub',
'publish',
skillDir,
'--slug',
@@ -326,7 +326,7 @@ describe('molthub e2e', () => {
],
{
cwd: process.cwd(),
env: { ...process.env, MOLTHUB_CONFIG_PATH: cfg.path, MOLTHUB_DISABLE_TELEMETRY: '1' },
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
encoding: 'utf8',
},
)
@@ -345,7 +345,7 @@ describe('molthub e2e', () => {
const install = spawnSync(
'bun',
[
'molthub',
'clawdhub',
'install',
slug,
'--version',
@@ -360,7 +360,7 @@ describe('molthub e2e', () => {
],
{
cwd: process.cwd(),
env: { ...process.env, MOLTHUB_CONFIG_PATH: cfg.path, MOLTHUB_DISABLE_TELEMETRY: '1' },
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
encoding: 'utf8',
},
)
@@ -368,10 +368,10 @@ describe('molthub e2e', () => {
const list = spawnSync(
'bun',
['molthub', 'list', '--site', site, '--registry', registry, '--workdir', installWorkdir],
['clawdhub', 'list', '--site', site, '--registry', registry, '--workdir', installWorkdir],
{
cwd: process.cwd(),
env: { ...process.env, MOLTHUB_CONFIG_PATH: cfg.path, MOLTHUB_DISABLE_TELEMETRY: '1' },
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
encoding: 'utf8',
},
)
@@ -381,7 +381,7 @@ describe('molthub e2e', () => {
const update = spawnSync(
'bun',
[
'molthub',
'clawdhub',
'update',
slug,
'--force',
@@ -394,7 +394,7 @@ describe('molthub e2e', () => {
],
{
cwd: process.cwd(),
env: { ...process.env, MOLTHUB_CONFIG_PATH: cfg.path, MOLTHUB_DISABLE_TELEMETRY: '1' },
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
encoding: 'utf8',
},
)
@@ -409,7 +409,7 @@ describe('molthub e2e', () => {
const del = spawnSync(
'bun',
[
'molthub',
'clawdhub',
'delete',
slug,
'--yes',
@@ -422,7 +422,7 @@ describe('molthub e2e', () => {
],
{
cwd: process.cwd(),
env: { ...process.env, MOLTHUB_CONFIG_PATH: cfg.path, MOLTHUB_DISABLE_TELEMETRY: '1' },
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
encoding: 'utf8',
},
)
@@ -439,7 +439,7 @@ describe('molthub e2e', () => {
const undelete = spawnSync(
'bun',
[
'molthub',
'clawdhub',
'undelete',
slug,
'--yes',
@@ -452,7 +452,7 @@ describe('molthub e2e', () => {
],
{
cwd: process.cwd(),
env: { ...process.env, MOLTHUB_CONFIG_PATH: cfg.path, MOLTHUB_DISABLE_TELEMETRY: '1' },
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
encoding: 'utf8',
},
)
@@ -466,7 +466,7 @@ describe('molthub e2e', () => {
const cleanup = spawnSync(
'bun',
[
'molthub',
'clawdhub',
'delete',
slug,
'--yes',
@@ -479,7 +479,7 @@ describe('molthub e2e', () => {
],
{
cwd: process.cwd(),
env: { ...process.env, MOLTHUB_CONFIG_PATH: cfg.path, MOLTHUB_DISABLE_TELEMETRY: '1' },
env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path, CLAWDHUB_DISABLE_TELEMETRY: '1' },
encoding: 'utf8',
},
)
+1 -1
View File
@@ -43,7 +43,7 @@ test('header menu routes render', async ({ page }) => {
if (label === 'Search') {
await expect(page).toHaveURL(/\/?(\?|$)/)
await expect(page.locator('h1', { hasText: 'MoltHub' })).toBeVisible()
await expect(page.locator('h1', { hasText: 'ClawdHub' })).toBeVisible()
}
}
})
+3 -3
View File
@@ -1,5 +1,5 @@
{
"name": "molthub",
"name": "clawdhub",
"private": true,
"type": "module",
"workspaces": [
@@ -21,7 +21,7 @@
"convex:deploy": "bunx convex deploy --typecheck=disable --yes",
"lint": "bun run lint:biome && bun run lint:oxlint",
"lint:biome": "biome check .",
"lint:oxlint": "oxlint --type-aware --tsconfig ./tsconfig.oxlint.json ./src ./convex ./packages/molthub/src ./packages/schema/src",
"lint:oxlint": "oxlint --type-aware --tsconfig ./tsconfig.oxlint.json ./src ./convex ./packages/clawdhub/src ./packages/schema/src",
"format": "biome format --write ."
},
"dependencies": {
@@ -41,7 +41,7 @@
"@tanstack/react-start": "^1.152.0",
"@tanstack/router-plugin": "^1.151.6",
"@vercel/analytics": "^1.6.1",
"molthub-schema": "workspace:*",
"clawdhub-schema": "workspace:*",
"clsx": "^2.1.1",
"convex": "^1.31.6",
"convex-helpers": "^0.1.111",
+57
View File
@@ -0,0 +1,57 @@
# `clawdhub`
ClawdHub CLI — install, update, search, and publish agent skills as folders.
## Install
```bash
# From this repo (shortcut script at repo root)
bun clawdhub --help
# Once published to npm
# npm i -g clawdhub
```
## Auth (publish)
```bash
clawdhub login
# or
clawdhub auth login
# Headless / token paste
# or (token paste / headless)
clawdhub login --token clh_...
```
Notes:
- Browser login opens `https://clawdhub.com/cli/auth` and completes via a loopback callback.
- Token stored in `~/Library/Application Support/clawdhub/config.json` on macOS (override via `CLAWDHUB_CONFIG_PATH`).
## Examples
```bash
clawdhub search "postgres backups"
clawdhub install my-skill-pack
clawdhub update --all
clawdhub update --all --no-input --force
clawdhub publish ./my-skill-pack --slug my-skill-pack --name "My Skill Pack" --version 1.2.0 --changelog "Fixes + docs"
```
## Sync (upload local skills)
```bash
# Start anywhere; scans workdir first, then legacy Clawdis/Clawd locations.
clawdhub sync
# Explicit roots + non-interactive dry-run
clawdhub sync --root ../clawdis/skills --all --dry-run
```
## Defaults
- Site: `https://clawdhub.com` (override via `--site` or `CLAWDHUB_SITE`)
- Registry: discovered from `/.well-known/clawdhub.json` on the site (override via `--registry` or `CLAWDHUB_REGISTRY`)
- Workdir: current directory (falls back to Clawdbot workspace if configured; override via `--workdir` or `CLAWDHUB_WORKDIR`)
- Install dir: `./skills` under workdir (override via `--dir`)
@@ -1,11 +1,11 @@
{
"name": "molthub",
"name": "clawdhub",
"version": "0.3.0",
"description": "MoltHub CLI \\u2014 install, update, search, and publish agent skills.",
"description": "ClawdHub CLI \\u2014 install, update, search, and publish agent skills.",
"license": "MIT",
"type": "module",
"bin": {
"molthub": "bin/molthub.js"
"clawdhub": "bin/clawdhub.js"
},
"files": [
"bin",
@@ -60,7 +60,7 @@ describe('browserAuth', () => {
const response = await fetch(server.redirectUri)
expect(response.status).toBe(200)
const text = await response.text()
expect(text).toContain('MoltHub CLI Login')
expect(text).toContain('ClawdHub CLI Login')
server.close()
})
@@ -127,7 +127,7 @@ const CALLBACK_HTML = `<!doctype html>
<html lang="en">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>MoltHub CLI Login</title>
<title>ClawdHub CLI Login</title>
<style>
:root { color-scheme: light dark; }
body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif; padding: 24px; }
@@ -3,6 +3,7 @@ import { stat } from 'node:fs/promises'
import { join, resolve } from 'node:path'
import { Command } from 'commander'
import { getCliBuildLabel, getCliVersion } from './cli/buildInfo.js'
import { resolveClawdbotDefaultWorkspace } from './cli/clawdbotConfig.js'
import { cmdLoginFlow, cmdLogout, cmdWhoami } from './cli/commands/auth.js'
import { cmdDeleteSkill, cmdUndeleteSkill } from './cli/commands/delete.js'
import { cmdPublish } from './cli/commands/publish.js'
@@ -11,16 +12,15 @@ import { cmdStarSkill } from './cli/commands/star.js'
import { cmdSync } from './cli/commands/sync.js'
import { cmdUnstarSkill } from './cli/commands/unstar.js'
import { configureCommanderHelp, styleEnvBlock, styleTitle } from './cli/helpStyle.js'
import { resolveMoltbotDefaultWorkspace } from './cli/moltbotConfig.js'
import { DEFAULT_REGISTRY, DEFAULT_SITE } from './cli/registry.js'
import type { GlobalOpts } from './cli/types.js'
import { fail } from './cli/ui.js'
import { readGlobalConfig } from './config.js'
const program = new Command()
.name('molthub')
.name('clawdhub')
.description(
`${styleTitle(`MoltHub CLI ${getCliBuildLabel()}`)}\n${styleEnvBlock(
`${styleTitle(`ClawdHub CLI ${getCliBuildLabel()}`)}\n${styleEnvBlock(
'install, update, search, and publish agent skills.',
)}`,
)
@@ -34,7 +34,7 @@ const program = new Command()
.showSuggestionAfterError()
.addHelpText(
'after',
styleEnvBlock('\nEnv:\n MOLTHUB_SITE\n MOLTHUB_REGISTRY\n MOLTHUB_WORKDIR\n'),
styleEnvBlock('\nEnv:\n CLAWDHUB_SITE\n CLAWDHUB_REGISTRY\n CLAWDHUB_WORKDIR\n'),
)
configureCommanderHelp(program)
@@ -43,9 +43,9 @@ async function resolveGlobalOpts(): Promise<GlobalOpts> {
const raw = program.opts<{ workdir?: string; dir?: string; site?: string; registry?: string }>()
const workdir = await resolveWorkdir(raw.workdir)
const dir = resolve(workdir, raw.dir ?? 'skills')
const site = raw.site ?? process.env.MOLTHUB_SITE ?? DEFAULT_SITE
const registrySource = raw.registry ? 'cli' : process.env.MOLTHUB_REGISTRY ? 'env' : 'default'
const registry = raw.registry ?? process.env.MOLTHUB_REGISTRY ?? DEFAULT_REGISTRY
const site = raw.site ?? process.env.CLAWDHUB_SITE ?? DEFAULT_SITE
const registrySource = raw.registry ? 'cli' : process.env.CLAWDHUB_REGISTRY ? 'env' : 'default'
const registry = raw.registry ?? process.env.CLAWDHUB_REGISTRY ?? DEFAULT_REGISTRY
return { workdir, dir, site, registry, registrySource }
}
@@ -56,21 +56,21 @@ function isInputAllowed() {
async function resolveWorkdir(explicit?: string) {
if (explicit?.trim()) return resolve(explicit.trim())
const envWorkdir = process.env.MOLTHUB_WORKDIR?.trim()
const envWorkdir = process.env.CLAWDHUB_WORKDIR?.trim()
if (envWorkdir) return resolve(envWorkdir)
const cwd = resolve(process.cwd())
const hasMarker = await hasMolthubMarker(cwd)
const hasMarker = await hasClawdhubMarker(cwd)
if (hasMarker) return cwd
const moltbotWorkspace = await resolveMoltbotDefaultWorkspace()
return moltbotWorkspace ? resolve(moltbotWorkspace) : cwd
const clawdbotWorkspace = await resolveClawdbotDefaultWorkspace()
return clawdbotWorkspace ? resolve(clawdbotWorkspace) : cwd
}
async function hasMolthubMarker(workdir: string) {
const lockfile = join(workdir, '.molthub', 'lock.json')
async function hasClawdhubMarker(workdir: string) {
const lockfile = join(workdir, '.clawdhub', 'lock.json')
if (await pathExists(lockfile)) return true
const markerDir = join(workdir, '.molthub')
const markerDir = join(workdir, '.clawdhub')
return pathExists(markerDir)
}
@@ -24,7 +24,7 @@ function shortCommit(value: string) {
export function getCliCommit() {
const candidates = [
process.env.MOLTHUB_COMMIT,
process.env.CLAWDHUB_COMMIT,
process.env.VERCEL_GIT_COMMIT_SHA,
process.env.GITHUB_SHA,
process.env.COMMIT_SHA,
@@ -3,7 +3,7 @@ import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { resolveMoltbotDefaultWorkspace, resolveMoltbotSkillRoots } from './moltbotConfig.js'
import { resolveClawdbotDefaultWorkspace, resolveClawdbotSkillRoots } from './clawdbotConfig.js'
const originalEnv = { ...process.env }
@@ -11,32 +11,32 @@ afterEach(() => {
process.env = { ...originalEnv }
})
describe('resolveMoltbotSkillRoots', () => {
describe('resolveClawdbotSkillRoots', () => {
it('reads JSON5 config and resolves per-agent + shared skill roots', async () => {
const base = await mkdtemp(join(tmpdir(), 'molthub-moltbot-'))
const base = await mkdtemp(join(tmpdir(), 'clawdhub-clawdbot-'))
const home = join(base, 'home')
const stateDir = join(base, 'state')
const configPath = join(base, 'moltbot.json')
const configPath = join(base, 'clawdbot.json')
process.env.HOME = home
process.env.MOLTBOT_STATE_DIR = stateDir
process.env.MOLTBOT_CONFIG_PATH = configPath
process.env.CLAWDBOT_STATE_DIR = stateDir
process.env.CLAWDBOT_CONFIG_PATH = configPath
const config = `{
// JSON5 comments + trailing commas supported
agents: {
defaults: { workspace: '~/molt-main', },
defaults: { workspace: '~/clawd-main', },
list: [
{ id: 'work', name: 'Work Bot', workspace: '~/molt-work', },
{ id: 'family', workspace: '~/molt-family', },
{ id: 'work', name: 'Work Bot', workspace: '~/clawd-work', },
{ id: 'family', workspace: '~/clawd-family', },
],
},
// legacy entries still supported
agent: { workspace: '~/molt-legacy', },
agent: { workspace: '~/clawd-legacy', },
routing: {
agents: {
work: { name: 'Work Bot', workspace: '~/molt-work', },
family: { workspace: '~/molt-family' },
work: { name: 'Work Bot', workspace: '~/clawd-work', },
family: { workspace: '~/clawd-family' },
},
},
skills: {
@@ -45,37 +45,37 @@ describe('resolveMoltbotSkillRoots', () => {
}`
await writeFile(configPath, config, 'utf8')
const { roots, labels } = await resolveMoltbotSkillRoots()
const { roots, labels } = await resolveClawdbotSkillRoots()
const expectedRoots = [
resolve(stateDir, 'skills'),
resolve(home, 'molt-main', 'skills'),
resolve(home, 'molt-work', 'skills'),
resolve(home, 'molt-family', 'skills'),
resolve(home, 'clawd-main', 'skills'),
resolve(home, 'clawd-work', 'skills'),
resolve(home, 'clawd-family', 'skills'),
resolve(home, 'shared', 'skills'),
resolve('/opt/skills'),
]
expect(roots).toEqual(expect.arrayContaining(expectedRoots))
expect(labels[resolve(stateDir, 'skills')]).toBe('Shared skills')
expect(labels[resolve(home, 'molt-main', 'skills')]).toBe('Agent: main')
expect(labels[resolve(home, 'molt-work', 'skills')]).toBe('Agent: Work Bot')
expect(labels[resolve(home, 'molt-family', 'skills')]).toBe('Agent: family')
expect(labels[resolve(home, 'clawd-main', 'skills')]).toBe('Agent: main')
expect(labels[resolve(home, 'clawd-work', 'skills')]).toBe('Agent: Work Bot')
expect(labels[resolve(home, 'clawd-family', 'skills')]).toBe('Agent: family')
expect(labels[resolve(home, 'shared', 'skills')]).toBe('Extra: skills')
expect(labels[resolve('/opt/skills')]).toBe('Extra: skills')
})
it('resolves default workspace from agents.defaults and agents.list', async () => {
const base = await mkdtemp(join(tmpdir(), 'molthub-moltbot-default-'))
const base = await mkdtemp(join(tmpdir(), 'clawdhub-clawdbot-default-'))
const home = join(base, 'home')
const stateDir = join(base, 'state')
const configPath = join(base, 'moltbot.json')
const configPath = join(base, 'clawdbot.json')
const workspaceMain = join(base, 'workspace-main')
const workspaceList = join(base, 'workspace-list')
process.env.HOME = home
process.env.MOLTBOT_STATE_DIR = stateDir
process.env.MOLTBOT_CONFIG_PATH = configPath
process.env.CLAWDBOT_STATE_DIR = stateDir
process.env.CLAWDBOT_CONFIG_PATH = configPath
const config = `{
agents: {
@@ -87,19 +87,19 @@ describe('resolveMoltbotSkillRoots', () => {
}`
await writeFile(configPath, config, 'utf8')
const workspace = await resolveMoltbotDefaultWorkspace()
const workspace = await resolveClawdbotDefaultWorkspace()
expect(workspace).toBe(resolve(workspaceMain))
})
it('falls back to default agent in agents.list when defaults missing', async () => {
const base = await mkdtemp(join(tmpdir(), 'molthub-moltbot-list-'))
const base = await mkdtemp(join(tmpdir(), 'clawdhub-clawdbot-list-'))
const home = join(base, 'home')
const configPath = join(base, 'moltbot.json')
const configPath = join(base, 'clawdbot.json')
const workspaceMain = join(base, 'workspace-main')
const workspaceWork = join(base, 'workspace-work')
process.env.HOME = home
process.env.MOLTBOT_CONFIG_PATH = configPath
process.env.CLAWDBOT_CONFIG_PATH = configPath
const config = `{
agents: {
@@ -111,19 +111,19 @@ describe('resolveMoltbotSkillRoots', () => {
}`
await writeFile(configPath, config, 'utf8')
const workspace = await resolveMoltbotDefaultWorkspace()
const workspace = await resolveClawdbotDefaultWorkspace()
expect(workspace).toBe(resolve(workspaceMain))
})
it('respects MOLTBOT_STATE_DIR and MOLTBOT_CONFIG_PATH overrides', async () => {
const base = await mkdtemp(join(tmpdir(), 'molthub-moltbot-override-'))
it('respects CLAWDBOT_STATE_DIR and CLAWDBOT_CONFIG_PATH overrides', async () => {
const base = await mkdtemp(join(tmpdir(), 'clawdhub-clawdbot-override-'))
const home = join(base, 'home')
const stateDir = join(base, 'custom-state')
const configPath = join(base, 'config', 'moltbot.json')
const configPath = join(base, 'config', 'clawdbot.json')
process.env.HOME = home
process.env.MOLTBOT_STATE_DIR = stateDir
process.env.MOLTBOT_CONFIG_PATH = configPath
process.env.CLAWDBOT_STATE_DIR = stateDir
process.env.CLAWDBOT_CONFIG_PATH = configPath
const config = `{
agent: { workspace: "${join(base, 'workspace-main')}" },
@@ -131,7 +131,7 @@ describe('resolveMoltbotSkillRoots', () => {
await mkdir(join(base, 'config'), { recursive: true })
await writeFile(configPath, config, 'utf8')
const { roots, labels } = await resolveMoltbotSkillRoots()
const { roots, labels } = await resolveClawdbotSkillRoots()
expect(roots).toEqual(
expect.arrayContaining([
@@ -144,14 +144,14 @@ describe('resolveMoltbotSkillRoots', () => {
})
it('returns shared skills root when config is missing', async () => {
const base = await mkdtemp(join(tmpdir(), 'molthub-moltbot-missing-'))
const base = await mkdtemp(join(tmpdir(), 'clawdhub-clawdbot-missing-'))
const stateDir = join(base, 'state')
const configPath = join(base, 'missing', 'moltbot.json')
const configPath = join(base, 'missing', 'clawdbot.json')
process.env.MOLTBOT_STATE_DIR = stateDir
process.env.MOLTBOT_CONFIG_PATH = configPath
process.env.CLAWDBOT_STATE_DIR = stateDir
process.env.CLAWDBOT_CONFIG_PATH = configPath
const { roots, labels } = await resolveMoltbotSkillRoots()
const { roots, labels } = await resolveClawdbotSkillRoots()
expect(roots).toEqual([resolve(stateDir, 'skills')])
expect(labels[resolve(stateDir, 'skills')]).toBe('Shared skills')
@@ -3,7 +3,7 @@ import { homedir } from 'node:os'
import { basename, join, resolve } from 'node:path'
import JSON5 from 'json5'
type MoltbotConfig = {
type ClawdbotConfig = {
agent?: { workspace?: string }
agents?: {
defaults?: { workspace?: string }
@@ -30,20 +30,20 @@ type MoltbotConfig = {
}
}
export type MoltbotSkillRoots = {
export type ClawdbotSkillRoots = {
roots: string[]
labels: Record<string, string>
}
export async function resolveMoltbotSkillRoots(): Promise<MoltbotSkillRoots> {
export async function resolveClawdbotSkillRoots(): Promise<ClawdbotSkillRoots> {
const roots: string[] = []
const labels: Record<string, string> = {}
const stateDir = resolveMoltbotStateDir()
const stateDir = resolveClawdbotStateDir()
const sharedSkills = resolveUserPath(join(stateDir, 'skills'))
pushRoot(roots, labels, sharedSkills, 'Shared skills')
const config = await readMoltbotConfig()
const config = await readClawdbotConfig()
if (!config) return { roots, labels }
const mainWorkspace = resolveUserPath(
@@ -80,8 +80,8 @@ export async function resolveMoltbotSkillRoots(): Promise<MoltbotSkillRoots> {
return { roots, labels }
}
export async function resolveMoltbotDefaultWorkspace(): Promise<string | null> {
const config = await readMoltbotConfig()
export async function resolveClawdbotDefaultWorkspace(): Promise<string | null> {
const config = await readClawdbotConfig()
if (!config) return null
const defaultsWorkspace = resolveUserPath(
@@ -96,16 +96,16 @@ export async function resolveMoltbotDefaultWorkspace(): Promise<string | null> {
return listWorkspace || null
}
function resolveMoltbotStateDir() {
const override = process.env.MOLTBOT_STATE_DIR?.trim()
function resolveClawdbotStateDir() {
const override = process.env.CLAWDBOT_STATE_DIR?.trim()
if (override) return resolveUserPath(override)
return join(homedir(), '.moltbot')
return join(homedir(), '.clawdbot')
}
function resolveMoltbotConfigPath() {
const override = process.env.MOLTBOT_CONFIG_PATH?.trim()
function resolveClawdbotConfigPath() {
const override = process.env.CLAWDBOT_CONFIG_PATH?.trim()
if (override) return resolveUserPath(override)
return join(resolveMoltbotStateDir(), 'moltbot.json')
return join(resolveClawdbotStateDir(), 'clawdbot.json')
}
function resolveUserPath(input: string) {
@@ -117,12 +117,12 @@ function resolveUserPath(input: string) {
return resolve(trimmed)
}
async function readMoltbotConfig(): Promise<MoltbotConfig | null> {
async function readClawdbotConfig(): Promise<ClawdbotConfig | null> {
try {
const raw = await readFile(resolveMoltbotConfigPath(), 'utf8')
const raw = await readFile(resolveClawdbotConfigPath(), 'utf8')
const parsed = JSON5.parse(raw)
if (!parsed || typeof parsed !== 'object') return null
return parsed as MoltbotConfig
return parsed as ClawdbotConfig
} catch {
return null
}
@@ -47,7 +47,7 @@ export async function cmdLogin(
) {
if (!tokenFlag && !inputAllowed) fail('Token required (use --token or remove --no-input)')
const token = tokenFlag || (await promptHidden('MoltHub token: '))
const token = tokenFlag || (await promptHidden('ClawdHub token: '))
if (!token) fail('Token required')
const registry = await getRegistry(opts, { cache: true })
@@ -79,7 +79,7 @@ export async function cmdLogout(opts: GlobalOpts) {
export async function cmdWhoami(opts: GlobalOpts) {
const cfg = await readGlobalConfig()
const token = cfg?.token
if (!token) fail('Not logged in. Run: molthub login')
if (!token) fail('Not logged in. Run: clawdhub login')
const registry = await getRegistry(opts, { cache: true })
const spinner = createSpinner('Checking token')
@@ -4,11 +4,11 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import type { GlobalOpts } from '../types'
vi.mock('../../config.js', () => ({
readGlobalConfig: vi.fn(async () => ({ registry: 'https://molthub.com', token: 'tkn' })),
readGlobalConfig: vi.fn(async () => ({ registry: 'https://clawdhub.com', token: 'tkn' })),
}))
vi.mock('../registry.js', () => ({
getRegistry: vi.fn(async () => 'https://molthub.com'),
getRegistry: vi.fn(async () => 'https://clawdhub.com'),
}))
const mockApiRequest = vi.fn()
@@ -35,8 +35,8 @@ function makeOpts(): GlobalOpts {
return {
workdir: '/work',
dir: '/work/skills',
site: 'https://molthub.com',
registry: 'https://molthub.com',
site: 'https://clawdhub.com',
registry: 'https://clawdhub.com',
registrySource: 'default',
}
}
@@ -8,7 +8,7 @@ import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '
async function requireToken() {
const cfg = await readGlobalConfig()
const token = cfg?.token
if (!token) fail('Not logged in. Run: molthub login')
if (!token) fail('Not logged in. Run: clawdhub login')
return token
}
@@ -7,10 +7,10 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import type { GlobalOpts } from '../types'
vi.mock('../../config.js', () => ({
readGlobalConfig: vi.fn(async () => ({ registry: 'https://molthub.com', token: 'tkn' })),
readGlobalConfig: vi.fn(async () => ({ registry: 'https://clawdhub.com', token: 'tkn' })),
}))
const mockGetRegistry = vi.fn(async (_opts: unknown, _params?: unknown) => 'https://molthub.com')
const mockGetRegistry = vi.fn(async (_opts: unknown, _params?: unknown) => 'https://clawdhub.com')
vi.mock('../registry.js', () => ({
getRegistry: (opts: unknown, params?: unknown) => mockGetRegistry(opts, params),
}))
@@ -34,7 +34,7 @@ vi.mock('../ui.js', () => ({
const { cmdPublish } = await import('./publish')
async function makeTmpWorkdir() {
const root = await mkdtemp(join(tmpdir(), 'molthub-publish-'))
const root = await mkdtemp(join(tmpdir(), 'clawdhub-publish-'))
return root
}
@@ -42,8 +42,8 @@ function makeOpts(workdir: string): GlobalOpts {
return {
workdir,
dir: join(workdir, 'skills'),
site: 'https://molthub.com',
registry: 'https://molthub.com',
site: 'https://clawdhub.com',
registry: 'https://clawdhub.com',
registrySource: 'default',
}
}
@@ -29,7 +29,7 @@ export async function cmdPublish(
const cfg = await readGlobalConfig()
const token = cfg?.token
if (!token) fail('Not logged in. Run: molthub login')
if (!token) fail('Not logged in. Run: clawdhub login')
const registry = await getRegistry(opts, { cache: true })
const slug = options.slug ?? sanitizeSlug(basename(folder))
@@ -11,7 +11,7 @@ vi.mock('../../http.js', () => ({
downloadZip: (...args: unknown[]) => mockDownloadZip(...args),
}))
const mockGetRegistry = vi.fn(async () => 'https://molthub.com')
const mockGetRegistry = vi.fn(async () => 'https://clawdhub.com')
vi.mock('../registry.js', () => ({
getRegistry: () => mockGetRegistry(),
}))
@@ -68,8 +68,8 @@ function makeOpts(): GlobalOpts {
return {
workdir: '/work',
dir: '/work/skills',
site: 'https://molthub.com',
registry: 'https://molthub.com',
site: 'https://clawdhub.com',
registry: 'https://clawdhub.com',
registrySource: 'default',
}
}
@@ -8,7 +8,7 @@ import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '
async function requireToken() {
const cfg = await readGlobalConfig()
const token = cfg?.token
if (!token) fail('Not logged in. Run: molthub login')
if (!token) fail('Not logged in. Run: clawdhub login')
return token
}
@@ -27,10 +27,10 @@ vi.mock('@clack/prompts', () => ({
}))
vi.mock('../../config.js', () => ({
readGlobalConfig: vi.fn(async () => ({ registry: 'https://molthub.com', token: 'tkn' })),
readGlobalConfig: vi.fn(async () => ({ registry: 'https://clawdhub.com', token: 'tkn' })),
}))
const mockGetRegistry = vi.fn(async () => 'https://molthub.com')
const mockGetRegistry = vi.fn(async () => 'https://clawdhub.com')
vi.mock('../registry.js', () => ({
getRegistry: () => mockGetRegistry(),
}))
@@ -57,15 +57,15 @@ vi.mock('../scanSkills.js', () => ({
getFallbackSkillRoots: vi.fn(() => []),
}))
const mockResolveMoltbotSkillRoots = vi.fn(
const mockResolveClawdbotSkillRoots = vi.fn(
async () =>
({
roots: [] as string[],
labels: {} as Record<string, string>,
}) as const,
)
vi.mock('../moltbotConfig.js', () => ({
resolveMoltbotSkillRoots: () => mockResolveMoltbotSkillRoots(),
vi.mock('../clawdbotConfig.js', () => ({
resolveClawdbotSkillRoots: () => mockResolveClawdbotSkillRoots(),
}))
vi.mock('../../skills.js', async () => {
@@ -89,8 +89,8 @@ function makeOpts(): GlobalOpts {
return {
workdir: '/work',
dir: '/work/skills',
site: 'https://molthub.com',
registry: 'https://molthub.com',
site: 'https://clawdhub.com',
registry: 'https://clawdhub.com',
registrySource: 'default',
}
}
@@ -230,9 +230,9 @@ describe('cmdSync', () => {
expect(output).toMatch(/dup-skill/)
})
it('prints labeled roots when moltbot roots are detected', async () => {
it('prints labeled roots when clawdbot roots are detected', async () => {
interactive = false
mockResolveMoltbotSkillRoots.mockResolvedValueOnce({
mockResolveClawdbotSkillRoots.mockResolvedValueOnce({
roots: ['/auto'],
labels: { '/auto': 'Agent: Work' },
})
@@ -290,9 +290,9 @@ describe('cmdSync', () => {
expect(update.changelog).toBe('')
})
it('skips telemetry when MOLTHUB_DISABLE_TELEMETRY is set', async () => {
it('skips telemetry when CLAWDHUB_DISABLE_TELEMETRY is set', async () => {
interactive = false
process.env.MOLTHUB_DISABLE_TELEMETRY = '1'
process.env.CLAWDHUB_DISABLE_TELEMETRY = '1'
mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => {
if (args.path === '/api/v1/whoami') return { user: { handle: 'steipete' } }
if (args.path.startsWith('/api/v1/resolve?')) {
@@ -305,6 +305,6 @@ describe('cmdSync', () => {
expect(
mockApiRequest.mock.calls.some((call) => call[1]?.path === '/api/cli/telemetry/sync'),
).toBe(false)
delete process.env.MOLTHUB_DISABLE_TELEMETRY
delete process.env.CLAWDHUB_DISABLE_TELEMETRY
})
})
@@ -1,7 +1,7 @@
import { intro, outro } from '@clack/prompts'
import { readGlobalConfig } from '../../config.js'
import { hashSkillFiles, listTextFiles, readSkillOrigin } from '../../skills.js'
import { resolveMoltbotSkillRoots } from '../moltbotConfig.js'
import { resolveClawdbotSkillRoots } from '../clawdbotConfig.js'
import { getFallbackSkillRoots } from '../scanSkills.js'
import type { GlobalOpts } from '../types.js'
import { createSpinner, fail, formatError, isInteractive } from '../ui.js'
@@ -30,22 +30,22 @@ import type { Candidate, LocalSkill, SyncOptions } from './syncTypes.js'
export async function cmdSync(opts: GlobalOpts, options: SyncOptions, inputAllowed: boolean) {
const allowPrompt = isInteractive() && inputAllowed !== false
intro('MoltHub sync')
intro('ClawdHub sync')
const cfg = await readGlobalConfig()
const token = cfg?.token
if (!token) fail('Not logged in. Run: molthub login')
if (!token) fail('Not logged in. Run: clawdhub login')
const registry = await getRegistryWithAuth(opts, token)
const selectedRoots = buildScanRoots(opts, options.root)
const moltbotRoots = await resolveMoltbotSkillRoots()
const clawdbotRoots = await resolveClawdbotSkillRoots()
const combinedRoots = Array.from(
new Set([...selectedRoots, ...moltbotRoots.roots].map((root) => root.trim()).filter(Boolean)),
new Set([...selectedRoots, ...clawdbotRoots.roots].map((root) => root.trim()).filter(Boolean)),
)
const concurrency = normalizeConcurrency(options.concurrency)
const spinner = createSpinner('Scanning for local skills')
const primaryScan = await scanRootsWithLabels(combinedRoots, moltbotRoots.labels)
const primaryScan = await scanRootsWithLabels(combinedRoots, clawdbotRoots.labels)
let scan = primaryScan
let telemetryScan = primaryScan
if (primaryScan.skills.length === 0) {
@@ -55,7 +55,7 @@ export async function cmdSync(opts: GlobalOpts, options: SyncOptions, inputAllow
telemetryScan = mergeScan(primaryScan, fallbackScan)
scan = fallbackScan
if (fallbackScan.skills.length === 0)
fail('No skills found (checked workdir and known Moltbot/Molt locations)')
fail('No skills found (checked workdir and known Clawdis/Clawd locations)')
printSection(
`No skills in workdir. Found ${fallbackScan.skills.length} in fallback locations.`,
formatList(fallbackScan.rootsWithSkills, 10),
@@ -58,7 +58,7 @@ export async function reportTelemetryIfEnabled(params: {
}
function isTelemetryDisabled() {
const raw = process.env.MOLTHUB_DISABLE_TELEMETRY
const raw = process.env.CLAWDHUB_DISABLE_TELEMETRY
if (!raw) return false
return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase())
}
@@ -8,7 +8,7 @@ import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '
async function requireToken() {
const cfg = await readGlobalConfig()
const token = cfg?.token
if (!token) fail('Not logged in. Run: molthub login')
if (!token) fail('Not logged in. Run: clawdhub login')
return token
}
@@ -22,7 +22,7 @@ function makeOpts(overrides: Partial<GlobalOpts> = {}): GlobalOpts {
return {
workdir: '/work',
dir: '/work/skills',
site: 'https://molthub.com',
site: 'https://clawdhub.com',
registry: DEFAULT_REGISTRY,
registrySource: 'default',
...overrides,
@@ -37,8 +37,8 @@ beforeEach(() => {
describe('registry resolution', () => {
it('prefers explicit registry over discovery/cache', async () => {
readGlobalConfig.mockResolvedValue({ registry: 'https://auth.molthub.com' })
discoverRegistryFromSite.mockResolvedValue({ apiBase: 'https://molthub.com' })
readGlobalConfig.mockResolvedValue({ registry: 'https://auth.clawdhub.com' })
discoverRegistryFromSite.mockResolvedValue({ apiBase: 'https://clawdhub.com' })
const registry = await resolveRegistry(
makeOpts({ registry: 'https://custom.example', registrySource: 'cli' }),
@@ -49,14 +49,14 @@ describe('registry resolution', () => {
})
it('ignores legacy registry and updates cache from discovery', async () => {
readGlobalConfig.mockResolvedValue({ registry: 'https://auth.molthub.com', token: 'tkn' })
discoverRegistryFromSite.mockResolvedValue({ apiBase: 'https://molthub.com' })
readGlobalConfig.mockResolvedValue({ registry: 'https://auth.clawdhub.com', token: 'tkn' })
discoverRegistryFromSite.mockResolvedValue({ apiBase: 'https://clawdhub.com' })
const registry = await getRegistry(makeOpts(), { cache: true })
expect(registry).toBe('https://molthub.com')
expect(registry).toBe('https://clawdhub.com')
expect(writeGlobalConfig).toHaveBeenCalledWith({
registry: 'https://molthub.com',
registry: 'https://clawdhub.com',
token: 'tkn',
})
})
@@ -2,9 +2,9 @@ import { readGlobalConfig, writeGlobalConfig } from '../config.js'
import { discoverRegistryFromSite } from '../discovery.js'
import type { GlobalOpts } from './types.js'
export const DEFAULT_SITE = 'https://molthub.com'
export const DEFAULT_REGISTRY = 'https://molthub.com'
const LEGACY_REGISTRY_HOSTS = new Set(['auth.molthub.com'])
export const DEFAULT_SITE = 'https://clawdhub.com'
export const DEFAULT_REGISTRY = 'https://clawdhub.com'
const LEGACY_REGISTRY_HOSTS = new Set(['auth.clawdhub.com'])
export async function resolveRegistry(opts: GlobalOpts) {
const explicit = opts.registrySource !== 'default' ? opts.registry.trim() : ''
@@ -7,7 +7,7 @@ import { describe, expect, it } from 'vitest'
import { findSkillFolders, getFallbackSkillRoots } from './scanSkills'
async function makeTmpDir() {
return mkdtemp(join(tmpdir(), 'molthub-scan-'))
return mkdtemp(join(tmpdir(), 'clawdhub-scan-'))
}
describe('scanSkills', () => {
@@ -57,8 +57,8 @@ describe('scanSkills', () => {
it('includes known legacy roots', () => {
const roots = getFallbackSkillRoots('/tmp/anywhere')
expect(roots.some((p) => p.endsWith('/moltbot/skills'))).toBe(true)
expect(roots.some((p) => p.endsWith('/molt/skills'))).toBe(true)
expect(roots.some((p) => p.endsWith('/moltbot/skills'))).toBe(true)
expect(roots.some((p) => p.endsWith('/clawdis/skills'))).toBe(true)
expect(roots.some((p) => p.endsWith('/clawd/skills'))).toBe(true)
expect(roots.some((p) => p.endsWith('/clawdbot/skills'))).toBe(true)
})
})
@@ -33,32 +33,32 @@ export function getFallbackSkillRoots(workdir: string) {
const home = homedir()
const roots = [
// adjacent repo installs
resolve(workdir, '..', 'moltbot', 'skills'),
resolve(workdir, '..', 'moltbot', 'Skills'),
resolve(workdir, '..', 'moltbot', 'skills'),
resolve(workdir, '..', 'moltbot', 'Skills'),
resolve(workdir, '..', 'clawdis', 'skills'),
resolve(workdir, '..', 'clawdis', 'Skills'),
resolve(workdir, '..', 'clawdbot', 'skills'),
resolve(workdir, '..', 'clawdbot', 'Skills'),
// legacy locations
resolve(home, 'molt', 'skills'),
resolve(home, 'molt', 'Skills'),
resolve(home, '.molt', 'skills'),
resolve(home, '.molt', 'Skills'),
resolve(home, 'clawd', 'skills'),
resolve(home, 'clawd', 'Skills'),
resolve(home, '.clawd', 'skills'),
resolve(home, '.clawd', 'Skills'),
resolve(home, 'moltbot', 'skills'),
resolve(home, 'moltbot', 'Skills'),
resolve(home, '.moltbot', 'skills'),
resolve(home, '.moltbot', 'Skills'),
resolve(home, 'clawdbot', 'skills'),
resolve(home, 'clawdbot', 'Skills'),
resolve(home, '.clawdbot', 'skills'),
resolve(home, '.clawdbot', 'Skills'),
resolve(home, 'moltbot', 'skills'),
resolve(home, 'moltbot', 'Skills'),
resolve(home, '.moltbot', 'skills'),
resolve(home, '.moltbot', 'Skills'),
resolve(home, 'clawdis', 'skills'),
resolve(home, 'clawdis', 'Skills'),
resolve(home, '.clawdis', 'skills'),
resolve(home, '.clawdis', 'Skills'),
// macOS App Support legacy
resolve(home, 'Library', 'Application Support', 'moltbot', 'skills'),
resolve(home, 'Library', 'Application Support', 'moltbot', 'Skills'),
resolve(home, 'Library', 'Application Support', 'moltbot', 'skills'),
resolve(home, 'Library', 'Application Support', 'moltbot', 'Skills'),
resolve(home, 'Library', 'Application Support', 'clawdbot', 'skills'),
resolve(home, 'Library', 'Application Support', 'clawdbot', 'Skills'),
resolve(home, 'Library', 'Application Support', 'clawdis', 'skills'),
resolve(home, 'Library', 'Application Support', 'clawdis', 'Skills'),
]
return Array.from(new Set(roots))
}
@@ -4,19 +4,19 @@ import { dirname, join, resolve } from 'node:path'
import { type GlobalConfig, GlobalConfigSchema, parseArk } from './schema/index.js'
export function getGlobalConfigPath() {
const override = process.env.MOLTHUB_CONFIG_PATH?.trim()
const override = process.env.CLAWDHUB_CONFIG_PATH?.trim()
if (override) return resolve(override)
const home = homedir()
if (process.platform === 'darwin') {
return join(home, 'Library', 'Application Support', 'molthub', 'config.json')
return join(home, 'Library', 'Application Support', 'clawdhub', 'config.json')
}
const xdg = process.env.XDG_CONFIG_HOME
if (xdg) return join(xdg, 'molthub', 'config.json')
if (xdg) return join(xdg, 'clawdhub', 'config.json')
if (process.platform === 'win32') {
const appData = process.env.APPDATA
if (appData) return join(appData, 'molthub', 'config.json')
if (appData) return join(appData, 'clawdhub', 'config.json')
}
return join(home, '.config', 'molthub', 'config.json')
return join(home, '.config', 'clawdhub', 'config.json')
}
export async function readGlobalConfig(): Promise<GlobalConfig | null> {
@@ -1,7 +1,7 @@
import { parseArk, WellKnownConfigSchema } from './schema/index.js'
export async function discoverRegistryFromSite(siteUrl: string) {
const url = new URL('/.well-known/molthub.json', siteUrl)
const url = new URL('/.well-known/clawdhub.json', siteUrl)
const response = await fetch(url.toString(), {
method: 'GET',
headers: { Accept: 'application/json' },
@@ -202,7 +202,7 @@ async function fetchJsonFormViaCurl(url: string, args: FormRequestArgs) {
headers.push('-H', `Authorization: Bearer ${args.token}`)
}
const tempDir = await mkdtemp(join(tmpdir(), 'molthub-upload-'))
const tempDir = await mkdtemp(join(tmpdir(), 'clawdhub-upload-'))
try {
const formArgs: string[] = []
for (const [key, value] of args.form.entries()) {
@@ -255,7 +255,7 @@ async function fetchJsonFormViaCurl(url: string, args: FormRequestArgs) {
}
async function fetchBinaryViaCurl(url: string) {
const tempDir = await mkdtemp(join(tmpdir(), 'molthub-download-'))
const tempDir = await mkdtemp(join(tmpdir(), 'clawdhub-download-'))
const filePath = join(tempDir, 'payload.bin')
try {
const curlArgs = [
@@ -239,22 +239,22 @@ export const SkillInstallSpecSchema = type({
})
export type SkillInstallSpec = (typeof SkillInstallSpecSchema)[inferred]
export const MoltbotRequiresSchema = type({
export const ClawdisRequiresSchema = type({
bins: 'string[]?',
anyBins: 'string[]?',
env: 'string[]?',
config: 'string[]?',
})
export type MoltbotRequires = (typeof MoltbotRequiresSchema)[inferred]
export type ClawdisRequires = (typeof ClawdisRequiresSchema)[inferred]
export const MoltbotSkillMetadataSchema = type({
export const ClawdisSkillMetadataSchema = type({
always: 'boolean?',
skillKey: 'string?',
primaryEnv: 'string?',
emoji: 'string?',
homepage: 'string?',
os: 'string[]?',
requires: MoltbotRequiresSchema.optional(),
requires: ClawdisRequiresSchema.optional(),
install: SkillInstallSpecSchema.array().optional(),
})
export type MoltbotSkillMetadata = (typeof MoltbotSkillMetadataSchema)[inferred]
export type ClawdisSkillMetadata = (typeof ClawdisSkillMetadataSchema)[inferred]
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'
import * as schema from '.'
import { isTextContentType, TEXT_FILE_EXTENSION_SET } from './textFiles'
describe('packages/molthub schema textFiles', () => {
describe('packages/clawdhub schema textFiles', () => {
it('exports text-file extension set', () => {
expect(TEXT_FILE_EXTENSION_SET.has('md')).toBe(true)
expect(TEXT_FILE_EXTENSION_SET.has('exe')).toBe(false)
@@ -20,7 +20,7 @@ import {
describe('skills', () => {
it('extracts zip into directory and skips traversal', async () => {
const dir = await mkdtemp(join(tmpdir(), 'molthub-'))
const dir = await mkdtemp(join(tmpdir(), 'clawdhub-'))
const zip = zipSync({
'SKILL.md': strToU8('hello'),
'../evil.txt': strToU8('nope'),
@@ -32,7 +32,7 @@ describe('skills', () => {
})
it('writes and reads lockfile', async () => {
const workdir = await mkdtemp(join(tmpdir(), 'molthub-work-'))
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-work-'))
await writeLockfile(workdir, {
version: 1,
skills: { demo: { version: '1.0.0', installedAt: 1 } },
@@ -42,18 +42,18 @@ describe('skills', () => {
})
it('returns empty lockfile on invalid json', async () => {
const workdir = await mkdtemp(join(tmpdir(), 'molthub-work-bad-'))
await mkdir(join(workdir, '.molthub'), { recursive: true })
await writeFile(join(workdir, '.molthub', 'lock.json'), '{', 'utf8')
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-work-bad-'))
await mkdir(join(workdir, '.clawdhub'), { recursive: true })
await writeFile(join(workdir, '.clawdhub', 'lock.json'), '{', 'utf8')
const read = await readLockfile(workdir)
expect(read).toEqual({ version: 1, skills: {} })
})
it('returns empty lockfile on schema mismatch', async () => {
const workdir = await mkdtemp(join(tmpdir(), 'molthub-work-schema-'))
await mkdir(join(workdir, '.molthub'), { recursive: true })
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-work-schema-'))
await mkdir(join(workdir, '.clawdhub'), { recursive: true })
await writeFile(
join(workdir, '.molthub', 'lock.json'),
join(workdir, '.clawdhub', 'lock.json'),
JSON.stringify({ version: 1, skills: 'nope' }),
'utf8',
)
@@ -62,7 +62,7 @@ describe('skills', () => {
})
it('skips dotfiles and node_modules when listing text files', async () => {
const workdir = await mkdtemp(join(tmpdir(), 'molthub-files-'))
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-files-'))
await writeFile(join(workdir, 'SKILL.md'), 'hi', 'utf8')
await writeFile(join(workdir, '.secret.txt'), 'no', 'utf8')
await mkdir(join(workdir, 'node_modules'), { recursive: true })
@@ -71,10 +71,10 @@ describe('skills', () => {
expect(files.map((file) => file.relPath)).toEqual(['SKILL.md'])
})
it('respects .gitignore and .molthubignore', async () => {
const workdir = await mkdtemp(join(tmpdir(), 'molthub-ignore-'))
it('respects .gitignore and .clawdhubignore', async () => {
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-ignore-'))
await writeFile(join(workdir, '.gitignore'), 'ignored.md\n', 'utf8')
await writeFile(join(workdir, '.molthubignore'), 'private.md\n', 'utf8')
await writeFile(join(workdir, '.clawdhubignore'), 'private.md\n', 'utf8')
await writeFile(join(workdir, 'SKILL.md'), 'hi', 'utf8')
await writeFile(join(workdir, 'ignored.md'), 'no', 'utf8')
await writeFile(join(workdir, 'private.md'), 'no', 'utf8')
@@ -90,7 +90,7 @@ describe('skills', () => {
})
it('falls back to text/plain for unknown text extensions', async () => {
const workdir = await mkdtemp(join(tmpdir(), 'molthub-env-'))
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-env-'))
await writeFile(join(workdir, 'SKILL.md'), 'hi', 'utf8')
await writeFile(join(workdir, 'config.env'), 'TOKEN=demo', 'utf8')
const files = await listTextFiles(workdir)
@@ -147,26 +147,26 @@ describe('skills', () => {
})
it('returns null for invalid skill origin metadata', async () => {
const workdir = await mkdtemp(join(tmpdir(), 'molthub-origin-'))
const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-origin-'))
expect(await readSkillOrigin(workdir)).toBeNull()
await mkdir(join(workdir, '.molthub'), { recursive: true })
await mkdir(join(workdir, '.clawdhub'), { recursive: true })
await writeFile(
join(workdir, '.molthub', 'origin.json'),
join(workdir, '.clawdhub', 'origin.json'),
JSON.stringify({ version: 2 }),
'utf8',
)
expect(await readSkillOrigin(workdir)).toBeNull()
await writeFile(
join(workdir, '.molthub', 'origin.json'),
join(workdir, '.clawdhub', 'origin.json'),
JSON.stringify({ version: 1, registry: 'demo', slug: 'x', installedAt: 1 }),
'utf8',
)
expect(await readSkillOrigin(workdir)).toBeNull()
await writeFile(
join(workdir, '.molthub', 'origin.json'),
join(workdir, '.clawdhub', 'origin.json'),
JSON.stringify({
version: 1,
registry: 'demo',
@@ -30,9 +30,9 @@ export async function listTextFiles(root: string) {
const files: Array<{ relPath: string; bytes: Uint8Array; contentType?: string }> = []
const absRoot = resolve(root)
const ig = ignore()
ig.add(['.git/', 'node_modules/', '.molthub/'])
ig.add(['.git/', 'node_modules/', '.clawdhub/'])
await addIgnoreFile(ig, join(absRoot, '.gitignore'))
await addIgnoreFile(ig, join(absRoot, '.molthubignore'))
await addIgnoreFile(ig, join(absRoot, '.clawdhubignore'))
await walk(absRoot, async (absPath) => {
const relPath = normalizePath(relative(absRoot, absPath))
@@ -87,7 +87,7 @@ export function hashSkillZip(zipBytes: Uint8Array) {
}
export async function readLockfile(workdir: string): Promise<Lockfile> {
const path = join(workdir, '.molthub', 'lock.json')
const path = join(workdir, '.clawdhub', 'lock.json')
try {
const raw = await readFile(path, 'utf8')
const parsed = JSON.parse(raw) as unknown
@@ -98,13 +98,13 @@ export async function readLockfile(workdir: string): Promise<Lockfile> {
}
export async function writeLockfile(workdir: string, lock: Lockfile) {
const path = join(workdir, '.molthub', 'lock.json')
const path = join(workdir, '.clawdhub', 'lock.json')
await mkdir(dirname(path), { recursive: true })
await writeFile(path, `${JSON.stringify(lock, null, 2)}\n`, 'utf8')
}
export async function readSkillOrigin(skillFolder: string): Promise<SkillOrigin | null> {
const path = join(skillFolder, '.molthub', 'origin.json')
const path = join(skillFolder, '.clawdhub', 'origin.json')
try {
const raw = await readFile(path, 'utf8')
const parsed = JSON.parse(raw) as Partial<SkillOrigin>
@@ -124,7 +124,7 @@ export async function readSkillOrigin(skillFolder: string): Promise<SkillOrigin
}
export async function writeSkillOrigin(skillFolder: string, origin: SkillOrigin) {
const path = join(skillFolder, '.molthub', 'origin.json')
const path = join(skillFolder, '.clawdhub', 'origin.json')
await mkdir(dirname(path), { recursive: true })
await writeFile(path, `${JSON.stringify(origin, null, 2)}\n`, 'utf8')
}
-57
View File
@@ -1,57 +0,0 @@
# `molthub`
MoltHub CLI — install, update, search, and publish agent skills as folders.
## Install
```bash
# From this repo (shortcut script at repo root)
bun molthub --help
# Once published to npm
# npm i -g molthub
```
## Auth (publish)
```bash
molthub login
# or
molthub auth login
# Headless / token paste
# or (token paste / headless)
molthub login --token clh_...
```
Notes:
- Browser login opens `https://molthub.com/cli/auth` and completes via a loopback callback.
- Token stored in `~/Library/Application Support/molthub/config.json` on macOS (override via `MOLTHUB_CONFIG_PATH`).
## Examples
```bash
molthub search "postgres backups"
molthub install my-skill-pack
molthub update --all
molthub update --all --no-input --force
molthub publish ./my-skill-pack --slug my-skill-pack --name "My Skill Pack" --version 1.2.0 --changelog "Fixes + docs"
```
## Sync (upload local skills)
```bash
# Start anywhere; scans workdir first, then legacy Moltbot/Molt locations.
molthub sync
# Explicit roots + non-interactive dry-run
molthub sync --root ../moltbot/skills --all --dry-run
```
## Defaults
- Site: `https://molthub.com` (override via `--site` or `MOLTHUB_SITE`)
- Registry: discovered from `/.well-known/molthub.json` on the site (override via `--registry` or `MOLTHUB_REGISTRY`)
- Workdir: current directory (falls back to Moltbot workspace if configured; override via `--workdir` or `MOLTHUB_WORKDIR`)
- Install dir: `./skills` under workdir (override via `--dir`)
+2 -2
View File
@@ -1,3 +1,3 @@
# molthub-schema
# clawdhub-schema
Shared runtime schemas (ArkType) for MoltHub.
Shared runtime schemas (ArkType) for ClawdHub.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "molthub-schema",
"name": "clawdhub-schema",
"version": "0.0.2",
"private": true,
"type": "module",
+8 -8
View File
@@ -10,7 +10,7 @@ import {
WellKnownConfigSchema,
} from './schemas'
describe('molthub-schema', () => {
describe('clawdhub-schema', () => {
it('parses lockfile records', () => {
const lock = parseArk(
LockfileSchema,
@@ -68,10 +68,10 @@ describe('molthub-schema', () => {
expect(
parseArk(
WellKnownConfigSchema,
{ registry: 'https://example.convex.site', authBase: 'https://molthub.com' },
{ registry: 'https://example.convex.site', authBase: 'https://clawdhub.com' },
'WellKnown',
),
).toEqual({ registry: 'https://example.convex.site', authBase: 'https://molthub.com' })
).toEqual({ registry: 'https://example.convex.site', authBase: 'https://clawdhub.com' })
expect(
parseArk(
@@ -84,14 +84,14 @@ describe('molthub-schema', () => {
const combined = parseArk(
WellKnownConfigSchema,
{
apiBase: 'https://molthub.com',
registry: 'https://molthub.com',
authBase: 'https://molthub.com',
apiBase: 'https://clawdhub.com',
registry: 'https://clawdhub.com',
authBase: 'https://clawdhub.com',
},
'WellKnown',
) as unknown as Record<string, unknown>
expect(combined.apiBase).toBe('https://molthub.com')
expect(combined.registry).toBe('https://molthub.com')
expect(combined.apiBase).toBe('https://clawdhub.com')
expect(combined.registry).toBe('https://clawdhub.com')
})
it('throws labeled errors', () => {
+8 -8
View File
@@ -256,22 +256,22 @@ export const NixPluginSpecSchema = type({
})
export type NixPluginSpec = (typeof NixPluginSpecSchema)[inferred]
export const MoltbotConfigSpecSchema = type({
export const ClawdbotConfigSpecSchema = type({
requiredEnv: 'string[]?',
stateDirs: 'string[]?',
example: 'string?',
})
export type MoltbotConfigSpec = (typeof MoltbotConfigSpecSchema)[inferred]
export type ClawdbotConfigSpec = (typeof ClawdbotConfigSpecSchema)[inferred]
export const MoltbotRequiresSchema = type({
export const ClawdisRequiresSchema = type({
bins: 'string[]?',
anyBins: 'string[]?',
env: 'string[]?',
config: 'string[]?',
})
export type MoltbotRequires = (typeof MoltbotRequiresSchema)[inferred]
export type ClawdisRequires = (typeof ClawdisRequiresSchema)[inferred]
export const MoltbotSkillMetadataSchema = type({
export const ClawdisSkillMetadataSchema = type({
always: 'boolean?',
skillKey: 'string?',
primaryEnv: 'string?',
@@ -279,9 +279,9 @@ export const MoltbotSkillMetadataSchema = type({
homepage: 'string?',
os: 'string[]?',
cliHelp: 'string?',
requires: MoltbotRequiresSchema.optional(),
requires: ClawdisRequiresSchema.optional(),
install: SkillInstallSpecSchema.array().optional(),
nix: NixPluginSpecSchema.optional(),
config: MoltbotConfigSpecSchema.optional(),
config: ClawdbotConfigSpecSchema.optional(),
})
export type MoltbotSkillMetadata = (typeof MoltbotSkillMetadataSchema)[inferred]
export type ClawdisSkillMetadata = (typeof ClawdisSkillMetadataSchema)[inferred]
+1 -1
View File
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'
import * as schema from '.'
import { isTextContentType, TEXT_FILE_EXTENSION_SET } from './textFiles'
describe('molthub-schema textFiles', () => {
describe('clawdhub-schema textFiles', () => {
it('exports text-file extension set', () => {
expect(TEXT_FILE_EXTENSION_SET.has('md')).toBe(true)
expect(TEXT_FILE_EXTENSION_SET.has('exe')).toBe(false)
+6
View File
@@ -0,0 +1,6 @@
{
"apiBase": "https://clawdhub.com",
"authBase": "https://clawdhub.com",
"minCliVersion": "0.1.0",
"registry": "https://clawdhub.com"
}
-6
View File
@@ -1,6 +0,0 @@
{
"apiBase": "https://molthub.com",
"authBase": "https://molthub.com",
"minCliVersion": "0.1.0",
"registry": "https://molthub.com"
}
+2 -2
View File
@@ -1,11 +1,11 @@
{
"openapi": "3.1.0",
"info": {
"title": "MoltHub API",
"title": "ClawdHub API",
"version": "1.0.0",
"description": "Public REST API for skills. Rate limits: read 120/min per IP + 600/min per key; write 30/min per IP + 120/min per key."
},
"servers": [{ "url": "https://molthub.com" }],
"servers": [{ "url": "https://clawdhub.com" }],
"components": {
"securitySchemes": {
"bearerAuth": {

Some files were not shown because too many files have changed in this diff Show More