withastro/rosie — docs/js-api
$ ls docs/
── js api

rosie is also a typed JavaScript library. install rosie-skills from npm and call it from node directly — no spawning, no parsing stdout. backed by the same C internals as the CLI, compiled to wasm and inlined in the package.

$ npm install rosie-skills

the package is esm-only. use a namespace import:

// every function returns a Promise. failures throw Error with a descriptive message.
import * as rosie from 'rosie-skills';

await rosie.install('anthropics/skills');
const skills = await rosie.list();
const agents = await rosie.agents();

install

every cli flag has a corresponding option. pass nothing extra and rosie does the right thing: install the whole repo, auto-detect agents, write to .agents/skills/.

// one skill, specific agents
await rosie.install('anthropics/skills', {
  skill: 'pdf',
  agent: ['claude', 'cursor'],
});

// install as a reference (.md doc indexed in AGENTS.md / CLAUDE.md / …)
await rosie.install('vercel/next.js', { ref: true });

// reference with a custom name
await rosie.install('anthropics/skills', {
  ref: true,
  skill: 'pdf',
  name: 'pdf-handling',
});

// from an npm package — symlinks .md files out of node_modules/
await rosie.install('react', {
  ref: true,
  npm: true,
  include: ['README.md'],
});

// global install (~/.agent/skills/ instead of ./.agents/skills/)
await rosie.install('anthropics/skills', { global: true });

// ad-hoc install, don't record in rosie.lock
await rosie.install('anthropics/skills', { lockfile: false });

// install into a different project directory
await rosie.install('anthropics/skills', { cwd: '/path/to/project' });

// reinstall everything in .agents/rosie.lock — no args needed
await rosie.installFromLockfile();

install result

install, installFromLockfile, and update return an InstallResult. per-skill detail in skills, deduped unions in installedAgents / failedAgents, and installedInstruction names the agent-instructions file that was written (or null when none was touched).

const result = await rosie.install('anthropics/skills');
// {
//   skills: [
//     { name: 'pdf', kind: 'skill',
//       installedAgents: ['claude', 'cursor'],
//       failedAgents: [] },
//     ...
//   ],
//   installedAgents: ['claude', 'cursor'],
//   failedAgents: [],
//   installedInstruction: null,
// }

failedAgents is non-fatal — rosie tries every agent and reports the misses. Common cause: an existing non-symlink file at ~/.<agent>/skills/<name> blocking the create, or restrictive permissions on the agent's skills/ dir. The canonical copy under .agents/skills/ still lands and the lockfile still records the entry; rerunning rosie.install after fixing permissions re-attempts the failed agents.

const result = await rosie.install('anthropics/skills');
if (result.failedAgents.length > 0) {
  console.warn(`couldn't symlink into: ${result.failedAgents.join(', ')}`);
}

reference installs land under .agents/references/ instead of agent dirs, so kind === 'reference' and the agent arrays are empty. installedInstruction is the path of the markdown file (AGENTS.md / CLAUDE.md / GEMINI.md / .github/copilot-instructions.md) whose references block was rewritten.

const result = await rosie.install('vercel/next.js', { ref: true });
// {
//   skills: [{ name: 'vercel-next.js', kind: 'reference',
//              installedAgents: [], failedAgents: [] }],
//   installedAgents: [],
//   failedAgents: [],
//   installedInstruction: 'AGENTS.md',
// }

references

pass ref: true to install a markdown doc into .agents/references/<name>/REFERENCE.md and append it to the project's agent-instructions file (AGENTS.md · CLAUDE.md · GEMINI.md · .github/copilot-instructions.md, first one found — else AGENTS.md is created).

// the repo's README.md becomes the reference
await rosie.install('vercel/next.js', { ref: true });

// pick a specific SKILL.md (frontmatter stripped) — source is recorded
// as owner/repo#skill so rosie.update() round-trips correctly
await rosie.install('anthropics/skills', {
  ref: true,
  skill: 'pdf',
});

// override the default install name (default: owner-repo[-skill])
await rosie.install('anthropics/skills', {
  ref: true,
  skill: 'docx',
  name: 'word-docs',
});

// from an npm package — symlinks .md files out of node_modules/<pkg>/
// (implies ref: true; tracks the installed version)
await rosie.install('react', { ref: true, npm: true });

// scope the npm walk — replaces the default README+docs/**.md set
await rosie.install('zod', {
  ref: true,
  npm: true,
  include: ['README.md'],
});

title in the index is re-extracted from each file's first H1 on every rebuild; falls back to the install name. references show up in rosie.list() with isReference: true:

const all = await rosie.list();
const refs = all.filter(s => s.isReference);
// [
//   { name: 'vercel-next.js', source: 'vercel/next.js',
//     ref: 'main', sha: '...', isReference: true },
//   ...
// ]

list, agents

read-only commands; structured results, no parsing.

const skills = await rosie.list();
// [
//   { name: 'pdf', source: 'anthropics/skills', ref: 'main',
//     sha: 'f458cee31...', isReference: false },
//   ...
// ]

const agents = await rosie.agents();
// [
//   { name: 'claude', display: 'Claude Code',
//     detected: true, installPath: '/home/me/.claude/skills' },
//   { name: 'cursor', display: 'Cursor',
//     detected: false, installPath: null },
//   ...
// ]

remove, update

// remove a skill from all agents (or pass agent: ['claude'] to scope)
await rosie.remove('pdf');

// update everything in rosie.lock
await rosie.update();

// update just one entry
await rosie.update('pdf');

logging

silent by default. pass onLog to observe progress. failures still throw — onLog is purely for visibility.

await rosie.install('anthropics/skills', {
  onLog: ({ level, message }) => {
    if (level === 'error') console.error(message);
    else if (level === 'info') console.log(message);
    // 'warn' and 'debug' levels also available
  },
});

error handling

try {
  await rosie.install('owner/nonexistent-repo');
} catch (err) {
  // err.message has the underlying log_error from the C side
  console.error('install failed:', err.message);
}

cwd and lockfile

every command accepts a cwd option, equivalent to cd'ing into that directory before running. process.cwd() is restored on exit. mirrors the cli's --cwd flag.

await rosie.install('owner/repo', { cwd: '/path/to/project' });

// cli equivalent
// $ rosie --cwd /path/to/project install owner/repo

pass lockfile: false on install, remove, or update to skip reads and writes to .agents/rosie.lock. files still land on disk; nothing is recorded. mirrors the cli's --no-lockfile.

await rosie.install('anthropics/skills', { lockfile: false });

// cli equivalent
// $ rosie install anthropics/skills --no-lockfile

how it runs

  • in-process wasm the api always uses the wasm build · no subprocess spawning · synchronous from the c side via asyncify
  • node 18+ uses built-in fetch for http · node fs for file i/o via NODERAWFS
  • esm-only "type": "module" · use import · no require
  • typescript types ships .d.ts alongside .js · works in any editor that resolves npm types
  • cli still works the same npx rosie-skills install … uses the native binary when available, falls back to wasm otherwise