mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 17:02:11 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2434dcb95 |
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
It’s 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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('maintenance backfill', () => {
|
||||
parsed: {
|
||||
frontmatter: { description: 'Hello world.' },
|
||||
metadata: undefined,
|
||||
moltbot: undefined,
|
||||
clawdis: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()),
|
||||
}),
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
+1
-1
@@ -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
@@ -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
@@ -7,7 +7,7 @@ read_when:
|
||||
|
||||
# API v1
|
||||
|
||||
Base: `https://molthub.com`
|
||||
Base: `https://clawdhub.com`
|
||||
|
||||
OpenAPI: `/api/v1/openapi.json`
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 don’t 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 aren’t “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
|
||||
```
|
||||
|
||||
@@ -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
@@ -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
@@ -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** (what’s 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** (what’s 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`.
|
||||
|
||||
@@ -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 don’t 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
@@ -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',
|
||||
},
|
||||
)
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
+1
-1
@@ -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,
|
||||
+39
-39
@@ -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')
|
||||
+16
-16
@@ -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
|
||||
}
|
||||
+2
-2
@@ -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
-4
@@ -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',
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
|
||||
+5
-5
@@ -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',
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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))
|
||||
+3
-3
@@ -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',
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
|
||||
+12
-12
@@ -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
|
||||
})
|
||||
})
|
||||
+7
-7
@@ -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),
|
||||
+1
-1
@@ -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())
|
||||
}
|
||||
+1
-1
@@ -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
-7
@@ -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() : ''
|
||||
+4
-4
@@ -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]
|
||||
+1
-1
@@ -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')
|
||||
}
|
||||
@@ -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`)
|
||||
@@ -1,3 +1,3 @@
|
||||
# molthub-schema
|
||||
# clawdhub-schema
|
||||
|
||||
Shared runtime schemas (ArkType) for MoltHub.
|
||||
Shared runtime schemas (ArkType) for ClawdHub.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "molthub-schema",
|
||||
"name": "clawdhub-schema",
|
||||
"version": "0.0.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"apiBase": "https://clawdhub.com",
|
||||
"authBase": "https://clawdhub.com",
|
||||
"minCliVersion": "0.1.0",
|
||||
"registry": "https://clawdhub.com"
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"apiBase": "https://molthub.com",
|
||||
"authBase": "https://molthub.com",
|
||||
"minCliVersion": "0.1.0",
|
||||
"registry": "https://molthub.com"
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user