@folpe/loom 0.1.0

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.
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: UX/UI
3
+ description: Designs UI components, creates design system tokens, and handles accessibility
4
+ role: design
5
+ color: "#EC4899"
6
+ tools:
7
+ - Read
8
+ - Write
9
+ - Edit
10
+ - Glob
11
+ - Grep
12
+ model: claude-sonnet-4-6
13
+ ---
14
+
15
+ # UX/UI Agent
16
+
17
+ You are a senior UX/UI designer and design engineer for the Loom project. You create design system foundations, component styles, interaction patterns, and ensure the application meets high standards for usability and accessibility.
18
+
19
+ ## Design System
20
+
21
+ - Maintain design tokens (colors, spacing, typography, radii, shadows) in a centralized configuration file (e.g., `tailwind.config.ts` or a dedicated `tokens.ts`).
22
+ - Use a consistent spacing scale based on multiples of 4px (0.25rem).
23
+ - Define a color palette with semantic names: `primary`, `secondary`, `accent`, `neutral`, `success`, `warning`, `error`.
24
+ - Every color must meet WCAG AA contrast ratios against its intended background (4.5:1 for normal text, 3:1 for large text).
25
+
26
+ ## Component Design
27
+
28
+ - Design components from the outside in: define the API (props) first, then the visual structure.
29
+ - Use composition over configuration. Prefer small, composable primitives over monolithic components with many props.
30
+ - Define component variants explicitly (e.g., `variant: "primary" | "secondary" | "ghost"`) rather than relying on arbitrary className overrides.
31
+ - Include hover, focus, active, and disabled states for all interactive elements.
32
+
33
+ ## Accessibility (a11y)
34
+
35
+ - Every interactive element must be keyboard-navigable. Use `tabIndex`, `onKeyDown`, and proper focus management.
36
+ - Use ARIA roles and properties correctly. Prefer native semantic HTML (`<button>`, `<nav>`, `<dialog>`) over `div` with ARIA.
37
+ - Ensure all form inputs have associated `<label>` elements. Use `aria-describedby` for help text and error messages.
38
+ - Test focus order: it should follow a logical reading sequence, not jump unpredictably.
39
+ - Provide visible focus indicators that meet the 3:1 contrast ratio requirement.
40
+
41
+ ## Responsive Design
42
+
43
+ - Design mobile-first. Start with the smallest breakpoint and layer on complexity for larger screens.
44
+ - Use Tailwind's responsive prefixes (`sm:`, `md:`, `lg:`, `xl:`) consistently.
45
+ - Ensure touch targets are at least 44x44px on mobile.
46
+ - Test layouts at common breakpoints: 320px, 768px, 1024px, 1440px.
47
+
48
+ ## Animation and Interaction
49
+
50
+ - Use subtle animations (150-300ms) for state transitions. Avoid animations that block user interaction.
51
+ - Respect `prefers-reduced-motion`. Provide a reduced or no-animation fallback.
52
+ - Use CSS transitions for simple state changes. Reserve JavaScript animation libraries for complex sequences.
53
+
54
+ ## Before Finishing
55
+
56
+ - Verify all colors meet contrast requirements.
57
+ - Check that every interactive element has visible focus and hover states.
58
+ - Confirm responsive behavior at all standard breakpoints.
@@ -0,0 +1,53 @@
1
+ name: SaaS Default
2
+ description: Standard SaaS project preset with full agent team, Next.js conventions, and Tailwind patterns. Includes
3
+ orchestrator-driven multi-agent workflow.
4
+ boilerplate:
5
+ repo: https://github.com/vercel/next.js.git
6
+ branch: canary
7
+ shallow: true
8
+ agents:
9
+ - orchestrator
10
+ - frontend
11
+ - backend
12
+ - marketing
13
+ - ux-ui
14
+ - database
15
+ - tests
16
+ - review-qa
17
+ - performance
18
+ - security
19
+ skills:
20
+ - nextjs-conventions
21
+ - tailwind-patterns
22
+ - hero-copywriting
23
+ constitution:
24
+ principles:
25
+ - Ship fast, iterate often — prefer working software over perfect plans
26
+ - User-first design — every feature must solve a real user problem
27
+ - Type safety everywhere — leverage TypeScript strict mode
28
+ - Convention over configuration — follow established patterns
29
+ stack:
30
+ - Next.js 16+ (App Router)
31
+ - React 19
32
+ - TypeScript 5 (strict)
33
+ - Tailwind CSS 4
34
+ - ShadCN UI
35
+ - Supabase (auth + database)
36
+ - Vercel (deployment)
37
+ conventions:
38
+ - Use Server Components by default, Client Components only when needed
39
+ - Server Actions for all mutations
40
+ - Zod validation at API boundaries
41
+ - Mobile-first responsive design
42
+ - Semantic HTML with ARIA attributes for accessibility
43
+ claudemd:
44
+ projectDescription: >
45
+ This is a SaaS application scaffolded with Loom. It uses a multi-agent architecture where the orchestrator delegates
46
+ tasks to specialized agents (frontend, backend, database, etc.). Each agent follows the conventions defined in the
47
+ linked skills.
48
+ orchestratorRef: >
49
+ The orchestrator agent (.claude/agents/orchestrator.md) is the main entry point. Always start by delegating to the
50
+ orchestrator, which will route tasks to the appropriate specialized agent.
51
+ specKit:
52
+ enabled: true
53
+ aiFlag: claude
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: hero-copywriting
3
+ description: "Guidelines for writing high-converting hero sections. Use when creating landing pages, marketing pages, or any page with a hero section."
4
+ allowed-tools: "Read, Write, Edit"
5
+ ---
6
+
7
+ # Hero Section Copywriting
8
+
9
+ ## Headline Rules
10
+
11
+ - **Lead with the outcome**, not the feature. What does the user GET?
12
+ - Bad: "AI-powered project management"
13
+ - Good: "Ship projects 3x faster with your AI co-pilot"
14
+ - Keep headlines to **8-12 words maximum**.
15
+ - Use **power words**: effortless, instant, proven, transform, unlock, accelerate.
16
+ - Include a **specific number or metric** when possible for credibility.
17
+
18
+ ## Subheadline
19
+
20
+ - Expand on the headline with **how** it works in one sentence.
21
+ - Keep it under 25 words.
22
+ - Address the primary pain point directly.
23
+ - Example: "Stop juggling spreadsheets. One dashboard to plan, track, and deliver — powered by AI."
24
+
25
+ ## Call to Action (CTA)
26
+
27
+ - Use **action verbs**: "Start", "Get", "Try", "Launch", "Build".
28
+ - Make the CTA specific: "Start your free trial" > "Sign up".
29
+ - Add urgency or ease: "Get started in 30 seconds" / "No credit card required".
30
+ - Primary CTA should be visually prominent (filled button, large size).
31
+ - Optional secondary CTA: "See how it works" / "Watch demo" (outline button).
32
+
33
+ ## Social Proof
34
+
35
+ - Place immediately below or beside the CTA.
36
+ - Options (pick 1-2):
37
+ - Customer logos: "Trusted by teams at [logos]"
38
+ - Metric: "Join 10,000+ teams already shipping faster"
39
+ - Rating: "4.9/5 on G2 — 500+ reviews"
40
+ - Testimonial snippet: short quote with name and company
41
+
42
+ ## Visual Guidelines
43
+
44
+ - Hero should occupy the full viewport height on desktop (`min-h-screen` or `min-h-[80vh]`).
45
+ - Text should be left-aligned or center-aligned. Never justify.
46
+ - Maximum content width: `max-w-2xl` for centered, `max-w-xl` for left-aligned.
47
+ - Generous whitespace. Don't crowd the hero.
48
+
49
+ ## Tone
50
+
51
+ - Conversational but confident. Not corporate-speak.
52
+ - Speak directly to the reader: "you" and "your".
53
+ - Avoid jargon unless the audience expects it.
54
+ - Be specific over generic. "Save 5 hours a week" > "Save time".
55
+
56
+ ## Structure Template
57
+
58
+ ```
59
+ [Badge or category label — optional]
60
+ [Headline — 8-12 words, outcome-focused]
61
+ [Subheadline — 1 sentence, addresses pain point]
62
+ [Primary CTA button] [Secondary CTA — optional]
63
+ [Social proof — logos, metric, or testimonial]
64
+ ```
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: nextjs-conventions
3
+ description: "Enforces Next.js 15+ / React 19 / TypeScript conventions and best practices. Use when working on any Next.js project to ensure consistent patterns."
4
+ allowed-tools: "Read, Write, Edit, Glob, Grep"
5
+ ---
6
+
7
+ # Next.js Conventions
8
+
9
+ ## App Router
10
+
11
+ - Use the App Router (`src/app/`) exclusively. Never use the Pages Router.
12
+ - Every route segment should have a `page.tsx`. Use `layout.tsx` for shared UI.
13
+ - Use `loading.tsx` for Suspense boundaries and `error.tsx` for error boundaries.
14
+ - Use `not-found.tsx` for 404 pages at the appropriate route level.
15
+
16
+ ## Server vs Client Components
17
+
18
+ - **Default to Server Components**. Only add `"use client"` when you need:
19
+ - Event handlers (`onClick`, `onChange`, etc.)
20
+ - React hooks (`useState`, `useEffect`, `useRef`, etc.)
21
+ - Browser-only APIs (`window`, `localStorage`, etc.)
22
+ - Never import server-only modules in client components.
23
+ - Pass server data to client components via props, not by importing server functions.
24
+
25
+ ## Data Fetching
26
+
27
+ - Fetch data in Server Components using `async/await` directly.
28
+ - Use Server Actions (`"use server"`) for mutations. Define them in `src/actions/`.
29
+ - Always revalidate with `revalidatePath()` or `revalidateTag()` after mutations.
30
+ - Use `Suspense` boundaries for streaming and progressive rendering.
31
+
32
+ ## File Naming
33
+
34
+ - Components: `PascalCase.tsx` (e.g., `UserProfile.tsx`)
35
+ - Utilities: `kebab-case.ts` (e.g., `format-date.ts`)
36
+ - Types: define in `src/types/` with `.ts` extension
37
+ - Server Actions: `src/actions/{entity}.actions.ts`
38
+
39
+ ## TypeScript
40
+
41
+ - Enable `strict: true` in `tsconfig.json`.
42
+ - Prefer `interface` over `type` for object shapes.
43
+ - Never use `any`. Use `unknown` if the type is truly unknown.
44
+ - Use `satisfies` operator for type-safe object literals.
45
+
46
+ ## Styling
47
+
48
+ - Use Tailwind CSS utility classes as the primary styling method.
49
+ - Use `cn()` from `@/lib/utils` to merge conditional classes.
50
+ - Avoid inline styles. Avoid CSS modules unless absolutely necessary.
51
+ - Follow mobile-first responsive design: base styles for mobile, `md:` and `lg:` for larger screens.
52
+
53
+ ## Imports
54
+
55
+ - Use the `@/` path alias for all imports from `src/`.
56
+ - Group imports: React/Next.js first, then external libs, then internal modules.
57
+ - Prefer named exports over default exports (except for page/layout components).
58
+
59
+ ## Performance
60
+
61
+ - Use `next/image` for all images with proper `width`, `height`, and `alt`.
62
+ - Use `next/link` for internal navigation. Never use `<a>` for internal links.
63
+ - Use `next/font` for font loading.
64
+ - Lazy load heavy client components with `dynamic()` from `next/dynamic`.
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: tailwind-patterns
3
+ description: "Provides Tailwind CSS patterns, utility conventions, and component styling guidelines. Use when building UI with Tailwind CSS."
4
+ allowed-tools: "Read, Write, Edit"
5
+ ---
6
+
7
+ # Tailwind CSS Patterns
8
+
9
+ ## Utility-First Approach
10
+
11
+ - Always use utility classes directly in JSX. Avoid `@apply` except in global base styles.
12
+ - Use `cn()` helper for conditional and merged classes:
13
+ ```tsx
14
+ className={cn("base-classes", condition && "conditional-classes", className)}
15
+ ```
16
+
17
+ ## Spacing & Layout
18
+
19
+ - Use consistent spacing scale: `gap-2` (8px), `gap-4` (16px), `gap-6` (24px), `gap-8` (32px).
20
+ - Prefer `flex` and `grid` for layout. Avoid absolute positioning except for overlays.
21
+ - Use `container mx-auto px-4` for page-level content width.
22
+ - Standard page padding: `p-6` for main content areas.
23
+
24
+ ## Responsive Design
25
+
26
+ - Mobile-first: write base styles for mobile, add `sm:`, `md:`, `lg:`, `xl:` for larger screens.
27
+ - Common breakpoints:
28
+ - `sm:` (640px) — small tablets
29
+ - `md:` (768px) — tablets
30
+ - `lg:` (1024px) — laptops
31
+ - `xl:` (1280px) — desktops
32
+ - Grid responsive pattern: `grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3`
33
+
34
+ ## Typography
35
+
36
+ - Headings: `text-3xl font-bold tracking-tight` (h1), `text-2xl font-semibold` (h2), `text-lg font-semibold` (h3)
37
+ - Body text: `text-sm` or `text-base`
38
+ - Muted text: `text-muted-foreground`
39
+ - Truncation: `truncate` or `line-clamp-2`
40
+
41
+ ## Colors & Theming
42
+
43
+ - Always use CSS variable-based colors from the design system: `bg-background`, `text-foreground`, `bg-card`, `text-muted-foreground`, etc.
44
+ - Never hardcode hex colors. Use the semantic tokens.
45
+ - For dark mode, rely on the `dark:` variant only when CSS variables don't handle it.
46
+
47
+ ## Interactive States
48
+
49
+ - Hover: `hover:bg-accent` or `hover:bg-accent/50`
50
+ - Focus: handled by ShadCN defaults via `outline-ring/50`
51
+ - Transitions: `transition-colors` for color changes, `transition-all` sparingly
52
+ - Disabled: `disabled:opacity-50 disabled:pointer-events-none`
53
+
54
+ ## Component Patterns
55
+
56
+ - Card: use ShadCN `Card` components. Add `hover:bg-accent/50 transition-colors` for clickable cards.
57
+ - Forms: stack fields with `space-y-4`, use `grid gap-4 md:grid-cols-2` for side-by-side fields.
58
+ - Buttons: use ShadCN `Button` with appropriate `variant` and `size`.
59
+ - Badges: use ShadCN `Badge` with `variant="secondary"` for tags, `variant="outline"` for less emphasis.
package/dist/index.js ADDED
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/list.ts
7
+ import pc from "picocolors";
8
+
9
+ // src/lib/library.ts
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import { fileURLToPath } from "url";
13
+ import matter from "gray-matter";
14
+ import YAML from "yaml";
15
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ var DATA_DIR = path.resolve(__dirname, "../data");
17
+ function listSubDirs(dir) {
18
+ if (!fs.existsSync(dir)) return [];
19
+ return fs.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
20
+ }
21
+ function listFiles(dir) {
22
+ if (!fs.existsSync(dir)) return [];
23
+ return fs.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile()).map((d) => d.name).sort();
24
+ }
25
+ async function listAgents() {
26
+ const agentsDir = path.join(DATA_DIR, "agents");
27
+ const slugs = listSubDirs(agentsDir);
28
+ const agents = [];
29
+ for (const slug of slugs) {
30
+ const filePath = path.join(agentsDir, slug, "AGENT.md");
31
+ try {
32
+ const raw = fs.readFileSync(filePath, "utf-8");
33
+ const { data } = matter(raw);
34
+ agents.push({
35
+ slug,
36
+ name: data.name || slug,
37
+ description: data.description || "",
38
+ role: data.role || ""
39
+ });
40
+ } catch {
41
+ agents.push({ slug, name: slug, description: "", role: "" });
42
+ }
43
+ }
44
+ return agents;
45
+ }
46
+ async function listSkills() {
47
+ const skillsDir = path.join(DATA_DIR, "skills");
48
+ const slugs = listSubDirs(skillsDir);
49
+ const skills = [];
50
+ for (const slug of slugs) {
51
+ const filePath = path.join(skillsDir, slug, "SKILL.md");
52
+ try {
53
+ const raw = fs.readFileSync(filePath, "utf-8");
54
+ const { data } = matter(raw);
55
+ skills.push({
56
+ slug,
57
+ name: data.name || slug,
58
+ description: data.description || ""
59
+ });
60
+ } catch {
61
+ skills.push({ slug, name: slug, description: "" });
62
+ }
63
+ }
64
+ return skills;
65
+ }
66
+ async function listPresets() {
67
+ const presetsDir = path.join(DATA_DIR, "presets");
68
+ const files = listFiles(presetsDir);
69
+ const presets = [];
70
+ for (const file of files) {
71
+ if (!file.endsWith(".yaml")) continue;
72
+ const slug = file.replace(/\.yaml$/, "");
73
+ try {
74
+ const raw = fs.readFileSync(path.join(presetsDir, file), "utf-8");
75
+ const data = YAML.parse(raw);
76
+ presets.push({
77
+ slug,
78
+ name: data.name || slug,
79
+ description: data.description || "",
80
+ agentCount: data.agents?.length || 0,
81
+ skillCount: data.skills?.length || 0
82
+ });
83
+ } catch {
84
+ presets.push({ slug, name: slug, description: "", agentCount: 0, skillCount: 0 });
85
+ }
86
+ }
87
+ return presets;
88
+ }
89
+ async function getAgent(slug) {
90
+ const filePath = path.join(DATA_DIR, "agents", slug, "AGENT.md");
91
+ const raw = fs.readFileSync(filePath, "utf-8");
92
+ return { slug, rawContent: raw };
93
+ }
94
+ async function getSkill(slug) {
95
+ const filePath = path.join(DATA_DIR, "skills", slug, "SKILL.md");
96
+ const raw = fs.readFileSync(filePath, "utf-8");
97
+ return { slug, rawContent: raw };
98
+ }
99
+ async function getPreset(slug) {
100
+ const filePath = path.join(DATA_DIR, "presets", `${slug}.yaml`);
101
+ const raw = fs.readFileSync(filePath, "utf-8");
102
+ const data = YAML.parse(raw);
103
+ return { ...data, slug };
104
+ }
105
+
106
+ // src/commands/list.ts
107
+ function truncate(str, max) {
108
+ if (str.length <= max) return str;
109
+ return str.slice(0, max - 1) + "\u2026";
110
+ }
111
+ function padEnd(str, len) {
112
+ return str + " ".repeat(Math.max(0, len - str.length));
113
+ }
114
+ async function listCommand(type) {
115
+ try {
116
+ if (!type || type === "agents") {
117
+ const agents = await listAgents();
118
+ console.log(pc.bold(pc.cyan("\n Agents")));
119
+ console.log(pc.dim(" " + "\u2500".repeat(60)));
120
+ if (agents.length === 0) {
121
+ console.log(pc.dim(" No agents found."));
122
+ }
123
+ for (const a of agents) {
124
+ console.log(
125
+ ` ${padEnd(pc.green(a.slug), 30)} ${padEnd(a.name, 25)} ${pc.dim(truncate(a.description, 40))}`
126
+ );
127
+ }
128
+ }
129
+ if (!type || type === "skills") {
130
+ const skills = await listSkills();
131
+ console.log(pc.bold(pc.cyan("\n Skills")));
132
+ console.log(pc.dim(" " + "\u2500".repeat(60)));
133
+ if (skills.length === 0) {
134
+ console.log(pc.dim(" No skills found."));
135
+ }
136
+ for (const s of skills) {
137
+ console.log(
138
+ ` ${padEnd(pc.green(s.slug), 30)} ${padEnd(s.name, 25)} ${pc.dim(truncate(s.description, 40))}`
139
+ );
140
+ }
141
+ }
142
+ if (!type || type === "presets") {
143
+ const presets = await listPresets();
144
+ console.log(pc.bold(pc.cyan("\n Presets")));
145
+ console.log(pc.dim(" " + "\u2500".repeat(60)));
146
+ if (presets.length === 0) {
147
+ console.log(pc.dim(" No presets found."));
148
+ }
149
+ for (const p of presets) {
150
+ const meta = pc.dim(`(${p.agentCount} agents, ${p.skillCount} skills)`);
151
+ console.log(
152
+ ` ${padEnd(pc.green(p.slug), 30)} ${padEnd(p.name, 25)} ${meta}`
153
+ );
154
+ }
155
+ }
156
+ console.log();
157
+ } catch (error) {
158
+ handleError(error);
159
+ }
160
+ }
161
+ function handleError(error) {
162
+ if (error instanceof Error) {
163
+ console.error(pc.red(`
164
+ Error: ${error.message}
165
+ `));
166
+ } else {
167
+ console.error(pc.red("\n An unknown error occurred.\n"));
168
+ }
169
+ process.exit(1);
170
+ }
171
+
172
+ // src/commands/add.ts
173
+ import pc2 from "picocolors";
174
+
175
+ // src/lib/writer.ts
176
+ import fs2 from "fs";
177
+ import path2 from "path";
178
+ var CLAUDE_DIR = ".claude";
179
+ function ensureDir(dirPath) {
180
+ fs2.mkdirSync(dirPath, { recursive: true });
181
+ }
182
+ function writeAgent(slug, content, cwd = process.cwd()) {
183
+ const dir = path2.join(cwd, CLAUDE_DIR, "agents");
184
+ ensureDir(dir);
185
+ const filePath = path2.join(dir, `${slug}.md`);
186
+ fs2.writeFileSync(filePath, content, "utf-8");
187
+ return filePath;
188
+ }
189
+ function writeSkill(slug, content, cwd = process.cwd()) {
190
+ const dir = path2.join(cwd, CLAUDE_DIR, "skills");
191
+ ensureDir(dir);
192
+ const filePath = path2.join(dir, `${slug}.md`);
193
+ fs2.writeFileSync(filePath, content, "utf-8");
194
+ return filePath;
195
+ }
196
+ function writeClaudeMd(content, cwd = process.cwd()) {
197
+ const filePath = path2.join(cwd, "CLAUDE.md");
198
+ fs2.writeFileSync(filePath, content, "utf-8");
199
+ return filePath;
200
+ }
201
+
202
+ // src/commands/add.ts
203
+ async function addCommand(type, slug) {
204
+ if (type !== "agent" && type !== "skill") {
205
+ console.error(pc2.red(`
206
+ Error: Invalid type "${type}". Use "agent" or "skill".
207
+ `));
208
+ process.exit(1);
209
+ }
210
+ try {
211
+ if (type === "agent") {
212
+ const agent = await getAgent(slug);
213
+ const filePath = writeAgent(slug, agent.rawContent);
214
+ console.log(pc2.green(`
215
+ \u2713 Agent "${slug}" written to ${filePath}
216
+ `));
217
+ } else {
218
+ const skill = await getSkill(slug);
219
+ const filePath = writeSkill(slug, skill.rawContent);
220
+ console.log(pc2.green(`
221
+ \u2713 Skill "${slug}" written to ${filePath}
222
+ `));
223
+ }
224
+ } catch (error) {
225
+ if (error instanceof Error) {
226
+ console.error(pc2.red(`
227
+ Error: ${error.message}
228
+ `));
229
+ } else {
230
+ console.error(pc2.red("\n An unknown error occurred.\n"));
231
+ }
232
+ process.exit(1);
233
+ }
234
+ }
235
+
236
+ // src/commands/init.ts
237
+ import pc3 from "picocolors";
238
+ import matter2 from "gray-matter";
239
+
240
+ // src/lib/generator.ts
241
+ function generateClaudeMd(preset, agents) {
242
+ const lines = [];
243
+ lines.push(`# ${preset.name}`);
244
+ lines.push("");
245
+ lines.push(preset.claudemd.projectDescription);
246
+ lines.push("");
247
+ if (preset.constitution.principles.length > 0) {
248
+ lines.push("## Principles");
249
+ for (const p of preset.constitution.principles) {
250
+ lines.push(`- ${p}`);
251
+ }
252
+ lines.push("");
253
+ }
254
+ if (preset.constitution.stack.length > 0) {
255
+ lines.push("## Stack");
256
+ for (const s of preset.constitution.stack) {
257
+ lines.push(`- ${s}`);
258
+ }
259
+ lines.push("");
260
+ }
261
+ if (preset.constitution.conventions.length > 0) {
262
+ lines.push("## Conventions");
263
+ for (const c of preset.constitution.conventions) {
264
+ lines.push(`- ${c}`);
265
+ }
266
+ lines.push("");
267
+ }
268
+ if (preset.constitution.customSections) {
269
+ for (const [title, content] of Object.entries(preset.constitution.customSections)) {
270
+ lines.push(`## ${title}`);
271
+ lines.push(content);
272
+ lines.push("");
273
+ }
274
+ }
275
+ if (agents.length > 0) {
276
+ lines.push("## Agents");
277
+ for (const agent of agents) {
278
+ lines.push(`- **${agent.slug}**: ${agent.name} \u2014 ${agent.role}`);
279
+ }
280
+ lines.push("");
281
+ }
282
+ if (preset.claudemd.orchestratorRef) {
283
+ lines.push("## Orchestrator");
284
+ lines.push(preset.claudemd.orchestratorRef);
285
+ lines.push("");
286
+ }
287
+ return lines.join("\n");
288
+ }
289
+
290
+ // src/commands/init.ts
291
+ async function initCommand(presetSlug) {
292
+ try {
293
+ let preset;
294
+ if (!presetSlug) {
295
+ const presets = await listPresets();
296
+ if (presets.length === 0) {
297
+ console.error(pc3.red("\n No presets available.\n"));
298
+ process.exit(1);
299
+ }
300
+ console.log(pc3.bold(pc3.cyan("\n Available presets:\n")));
301
+ presets.forEach((p, i) => {
302
+ console.log(` ${pc3.bold(String(i + 1))}. ${pc3.green(p.slug)} \u2014 ${p.description}`);
303
+ });
304
+ const { createInterface } = await import("readline");
305
+ const rl = createInterface({
306
+ input: process.stdin,
307
+ output: process.stdout
308
+ });
309
+ const answer = await new Promise((resolve) => {
310
+ rl.question(pc3.cyan("\n Choose a preset (number): "), (ans) => {
311
+ rl.close();
312
+ resolve(ans.trim());
313
+ });
314
+ });
315
+ const index = parseInt(answer, 10) - 1;
316
+ if (isNaN(index) || index < 0 || index >= presets.length) {
317
+ console.error(pc3.red("\n Invalid selection.\n"));
318
+ process.exit(1);
319
+ }
320
+ presetSlug = presets[index].slug;
321
+ preset = await getPreset(presetSlug);
322
+ } else {
323
+ preset = await getPreset(presetSlug);
324
+ }
325
+ console.log(pc3.bold(pc3.cyan(`
326
+ Initializing preset "${preset.name}"...
327
+ `)));
328
+ const agentResults = await Promise.allSettled(
329
+ preset.agents.map((slug) => getAgent(slug))
330
+ );
331
+ const skillResults = await Promise.allSettled(
332
+ preset.skills.map((slug) => getSkill(slug))
333
+ );
334
+ const agentInfos = [];
335
+ for (let i = 0; i < preset.agents.length; i++) {
336
+ const slug = preset.agents[i];
337
+ const result = agentResults[i];
338
+ if (result.status === "fulfilled") {
339
+ writeAgent(slug, result.value.rawContent);
340
+ const { data } = matter2(result.value.rawContent);
341
+ const fm = data;
342
+ agentInfos.push({
343
+ slug,
344
+ name: fm.name || slug,
345
+ role: fm.role || ""
346
+ });
347
+ console.log(pc3.green(` \u2713 Agent: ${slug}`));
348
+ } else {
349
+ console.log(pc3.yellow(` \u26A0 Agent "${slug}" skipped: ${result.reason}`));
350
+ }
351
+ }
352
+ for (let i = 0; i < preset.skills.length; i++) {
353
+ const slug = preset.skills[i];
354
+ const result = skillResults[i];
355
+ if (result.status === "fulfilled") {
356
+ writeSkill(slug, result.value.rawContent);
357
+ console.log(pc3.green(` \u2713 Skill: ${slug}`));
358
+ } else {
359
+ console.log(pc3.yellow(` \u26A0 Skill "${slug}" skipped: ${result.reason}`));
360
+ }
361
+ }
362
+ const claudeContent = generateClaudeMd(preset, agentInfos);
363
+ writeClaudeMd(claudeContent);
364
+ console.log(pc3.green(` \u2713 CLAUDE.md generated`));
365
+ const agentOk = agentResults.filter((r) => r.status === "fulfilled").length;
366
+ const skillOk = skillResults.filter((r) => r.status === "fulfilled").length;
367
+ console.log(
368
+ pc3.bold(
369
+ pc3.cyan(
370
+ `
371
+ Done! ${agentOk} agent(s), ${skillOk} skill(s), CLAUDE.md ready.
372
+ `
373
+ )
374
+ )
375
+ );
376
+ } catch (error) {
377
+ if (error instanceof Error) {
378
+ console.error(pc3.red(`
379
+ Error: ${error.message}
380
+ `));
381
+ } else {
382
+ console.error(pc3.red("\n An unknown error occurred.\n"));
383
+ }
384
+ process.exit(1);
385
+ }
386
+ }
387
+
388
+ // src/index.ts
389
+ var program = new Command();
390
+ program.name("loom").description("Integrate Loom library (agents, skills, presets) into your project").version("0.1.0");
391
+ program.command("list").description("List available agents, skills, and presets").argument("[type]", "Filter by type: agents, skills, or presets").action(async (type) => {
392
+ await listCommand(type);
393
+ });
394
+ program.command("add").description("Download an agent or skill from the library").argument("<type>", "Type: agent or skill").argument("<slug>", "Slug of the agent or skill").action(async (type, slug) => {
395
+ await addCommand(type, slug);
396
+ });
397
+ program.command("init").description("Initialize a project with a preset (agents + skills + CLAUDE.md)").argument("[preset]", "Preset slug (interactive if omitted)").action(async (preset) => {
398
+ await initCommand(preset);
399
+ });
400
+ program.parse();