@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.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/data/agents/backend/AGENT.md +55 -0
- package/data/agents/database/AGENT.md +58 -0
- package/data/agents/frontend/AGENT.md +51 -0
- package/data/agents/marketing/AGENT.md +55 -0
- package/data/agents/orchestrator/AGENT.md +47 -0
- package/data/agents/performance/AGENT.md +70 -0
- package/data/agents/review-qa/AGENT.md +66 -0
- package/data/agents/security/AGENT.md +96 -0
- package/data/agents/tests/AGENT.md +63 -0
- package/data/agents/ux-ui/AGENT.md +58 -0
- package/data/presets/saas-default.yaml +53 -0
- package/data/skills/hero-copywriting/SKILL.md +64 -0
- package/data/skills/nextjs-conventions/SKILL.md +64 -0
- package/data/skills/tailwind-patterns/SKILL.md +59 -0
- package/dist/index.js +400 -0
- package/package.json +51 -0
|
@@ -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();
|