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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ WebView (Svelte) ──invoke('send_op', …)──▶ src-tauri (Rust) ─
- **Setup wizard** — first-run environment check (git + engine), guided/auto install,
JuCode OAuth login or API-key path; **logout** per provider in settings.
- **Branch tree** (`/tree`), **resume** (`/resume`), **model picker** (`/model`),
context/cost ring, skills marketplace.
context/cost ring, combined [JuCode + Anthropic skills marketplace](docs/skills.md).
- **Theming** — system / light / dark; image paste & drag-drop; desktop notifications.

Keyboard: `⌘K` palette · `⌘F` find · `⌘N` new session · `⌘B` toggle panel · `⌘,` settings.
Expand Down
48 changes: 48 additions & 0 deletions docs/skills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Skills marketplace

JuCode Desktop combines two upstream sources in one marketplace:

- the official JuCode marketplace at `/v1/skills/marketplace`;
- [`anthropics/skills`](https://github.com/anthropics/skills), fetched directly from GitHub.

Desktop does not mirror or self-host either source. The Anthropic listing uses the small public
metadata snapshot in `src-tauri/resources/anthropic-skills.json` so GitHub API availability and
anonymous rate limits cannot make the source disappear. Installing one of those skills fetches its
current files directly from `raw.githubusercontent.com` after reading the repository tree from the
GitHub API.

Use `node scripts/refresh-anthropic-skills.mjs` to refresh the snapshot. The script discovers public
skill directories through the GitHub Contents API, reads each `SKILL.md` frontmatter, checks its
`LICENSE.txt`, and preserves curated tags already in the snapshot. Review the diff, especially
license classification, before committing an update.

## Install location and safety

The active session backend selects the personal install directory:

- JuCode and Codex sessions: `~/.jucode/skills/<id>`;
- Claude Code sessions: `~/.claude/skills/<id>`.

Skill IDs and every downloaded relative path are validated before joining them to that directory.
GitHub tree links, submodules, absolute paths, and parent traversal are rejected. Downloads are
limited to 20 MiB per file, 100 MiB total, and 4,096 files. JuCode zip and tar.gz packages retain
their existing 20 MiB compressed limit, 100 MiB extracted limit, and the same traversal/link/file
count checks. Installation stages files next to the destination and atomically replaces the old
version only after a valid `SKILL.md` exists.

Skills are executable instructions and may include scripts. Treat installation like installing
software and review upstream content before using it with sensitive projects.

## Anthropic document-skill licensing

Most examples in `anthropics/skills` are Apache-2.0. The `docx`, `pdf`, `pptx`, and `xlsx` folders
are different: Anthropic describes them as source-available rather than open source, and their
terms prohibit redistribution. JuCode does not bundle or redistribute those files; an explicit
install downloads the selected folder from Anthropic's GitHub repository to the user's machine.
Users remain responsible for the upstream terms.

Anthropic's preset document skills for Word, PDF, PowerPoint, and Excel are available in supported
Claude API/hosted surfaces, but they are not preset skills in Claude Code. Claude Code supports
filesystem-based custom skills instead. A repository copy installed into `~/.claude/skills` is a
custom skill and does not become, or carry the runtime guarantees of, Anthropic's preset hosted
skill.
76 changes: 76 additions & 0 deletions scripts/refresh-anthropic-skills.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { readFile, writeFile } from 'node:fs/promises';

const output = new URL('../src-tauri/resources/anthropic-skills.json', import.meta.url);
const repository = 'https://github.com/anthropics/skills';
const api = 'https://api.github.com/repos/anthropics/skills/contents/skills?ref=main';
const headers = {
Accept: 'application/vnd.github+json',
'User-Agent': 'JuCode-Desktop-index-refresh'
};

async function get(url, optional = false) {
const response = await fetch(url, { headers });
if (optional && response.status === 404) return '';
if (!response.ok) throw new Error(`${response.status} ${response.statusText}: ${url}`);
return response.text();
}

function scalar(value) {
const trimmed = value.trim();
if (trimmed.startsWith('"')) {
try {
return JSON.parse(trimmed);
} catch {
// Keep the source text when an upstream scalar is not JSON-compatible.
}
}
if (trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1);
return trimmed;
}

function frontmatter(markdown, key) {
const block = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? '';
const value = block.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'))?.[1] ?? '';
return scalar(value);
}

const previous = JSON.parse(await readFile(output, 'utf8'));
const known = new Map(previous.skills.map((skill) => [skill.id, skill]));
const directories = JSON.parse(await get(api))
.filter((entry) => entry.type === 'dir')
.sort((left, right) => left.name.localeCompare(right.name));

const skills = [];
for (const directory of directories) {
const id = directory.name;
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id)) {
throw new Error(`Refusing unsafe skill id: ${id}`);
}
const rawBase = `https://raw.githubusercontent.com/anthropics/skills/main/skills/${id}`;
const markdown = await get(`${rawBase}/SKILL.md`);
const licenseText = await get(`${rawBase}/LICENSE.txt`, true);
const redistributable =
!licenseText.includes('All rights reserved') &&
!licenseText.includes('ADDITIONAL RESTRICTIONS');
const old = known.get(id);
skills.push({
id,
name: old?.name ?? frontmatter(markdown, 'name') ?? id,
description: frontmatter(markdown, 'description') || old?.description || `Anthropic ${id} skill`,
tags: old?.tags ?? [],
skill_url: `${rawBase}/SKILL.md`,
homepage: `${repository}/tree/main/skills/${id}`,
license: redistributable ? 'Apache-2.0' : 'Anthropic source-available',
redistributable
});
}

const index = {
schema_version: 1,
repository,
ref: 'main',
tree_url: 'https://api.github.com/repos/anthropics/skills/git/trees/main?recursive=1',
skills
};
await writeFile(output, `${JSON.stringify(index, null, 2)}\n`);
console.log(`Updated ${skills.length} Anthropic skills in ${output.pathname}`);
Loading
Loading