Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ venv/
*.log
logs/
.omc/

# ─── ResearchStudio installer output (skills links, runtime settings) ───
.claude/
.codex/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
[![Status: Active](https://img.shields.io/badge/Status-Active-brightgreen)](https://aka.ms/ResearchStudio)

ResearchStudio is a collection of skills covering the **entire research lifecycle** — from an under-specified research direction to a published paper.
The skills run on [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) and [Codex](https://developers.openai.com/codex).
The skills run on [Claude Code](https://docs.claude.com/en/docs/claude-code/overview), [Codex](https://developers.openai.com/codex), and [QwenPaw](https://github.com/agentscope-ai/QwenPaw).

---

Expand Down
2 changes: 1 addition & 1 deletion ResearchStudio-Idea/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Full protocol, IdeaSpark vs the baselines, and the scoring skill are in [`evalua

## Acknowledgements

- [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) and [Codex](https://developers.openai.com/codex) — the agent runtime that drives every skill.
- [Claude Code](https://docs.claude.com/en/docs/claude-code/overview), [Codex](https://developers.openai.com/codex), and [QwenPaw](https://github.com/agentscope-ai/QwenPaw) — the agent runtimes that drive every skill.
- [arXiv](https://arxiv.org/), [OpenAlex](https://openalex.org/), [Semantic Scholar](https://www.semanticscholar.org/product/api), [OpenReview](https://openreview.net/) — the Phase 0 / 3.1 literature connectors.
- [PyMuPDF](https://pymupdf.readthedocs.io/) + [BeautifulSoup](https://www.crummy.com/software/BeautifulSoup/) — full-text fetch and parsing.
- [tectonic](https://tectonic-typesetting.github.io/) / XeLaTeX — optional PDF compilation of the idea cards.
Expand Down
4 changes: 2 additions & 2 deletions ResearchStudio-Reel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
| **[Paper2Blog](skills/paper2blog/README.md)** | paper PDF or assets | two blogs (DOCX) | publicity push after acceptance — bilingual outreach in one pass |
| **[Paper2Reel](skills/paper2reel/README.md)** | poster + deck artifacts | an interactive reel viewer (HTML) | one scrollable view that aligns the poster with slide / video frames |

Each skill is shipped as a [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) and [Codex](https://developers.openai.com/codex) **skill**.
Each skill is shipped as a [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) / [Codex](https://developers.openai.com/codex) / [QwenPaw](https://github.com/agentscope-ai/QwenPaw) **skill**.

---

Expand Down Expand Up @@ -210,7 +210,7 @@ For the full bootstrap behavior, alignment map, section-modal UI contract, brows

## Acknowledgements

- [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) and [Codex](https://developers.openai.com/codex) — the agent runtime that drives every skill.
- [Claude Code](https://docs.claude.com/en/docs/claude-code/overview), [Codex](https://developers.openai.com/codex), and [QwenPaw](https://github.com/agentscope-ai/QwenPaw) — the agent runtimes that drive every skill.
- [PyMuPDF](https://pymupdf.readthedocs.io/), [Poppler](https://poppler.freedesktop.org/), [Pillow](https://python-pillow.org/) — PDF + image processing.
- [Playwright](https://playwright.dev/) + Chromium — HTML → PDF / PNG rendering for posters.
- [LibreOffice](https://www.libreoffice.org/) + [FFmpeg](https://ffmpeg.org/) — slide rasterization and video muxing.
Expand Down
110 changes: 87 additions & 23 deletions bin/install.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* up from each skill dir and that is the nearest common ancestor they check.
*
* Non-interactive (--yes or no TTY) takes env answers:
* RS_PLUGINS=idea,reel RS_SCOPE=global|project RS_AGENTS=claude,codex
* RS_PLUGINS=idea,reel RS_SCOPE=global|project RS_AGENTS=claude,codex,qwenpaw
* RS_PIP=0|1 + the key env vars.
*/
import fs from 'node:fs';
Expand All @@ -25,9 +25,45 @@ import { fileURLToPath } from 'node:url';
import { execFileSync } from 'node:child_process';

const PKG_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const AGENTS = { claude: '.claude', codex: '.codex' };
const PYTHON = process.env.PYTHON || (process.platform === 'win32' ? 'python' : 'python3');

const expandHome = (p) => (p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p);

// Agent registry — every runtime that can host ResearchStudio skills. Adding
// support for a new agent host is one entry here. `skillsDir(base)` returns
// where SKILL.md folders go for a scope base (home dir for global, cwd for
// project). `pool: true` marks agents without a project-scoped skills dir:
// they install into one shared location regardless of scope.
const AGENTS = {
claude: { label: 'Claude Code', skillsDir: (base) => path.join(base, '.claude', 'skills') },
codex: { label: 'Codex CLI', skillsDir: (base) => path.join(base, '.codex', 'skills') },
qwenpaw: { label: 'QwenPaw', skillsDir: () => qwenpawDirs().pool, pool: true },
};

// QwenPaw keeps one shared skill pool under its working dir; pool entries are
// then broadcast into per-agent workspaces from the Console. The working dir
// resolution mirrors QwenPaw's constant.py: QWENPAW_WORKING_DIR env var first,
// then the legacy ~/.copaw layout, then ~/.qwenpaw.
function qwenpawDirs() {
const home = process.env.QWENPAW_WORKING_DIR
? path.resolve(expandHome(process.env.QWENPAW_WORKING_DIR))
: [path.join(os.homedir(), '.copaw'), path.join(os.homedir(), '.qwenpaw')]
.find((d) => fs.existsSync(d)) || path.join(os.homedir(), '.qwenpaw');
return { home, pool: path.join(home, 'skill_pool') };
}

// QwenPaw picks up manually placed pool skills on its next manifest reconcile.
// If its CLI is on PATH, nudge one now so the skills show up immediately;
// otherwise the next Console/CLI skill operation reconciles the manifest.
function qwenpawReconcile() {
try {
execFileSync('qwenpaw', ['skills', 'list', '--pool'], { stdio: 'ignore', timeout: 60_000 });
return true;
} catch {
return false;
}
}

// Preserve an explicit editor choice while giving installer subprocesses a
// predictable fallback on machines where EDITOR is unset or empty.
process.env.EDITOR ||= 'vim';
Expand Down Expand Up @@ -153,33 +189,34 @@ function installSkills(skillsDir, srcDir, names) {
// pptx2video. Fetch both skills from their upstream repos via `npx skills add`,
// installing into skillsDir. Returns the names that were added.
function fetchDependencySkills(skillsDir) {
fs.mkdirSync(skillsDir, { recursive: true });
const DEPS = [
{ repo: 'hugohe3/ppt-master', name: 'ppt-master' },
{ repo: 'ai-nuts/pptx2video', name: 'pptx2video' },
];
const added = [];
// The skills CLI refuses non-interactive runs without --agent, and the agent
// choice only decides where content lands. Fetch into a throwaway dir, then
// copy the skill content into skillsDir. Both CLI layouts are accepted: the
// current direct .claude/skills/<name> and the legacy .agents/skills/<name>.
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'rs-deps-'));
for (const d of DEPS) {
try {
execFileSync('npx', ['-y', 'skills', 'add', d.repo, '--skill', d.name],
{ cwd: skillsDir, stdio: 'inherit' });
// `skills add` drops content under <skillsDir>/.agents/skills/<name>
// rather than the top level where the Reel/Idea skills live. Move it up
// to the top level so the agent's top-level scan finds it.
const src = path.join(skillsDir, '.agents', 'skills', d.name);
const dst = path.join(skillsDir, d.name);
if (fs.existsSync(src)) {
fs.rmSync(dst, { recursive: true, force: true });
fs.renameSync(src, dst);
}
execFileSync('npx', ['-y', 'skills', 'add', d.repo, '--skill', d.name, '-y', '--agent', 'claude-code'],
{ cwd: tmp, stdio: 'inherit' });
const src = [
path.join(tmp, '.claude', 'skills', d.name),
path.join(tmp, '.agents', 'skills', d.name),
].find((c) => fs.existsSync(c));
if (!src) throw new Error(`no skill content found for ${d.name}`);
fs.rmSync(path.join(skillsDir, d.name), { recursive: true, force: true });
fs.cpSync(src, path.join(skillsDir, d.name), { recursive: true });
added.push(d.name);
} catch {
say(` ${C.y}! failed to fetch ${d.name} via npx skills add — install it manually:${C.r}`);
say(` ${C.c}npx skills add ${d.repo} --skill ${d.name}${C.r}`);
say(` ${C.c}npx skills add ${d.repo} --skill ${d.name} -y --agent claude-code${C.r}`);
}
}
// Remove the now-empty .agents scaffold left by `skills add`.
fs.rmSync(path.join(skillsDir, '.agents'), { recursive: true, force: true });
fs.rmSync(tmp, { recursive: true, force: true });
return added;
}

Expand Down Expand Up @@ -255,13 +292,24 @@ async function main() {
const a = (await ask('Install globally (all projects) or into this project? g/p', 'g')).toLowerCase();
scope = a.startsWith('p') ? 'project' : 'global';
}
const base = scope === 'global' ? os.homedir() : process.cwd();
const agentList = Object.entries(AGENTS);
let agents = (process.env.RS_AGENTS || 'claude').split(',').map((s) => s.trim()).filter(Boolean);
if (!NONINTERACTIVE) {
agents = ['claude'];
if (await askYN('Also install for Codex (in addition to Claude Code)?', false)) agents.push('codex');
say(`\n${C.b}Agents${C.r}`);
agentList.forEach(([key, a], i) => say(
` ${i + 1}) ${a.label} ${C.d}(→ ${a.skillsDir(base)})${C.r}`));
const ans = (await ask('Install for which agents? numbers e.g. 1 or 1,3', '1')).toLowerCase();
const picked = ans === 'all' ? agentList.map(([k]) => k)
: ans.split(/[\s,]+/).map((n) => agentList[parseInt(n, 10) - 1]?.[0]).filter(Boolean);
agents = picked.length ? picked : ['claude'];
}
const base = scope === 'global' ? os.homedir() : process.cwd();
const targets = agents.filter((a) => AGENTS[a]).map((a) => path.join(base, AGENTS[a], 'skills'));
agents = agents.filter((a) => {
if (AGENTS[a]) return true;
say(`${C.y} skipping unknown agent '${a}' — known: ${Object.keys(AGENTS).join(', ')}${C.r}`);
return false;
});
if (!agents.length) { say(`${C.y}No agent selected.${C.r}`); process.exit(1); }

// union of keys from the selected plugins' templates (dedup by name)
const keySpec = []; const seen = new Set();
Expand All @@ -283,7 +331,12 @@ async function main() {
// install
say('');
const reelSelected = selected.some((p) => p.key === 'reel');
for (const dir of targets) {
for (const key of agents) {
const agent = AGENTS[key];
const dir = agent.skillsDir(base);
if (agent.pool && scope === 'project') {
say(`${C.d} ${agent.label} has no project-scoped skills dir — installing into its shared pool instead${C.r}`);
}
for (const p of selected) installSkills(dir, p.src, p.skills);
const total = selected.reduce((n, p) => n + p.skills.length, 0);
say(`${C.g}✓${C.r} installed ${total} skills (${selected.map((p) => p.key).join(', ')}) → ${C.c}${dir}${C.r}`);
Expand All @@ -293,8 +346,19 @@ async function main() {
const deps = fetchDependencySkills(dir);
if (deps.length) say(` ${C.g}✓${C.r} provisioned ${deps.length} Paper2Video dep skill(s) (${deps.join(', ')}) → ${C.c}${dir}${C.r}`);
}
const w = writeEnv(dir, values);
// QwenPaw's runtime loads <working-dir>/.env into the process env its
// skills run in, and the skills' own .env walker reaches that file from
// the pool — so one merged .env there covers pool skills wherever they run.
// Other agents keep the skills-dir .env (the nearest common ancestor their
// loaders check).
const envDir = key === 'qwenpaw' ? qwenpawDirs().home : dir;
const w = writeEnv(envDir, values);
if (w) say(` ${C.g}✓${C.r} wrote ${Object.keys(values).length} key(s) → ${w.envPath}${w.backedUp ? ` ${C.d}(backup: .env.bak)${C.r}` : ''}`);
if (key === 'qwenpaw') {
const ok = qwenpawReconcile();
say(` ${C.d}pool skills are shared — load them into a workspace from QwenPaw Console → Workspace → Skills` +
`${ok ? '' : ' (they appear after the next manifest reconcile)'}`);
}
}

// python deps for whichever selected plugins declare them
Expand Down
Loading