floop 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
floop/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """floop — AI-native prototype quality toolkit."""
2
+
3
+ __version__ = "0.1.0"
floop/adapters.py ADDED
@@ -0,0 +1,320 @@
1
+ """Agent platform adapters for floop skill installation.
2
+
3
+ Each adapter knows how to write floop skills into the target agent's
4
+ configuration format and directory structure.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Protocol
12
+
13
+ import click
14
+
15
+ from floop.skills import INSTRUCTION, SKILLS
16
+
17
+
18
+ class AgentAdapter(Protocol):
19
+ """Interface for agent platform adapters."""
20
+
21
+ name: str
22
+
23
+ def install(self, project_dir: Path) -> list[Path]:
24
+ """Install all floop skills into the project. Returns created file paths."""
25
+ ...
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Copilot (VS Code / GitHub Copilot)
30
+ # Format: .github/skills/<name>/SKILL.md with YAML frontmatter
31
+ # ---------------------------------------------------------------------------
32
+
33
+ class CopilotAdapter:
34
+ name = "copilot"
35
+
36
+ def install(self, project_dir: Path) -> list[Path]:
37
+ created: list[Path] = []
38
+
39
+ # Write instruction file (always-on context)
40
+ instr_dir = project_dir / ".github" / "instructions"
41
+ instr_dir.mkdir(parents=True, exist_ok=True)
42
+ instr_path = instr_dir / "floop.instructions.md"
43
+ instr_path.write_text(
44
+ "---\n"
45
+ "description: 'This project uses floop for prototype development. "
46
+ "Follow the floop workflow for design tokens, sitemap, components, and page generation.'\n"
47
+ "applyTo: '**'\n"
48
+ "---\n\n"
49
+ + INSTRUCTION,
50
+ encoding="utf-8",
51
+ )
52
+ created.append(instr_path)
53
+
54
+ # Write skill files
55
+ skills_dir = project_dir / ".github" / "skills"
56
+
57
+ for skill in SKILLS.values():
58
+ skill_dir = skills_dir / skill["name"]
59
+ skill_dir.mkdir(parents=True, exist_ok=True)
60
+ path = skill_dir / "SKILL.md"
61
+ path.write_text(self._render(skill), encoding="utf-8")
62
+ created.append(path)
63
+
64
+ return created
65
+
66
+ @staticmethod
67
+ def _render(skill: dict) -> str:
68
+ return (
69
+ f'---\n'
70
+ f'name: {skill["name"]}\n'
71
+ f'description: "{skill["description"]}"\n'
72
+ f'---\n\n'
73
+ f'{skill["content"]}'
74
+ )
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Cursor
79
+ # Format: .cursor/rules/<name>.mdc with frontmatter
80
+ # ---------------------------------------------------------------------------
81
+
82
+ class CursorAdapter:
83
+ name = "cursor"
84
+
85
+ def install(self, project_dir: Path) -> list[Path]:
86
+ created: list[Path] = []
87
+ rules_dir = project_dir / ".cursor" / "rules"
88
+ rules_dir.mkdir(parents=True, exist_ok=True)
89
+
90
+ # Write always-on instruction rule
91
+ instr_path = rules_dir / "floop.mdc"
92
+ instr_path.write_text(
93
+ '---\n'
94
+ 'description: "floop prototype workflow — always-on context"\n'
95
+ 'globs: \n'
96
+ 'alwaysApply: true\n'
97
+ '---\n\n'
98
+ + INSTRUCTION,
99
+ encoding="utf-8",
100
+ )
101
+ created.append(instr_path)
102
+
103
+ # Write skill rules
104
+
105
+ for skill in SKILLS.values():
106
+ path = rules_dir / f'{skill["name"]}.mdc'
107
+ path.write_text(self._render(skill), encoding="utf-8")
108
+ created.append(path)
109
+
110
+ return created
111
+
112
+ @staticmethod
113
+ def _render(skill: dict) -> str:
114
+ return (
115
+ f'---\n'
116
+ f'description: "{skill["description"]}"\n'
117
+ f'globs: \n'
118
+ f'alwaysApply: false\n'
119
+ f'---\n\n'
120
+ f'{skill["content"]}'
121
+ )
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # Claude (Claude Code / Claude Desktop)
126
+ # Format: .claude/skills/<name>/SKILL.md (same as Copilot structure)
127
+ # or appended to CLAUDE.md
128
+ # ---------------------------------------------------------------------------
129
+
130
+ class ClaudeAdapter:
131
+ name = "claude"
132
+
133
+ def install(self, project_dir: Path) -> list[Path]:
134
+ created: list[Path] = []
135
+
136
+ # Write individual skill files
137
+ skills_dir = project_dir / ".claude" / "skills"
138
+ for skill in SKILLS.values():
139
+ skill_dir = skills_dir / skill["name"]
140
+ skill_dir.mkdir(parents=True, exist_ok=True)
141
+ path = skill_dir / "SKILL.md"
142
+ path.write_text(self._render(skill), encoding="utf-8")
143
+ created.append(path)
144
+
145
+ # Also append summary to CLAUDE.md for discovery
146
+ claude_md = project_dir / "CLAUDE.md"
147
+ marker = "<!-- floop:skills -->"
148
+ section = self._render_claude_md()
149
+
150
+ if claude_md.exists():
151
+ content = claude_md.read_text(encoding="utf-8")
152
+ if marker in content:
153
+ # Replace existing floop section
154
+ before = content[: content.index(marker)]
155
+ end_marker = "<!-- /floop:skills -->"
156
+ if end_marker in content:
157
+ after = content[content.index(end_marker) + len(end_marker) :]
158
+ else:
159
+ after = ""
160
+ content = before + section + after
161
+ else:
162
+ content = content.rstrip() + "\n\n" + section
163
+ else:
164
+ content = section
165
+
166
+ claude_md.write_text(content, encoding="utf-8")
167
+ created.append(claude_md)
168
+
169
+ return created
170
+
171
+ @staticmethod
172
+ def _render(skill: dict) -> str:
173
+ return (
174
+ f'---\n'
175
+ f'name: {skill["name"]}\n'
176
+ f'description: "{skill["description"]}"\n'
177
+ f'---\n\n'
178
+ f'{skill["content"]}'
179
+ )
180
+
181
+ @staticmethod
182
+ def _render_claude_md() -> str:
183
+ lines = ["<!-- floop:skills -->", "## floop Skills\n"]
184
+ for skill in SKILLS.values():
185
+ lines.append(f'- **{skill["name"]}**: {skill["description"]}')
186
+ lines.append(
187
+ f' - See `.claude/skills/{skill["name"]}/SKILL.md` for full rules'
188
+ )
189
+ lines.append("\n<!-- /floop:skills -->")
190
+ return "\n".join(lines) + "\n"
191
+
192
+
193
+ # ---------------------------------------------------------------------------
194
+ # Trae IDE
195
+ # Format: .trae/project_rules.md — single plain-Markdown rules file
196
+ # ---------------------------------------------------------------------------
197
+
198
+ class TraeAdapter:
199
+ name = "trae"
200
+
201
+ def install(self, project_dir: Path) -> list[Path]:
202
+ rules_dir = project_dir / ".trae"
203
+ rules_dir.mkdir(parents=True, exist_ok=True)
204
+ path = rules_dir / "project_rules.md"
205
+ path.write_text(self._render(), encoding="utf-8")
206
+ return [path]
207
+
208
+ @staticmethod
209
+ def _render() -> str:
210
+ lines = ["# floop Workflow Rules\n", INSTRUCTION, "\n## floop Skills\n"]
211
+ for skill in SKILLS.values():
212
+ lines.append(f'### {skill["name"]}\n')
213
+ lines.append(skill["content"])
214
+ return "\n".join(lines)
215
+
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # AGENTS.md-based adapters (Qwen Code, OpenCode, OpenClaw)
219
+ # All write to AGENTS.md using <!-- floop:skills --> markers.
220
+ # OpenCode and OpenClaw also write per-skill files under .<agent>/skills/.
221
+ # ---------------------------------------------------------------------------
222
+
223
+ class _AgentsMdAdapter:
224
+ """Base adapter for agents that read AGENTS.md at the project root."""
225
+
226
+ name: str
227
+ _skills_subdir: str | None = None # e.g. "opencode" → .opencode/skills/
228
+
229
+ def install(self, project_dir: Path) -> list[Path]:
230
+ created: list[Path] = []
231
+
232
+ if self._skills_subdir:
233
+ skills_dir = project_dir / f".{self._skills_subdir}" / "skills"
234
+ for skill in SKILLS.values():
235
+ skill_dir = skills_dir / skill["name"]
236
+ skill_dir.mkdir(parents=True, exist_ok=True)
237
+ path = skill_dir / "SKILL.md"
238
+ path.write_text(self._render_skill(skill), encoding="utf-8")
239
+ created.append(path)
240
+
241
+ agents_md = project_dir / "AGENTS.md"
242
+ marker = "<!-- floop:skills -->"
243
+ section = self._render_agents_md()
244
+
245
+ if agents_md.exists():
246
+ content = agents_md.read_text(encoding="utf-8")
247
+ if marker in content:
248
+ before = content[: content.index(marker)]
249
+ end_marker = "<!-- /floop:skills -->"
250
+ if end_marker in content:
251
+ after = content[content.index(end_marker) + len(end_marker):]
252
+ else:
253
+ after = ""
254
+ content = before + section + after
255
+ else:
256
+ content = content.rstrip() + "\n\n" + section
257
+ else:
258
+ content = section
259
+
260
+ agents_md.write_text(content, encoding="utf-8")
261
+ created.append(agents_md)
262
+ return created
263
+
264
+ @staticmethod
265
+ def _render_skill(skill: dict) -> str:
266
+ return (
267
+ f'---\n'
268
+ f'name: {skill["name"]}\n'
269
+ f'description: "{skill["description"]}"\n'
270
+ f'---\n\n'
271
+ f'{skill["content"]}'
272
+ )
273
+
274
+ def _render_agents_md(self) -> str:
275
+ lines = ["<!-- floop:skills -->", "## floop\n", INSTRUCTION]
276
+ if self._skills_subdir:
277
+ lines += ["", "## floop Skills\n"]
278
+ for skill in SKILLS.values():
279
+ lines.append(f'- **{skill["name"]}**: {skill["description"]}')
280
+ lines.append(
281
+ f' - See `.{self._skills_subdir}/skills/'
282
+ f'{skill["name"]}/SKILL.md` for full workflow'
283
+ )
284
+ lines.append("\n<!-- /floop:skills -->")
285
+ return "\n".join(lines) + "\n"
286
+
287
+
288
+ class QwenCodeAdapter(_AgentsMdAdapter):
289
+ """Qwen Code (terminal CLI, Gemini CLI fork) — AGENTS.md only."""
290
+ name = "qwen-code"
291
+ _skills_subdir = None
292
+
293
+
294
+ class OpenCodeAdapter(_AgentsMdAdapter):
295
+ """OpenCode (terminal CLI) — AGENTS.md + .opencode/skills/."""
296
+ name = "opencode"
297
+ _skills_subdir = "opencode"
298
+
299
+
300
+ class OpenClawAdapter(_AgentsMdAdapter):
301
+ """OpenClaw — AGENTS.md + .openclaw/skills/."""
302
+ name = "openclaw"
303
+ _skills_subdir = "openclaw"
304
+
305
+
306
+ # ---------------------------------------------------------------------------
307
+ # Registry
308
+ # ---------------------------------------------------------------------------
309
+
310
+ ADAPTERS: dict[str, type[AgentAdapter]] = {
311
+ "copilot": CopilotAdapter,
312
+ "cursor": CursorAdapter,
313
+ "claude": ClaudeAdapter,
314
+ "trae": TraeAdapter,
315
+ "qwen-code": QwenCodeAdapter,
316
+ "opencode": OpenCodeAdapter,
317
+ "openclaw": OpenClawAdapter,
318
+ }
319
+
320
+ SUPPORTED_AGENTS = list(ADAPTERS.keys())