feat: Phase 45 — Content as Skills (9 SKILL.md files, Creative group, gap fixes)
This commit is contained in:
parent
d25d88d053
commit
1c8a26dbb4
27 changed files with 2554 additions and 59 deletions
|
|
@ -1,5 +1,35 @@
|
|||
# Milestones
|
||||
|
||||
## v1.7 Content Generation (Shipped: 2026-04-05)
|
||||
|
||||
**Phases completed:** 6 phases, 21 plans, 38 tasks
|
||||
|
||||
**Key accomplishments:**
|
||||
|
||||
- One-liner:
|
||||
- One-liner:
|
||||
- culori/resvg/svgo deps installed, RenderResult bundle types defined, content-job-runner wired to diagram/icon-set/theme-palette switch, and useContentJob SSE hook ready for UI plans
|
||||
- Diagram renderer synthesizes Mermaid from natural language via LLM (DIAG-01), strips unsafe directives (DIAG-05), renders SVG+PNG via Playwright+DOMPurify+Resvg; icon renderer generates SVG icon sets via LLM, cleans with SVGO, rasterizes to 3 PNG sizes via sharp
|
||||
- OKLCH palette engine with 7-role dark/light generation from a single hex seed, WCAG AA validation via culori+wcag-contrast, four export formatters (CSS custom props, Tailwind config, VS Code theme, JSON), and nexus-settings.json extended with customTheme persistence
|
||||
- ContentStudio page with Diagrams and Icons tabs fully functional: prompt+type selector, generate button, SSE progress, SVG preview with download, collapsible source editor (TDD-tested), icon grid with selection and bulk download bar
|
||||
- All theme UI components built and wired: seed input, palette grid with WCAG badges, scoped live preview (with TDD test coverage for THEME-04), 4-format export tabs, apply confirmation dialog, ThemeContext custom theme extension, ContentStudio Themes tab fully functional
|
||||
- Full test suite run (30 server + 13 UI component tests passing), server tsc clean, UI tsc clean for Phase 41 files; ThemeContext backward-compat exports restored to fix regression from 41-05 worktree commit
|
||||
- file-type/xlsx/csv-parse installed, four bundle types added to types.ts, wallpaper/social-post/convert cases wired in content-job-runner, and converter-capabilities service probing pandoc/libreoffice via new execFileNoThrow utility
|
||||
- LLM SVG wallpaper generation rasterized via sharp at 12 platform dimensions, plus platform-aware social post JSON renderer with hashtags and Instagram carousel support
|
||||
- Full convert-renderer.ts routing sharp/ffmpeg/xlsx/AI-bridge by format pair, multipart upload route with magic-byte MIME validation returning 202, and GET /api/system/converters capability endpoint
|
||||
- useSystemProviders hook wires /api/system/providers to ChatInput offline badge, surfacing local Whisper availability via WifiOff icon when voice mode is active
|
||||
- Four React components + extended ContentStudio tabs: wallpaper generator with 12-platform grouped selector and multi-size app icon grid, social post generator with live character count and hashtag chip copy, carousel collapsible slides
|
||||
- Drag-drop ConvertPanel with grouped format chips, AI fallback notice, MIME error display, SSE job tracking, and deep-link /convert/:src/:tgt routing
|
||||
- Playwright HTML-to-PDF renderer for 4 document types (report/invoice/api-docs/one-pager) with PdfDocumentBundle/BrandKitBundle types and content-job-runner wiring
|
||||
- Full brand identity kit renderer orchestrating logo SVG, 5 avatar sizes, 5 social platform images, email signature HTML, letterhead HTML, guidelines PDF, and ZIP packaging via archiver
|
||||
- DocumentGeneratePanel, BrandKitPanel, BrandKitResult UI components with pdf-document and brand-kit job submission, PDF/ZIP download, and Documents/Brand tabs added to ContentStudio (7 tabs total)
|
||||
- Remotion 4.0.445 workspace package with PitchDeck/DemoVideo compositions, cached getBundlePath, concurrency:1 renderPresentationComposition, and presentation job wiring in content-job-runner
|
||||
- renderPresentation function with LLM pitch-deck/demo-video slide generation (puterChatComplete), Remotion MP4 rendering (concurrency:1 via content-renderer), and SSE content_job.progress events
|
||||
- PresentationPanel with real-time SSE progress bar, MP4 video player + download, and Presentations tab in ContentStudio — backed by fine-grained data.progress in useContentJob
|
||||
- 9 content generators registered as installable Nexus skills via local-nexus-content source type, with Creative group seeded and startup sequence unified
|
||||
|
||||
---
|
||||
|
||||
## v1.6 Voice Pipeline + Minimal Message Bridge (Shipped: 2026-04-04)
|
||||
|
||||
**Phases completed:** 4 phases, 12 plans, 14 tasks
|
||||
|
|
|
|||
|
|
@ -93,9 +93,9 @@ Requirements for Content Generation milestone. Each maps to roadmap phases.
|
|||
|
||||
### Content as Skills
|
||||
|
||||
- [ ] **SKILL-01**: Each content type is implemented as an installable Nexus skill
|
||||
- [ ] **SKILL-02**: Generalist agent is pre-loaded with a "Creative" skill group
|
||||
- [ ] **SKILL-03**: Users can add or remove content type skills through the Skill Aggregator
|
||||
- [x] **SKILL-01**: Each content type is implemented as an installable Nexus skill
|
||||
- [x] **SKILL-02**: Generalist agent is pre-loaded with a "Creative" skill group
|
||||
- [x] **SKILL-03**: Users can add or remove content type skills through the Skill Aggregator
|
||||
|
||||
## Future Requirements
|
||||
|
||||
|
|
@ -179,9 +179,9 @@ Which phases cover which requirements. Updated during roadmap creation.
|
|||
| PRES-02 | Phase 44 | Complete |
|
||||
| PRES-03 | Phase 44 | Complete |
|
||||
| PRES-04 | Phase 44 | Complete |
|
||||
| SKILL-01 | Phase 45 | Pending |
|
||||
| SKILL-02 | Phase 45 | Pending |
|
||||
| SKILL-03 | Phase 45 | Pending |
|
||||
| SKILL-01 | Phase 45 | Complete |
|
||||
| SKILL-02 | Phase 45 | Complete |
|
||||
| SKILL-03 | Phase 45 | Complete |
|
||||
|
||||
**Coverage:**
|
||||
- v1.7 requirements: 52 total
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ Plans:
|
|||
- [x] **Phase 42: Wallpapers, Social, Format Conversion & Voice** — LLM SVG + sharp wallpapers, social content, format conversion registry with AI fallback, Whisper web chat mic (WALL-01..04, SOCIAL-01..03, CONV-01..09, VOICE-01..03) (completed 2026-04-04)
|
||||
- [x] **Phase 43: Documents & Branding** — Playwright PDF reports and invoices, full brand identity kit with zip export (DOC-01..03, BRAND-01..06) (completed 2026-04-04)
|
||||
- [x] **Phase 44: Video & Presentations** — Remotion workspace package, pitch decks and demo videos, SSE render progress (PRES-01..04) (completed 2026-04-04)
|
||||
- [ ] **Phase 45: Content as Skills** — Markdown skill files for all content types, Creative skill group on generalist agent (SKILL-01..03)
|
||||
- [x] **Phase 45: Content as Skills** — Markdown skill files for all content types, Creative skill group on generalist agent (SKILL-01..03) (completed 2026-04-04)
|
||||
|
||||
## Phase Details
|
||||
|
||||
|
|
@ -278,8 +278,8 @@ Plans:
|
|||
|
||||
Plans:
|
||||
- [x] 44-01-PLAN.md — Remotion workspace package, compositions, shared constants, types, job-runner wiring
|
||||
- [ ] 44-02-PLAN.md — Presentation renderer with LLM slide generation, Remotion render, SSE progress
|
||||
- [ ] 44-03-PLAN.md — PresentationPanel UI, useContentJob progress extension, ContentStudio tab
|
||||
- [x] 44-02-PLAN.md — Presentation renderer with LLM slide generation, Remotion render, SSE progress
|
||||
- [x] 44-03-PLAN.md — PresentationPanel UI, useContentJob progress extension, ContentStudio tab
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 45: Content as Skills
|
||||
|
|
@ -290,7 +290,10 @@ Plans:
|
|||
1. Each content type (diagram, theme, icon, wallpaper, social post, PDF, brand kit, video) has a corresponding skill file that an agent can load and use to call the correct content job API
|
||||
2. A freshly created generalist agent has the Creative skill group pre-loaded — it can generate diagrams and themes without any manual skill configuration
|
||||
3. A user can add or remove individual content type skills through the Skill Aggregator UI without touching configuration files
|
||||
**Plans**: TBD
|
||||
**Plans**: 1 plan
|
||||
|
||||
Plans:
|
||||
- [x] 45-01-PLAN.md — 9 SKILL.md files, local-nexus-content source type, Creative group seeding, startup wiring
|
||||
**UI hint**: yes
|
||||
|
||||
---
|
||||
|
|
@ -387,4 +390,4 @@ All 52 v1.7 requirements are mapped to exactly one phase. No orphans.
|
|||
| 42. Wallpapers, Social, Format Conversion & Voice | v1.7 | 6/6 | Complete | 2026-04-04 |
|
||||
| 43. Documents & Branding | v1.7 | 3/3 | Complete | 2026-04-04 |
|
||||
| 44. Video & Presentations | v1.7 | 3/3 | Complete | 2026-04-04 |
|
||||
| 45. Content as Skills | v1.7 | 0/TBD | Not started | - |
|
||||
| 45. Content as Skills | v1.7 | 1/1 | Complete | 2026-04-04 |
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ gsd_state_version: 1.0
|
|||
milestone: v1.7
|
||||
milestone_name: Content Generation
|
||||
status: verifying
|
||||
stopped_at: Completed 44-02-PLAN.md — presentation-renderer with LLM slide generation, Remotion render pipeline, SSE progress events, tsc compilation verified
|
||||
last_updated: "2026-04-04T23:36:43.205Z"
|
||||
last_activity: 2026-04-04
|
||||
stopped_at: Completed 45-01-PLAN.md — 9 content SKILL.md files, local-nexus-content source type, Creative group seeded, unified startup sequence, tsc clean
|
||||
last_updated: "2026-04-05T08:46:16.382Z"
|
||||
last_activity: 2026-04-05
|
||||
progress:
|
||||
total_phases: 6
|
||||
completed_phases: 5
|
||||
total_plans: 20
|
||||
completed_plans: 20
|
||||
completed_phases: 6
|
||||
total_plans: 21
|
||||
completed_plans: 21
|
||||
percent: 0
|
||||
---
|
||||
|
||||
|
|
@ -21,14 +21,14 @@ progress:
|
|||
See: .planning/PROJECT.md (updated 2026-04-04)
|
||||
|
||||
**Core value:** A fresh onboard asks for ONE thing (root directory), auto-creates PM + Engineer agents, and drops you in the dashboard.
|
||||
**Current focus:** Phase 44 — video-presentations
|
||||
**Current focus:** Phase 45 — content-as-skills
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: 45
|
||||
Plan: Not started
|
||||
Status: Phase complete — ready for verification
|
||||
Last activity: 2026-04-04
|
||||
Last activity: 2026-04-05
|
||||
|
||||
Progress: [░░░░░░░░░░] 0%
|
||||
|
||||
|
|
@ -99,6 +99,7 @@ Key constraints for v1.7:
|
|||
- [Phase 44-video-presentations]: mp4Base64 blob URL created in useMemo and revoked in useEffect return — correct lifecycle for React renders
|
||||
- [Phase 44-video-presentations]: Ambient module declaration in server/src/types/content-renderer.d.ts provides type safety for dynamic import without pulling JSX into server tsc context
|
||||
- [Phase 44-video-presentations]: content-renderer NOT added as workspace dep in server/package.json — symlink causes tsc to walk JSX source; ambient declaration is sufficient for type safety and runtime works via monorepo node_modules
|
||||
- [Phase 45-content-as-skills]: SkillSourceConfig changed to discriminated union; local-nexus-content source uses SHA-1 content hash for idempotency; seedCreativeGroupMembers runs after fetchAll in unified startup block
|
||||
|
||||
### Pending Todos
|
||||
|
||||
|
|
@ -113,6 +114,6 @@ None yet.
|
|||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-04-04T23:35:57.656Z
|
||||
Stopped at: Completed 44-02-PLAN.md — presentation-renderer with LLM slide generation, Remotion render pipeline, SSE progress events, tsc compilation verified
|
||||
Last session: 2026-04-04T23:55:26.416Z
|
||||
Stopped at: Completed 45-01-PLAN.md — 9 content SKILL.md files, local-nexus-content source type, Creative group seeded, unified startup sequence, tsc clean
|
||||
Resume file: None
|
||||
|
|
|
|||
115
.planning/milestones/v1.7-MILESTONE-AUDIT.md
Normal file
115
.planning/milestones/v1.7-MILESTONE-AUDIT.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
---
|
||||
milestone: v1.7
|
||||
audited: 2026-04-05T00:00:00Z
|
||||
status: gaps_found
|
||||
scores:
|
||||
requirements: 49/55
|
||||
phases: 6/6
|
||||
integration: 49/55
|
||||
flows: 7/9
|
||||
gaps:
|
||||
requirements:
|
||||
- id: "THEME-01"
|
||||
status: "unsatisfied"
|
||||
phase: "41"
|
||||
evidence: "ThemeSeedInput prop interface mismatch — ContentStudio passes wrong props"
|
||||
- id: "THEME-02"
|
||||
status: "partial"
|
||||
phase: "41"
|
||||
evidence: "Server renderer works but UI never displays palette (bundle undefined)"
|
||||
- id: "THEME-03"
|
||||
status: "unsatisfied"
|
||||
phase: "41"
|
||||
evidence: "themeJob.bundle does not exist on useContentJob — palette grid never renders"
|
||||
- id: "THEME-04"
|
||||
status: "unsatisfied"
|
||||
phase: "41"
|
||||
evidence: "ThemePreviewPanel never renders — gated on undefined bundle"
|
||||
- id: "THEME-05"
|
||||
status: "unsatisfied"
|
||||
phase: "41"
|
||||
evidence: "ThemeExportTabs never renders — gated on undefined bundle"
|
||||
- id: "THEME-06"
|
||||
status: "unsatisfied"
|
||||
phase: "41"
|
||||
evidence: "ThemeApplyConfirmDialog receives onOpenChange but expects onCancel prop"
|
||||
- id: "CONV-09"
|
||||
status: "unsatisfied"
|
||||
phase: "42"
|
||||
evidence: "convert.ts passes {} as StorageService — storage.putFile throws at runtime"
|
||||
- id: "SKILL-02"
|
||||
status: "partial"
|
||||
phase: "45"
|
||||
evidence: "All 9 SKILL.md files document wrong API path and 3 have wrong input field names"
|
||||
integration:
|
||||
- from: "Phase 41-05 ThemeSeedInput"
|
||||
to: "Phase 41-06 ContentStudio Themes tab"
|
||||
issue: "Incompatible prop interfaces from parallel worktree execution"
|
||||
- from: "Phase 42-03 convert route"
|
||||
to: "Phase 40 content-job-runner"
|
||||
issue: "StorageService passed as empty object — runtime crash on job completion"
|
||||
- from: "Phase 45 SKILL.md files"
|
||||
to: "Phase 40-42 routes and renderers"
|
||||
issue: "Wrong API path and input field names in skill documentation"
|
||||
flows:
|
||||
- name: "Themes tab"
|
||||
breaks_at: "ContentStudio.tsx — 3 prop mismatches + missing bundle state"
|
||||
- name: "Format conversion delivery"
|
||||
breaks_at: "convert.ts line 142 — StorageService is empty object"
|
||||
tech_debt: []
|
||||
nyquist:
|
||||
compliant_phases: 0
|
||||
partial_phases: 1
|
||||
missing_phases: 5
|
||||
overall: "partial"
|
||||
---
|
||||
|
||||
# Milestone v1.7 — Content Generation Audit
|
||||
|
||||
## Summary
|
||||
|
||||
| Metric | Score |
|
||||
|--------|-------|
|
||||
| Requirements | 49/55 satisfied |
|
||||
| Phases | 6/6 executed |
|
||||
| Integration | 49/55 wired |
|
||||
| E2E Flows | 7/9 complete |
|
||||
|
||||
## Unsatisfied Requirements
|
||||
|
||||
### THEME-01 through THEME-06 (Phase 41)
|
||||
The theme engine server-side implementation is complete (palette generation, WCAG validation, exports, nexus-settings persistence). However, the ContentStudio Themes tab has 3 compile-time errors from worktree merge divergence:
|
||||
1. `ThemeSeedInput` receives wrong props (`companyId/onSubmit/isLoading` vs `value/onChange/className`)
|
||||
2. `themeJob.bundle` does not exist on `useContentJob` — palette display gated on undefined
|
||||
3. `ThemeApplyConfirmDialog` receives `onOpenChange` but expects `onCancel`
|
||||
|
||||
### CONV-09 (Phase 42)
|
||||
Convert route passes `{} as StorageService` to `contentJobRunner.dispatch`. Jobs start but crash on completion when `storage.putFile` is called.
|
||||
|
||||
### SKILL-02 (Phase 45) — Partial
|
||||
All 9 SKILL.md files document `POST /api/content-jobs` but actual route is `POST /api/companies/:companyId/content-jobs`. Three files also have input field name mismatches.
|
||||
|
||||
## Working Flows (7/9)
|
||||
|
||||
| Flow | Status |
|
||||
|------|--------|
|
||||
| Diagrams | ✓ Complete |
|
||||
| Icons | ✓ Complete |
|
||||
| Wallpapers | ✓ Complete |
|
||||
| Social Posts | ✓ Complete |
|
||||
| Documents | ✓ Complete |
|
||||
| Brand Kit | ✓ Complete |
|
||||
| Presentations | ✓ Complete |
|
||||
| Themes | ✗ Broken (UI compile errors) |
|
||||
| Format Conversion | ✗ Broken (runtime crash on delivery) |
|
||||
|
||||
## Nyquist Compliance
|
||||
|
||||
| Phase | VALIDATION.md | Compliant |
|
||||
|-------|---------------|-----------|
|
||||
| 40 | exists | partial |
|
||||
| 41 | exists | partial |
|
||||
| 42 | missing | — |
|
||||
| 43 | missing | — |
|
||||
| 44 | missing | — |
|
||||
| 45 | missing | — |
|
||||
202
.planning/milestones/v1.7-REQUIREMENTS.md
Normal file
202
.planning/milestones/v1.7-REQUIREMENTS.md
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
# Requirements Archive: v1.7 Content Generation
|
||||
|
||||
**Archived:** 2026-04-05
|
||||
**Status:** SHIPPED
|
||||
|
||||
For current requirements, see `.planning/REQUIREMENTS.md`.
|
||||
|
||||
---
|
||||
|
||||
# Requirements: Nexus v1.7 — Content Generation
|
||||
|
||||
**Defined:** 2026-04-04
|
||||
**Core Value:** A fresh onboard asks for ONE thing (root directory), auto-creates PM + Engineer agents, and drops you in the dashboard — no company names, missions, or corporate language anywhere.
|
||||
|
||||
## v1.7 Requirements
|
||||
|
||||
Requirements for Content Generation milestone. Each maps to roadmap phases.
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- [x] **INFRA-01**: System processes content generation jobs asynchronously with queued → running → done/failed lifecycle
|
||||
- [x] **INFRA-02**: System pushes job progress updates via SSE to connected clients
|
||||
- [x] **INFRA-03**: Generated content stored in namespaced storage without size restrictions blocking video/images
|
||||
- [x] **INFRA-04**: All generated content tracked in database with source conversation linkage
|
||||
|
||||
### Diagram Generation
|
||||
|
||||
- [x] **DIAG-01**: User can generate diagrams from natural language description
|
||||
- [x] **DIAG-02**: System renders Mermaid syntax to SVG and PNG formats
|
||||
- [x] **DIAG-03**: User can view and edit the Mermaid source for refinement
|
||||
- [x] **DIAG-04**: System supports architecture, flowchart, ERD, sequence, and mind map diagram types
|
||||
- [x] **DIAG-05**: Mermaid rendering enforces strict security level to prevent XSS
|
||||
|
||||
### Theme & Palette
|
||||
|
||||
- [x] **THEME-01**: User can pick a seed color and receive a complete palette (background, surface, overlay, text, accents)
|
||||
- [x] **THEME-02**: System generates palette in OKLCH color space with Catppuccin-style naming
|
||||
- [x] **THEME-03**: System validates WCAG AA contrast for all foreground/background pairs
|
||||
- [x] **THEME-04**: User can preview Nexus UI with the generated palette live
|
||||
- [x] **THEME-05**: User can export palette as CSS custom properties, Tailwind config, VS Code theme, or JSON
|
||||
- [x] **THEME-06**: System generates dark and light variants from single seed color
|
||||
- [x] **THEME-07**: User can apply generated theme to their Nexus instance in one click
|
||||
|
||||
### Document Generation
|
||||
|
||||
- [x] **DOC-01**: User can generate formatted PDF reports from conversation content
|
||||
- [x] **DOC-02**: User can generate invoices and contracts from templates
|
||||
- [x] **DOC-03**: User can generate one-pagers and API documentation
|
||||
|
||||
### Icon Generation
|
||||
|
||||
- [x] **ICON-01**: User can generate SVG icons from a text description
|
||||
- [x] **ICON-02**: System produces icon sets with consistent visual style
|
||||
- [x] **ICON-03**: User can export icons in multiple sizes and formats (SVG, PNG)
|
||||
|
||||
### Wallpapers & Visual Assets
|
||||
|
||||
- [x] **WALL-01**: User can generate desktop and mobile wallpapers from a description
|
||||
- [x] **WALL-02**: User can generate social media banners with correct dimensions per platform
|
||||
- [x] **WALL-03**: User can generate Open Graph and social preview images
|
||||
- [x] **WALL-04**: User can generate app icons and favicons in multiple sizes
|
||||
|
||||
### Presentations & Video
|
||||
|
||||
- [x] **PRES-01**: User can generate pitch deck presentations from a conversation
|
||||
- [x] **PRES-02**: System renders presentations via Remotion to interactive web or MP4
|
||||
- [x] **PRES-03**: User can generate demo and explainer videos from conversation content
|
||||
- [x] **PRES-04**: System shows render progress via SSE during video generation
|
||||
|
||||
### Social Media Content
|
||||
|
||||
- [x] **SOCIAL-01**: User can generate platform-ready posts respecting character limits (Twitter, LinkedIn)
|
||||
- [x] **SOCIAL-02**: User can generate Instagram carousels and thread sequences
|
||||
- [x] **SOCIAL-03**: System suggests relevant hashtags for generated content
|
||||
|
||||
### Branding Media Kit
|
||||
|
||||
- [x] **BRAND-01**: User can generate a full brand identity from a single conversation
|
||||
- [x] **BRAND-02**: System produces logo mark (SVG), avatar in multiple sizes
|
||||
- [x] **BRAND-03**: System produces social media profile images and banners per platform
|
||||
- [x] **BRAND-04**: System produces email signature and letterhead templates
|
||||
- [x] **BRAND-05**: System produces a brand guidelines document (PDF)
|
||||
- [x] **BRAND-06**: User can download all brand assets as a zip package
|
||||
|
||||
### Format Conversion
|
||||
|
||||
- [x] **CONV-01**: User can convert between image formats (PNG, JPG, SVG, WebP, GIF) via sharp
|
||||
- [x] **CONV-02**: User can convert between audio/video formats via ffmpeg
|
||||
- [x] **CONV-03**: User can convert between document formats (Markdown, HTML, PDF, DOCX) via Pandoc/LibreOffice
|
||||
- [x] **CONV-04**: User can convert between data formats (CSV, JSON, XLSX) via direct tooling
|
||||
- [x] **CONV-05**: User can convert between any format pair via AI-bridged conversion for semantically complex transforms
|
||||
- [x] **CONV-06**: System provides a conversion UI with source/target format selection and drag-drop input
|
||||
- [x] **CONV-07**: User can deep-link to specific conversion flows via URL (e.g. `/convert/png/svg`)
|
||||
- [x] **CONV-08**: System detects available direct converters at startup and degrades gracefully — unavailable direct paths fall through to AI-bridged conversion rather than showing as blocked
|
||||
- [x] **CONV-09**: System validates uploaded file MIME type via magic-byte detection before processing
|
||||
|
||||
### Whisper Web Chat
|
||||
|
||||
- [x] **VOICE-01**: User can click a mic button in web chat to record and auto-transcribe via Whisper
|
||||
- [x] **VOICE-02**: User can toggle between text-only, voice-input, and full-voice modes
|
||||
- [x] **VOICE-03**: Voice input works offline with local Whisper model
|
||||
|
||||
### Content as Skills
|
||||
|
||||
- [x] **SKILL-01**: Each content type is implemented as an installable Nexus skill
|
||||
- [x] **SKILL-02**: Generalist agent is pre-loaded with a "Creative" skill group
|
||||
- [x] **SKILL-03**: Users can add or remove content type skills through the Skill Aggregator
|
||||
|
||||
## Future Requirements
|
||||
|
||||
Deferred to future release. Tracked but not in current roadmap.
|
||||
|
||||
### AI Image Generation
|
||||
|
||||
- **AIGEN-01**: User can generate images via local Stable Diffusion / ComfyUI
|
||||
- **AIGEN-02**: User can generate images via cloud APIs (DALL-E, Midjourney)
|
||||
|
||||
### Advanced Voice
|
||||
|
||||
- **AVOICE-01**: Wake word detection ("Hey Nexus")
|
||||
- **AVOICE-02**: Voice call / real-time audio streaming
|
||||
|
||||
## Out of Scope
|
||||
|
||||
| Feature | Reason |
|
||||
|---------|--------|
|
||||
| AI image generation (SD/DALL-E) | VRAM conflicts with LLM on M4; cloud sends data externally |
|
||||
| Social media publishing | API rate limits, auth complexity; generation only for v1.7 |
|
||||
| Batch conversion queue | Single-user deployment; one-at-a-time sufficient |
|
||||
| Real-time collaborative editing of generated content | Single-user target |
|
||||
| Wake word detection | Future consideration |
|
||||
| Voice call / real-time audio streaming | Future consideration |
|
||||
|
||||
## Traceability
|
||||
|
||||
Which phases cover which requirements. Updated during roadmap creation.
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| INFRA-01 | Phase 40 | Complete |
|
||||
| INFRA-02 | Phase 40 | Complete |
|
||||
| INFRA-03 | Phase 40 | Complete |
|
||||
| INFRA-04 | Phase 40 | Complete |
|
||||
| DIAG-01 | Phase 41 | Complete |
|
||||
| DIAG-02 | Phase 41 | Complete |
|
||||
| DIAG-03 | Phase 41 | Complete |
|
||||
| DIAG-04 | Phase 41 | Complete |
|
||||
| DIAG-05 | Phase 41 | Complete |
|
||||
| THEME-01 | Phase 41 | Complete |
|
||||
| THEME-02 | Phase 41 | Complete |
|
||||
| THEME-03 | Phase 41 | Complete |
|
||||
| THEME-04 | Phase 41 | Complete |
|
||||
| THEME-05 | Phase 41 | Complete |
|
||||
| THEME-06 | Phase 41 | Complete |
|
||||
| THEME-07 | Phase 41 | Complete |
|
||||
| ICON-01 | Phase 41 | Complete |
|
||||
| ICON-02 | Phase 41 | Complete |
|
||||
| ICON-03 | Phase 41 | Complete |
|
||||
| WALL-01 | Phase 42 | Complete |
|
||||
| WALL-02 | Phase 42 | Complete |
|
||||
| WALL-03 | Phase 42 | Complete |
|
||||
| WALL-04 | Phase 42 | Complete |
|
||||
| SOCIAL-01 | Phase 42 | Complete |
|
||||
| SOCIAL-02 | Phase 42 | Complete |
|
||||
| SOCIAL-03 | Phase 42 | Complete |
|
||||
| CONV-01 | Phase 42 | Complete |
|
||||
| CONV-02 | Phase 42 | Complete |
|
||||
| CONV-03 | Phase 42 | Complete |
|
||||
| CONV-04 | Phase 42 | Complete |
|
||||
| CONV-05 | Phase 42 | Complete |
|
||||
| CONV-06 | Phase 42 | Complete |
|
||||
| CONV-07 | Phase 42 | Complete |
|
||||
| CONV-08 | Phase 42 | Complete |
|
||||
| CONV-09 | Phase 42 | Complete |
|
||||
| VOICE-01 | Phase 42 | Complete |
|
||||
| VOICE-02 | Phase 42 | Complete |
|
||||
| VOICE-03 | Phase 42 | Complete |
|
||||
| DOC-01 | Phase 43 | Complete |
|
||||
| DOC-02 | Phase 43 | Complete |
|
||||
| DOC-03 | Phase 43 | Complete |
|
||||
| BRAND-01 | Phase 43 | Complete |
|
||||
| BRAND-02 | Phase 43 | Complete |
|
||||
| BRAND-03 | Phase 43 | Complete |
|
||||
| BRAND-04 | Phase 43 | Complete |
|
||||
| BRAND-05 | Phase 43 | Complete |
|
||||
| BRAND-06 | Phase 43 | Complete |
|
||||
| PRES-01 | Phase 44 | Complete |
|
||||
| PRES-02 | Phase 44 | Complete |
|
||||
| PRES-03 | Phase 44 | Complete |
|
||||
| PRES-04 | Phase 44 | Complete |
|
||||
| SKILL-01 | Phase 45 | Complete |
|
||||
| SKILL-02 | Phase 45 | Complete |
|
||||
| SKILL-03 | Phase 45 | Complete |
|
||||
|
||||
**Coverage:**
|
||||
- v1.7 requirements: 52 total
|
||||
- Mapped to phases: 52
|
||||
- Unmapped: 0
|
||||
|
||||
---
|
||||
*Requirements defined: 2026-04-04*
|
||||
*Last updated: 2026-04-04 after roadmap creation (v1.7)*
|
||||
393
.planning/milestones/v1.7-ROADMAP.md
Normal file
393
.planning/milestones/v1.7-ROADMAP.md
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
# Roadmap: Nexus
|
||||
|
||||
## Milestones
|
||||
|
||||
- ✅ **v1.2.1 Universal Skill Management** - Phase 1 (shipped 2026-04-01)
|
||||
- ✅ **v1.3 Chat & PWA** - Phases 21-26 (shipped 2026-04-02)
|
||||
- ✅ **v1.4 Hermes Default Provider** - Phases 27-29 (shipped 2026-04-02)
|
||||
- ✅ **v1.5 Smart Onboarding + Personal AI Assistant** - Phases 30-35 (shipped 2026-04-03)
|
||||
- ✅ **v1.6 Voice Pipeline + Minimal Message Bridge** - Phases 36-39 (shipped 2026-04-04)
|
||||
- 🚧 **v1.7 Content Generation** - Phases 40-45 (in progress)
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary>✅ v1.2.1 Universal Skill Management (Phase 1) - SHIPPED 2026-04-01</summary>
|
||||
|
||||
### Phase 1: Foundation
|
||||
**Goal**: Establish the display-layer rename infrastructure, git hygiene tooling, and rebase safety primitives that all subsequent phases depend on
|
||||
**Plans**: 2/2 plans complete
|
||||
|
||||
Plans:
|
||||
- [x] 01-01-PLAN.md — Branding package, VOCAB constants, commit-msg hook
|
||||
- [x] 01-02-PLAN.md — Zone taxonomy, rerere config, rebase safety infrastructure
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>✅ v1.3 Chat & PWA (Phases 21-26) - SHIPPED 2026-04-02</summary>
|
||||
|
||||
### Phase 21: Chat Foundation
|
||||
**Goal**: Users can have real-time chat conversations with agents
|
||||
**Plans**: 7/7 plans complete
|
||||
|
||||
### Phase 22: Agent Streaming
|
||||
**Goal**: Agent responses stream in real-time with identity, edit, retry, and stop controls
|
||||
**Plans**: 5/5 plans complete
|
||||
|
||||
### Phase 23: Brainstormer Flow
|
||||
**Goal**: Users can turn a chat conversation into a tracked project with one handoff action
|
||||
**Plans**: 4/4 plans complete
|
||||
|
||||
### Phase 24: Search, History & Branching
|
||||
**Goal**: Users can find, bookmark, branch, and export any conversation
|
||||
**Plans**: 4/4 plans complete
|
||||
|
||||
### Phase 25: File System
|
||||
**Goal**: Users can upload, preview, and version files within chat; voice input transcribes speech to text
|
||||
**Plans**: 9/9 plans complete
|
||||
|
||||
### Phase 26: PWA & Performance
|
||||
**Goal**: Nexus installs as a PWA, works offline, and loads fast on mobile
|
||||
**Plans**: 5/5 plans complete
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>✅ v1.4 Hermes Default Provider (Phases 27-29) - SHIPPED 2026-04-02</summary>
|
||||
|
||||
### Phase 27: Hermes Adapter
|
||||
**Goal**: Users can create a Hermes agent in Nexus, configure it, and have it execute heartbeats that spawn `hermes chat -q`, return a result, and persist the session across runs
|
||||
**Plans**: 1/1 plans complete
|
||||
|
||||
### Phase 28: Ollama Integration & Agent Surface
|
||||
**Goal**: Users can see which Ollama models are available, get a recommendation for their hardware, configure any Hermes agent to use a local model, and see Hermes-specific runtime data in the dashboard and agent config
|
||||
**Plans**: 3/3 plans complete
|
||||
|
||||
### Phase 29: Default Provider & End-to-End
|
||||
**Goal**: A fresh Nexus install with only Hermes and Ollama works end-to-end — onboarding offers Hermes as the default, PM and Engineer templates run correctly on the Hermes runtime, and GSD workflow tasks complete successfully
|
||||
**Plans**: 2/2 plans complete
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>✅ v1.5 Smart Onboarding + Personal AI Assistant (Phases 30-35) - SHIPPED 2026-04-03</summary>
|
||||
|
||||
### Phase 30: Hardware Detection + Mode Selection
|
||||
**Goal**: Users see accurate hardware information during onboarding, get a model recommendation matched to their machine, and choose a mode that correctly gates all downstream features
|
||||
**Plans**: 2/2 plans complete
|
||||
|
||||
### Phase 31: Puter.js Zero-Config Cloud
|
||||
**Goal**: Users without Ollama installed can reach working AI in one click via Puter.js
|
||||
**Plans**: 4/4 plans complete
|
||||
|
||||
### Phase 32: Multi-Step Onboarding Wizard
|
||||
**Goal**: Users move through a complete, skippable onboarding flow that assembles hardware data, provider selection, and voice options into a summary screen
|
||||
**Plans**: 1/1 plans complete
|
||||
|
||||
### Phase 33: Persistent Memory + Personal Assistant Mode
|
||||
**Goal**: Users in Personal AI Assistant mode accumulate memory across sessions that shapes future responses
|
||||
**Plans**: 3/3 plans complete
|
||||
|
||||
### Phase 34: Voice
|
||||
**Goal**: Users can speak to the assistant (Whisper STT) and hear responses read aloud (Piper TTS)
|
||||
**Plans**: 2/2 plans complete
|
||||
|
||||
### Phase 35: npx buildthis CLI
|
||||
**Goal**: A developer can run `npx buildthis` on a fresh machine and either open an already-running Nexus or be guided through install
|
||||
**Plans**: 1/1 plans complete
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>✅ v1.6 Voice Pipeline + Minimal Message Bridge (Phases 36-39) - SHIPPED 2026-04-04</summary>
|
||||
|
||||
### Phase 36: Voice Pipeline Foundation
|
||||
**Goal**: The transport-agnostic voice pipeline is live and callable from any consumer — web chat, Telegram, or future integrations — with correct audio transcoding, voice mode flag propagation, and dual output formatting baked in from the start
|
||||
**Depends on**: Phase 35 (v1.5 shipped)
|
||||
**Requirements**: VPIPE-01, VPIPE-02, VPIPE-03, VPIPE-04, VPIPE-05, VPIPE-06
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Posting a WAV audio file to `POST /api/transcribe` returns a transcription with detected language, regardless of whether the request came from the web UI or a test harness
|
||||
2. Calling `POST /api/synthesize` with a markdown-heavy agent response returns two outputs: a voice-optimized prose version (no markdown) and the original full text with code blocks
|
||||
3. A WebM/Opus browser recording and an OGG/Opus Telegram voice note both produce identical Whisper transcription quality after ffmpeg transcodes each to WAV 16kHz mono
|
||||
4. The `voiceMode` flag on a chat message survives from client request through Express route to message persistence — verifiable in the DB record
|
||||
5. `nexus-settings.json` accepts `voiceMode: "text" | "voice_input" | "full_voice"` and `telegramToken` fields without breaking existing settings reads
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [x] 36-01-PLAN.md — VoicePipelineService: ffmpeg transcoding, Whisper STT, Piper TTS, formatForVoice
|
||||
- [x] 36-02-PLAN.md — Schema extensions: voiceMode in shared validators/types + nexus-settings
|
||||
- [ ] 36-03-PLAN.md — Voice routes, chat.ts voiceMode wiring, app.ts mount, old transcribe removal
|
||||
|
||||
### Phase 37: Web Chat Voice UI
|
||||
**Goal**: Users can speak to any agent in web chat — recording auto-stops on silence, a live waveform confirms the mic is active, responses play back automatically (toggleable), and voice mode is a first-class setting
|
||||
**Depends on**: Phase 36
|
||||
**Requirements**: WCHAT-01, WCHAT-02, WCHAT-03, WCHAT-04, WCHAT-05, WCHAT-06
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Clicking the mic button starts recording; the waveform animates to show audio levels; speaking and then pausing for 1.5 seconds auto-submits the recording without pressing any button
|
||||
2. The voice mode toggle has three visible states (text only / voice input / full voice) and persists the selected mode across page refreshes
|
||||
3. An agent response delivered in full voice mode plays back automatically in the chat thread; the auto-play can be turned off in settings and stays off after a page reload
|
||||
4. The chat message for a voice interaction shows a voice badge and an expandable section revealing the full markdown response with code blocks intact
|
||||
5. Voice recording and VAD work correctly in Chrome and Firefox on the Mac Mini (COOP/COEP headers satisfy SharedArrayBuffer requirements)
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [x] 44-01-PLAN.md — Remotion workspace package, compositions, shared constants, types, job-runner wiring
|
||||
- [x] 44-02-PLAN.md — Presentation renderer with LLM slide generation, Remotion render, SSE progress
|
||||
- [x] 44-03-PLAN.md — PresentationPanel UI, useContentJob progress extension, ContentStudio tab
|
||||
**UI hint**: yes
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 38: Telegram Bridge
|
||||
**Goal**: The user can message any Nexus agent from their phone via Telegram — text and voice notes both work, agent identity is visible on every reply, and the bot is set up through guided onboarding with no manual token entry in config files
|
||||
**Depends on**: Phase 36
|
||||
**Requirements**: TGRAM-01, TGRAM-02, TGRAM-03, TGRAM-04, TGRAM-05, TGRAM-06, ONBRD-03
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Sending a text message to the Nexus Telegram bot from a phone produces an agent reply prefixed with the agent name (e.g. `[PM]: response`) within 10 seconds
|
||||
2. Sending a voice note to the Telegram bot produces a transcription confirmation message followed by the agent's text reply — the bot does not silently fail or miss the update
|
||||
3. Requesting a voice reply from the bot returns an OGG voice note that plays back correctly in the Telegram mobile app
|
||||
4. The Telegram bridge runs via long polling with no public HTTPS endpoint required — verified by running on the Mac Mini behind NAT
|
||||
5. The entire `telegram.ts` service file is under 500 lines
|
||||
6. The onboarding wizard includes a BotFather setup step that walks through creating a bot token and saves it to `nexus-settings.json` without manual file editing
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [x] 44-01-PLAN.md — Remotion workspace package, compositions, shared constants, types, job-runner wiring
|
||||
- [ ] 44-02-PLAN.md — Presentation renderer with LLM slide generation, Remotion render, SSE progress
|
||||
- [x] 44-03-PLAN.md — PresentationPanel UI, useContentJob progress extension, ContentStudio tab
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 39: Voice Polish
|
||||
**Goal**: Voice responses begin playing before synthesis is complete (sentence-buffered), a single response can be synthesized in multiple languages simultaneously, and new installs can detect STT/TTS hardware capability during onboarding and enable voice in one step
|
||||
**Depends on**: Phase 37
|
||||
**Requirements**: VPIPE-07, VPIPE-08, ONBRD-01, ONBRD-02
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. For a multi-sentence agent response, the first sentence begins playing in the browser before the second sentence has finished synthesizing — the gap between text completion and first audio is under 1 second
|
||||
2. A user can request the same agent response as audio in both English and Danish; both OGG files are generated and available for playback without a second agent call
|
||||
3. On a fresh install, the onboarding hardware probe reports whether Whisper STT and Piper TTS are runnable on the detected hardware tier
|
||||
4. The onboarding voice step activates (showing enable/skip options) only when the hardware probe confirms sufficient capability; on hardware below threshold it shows a capability note and skips to the next step
|
||||
**Plans**: 2 plans
|
||||
|
||||
Plans:
|
||||
- [x] 39-01-PLAN.md — Sentence-buffered TTS streaming + multi-language synthesis
|
||||
- [ ] 39-02-PLAN.md — Onboarding voice hardware capability probe
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 🚧 v1.7 Content Generation (In Progress)
|
||||
|
||||
**Milestone Goal:** Agents produce real deliverables — diagrams, themes, PDFs, wallpapers, social assets, icons, and video — entirely on-device. Every content type is an installable skill. Long-running renders are async with SSE progress from the first request.
|
||||
|
||||
## Phases
|
||||
|
||||
- [x] **Phase 40: Job Infrastructure** — content_jobs table, async render lifecycle, SSE progress events, namespaced storage without size limit (INFRA-01..04) (completed 2026-04-04)
|
||||
- [x] **Phase 41: Diagrams, Icons & Theme Engine** — Mermaid diagrams, SVG icon generation, OKLCH theme palette with WCAG AA and live preview (DIAG-01..05, ICON-01..03, THEME-01..07) (completed 2026-04-04)
|
||||
- [x] **Phase 42: Wallpapers, Social, Format Conversion & Voice** — LLM SVG + sharp wallpapers, social content, format conversion registry with AI fallback, Whisper web chat mic (WALL-01..04, SOCIAL-01..03, CONV-01..09, VOICE-01..03) (completed 2026-04-04)
|
||||
- [x] **Phase 43: Documents & Branding** — Playwright PDF reports and invoices, full brand identity kit with zip export (DOC-01..03, BRAND-01..06) (completed 2026-04-04)
|
||||
- [x] **Phase 44: Video & Presentations** — Remotion workspace package, pitch decks and demo videos, SSE render progress (PRES-01..04) (completed 2026-04-04)
|
||||
- [x] **Phase 45: Content as Skills** — Markdown skill files for all content types, Creative skill group on generalist agent (SKILL-01..03) (completed 2026-04-04)
|
||||
|
||||
## Phase Details
|
||||
|
||||
### Phase 40: Job Infrastructure
|
||||
**Goal**: Every content generation request returns a job ID immediately, progresses through a tracked lifecycle, and stores its output in namespaced storage — so nothing blocks and nothing is orphaned
|
||||
**Depends on**: Phase 39 (v1.6 shipped)
|
||||
**Requirements**: INFRA-01, INFRA-02, INFRA-03, INFRA-04
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Submitting a content generation request returns HTTP 202 with a job ID within 200ms, regardless of how long the render takes
|
||||
2. A connected browser receives SSE events as a job progresses through queued → generating → ready (or error), with no polling required
|
||||
3. A generated video file larger than 10MB can be stored and retrieved without a size-limit error — the generated/ storage namespace bypasses the upload route limit
|
||||
4. Every generated asset in the database has a sourceTaskId linking it to the originating conversation task, visible via the asset list API
|
||||
**Plans**: 2 plans
|
||||
|
||||
Plans:
|
||||
- [x] 40-01-PLAN.md — Schema, constants, migrations, contentJobStore + contentJobRunner services
|
||||
- [x] 40-02-PLAN.md — HTTP routes (POST 202, GET, SSE), app.ts wiring, integration tests
|
||||
|
||||
### Phase 41: Diagrams, Icons & Theme Engine
|
||||
**Goal**: Users can generate diagrams from natural language, produce SVG icon sets from descriptions, and create a complete OKLCH color theme from a single seed color — all without binary dependencies beyond what is already installed
|
||||
**Depends on**: Phase 40
|
||||
**Requirements**: DIAG-01, DIAG-02, DIAG-03, DIAG-04, DIAG-05, ICON-01, ICON-02, ICON-03, THEME-01, THEME-02, THEME-03, THEME-04, THEME-05, THEME-06, THEME-07
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Describing an architecture in chat produces a rendered Mermaid diagram (SVG and PNG) attached to the conversation, with the editable Mermaid source visible in a collapsible panel
|
||||
2. Mermaid rendering uses strict security level — a diagram with a `click` directive or `%%{init}%%` override is stripped before render, and SVG output passes DOMPurify before reaching the DOM
|
||||
3. Requesting an icon set from a description returns a cohesive set of SVG icons downloadable in SVG and PNG formats at multiple sizes
|
||||
4. Picking a seed color produces a full palette (background, surface, overlay, text, accents) in OKLCH with separate dark and light variants, all passing WCAG AA contrast checks
|
||||
5. The generated theme can be previewed live in the Nexus UI via CSS custom property injection and applied permanently in one click; export works for CSS variables, Tailwind config, VS Code theme, and JSON
|
||||
**Plans**: 6 plans
|
||||
|
||||
Plans:
|
||||
- [x] 41-01-PLAN.md — Dependencies, shared types, content-job-runner switch, useContentJob hook
|
||||
- [x] 41-02-PLAN.md — Diagram renderer (Playwright Mermaid + DOMPurify) and icon renderer (LLM SVG + SVGO)
|
||||
- [x] 41-03-PLAN.md — OKLCH theme palette engine, WCAG validation, export formatters, nexus-settings extension
|
||||
- [x] 41-04-PLAN.md — ContentStudio page, Diagram UI (generate, preview, source editor), Icon UI (grid, download)
|
||||
- [x] 41-05-PLAN.md — Theme UI (seed input, palette grid, live preview, export tabs, apply flow)
|
||||
- [x] 41-06-PLAN.md — Full test suite + visual checkpoint verification
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 42: Wallpapers, Social, Format Conversion & Voice
|
||||
**Goal**: Users can generate platform-ready images (wallpapers, OG images, social banners) via LLM SVG + sharp rasterization, convert between any file format pair, and record voice directly in web chat via the Whisper mic button
|
||||
**Depends on**: Phase 40
|
||||
**Requirements**: WALL-01, WALL-02, WALL-03, WALL-04, SOCIAL-01, SOCIAL-02, SOCIAL-03, CONV-01, CONV-02, CONV-03, CONV-04, CONV-05, CONV-06, CONV-07, CONV-08, CONV-09, VOICE-01, VOICE-02, VOICE-03
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Requesting a desktop wallpaper returns a 2560x1440 PNG; requesting an Instagram banner returns a correctly-dimensioned image — platform dimensions are constants, not magic numbers
|
||||
2. The format conversion UI allows drag-drop of a source file, selection of a target format, and download of the converted file; direct conversion pairs (image, audio/video, document, data) use native tools; any unsupported pair falls through to AI-bridged conversion rather than showing as unavailable
|
||||
3. Navigating to `/convert/png/svg` deep-links directly to the PNG->SVG conversion flow with source and target pre-selected
|
||||
4. An uploaded file is validated against its magic bytes before processing — a JPEG renamed to `.png` is rejected with a clear error, not silently misprocessed
|
||||
5. Clicking the mic button in web chat records audio, transcribes it via local Whisper, and populates the chat input — works offline with the locally cached model
|
||||
**Plans**: 6 plans
|
||||
|
||||
Plans:
|
||||
- [x] 42-01-PLAN.md — Dependencies, bundle types, job-runner switch, converter capabilities probe
|
||||
- [x] 42-02-PLAN.md — Wallpaper renderer (LLM SVG + sharp) and social post renderer (LLM JSON + hashtags)
|
||||
- [x] 42-03-PLAN.md — Convert renderer (sharp/ffmpeg/xlsx/AI-bridge) and multipart upload route with MIME validation
|
||||
- [x] 42-04-PLAN.md — Voice offline badge wiring (useSystemProviders hook + ChatInput badge)
|
||||
- [x] 42-05-PLAN.md — Wallpaper/Social UI panels + ContentStudio tab extensions
|
||||
- [x] 42-06-PLAN.md — Format conversion UI page with drag-drop, format chips, deep-link routing
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 43: Documents & Branding
|
||||
**Goal**: Users can generate polished PDF reports and invoices via Playwright, and create a complete brand identity (logo, avatars, social profiles, letterhead, guidelines PDF, zip package) from a single conversation
|
||||
**Depends on**: Phase 41
|
||||
**Requirements**: DOC-01, DOC-02, DOC-03, BRAND-01, BRAND-02, BRAND-03, BRAND-04, BRAND-05, BRAND-06
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Generating a PDF report from a conversation produces a downloadable PDF with correct layout; generating an invoice from a template produces a filled invoice PDF with correct line items
|
||||
2. Generating a one-pager or API reference document produces a styled PDF with navigable headings
|
||||
3. Starting a brand identity conversation produces a logo mark (SVG), avatar at multiple sizes, platform-specific social images, an email signature, and a brand guidelines PDF — all in a single brand kit
|
||||
4. The complete brand kit can be downloaded as a single zip file with assets organized by type
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [x] 43-01-PLAN.md — Types, archiver install, PDF renderer (Playwright HTML-to-PDF), job-runner wiring
|
||||
- [x] 43-02-PLAN.md — Brand kit renderer (logo, avatars, social images, templates, guidelines PDF, ZIP packaging)
|
||||
- [x] 43-03-PLAN.md — Document and Brand UI panels, ContentStudio tab extensions
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 44: Video & Presentations
|
||||
**Goal**: Agents can produce pitch deck presentations and demo videos rendered by Remotion from a conversation, with SSE progress updates throughout the render — which may take several minutes on the M4
|
||||
**Depends on**: Phase 40
|
||||
**Requirements**: PRES-01, PRES-02, PRES-03, PRES-04
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Requesting a pitch deck from a conversation description produces a Remotion-rendered interactive web presentation or MP4; the render runs in a separate workspace package and does not block the main server process
|
||||
2. The Remotion bundle is compiled once at server startup and reused for all renders — submitting a second render request does not trigger a second webpack compilation
|
||||
3. A browser connected during a video render receives SSE progress events (percentage complete) throughout the render; the final event delivers the download URL
|
||||
4. Concurrent LLM inference and video rendering do not cause the server to become unresponsive — render concurrency is capped and serialized with LLM workloads
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [x] 44-01-PLAN.md — Remotion workspace package, compositions, shared constants, types, job-runner wiring
|
||||
- [x] 44-02-PLAN.md — Presentation renderer with LLM slide generation, Remotion render, SSE progress
|
||||
- [x] 44-03-PLAN.md — PresentationPanel UI, useContentJob progress extension, ContentStudio tab
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 45: Content as Skills
|
||||
**Goal**: Every content type built in Phases 41-44 is accessible to agents as an installable Markdown skill, and the generalist agent ships pre-loaded with the Creative skill group
|
||||
**Depends on**: Phase 44
|
||||
**Requirements**: SKILL-01, SKILL-02, SKILL-03
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Each content type (diagram, theme, icon, wallpaper, social post, PDF, brand kit, video) has a corresponding skill file that an agent can load and use to call the correct content job API
|
||||
2. A freshly created generalist agent has the Creative skill group pre-loaded — it can generate diagrams and themes without any manual skill configuration
|
||||
3. A user can add or remove individual content type skills through the Skill Aggregator UI without touching configuration files
|
||||
**Plans**: 1 plan
|
||||
|
||||
Plans:
|
||||
- [x] 45-01-PLAN.md — 9 SKILL.md files, local-nexus-content source type, Creative group seeding, startup wiring
|
||||
**UI hint**: yes
|
||||
|
||||
---
|
||||
|
||||
## Coverage Validation
|
||||
|
||||
All 52 v1.7 requirements are mapped to exactly one phase. No orphans.
|
||||
|
||||
| Requirement | Phase |
|
||||
|-------------|-------|
|
||||
| INFRA-01 | 40 |
|
||||
| INFRA-02 | 40 |
|
||||
| INFRA-03 | 40 |
|
||||
| INFRA-04 | 40 |
|
||||
| DIAG-01 | 41 |
|
||||
| DIAG-02 | 41 |
|
||||
| DIAG-03 | 41 |
|
||||
| DIAG-04 | 41 |
|
||||
| DIAG-05 | 41 |
|
||||
| THEME-01 | 41 |
|
||||
| THEME-02 | 41 |
|
||||
| THEME-03 | 41 |
|
||||
| THEME-04 | 41 |
|
||||
| THEME-05 | 41 |
|
||||
| THEME-06 | 41 |
|
||||
| THEME-07 | 41 |
|
||||
| ICON-01 | 41 |
|
||||
| ICON-02 | 41 |
|
||||
| ICON-03 | 41 |
|
||||
| WALL-01 | 42 |
|
||||
| WALL-02 | 42 |
|
||||
| WALL-03 | 42 |
|
||||
| WALL-04 | 42 |
|
||||
| SOCIAL-01 | 42 |
|
||||
| SOCIAL-02 | 42 |
|
||||
| SOCIAL-03 | 42 |
|
||||
| CONV-01 | 42 |
|
||||
| CONV-02 | 42 |
|
||||
| CONV-03 | 42 |
|
||||
| CONV-04 | 42 |
|
||||
| CONV-05 | 42 |
|
||||
| CONV-06 | 42 |
|
||||
| CONV-07 | 42 |
|
||||
| CONV-08 | 42 |
|
||||
| CONV-09 | 42 |
|
||||
| VOICE-01 | 42 |
|
||||
| VOICE-02 | 42 |
|
||||
| VOICE-03 | 42 |
|
||||
| DOC-01 | 43 |
|
||||
| DOC-02 | 43 |
|
||||
| DOC-03 | 43 |
|
||||
| BRAND-01 | 43 |
|
||||
| BRAND-02 | 43 |
|
||||
| BRAND-03 | 43 |
|
||||
| BRAND-04 | 43 |
|
||||
| BRAND-05 | 43 |
|
||||
| BRAND-06 | 43 |
|
||||
| PRES-01 | 44 |
|
||||
| PRES-02 | 44 |
|
||||
| PRES-03 | 44 |
|
||||
| PRES-04 | 44 |
|
||||
| SKILL-01 | 45 |
|
||||
| SKILL-02 | 45 |
|
||||
| SKILL-03 | 45 |
|
||||
|
||||
---
|
||||
|
||||
## Progress
|
||||
|
||||
| Phase | Milestone | Plans Complete | Status | Completed |
|
||||
|-------|-----------|----------------|--------|-----------|
|
||||
| 1. Foundation | v1.2.1 | 2/2 | Complete | 2026-04-01 |
|
||||
| 21. Chat Foundation | v1.3 | 7/7 | Complete | 2026-04-02 |
|
||||
| 22. Agent Streaming | v1.3 | 5/5 | Complete | 2026-04-02 |
|
||||
| 23. Brainstormer Flow | v1.3 | 4/4 | Complete | 2026-04-02 |
|
||||
| 24. Search, History & Branching | v1.3 | 4/4 | Complete | 2026-04-02 |
|
||||
| 25. File System | v1.3 | 9/9 | Complete | 2026-04-02 |
|
||||
| 26. PWA & Performance | v1.3 | 5/5 | Complete | 2026-04-02 |
|
||||
| 27. Hermes Adapter | v1.4 | 1/1 | Complete | 2026-04-02 |
|
||||
| 28. Ollama Integration & Agent Surface | v1.4 | 3/3 | Complete | 2026-04-02 |
|
||||
| 29. Default Provider & End-to-End | v1.4 | 2/2 | Complete | 2026-04-02 |
|
||||
| 30. Hardware Detection + Mode Selection | v1.5 | 2/2 | Complete | 2026-04-03 |
|
||||
| 31. Puter.js Zero-Config Cloud | v1.5 | 4/4 | Complete | 2026-04-03 |
|
||||
| 32. Multi-Step Onboarding Wizard | v1.5 | 1/1 | Complete | 2026-04-03 |
|
||||
| 33. Persistent Memory + Personal Assistant Mode | v1.5 | 3/3 | Complete | 2026-04-03 |
|
||||
| 34. Voice | v1.5 | 2/2 | Complete | 2026-04-03 |
|
||||
| 35. npx buildthis CLI | v1.5 | 1/1 | Complete | 2026-04-03 |
|
||||
| 36. Voice Pipeline Foundation | v1.6 | 2/3 | Complete | 2026-04-04 |
|
||||
| 37. Web Chat Voice UI | v1.6 | 3/4 | Complete | 2026-04-04 |
|
||||
| 38. Telegram Bridge | v1.6 | 3/3 | Complete | 2026-04-04 |
|
||||
| 39. Voice Polish | v1.6 | 1/2 | Complete | 2026-04-04 |
|
||||
| 40. Job Infrastructure | v1.7 | 2/2 | Complete | 2026-04-04 |
|
||||
| 41. Diagrams, Icons & Theme Engine | v1.7 | 6/6 | Complete | 2026-04-04 |
|
||||
| 42. Wallpapers, Social, Format Conversion & Voice | v1.7 | 6/6 | Complete | 2026-04-04 |
|
||||
| 43. Documents & Branding | v1.7 | 3/3 | Complete | 2026-04-04 |
|
||||
| 44. Video & Presentations | v1.7 | 3/3 | Complete | 2026-04-04 |
|
||||
| 45. Content as Skills | v1.7 | 1/1 | Complete | 2026-04-04 |
|
||||
392
.planning/phases/45-content-as-skills/45-01-PLAN.md
Normal file
392
.planning/phases/45-content-as-skills/45-01-PLAN.md
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
---
|
||||
phase: 45-content-as-skills
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- server/src/skills/content/diagram.SKILL.md
|
||||
- server/src/skills/content/icon-set.SKILL.md
|
||||
- server/src/skills/content/theme-palette.SKILL.md
|
||||
- server/src/skills/content/wallpaper.SKILL.md
|
||||
- server/src/skills/content/social-post.SKILL.md
|
||||
- server/src/skills/content/convert.SKILL.md
|
||||
- server/src/skills/content/pdf-document.SKILL.md
|
||||
- server/src/skills/content/brand-kit.SKILL.md
|
||||
- server/src/skills/content/presentation.SKILL.md
|
||||
- server/src/services/skill-registry-fetcher.ts
|
||||
- server/src/services/skill-registry-db.ts
|
||||
- server/src/index.ts
|
||||
- server/src/__tests__/skill-registry-content-skills.test.ts
|
||||
autonomous: true
|
||||
requirements:
|
||||
- SKILL-01
|
||||
- SKILL-02
|
||||
- SKILL-03
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Each of the 9 content types (diagram, icon-set, theme-palette, wallpaper, social-post, convert, pdf-document, brand-kit, presentation) has a SKILL.md file on disk"
|
||||
- "Local content skills are registered in the skill registry DB on server startup"
|
||||
- "The builtin/creative group has all 9 content skill IDs as members after startup"
|
||||
- "Content skills appear in skill registry list and are installable/removable through existing API"
|
||||
- "Generalist agent gets Creative group skills via the existing pendingSkillGroups reconciler"
|
||||
artifacts:
|
||||
- path: "server/src/skills/content/diagram.SKILL.md"
|
||||
provides: "Diagram generation skill definition"
|
||||
contains: "jobType"
|
||||
- path: "server/src/skills/content/presentation.SKILL.md"
|
||||
provides: "Presentation generation skill definition"
|
||||
contains: "jobType"
|
||||
- path: "server/src/services/skill-registry-fetcher.ts"
|
||||
provides: "local-nexus-content source type handler"
|
||||
contains: "local-nexus-content"
|
||||
- path: "server/src/services/skill-registry-db.ts"
|
||||
provides: "Creative group member seeding"
|
||||
contains: "seedCreativeGroupMembers"
|
||||
- path: "server/src/__tests__/skill-registry-content-skills.test.ts"
|
||||
provides: "Tests for SKILL-01, SKILL-02, SKILL-03"
|
||||
contains: "content skills"
|
||||
key_links:
|
||||
- from: "server/src/services/skill-registry-fetcher.ts"
|
||||
to: "server/src/skills/content/*.SKILL.md"
|
||||
via: "readdir + readFile in fetchLocalNexusContent"
|
||||
pattern: "readdir.*SKILL\\.md"
|
||||
- from: "server/src/index.ts"
|
||||
to: "server/src/services/skill-registry.ts"
|
||||
via: "skillRegistryService().fetchAll() on startup"
|
||||
pattern: "fetchAll"
|
||||
- from: "server/src/services/skill-registry-db.ts"
|
||||
to: "skill_group_members table"
|
||||
via: "seedCreativeGroupMembers after getSkillRegistryDb"
|
||||
pattern: "builtin/creative"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Register all 9 content generation types as installable Nexus skills with a local filesystem source, seed the Creative group, and wire startup.
|
||||
|
||||
Purpose: Completes the final v1.7 phase by exposing content generators (diagrams, themes, icons, wallpapers, social posts, format conversion, PDFs, brand kits, presentations) as skills that agents can discover and use.
|
||||
Output: 9 SKILL.md files, extended fetcher with local-nexus-content source type, Creative group membership, startup wiring, unit tests.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/45-content-as-skills/45-RESEARCH.md
|
||||
|
||||
@server/src/services/skill-registry-fetcher.ts
|
||||
@server/src/services/skill-registry-db.ts
|
||||
@server/src/services/skill-registry.ts
|
||||
@server/src/services/content-job-runner.ts
|
||||
@server/src/index.ts
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs. -->
|
||||
|
||||
From server/src/services/skill-registry-fetcher.ts:
|
||||
```typescript
|
||||
export type SkillSourceConfig = {
|
||||
id: string;
|
||||
type: "anthropic-marketplace" | "github-tree";
|
||||
owner: string;
|
||||
repo: string;
|
||||
ref: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const BUILT_IN_SOURCES: SkillSourceConfig[] = [ /* remote sources */ ];
|
||||
|
||||
export function parseSkillFrontmatter(markdown: string): { name?: string; description?: string };
|
||||
|
||||
// Private helpers used inside fetcher (same pattern needed for local):
|
||||
// upsertSkill(db, { skillId, sourceId, name, description, sourceUrl })
|
||||
// cacheSkillVersion(db, { skillId, sha, skillMdContent, skillMdUrl })
|
||||
// upsertCommunityRatingsStub(db, skillId, sourceId)
|
||||
// versionExists(db, versionId) — idempotency guard
|
||||
|
||||
export async function fetchAllSources(sources?: SkillSourceConfig[]): Promise<{ fetched: number; errors: string[] }>;
|
||||
```
|
||||
|
||||
From server/src/services/skill-registry-db.ts:
|
||||
```typescript
|
||||
export type SkillRegistryDb = /* drizzle instance */;
|
||||
export async function getSkillRegistryDb(): Promise<SkillRegistryDb>;
|
||||
// seedBuiltinGroups(client) is called inside getSkillRegistryDb — creates builtin/creative group row
|
||||
```
|
||||
|
||||
From server/src/services/skill-registry.ts:
|
||||
```typescript
|
||||
export function skillRegistryService() {
|
||||
return {
|
||||
async list(opts?: { includeRemoved?: boolean }): Promise<SkillListItem[]>;
|
||||
async install(skillId: string, agentId: string, agentSkillsDir: string): Promise<void>;
|
||||
async uninstall(skillId: string, agentId: string, agentSkillsDir: string): Promise<void>;
|
||||
async fetchAll(sources?: SkillSourceConfig[]): Promise<{ fetched: number; errors: string[] }>;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
From server/src/services/content-job-runner.ts — jobType values:
|
||||
```
|
||||
"diagram", "icon-set", "theme-palette", "wallpaper", "social-post",
|
||||
"convert", "pdf-document", "brand-kit", "presentation"
|
||||
```
|
||||
|
||||
From server/src/index.ts — startup blocks:
|
||||
```typescript
|
||||
// Line 617-626: skill registry init (fire-and-forget)
|
||||
void (async () => {
|
||||
const { getSkillRegistryDb } = await import("./services/skill-registry-db.js");
|
||||
await getSkillRegistryDb();
|
||||
logger.info("skill registry database initialized");
|
||||
})();
|
||||
|
||||
// Line 628-659: pendingSkillGroups reconciler (fire-and-forget)
|
||||
// Assigns groups to agents with metadata.pendingSkillGroups
|
||||
// GROUP_NAME_MAP: { "Creative": "builtin/creative", ... }
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Author 9 SKILL.md files and extend fetcher with local-nexus-content source</name>
|
||||
<files>
|
||||
server/src/skills/content/diagram.SKILL.md
|
||||
server/src/skills/content/icon-set.SKILL.md
|
||||
server/src/skills/content/theme-palette.SKILL.md
|
||||
server/src/skills/content/wallpaper.SKILL.md
|
||||
server/src/skills/content/social-post.SKILL.md
|
||||
server/src/skills/content/convert.SKILL.md
|
||||
server/src/skills/content/pdf-document.SKILL.md
|
||||
server/src/skills/content/brand-kit.SKILL.md
|
||||
server/src/skills/content/presentation.SKILL.md
|
||||
server/src/services/skill-registry-fetcher.ts
|
||||
server/src/__tests__/skill-registry-content-skills.test.ts
|
||||
</files>
|
||||
<read_first>
|
||||
server/src/services/skill-registry-fetcher.ts
|
||||
server/src/services/skill-registry-db.ts
|
||||
server/src/services/content-job-runner.ts
|
||||
server/src/__tests__/skill-registry-fetch.test.ts
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test: 9 .SKILL.md files exist in server/src/skills/content/ with correct names
|
||||
- Test: Each SKILL.md has valid frontmatter with name and description fields
|
||||
- Test: fetchLocalNexusContent reads all 9 files, upserts skills, caches versions
|
||||
- Test: fetchAllSources with local-nexus-content source returns fetched=9
|
||||
- Test: Duplicate fetch (restart) does not create duplicate rows (idempotency)
|
||||
- Test: Missing directory returns 0 fetched, no error
|
||||
</behavior>
|
||||
<action>
|
||||
1. Create directory `server/src/skills/content/`.
|
||||
|
||||
2. Author 9 SKILL.md files, one per content type. Each file MUST have:
|
||||
- YAML frontmatter with single-line `name` and single-line `description` (parseSkillFrontmatter uses single-line regex — do NOT use YAML block scalars like `>` or `|`)
|
||||
- H1 heading matching the content type
|
||||
- "Usage" section with the exact `POST /api/content-jobs` payload: `jobType` value and `input` fields
|
||||
- "Output" section describing what the job returns
|
||||
- Keep each file under 40 lines — these are agent-consumable instructions, not documentation
|
||||
|
||||
Skill files and their jobType values:
|
||||
- diagram.SKILL.md — jobType: "diagram", input: { description, type? }
|
||||
- icon-set.SKILL.md — jobType: "icon-set", input: { description, style?, count? }
|
||||
- theme-palette.SKILL.md — jobType: "theme-palette", input: { seedColor, name? }
|
||||
- wallpaper.SKILL.md — jobType: "wallpaper", input: { description, resolution? }
|
||||
- social-post.SKILL.md — jobType: "social-post", input: { topic, platform?, includeHashtags? }
|
||||
- convert.SKILL.md — jobType: "convert", input: { sourceAssetId, targetFormat }
|
||||
- pdf-document.SKILL.md — jobType: "pdf-document", input: { content, docType? }
|
||||
- brand-kit.SKILL.md — jobType: "brand-kit", input: { companyName, description, seedColor? }
|
||||
- presentation.SKILL.md — jobType: "presentation", input: { topic, slideCount?, style? }
|
||||
|
||||
3. Extend `SkillSourceConfig` type in skill-registry-fetcher.ts to be a discriminated union:
|
||||
```typescript
|
||||
export type SkillSourceConfig =
|
||||
| { id: string; type: "anthropic-marketplace"; owner: string; repo: string; ref: string; label: string }
|
||||
| { id: string; type: "github-tree"; owner: string; repo: string; ref: string; label: string }
|
||||
| { id: string; type: "local-nexus-content"; dir: string; label: string };
|
||||
```
|
||||
|
||||
4. Add `fetchLocalNexusContent` function (NOT exported — private like the other fetch handlers):
|
||||
- Reads `source.dir` with `readdir({ withFileTypes: true })`
|
||||
- Filters for `.SKILL.md` suffix
|
||||
- For each file: derives `slug` from filename, `skillId` = `${source.id}/${slug}`
|
||||
- Reads file content, computes SHA-1 content hash
|
||||
- Calls `upsertSkill`, `cacheSkillVersion`, `upsertCommunityRatingsStub` (same pattern as fetchGitHubTree)
|
||||
- Wraps `readdir` in try/catch — returns 0 if dir missing
|
||||
- Uses `readdir` and `readFile` from `node:fs/promises` (already imported)
|
||||
|
||||
5. Add `"local-nexus-content"` branch in `fetchAllSources`:
|
||||
```typescript
|
||||
} else if (source.type === "local-nexus-content") {
|
||||
fetched += await fetchLocalNexusContent(source, db);
|
||||
}
|
||||
```
|
||||
|
||||
6. Add local source to `BUILT_IN_SOURCES` array:
|
||||
```typescript
|
||||
{
|
||||
id: "nexus-content",
|
||||
type: "local-nexus-content",
|
||||
dir: path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills", "content"),
|
||||
label: "Nexus Content Tools",
|
||||
},
|
||||
```
|
||||
Add `import { fileURLToPath } from "node:url";` if not already present.
|
||||
|
||||
7. Write test file `server/src/__tests__/skill-registry-content-skills.test.ts`:
|
||||
- Use vitest + temp directory pattern from existing skill-registry-fetch.test.ts
|
||||
- Test that all 9 SKILL.md files exist and have parseable frontmatter
|
||||
- Test fetchLocalNexusContent with a temp dir containing mock SKILL.md files
|
||||
- Test idempotency (calling twice does not duplicate rows)
|
||||
- Test missing directory returns 0
|
||||
- Test Creative group members (see Task 2 — leave placeholder describe block)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /opt/nexus/server && npx vitest run src/__tests__/skill-registry-content-skills.test.ts --reporter=verbose 2>&1 | tail -30</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -r "jobType" server/src/skills/content/*.SKILL.md | wc -l returns 9
|
||||
- grep "local-nexus-content" server/src/services/skill-registry-fetcher.ts returns matches
|
||||
- grep "nexus-content" server/src/services/skill-registry-fetcher.ts returns match in BUILT_IN_SOURCES
|
||||
- ls server/src/skills/content/*.SKILL.md | wc -l returns 9
|
||||
</acceptance_criteria>
|
||||
<done>9 SKILL.md files authored with correct frontmatter, local-nexus-content source type added to fetcher, all tests pass</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Seed Creative group members and wire startup sequence</name>
|
||||
<files>
|
||||
server/src/services/skill-registry-db.ts
|
||||
server/src/index.ts
|
||||
server/src/__tests__/skill-registry-content-skills.test.ts
|
||||
</files>
|
||||
<read_first>
|
||||
server/src/services/skill-registry-db.ts
|
||||
server/src/index.ts
|
||||
server/src/__tests__/skill-registry-content-skills.test.ts
|
||||
</read_first>
|
||||
<action>
|
||||
1. In `skill-registry-db.ts`, add a `seedCreativeGroupMembers` function (exported for testing):
|
||||
```typescript
|
||||
const NEXUS_CONTENT_SKILL_IDS = [
|
||||
"nexus-content/diagram",
|
||||
"nexus-content/icon-set",
|
||||
"nexus-content/theme-palette",
|
||||
"nexus-content/wallpaper",
|
||||
"nexus-content/social-post",
|
||||
"nexus-content/convert",
|
||||
"nexus-content/pdf-document",
|
||||
"nexus-content/brand-kit",
|
||||
"nexus-content/presentation",
|
||||
] as const;
|
||||
|
||||
export async function seedCreativeGroupMembers(client: LibSQLClient): Promise<void> {
|
||||
const now = Date.now();
|
||||
for (const skillId of NEXUS_CONTENT_SKILL_IDS) {
|
||||
await client.execute({
|
||||
sql: `INSERT OR IGNORE INTO skill_group_members (group_id, skill_id, added_at) VALUES (?, ?, ?)`,
|
||||
args: ["builtin/creative", skillId, now],
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
IMPORTANT: Do NOT call seedCreativeGroupMembers inside getSkillRegistryDb(). The skills must be registered first (via fetchAll), then group members seeded. This is wired in index.ts startup.
|
||||
|
||||
2. In `server/src/index.ts`, modify the existing skill registry init block (lines ~617-626) to also call fetchAll and seed Creative group members. The sequence must be:
|
||||
a. `getSkillRegistryDb()` — creates tables + builtin group rows
|
||||
b. `skillRegistryService().fetchAll()` — registers local skills (+ remote)
|
||||
c. `seedCreativeGroupMembers(client)` — adds skill IDs to builtin/creative group
|
||||
|
||||
Updated block:
|
||||
```typescript
|
||||
// [nexus] Initialize skill registry, seed content skills, and populate Creative group
|
||||
void (async () => {
|
||||
try {
|
||||
const { getSkillRegistryDb } = await import("./services/skill-registry-db.js");
|
||||
const db = await getSkillRegistryDb();
|
||||
const { skillRegistryService } = await import("./services/skill-registry.js");
|
||||
await skillRegistryService().fetchAll();
|
||||
// Seed Creative group members AFTER skills are registered
|
||||
const { seedCreativeGroupMembers } = await import("./services/skill-registry-db.js");
|
||||
// getSkillRegistryDb uses libSQL client internally — get the raw client for SQL
|
||||
// Use a direct import of createClient since we need the raw client for INSERT OR IGNORE
|
||||
const { _getClient } = await import("./services/skill-registry-db.js");
|
||||
await seedCreativeGroupMembers(_getClient());
|
||||
logger.info("skill registry initialized, content skills seeded, Creative group populated");
|
||||
} catch (err) {
|
||||
logger.error({ err }, "skill registry init failed");
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
ALTERNATIVELY (simpler): Export a helper from skill-registry-db.ts that wraps the client access:
|
||||
```typescript
|
||||
export async function seedCreativeGroupMembersFromDb(): Promise<void> {
|
||||
// Re-use the existing singleton client from getSkillRegistryDb
|
||||
const db = await getSkillRegistryDb();
|
||||
// Access the raw client via drizzle's session
|
||||
// ... or just use db.run with raw SQL
|
||||
```
|
||||
|
||||
The simplest approach: expose the raw `_client` from skill-registry-db.ts (already has the singleton pattern), or use `db.run(sql\`INSERT OR IGNORE...\`)` with drizzle's raw SQL.
|
||||
|
||||
Check how `seedBuiltinGroups` accesses the client — it receives `client: LibSQLClient` directly inside `getSkillRegistryDb()`. For the external call, either:
|
||||
- Export a `getClient()` function that returns the singleton LibSQLClient
|
||||
- Or have `seedCreativeGroupMembers` accept the drizzle db and use `db.run()`
|
||||
|
||||
Choose the approach that requires minimal changes. The `_db` singleton is already module-scoped in skill-registry-db.ts — adding an exported `getRawClient()` that returns the `_client` singleton is the cleanest path.
|
||||
|
||||
3. CRITICAL ORDERING: The Creative group seeding MUST happen AFTER fetchAll completes, and BEFORE the pendingSkillGroups reconciler runs. Since both are fire-and-forget async blocks, merge them into a single sequential block:
|
||||
- Move the pendingSkillGroups reconciler (lines ~628-659) inside the skill registry init block, after seedCreativeGroupMembers
|
||||
- This guarantees: DB init -> fetchAll -> seed group members -> reconcile pending groups
|
||||
|
||||
4. Add tests to the placeholder describe block in skill-registry-content-skills.test.ts:
|
||||
- Test: After fetchAll + seedCreativeGroupMembers, querying skill_group_members for "builtin/creative" returns 9 rows
|
||||
- Test: Content skills appear in skillRegistryService().list() result
|
||||
- Test: A content skill can be installed to a temp agent dir via svc.install()
|
||||
|
||||
5. Run `cd /opt/nexus/server && npx tsc --noEmit` to verify no type errors.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /opt/nexus/server && npx vitest run src/__tests__/skill-registry-content-skills.test.ts --reporter=verbose 2>&1 | tail -30 && npx tsc --noEmit 2>&1 | tail -20</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep "seedCreativeGroupMembers" server/src/services/skill-registry-db.ts returns matches
|
||||
- grep "fetchAll" server/src/index.ts returns match in the skill registry init block
|
||||
- grep "seedCreativeGroupMembers" server/src/index.ts returns match
|
||||
- grep "builtin/creative" server/src/__tests__/skill-registry-content-skills.test.ts returns matches
|
||||
- npx tsc --noEmit exits 0
|
||||
</acceptance_criteria>
|
||||
<done>Creative group has 9 content skill members after startup, pendingSkillGroups reconciler runs after group is populated, all tests pass, tsc clean</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `ls server/src/skills/content/*.SKILL.md | wc -l` returns 9
|
||||
2. `cd /opt/nexus/server && npx vitest run src/__tests__/skill-registry-content-skills.test.ts` — all tests pass
|
||||
3. `cd /opt/nexus/server && npx tsc --noEmit` — no type errors
|
||||
4. `grep -c "local-nexus-content" server/src/services/skill-registry-fetcher.ts` returns >= 2 (type + BUILT_IN_SOURCES)
|
||||
5. `grep -c "seedCreativeGroupMembers" server/src/services/skill-registry-db.ts` returns >= 1
|
||||
6. `grep "fetchAll\|seedCreativeGroupMembers" server/src/index.ts` shows both in the startup block
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 9 SKILL.md files exist with valid frontmatter and jobType documentation
|
||||
- Local-nexus-content source type registered and functional
|
||||
- Creative group populated with all 9 content skill IDs
|
||||
- Startup sequence correct: DB init -> fetchAll -> seed group -> reconcile pending
|
||||
- All unit tests pass, tsc clean
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/45-content-as-skills/45-01-SUMMARY.md`
|
||||
</output>
|
||||
174
.planning/phases/45-content-as-skills/45-01-SUMMARY.md
Normal file
174
.planning/phases/45-content-as-skills/45-01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
---
|
||||
phase: 45-content-as-skills
|
||||
plan: 01
|
||||
subsystem: skills
|
||||
tags: [skill-registry, content-generation, local-nexus-content, creative-group, libsql, drizzle]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 40-job-infrastructure
|
||||
provides: content_jobs table and renderContent dispatcher with all 9 jobTypes
|
||||
- phase: 41-diagrams-icons-theme-engine
|
||||
provides: diagram, icon-set, theme-palette renderers
|
||||
- phase: 42-wallpapers-social-format-conversion-voice
|
||||
provides: wallpaper, social-post, convert renderers
|
||||
- phase: 43-documents-branding
|
||||
provides: pdf-document, brand-kit renderers
|
||||
- phase: 44-video-presentations
|
||||
provides: presentation renderer
|
||||
provides:
|
||||
- 9 SKILL.md files in server/src/skills/content/ with frontmatter, jobType docs, and output docs
|
||||
- local-nexus-content source type in skill-registry-fetcher.ts
|
||||
- fetchLocalNexusContent function that reads .SKILL.md files, computes SHA-1 hash, upserts skills
|
||||
- nexus-content entry in BUILT_IN_SOURCES pointing to local skills/content dir
|
||||
- seedCreativeGroupMembers() export in skill-registry-db.ts seeding 9 content skills into builtin/creative group
|
||||
- getRawClient() export for test access to raw LibSQL client
|
||||
- Unified startup block in index.ts: DB init -> fetchAll -> seedCreativeGroupMembers -> reconcile pending groups
|
||||
- 9 unit tests for SKILL.md files, fetchLocalNexusContent, and Creative group seeding
|
||||
affects: [agents-with-creative-group, skill-registry-api, pendingSkillGroups-reconciler]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- local-nexus-content source type: discriminated union variant in SkillSourceConfig for filesystem-based skill discovery
|
||||
- SHA-1 content hash for idempotency on local files (no commit SHA available)
|
||||
- seedCreativeGroupMembers uses INSERT OR IGNORE — safe to call multiple times
|
||||
- getRawClient() exposes LibSQL singleton for direct SQL outside drizzle context
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- server/src/skills/content/diagram.SKILL.md
|
||||
- server/src/skills/content/icon-set.SKILL.md
|
||||
- server/src/skills/content/theme-palette.SKILL.md
|
||||
- server/src/skills/content/wallpaper.SKILL.md
|
||||
- server/src/skills/content/social-post.SKILL.md
|
||||
- server/src/skills/content/convert.SKILL.md
|
||||
- server/src/skills/content/pdf-document.SKILL.md
|
||||
- server/src/skills/content/brand-kit.SKILL.md
|
||||
- server/src/skills/content/presentation.SKILL.md
|
||||
- server/src/__tests__/skill-registry-content-skills.test.ts
|
||||
modified:
|
||||
- server/src/services/skill-registry-fetcher.ts
|
||||
- server/src/services/skill-registry-db.ts
|
||||
- server/src/index.ts
|
||||
- server/src/__tests__/skill-registry-fetch.test.ts
|
||||
|
||||
key-decisions:
|
||||
- "SkillSourceConfig changed to discriminated union — fetchAnthropicMarketplace/fetchGitHubTree narrowed to Extract types for tsc safety"
|
||||
- "SHA-1 content hash used for local SKILL.md idempotency — no Git commit SHA available for local files"
|
||||
- "seedCreativeGroupMembers does NOT run inside getSkillRegistryDb() — must run after fetchAll so skill rows exist first"
|
||||
- "getRawClient() exported for test access — tests need raw SQL for skill_group_members queries"
|
||||
- "pendingSkillGroups reconciler merged into skill registry init block — guarantees Creative group is seeded before reconciliation"
|
||||
- "skill-registry-fetch.test.ts Test 7 updated to expect 4 BUILT_IN_SOURCES — auto-fixed to reflect new local source"
|
||||
|
||||
patterns-established:
|
||||
- "local-nexus-content pattern: filesystem skill source reads .SKILL.md files, derives slug from filename sans extension"
|
||||
- "Content hash idempotency: SHA-1 of file content serves as version identifier for local files"
|
||||
|
||||
requirements-completed: [SKILL-01, SKILL-02, SKILL-03]
|
||||
|
||||
# Metrics
|
||||
duration: 3min
|
||||
completed: 2026-04-04
|
||||
---
|
||||
|
||||
# Phase 45 Plan 01: Content-as-Skills Summary
|
||||
|
||||
**9 content generators registered as installable Nexus skills via local-nexus-content source type, with Creative group seeded and startup sequence unified**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~3 min
|
||||
- **Started:** 2026-04-04T23:51:00Z
|
||||
- **Completed:** 2026-04-04T23:54:15Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified:** 14
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Authored 9 SKILL.md files (diagram, icon-set, theme-palette, wallpaper, social-post, convert, pdf-document, brand-kit, presentation) with valid YAML frontmatter, jobType usage docs, and output sections
|
||||
- Extended skill-registry-fetcher.ts with local-nexus-content source type: discriminated union SkillSourceConfig, fetchLocalNexusContent private handler, nexus-content entry in BUILT_IN_SOURCES
|
||||
- Added seedCreativeGroupMembers() and getRawClient() to skill-registry-db.ts; merged skill registry init and pendingSkillGroups reconciler into a single sequential startup block ensuring correct ordering
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1: Author 9 SKILL.md files and extend fetcher with local-nexus-content source** - `5138572d` (feat)
|
||||
2. **Task 2: Seed Creative group members and wire startup sequence** - `98f0b8f8` (feat)
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `server/src/skills/content/diagram.SKILL.md` - Diagram generation skill with Mermaid jobType docs
|
||||
- `server/src/skills/content/icon-set.SKILL.md` - SVG icon set generation skill docs
|
||||
- `server/src/skills/content/theme-palette.SKILL.md` - OKLCH theme palette generation skill docs
|
||||
- `server/src/skills/content/wallpaper.SKILL.md` - Desktop wallpaper generation skill docs
|
||||
- `server/src/skills/content/social-post.SKILL.md` - Social media post generation skill docs
|
||||
- `server/src/skills/content/convert.SKILL.md` - Format conversion skill docs
|
||||
- `server/src/skills/content/pdf-document.SKILL.md` - PDF document generation skill docs
|
||||
- `server/src/skills/content/brand-kit.SKILL.md` - Brand identity kit generation skill docs
|
||||
- `server/src/skills/content/presentation.SKILL.md` - Video presentation generation skill docs
|
||||
- `server/src/services/skill-registry-fetcher.ts` - Added local-nexus-content source type and handler
|
||||
- `server/src/services/skill-registry-db.ts` - Added getRawClient(), seedCreativeGroupMembers(), _client singleton
|
||||
- `server/src/index.ts` - Unified startup block with correct sequence
|
||||
- `server/src/__tests__/skill-registry-content-skills.test.ts` - 9 tests for SKILL.md files, fetch handler, Creative group
|
||||
- `server/src/__tests__/skill-registry-fetch.test.ts` - Updated Test 7 to expect 4 BUILT_IN_SOURCES
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- SkillSourceConfig changed from flat type to discriminated union — required narrowing fetchAnthropicMarketplace/fetchGitHubTree to Extract types to avoid TypeScript access errors on local-nexus-content variant
|
||||
- SHA-1 of file content used as version identifier for local SKILL.md files — no Git commit SHA available; provides idempotency across server restarts
|
||||
- seedCreativeGroupMembers() separated from getSkillRegistryDb() initialization — must run after fetchAll() so the skills rows exist for foreign key constraints; wired sequentially in index.ts
|
||||
- getRawClient() exported to give tests direct SQL access for querying skill_group_members table
|
||||
- pendingSkillGroups reconciler moved inside the skill registry init block — eliminates race where reconciler could run before Creative group was seeded, causing missed group assignments
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] Updated skill-registry-fetch.test.ts Test 7 to expect 4 BUILT_IN_SOURCES**
|
||||
- **Found during:** Task 1 (extending fetcher)
|
||||
- **Issue:** Existing test asserted `toHaveLength(3)` and `type` to match remote-only regex — both failed after adding nexus-content local source
|
||||
- **Fix:** Updated count to 4, added nexus-content to expected IDs, made type check conditional on non-local sources
|
||||
- **Files modified:** server/src/__tests__/skill-registry-fetch.test.ts
|
||||
- **Verification:** All 7 existing fetch tests pass after update
|
||||
- **Committed in:** 5138572d (Task 1 commit)
|
||||
|
||||
**2. [Rule 1 - Bug] Narrowed fetchAnthropicMarketplace/fetchGitHubTree parameter types**
|
||||
- **Found during:** Task 2 (tsc check)
|
||||
- **Issue:** Converting SkillSourceConfig to discriminated union caused tsc errors — functions accessed `.owner`, `.repo`, `.ref` which don't exist on local-nexus-content variant
|
||||
- **Fix:** Changed parameter types to `Extract<SkillSourceConfig, { type: "anthropic-marketplace" }>` and `Extract<SkillSourceConfig, { type: "github-tree" }>` respectively
|
||||
- **Files modified:** server/src/services/skill-registry-fetcher.ts
|
||||
- **Verification:** `npx tsc --noEmit` exits 0
|
||||
- **Committed in:** 98f0b8f8 (Task 2 commit)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 2 auto-fixed (2 Rule 1 bugs)
|
||||
**Impact on plan:** Both auto-fixes required for correctness. No scope creep.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None — plan executed cleanly with 2 small tsc/test fixes applied automatically.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None — all 9 SKILL.md files contain complete documentation with jobType, input fields, and output descriptions. The skills point to fully-implemented renderers from Phases 41-44.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
Phase 45 is the final phase of milestone v1.7. All content generators are now registered as installable Nexus skills discoverable by agents. The Creative group is seeded with all 9 skill IDs and the startup sequence ensures agents with `pendingSkillGroups: ["Creative"]` receive their skill group assignments.
|
||||
|
||||
---
|
||||
*Phase: 45-content-as-skills*
|
||||
*Completed: 2026-04-04*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- FOUND: server/src/skills/content/diagram.SKILL.md
|
||||
- FOUND: server/src/skills/content/presentation.SKILL.md
|
||||
- FOUND: server/src/services/skill-registry-fetcher.ts
|
||||
- FOUND: server/src/services/skill-registry-db.ts
|
||||
- FOUND: server/src/__tests__/skill-registry-content-skills.test.ts
|
||||
- FOUND: .planning/phases/45-content-as-skills/45-01-SUMMARY.md
|
||||
- COMMIT 5138572d: feat(45-01): author 9 content SKILL.md files and extend fetcher with local-nexus-content source
|
||||
- COMMIT 98f0b8f8: feat(45-01): seed Creative group members and wire unified startup sequence
|
||||
41
.planning/phases/45-content-as-skills/45-CONTEXT.md
Normal file
41
.planning/phases/45-content-as-skills/45-CONTEXT.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Phase 45: Content as Skills - Context
|
||||
|
||||
**Gathered:** 2026-04-04
|
||||
**Status:** Ready for planning
|
||||
**Mode:** Auto-generated (discuss skipped via workflow.skip_discuss)
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Every content type built in Phases 41-44 is accessible to agents as an installable Markdown skill, and the generalist agent ships pre-loaded with the Creative skill group
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Claude's Discretion
|
||||
All implementation choices are at Claude's discretion — discuss phase was skipped per user setting. Use ROADMAP phase goal, success criteria, and codebase conventions to guide decisions.
|
||||
|
||||
</decisions>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
Codebase context will be gathered during plan-phase research.
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
No specific requirements — discuss phase skipped. Refer to ROADMAP phase description and success criteria.
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
None — discuss phase skipped.
|
||||
|
||||
</deferred>
|
||||
441
.planning/phases/45-content-as-skills/45-RESEARCH.md
Normal file
441
.planning/phases/45-content-as-skills/45-RESEARCH.md
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
# Phase 45: Content as Skills - Research
|
||||
|
||||
**Researched:** 2026-04-04
|
||||
**Domain:** Nexus skill registry — local SKILL.md authoring, source seeding, group membership, generalist agent preloading
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 45 bridges the content generation work of Phases 41-44 with the skill registry system already in place. Every content type (diagram, icon, theme, wallpaper, social, convert, pdf-document, brand-kit, presentation) must become an installable Markdown skill, the built-in "Creative" group must contain all of them, and the Generalist agent must ship pre-loaded with that group.
|
||||
|
||||
The skill registry infrastructure is complete. The registry stores skills in a libSQL database under `~/.paperclip/instances/default/skills/registry.db`. Skills are fetched from remote GitHub sources (`anthropic-marketplace` and `github-tree` types) or from native agent runtimes (Hermes). There is currently no local-filesystem source type. Phase 45 must add one, seed it on server startup, and wire the Creative group to the new skills.
|
||||
|
||||
The generalist agent preloading mechanism already exists: `index.ts` seeds `pendingSkillGroups: ["Creative"]` in agent metadata at creation time, and a fire-and-forget startup reconciler assigns the group (using `assignGroup`) to every agent that has that metadata flag. Phase 45 needs the Creative group to have members before the reconciler runs — which means the skill SKILL.md files and their registry entries must be in place before any agent is created.
|
||||
|
||||
**Primary recommendation:** Add a `"local-nexus-content"` source type to the skill registry fetcher. On server startup, register nine content SKILL.md files from `server/src/skills/content/` into the registry DB and add all of them to the `builtin/creative` group.
|
||||
|
||||
---
|
||||
|
||||
<user_constraints>
|
||||
## User Constraints (from CONTEXT.md)
|
||||
|
||||
### Locked Decisions
|
||||
All implementation choices are at Claude's discretion — discuss phase was skipped per user setting.
|
||||
|
||||
### Claude's Discretion
|
||||
All implementation choices are at Claude's discretion. Use ROADMAP phase goal, success criteria, and codebase conventions to guide decisions.
|
||||
|
||||
### Deferred Ideas (OUT OF SCOPE)
|
||||
None.
|
||||
</user_constraints>
|
||||
|
||||
---
|
||||
|
||||
<phase_requirements>
|
||||
## Phase Requirements
|
||||
|
||||
| ID | Description | Research Support |
|
||||
|----|-------------|------------------|
|
||||
| SKILL-01 | Each content type is implemented as an installable Nexus skill | Nine SKILL.md files authored; registered in skill registry DB via local-nexus-content source type; installable through existing `svc.install()` path |
|
||||
| SKILL-02 | Generalist agent is pre-loaded with a "Creative" skill group | `builtin/creative` group already exists in DB seed; group members populated on startup; `pendingSkillGroups` reconciler already assigns the group to Generalist agents |
|
||||
| SKILL-03 | Users can add or remove content type skills through the Skill Aggregator | Content skills appear in the existing SkillBrowser UI once registered; install/uninstall routes already work for any registered skill |
|
||||
</phase_requirements>
|
||||
|
||||
---
|
||||
|
||||
## Standard Stack
|
||||
|
||||
### Core
|
||||
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| libSQL / drizzle-orm | Already installed | Skill registry persistence | All skill data lives in this SQLite DB — no new dep needed |
|
||||
| node:fs/promises | Node built-in | Read SKILL.md files from disk | Used by existing skill cache logic |
|
||||
| vitest | Already installed | Unit tests | Project standard in `server/` |
|
||||
|
||||
### Supporting
|
||||
|
||||
No new packages required. The full content generation stack (jobType dispatch, renderers, SSE) is already in place from Phases 40-44.
|
||||
|
||||
**Installation:** None required.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Recommended Project Structure
|
||||
|
||||
```
|
||||
server/src/
|
||||
├── skills/
|
||||
│ └── content/
|
||||
│ ├── diagram.SKILL.md
|
||||
│ ├── icon-set.SKILL.md
|
||||
│ ├── theme-palette.SKILL.md
|
||||
│ ├── wallpaper.SKILL.md
|
||||
│ ├── social-post.SKILL.md
|
||||
│ ├── convert.SKILL.md
|
||||
│ ├── pdf-document.SKILL.md
|
||||
│ ├── brand-kit.SKILL.md
|
||||
│ └── presentation.SKILL.md
|
||||
├── services/
|
||||
│ └── skill-registry-fetcher.ts ← add local-nexus-content source type
|
||||
│ └── skill-registry-db.ts ← seed Creative group members on init
|
||||
└── index.ts ← call seedContentSkills on startup
|
||||
```
|
||||
|
||||
### Pattern 1: Local-Filesystem Source Type
|
||||
|
||||
**What:** A new `"local-nexus-content"` entry in `SkillSourceConfig` union and a `fetchLocalNexusContent()` function that reads SKILL.md files from `server/src/skills/content/` and upserts them into the registry DB.
|
||||
|
||||
**When to use:** Server startup — called once after `getSkillRegistryDb()` initialises.
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// In skill-registry-fetcher.ts — extend the union type
|
||||
export type SkillSourceConfig =
|
||||
| { id: string; type: "anthropic-marketplace"; owner: string; repo: string; ref: string; label: string }
|
||||
| { id: string; type: "github-tree"; owner: string; repo: string; ref: string; label: string }
|
||||
| { id: string; type: "local-nexus-content"; dir: string; label: string };
|
||||
|
||||
// New handler called by fetchAllSources when type === "local-nexus-content"
|
||||
async function fetchLocalNexusContent(
|
||||
source: Extract<SkillSourceConfig, { type: "local-nexus-content" }>,
|
||||
db: SkillRegistryDb,
|
||||
): Promise<number> {
|
||||
const entries = await readdir(source.dir, { withFileTypes: true });
|
||||
let fetched = 0;
|
||||
for (const entry of entries) {
|
||||
if (!entry.name.endsWith(".SKILL.md")) continue;
|
||||
const slug = entry.name.replace(/\.SKILL\.md$/, "");
|
||||
const skillId = `${source.id}/${slug}`;
|
||||
const skillMdContent = await readFile(path.join(source.dir, entry.name), "utf-8");
|
||||
const { name, description } = parseSkillFrontmatter(skillMdContent);
|
||||
// Use a content hash as the version SHA (deterministic, no network needed)
|
||||
const sha = crypto.createHash("sha1").update(skillMdContent).digest("hex");
|
||||
await upsertSkill(db, { skillId, sourceId: source.id, name: name ?? slug, description, sourceUrl: "" });
|
||||
await cacheSkillVersion(db, { skillId, sha, skillMdContent, skillMdUrl: "" });
|
||||
await upsertCommunityRatingsStub(db, skillId, source.id);
|
||||
fetched++;
|
||||
}
|
||||
return fetched;
|
||||
}
|
||||
```
|
||||
|
||||
**BUILT_IN_SOURCES addition:**
|
||||
|
||||
```typescript
|
||||
export const BUILT_IN_SOURCES: SkillSourceConfig[] = [
|
||||
// … existing remote sources …
|
||||
{
|
||||
id: "nexus-content",
|
||||
type: "local-nexus-content",
|
||||
dir: path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills", "content"),
|
||||
label: "Nexus Content Tools",
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### Pattern 2: Creative Group Membership Seeding
|
||||
|
||||
**What:** After the local skills are registered, add them as members of the `builtin/creative` group. This should happen inside `getSkillRegistryDb()` (or a startup helper called right after) so the group has members before the `pendingSkillGroups` reconciler runs.
|
||||
|
||||
**When to use:** Server startup, idempotent (uses `INSERT OR IGNORE`).
|
||||
|
||||
```typescript
|
||||
// In skill-registry-db.ts — extend seedBuiltinGroups or add seedCreativeGroupMembers
|
||||
const NEXUS_CONTENT_SKILL_IDS = [
|
||||
"nexus-content/diagram",
|
||||
"nexus-content/icon-set",
|
||||
"nexus-content/theme-palette",
|
||||
"nexus-content/wallpaper",
|
||||
"nexus-content/social-post",
|
||||
"nexus-content/convert",
|
||||
"nexus-content/pdf-document",
|
||||
"nexus-content/brand-kit",
|
||||
"nexus-content/presentation",
|
||||
] as const;
|
||||
|
||||
async function seedCreativeGroupMembers(client: LibSQLClient): Promise<void> {
|
||||
const now = Date.now();
|
||||
for (const skillId of NEXUS_CONTENT_SKILL_IDS) {
|
||||
await client.execute({
|
||||
sql: `INSERT OR IGNORE INTO skill_group_members (group_id, skill_id, added_at) VALUES (?, ?, ?)`,
|
||||
args: ["builtin/creative", skillId, now],
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: SKILL.md Authoring Convention
|
||||
|
||||
Each content skill SKILL.md must follow the same frontmatter convention as existing skills:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: diagram
|
||||
description: >
|
||||
Generate diagrams from a natural language description. Produces Mermaid
|
||||
syntax rendered to SVG/PNG. Supports architecture, flowchart, ERD,
|
||||
sequence, and mind-map types. Use when a user asks to visualise a
|
||||
system, process, or data structure as a diagram.
|
||||
---
|
||||
|
||||
# Diagram Generation
|
||||
|
||||
Generates diagrams from a text description via the Nexus content API.
|
||||
|
||||
## Usage
|
||||
|
||||
Submit a `POST /api/content-jobs` with:
|
||||
- `jobType: "diagram"`
|
||||
- `input.description` — natural language description of the diagram
|
||||
- `input.type` (optional) — `"flowchart"`, `"architecture"`, `"erd"`, `"sequence"`, or `"mindmap"`
|
||||
|
||||
The job returns a 202 with a `jobId`. Poll `GET /api/content-jobs/:jobId` or subscribe
|
||||
to SSE `content_job.done` events to retrieve the resulting asset URL.
|
||||
|
||||
## Output
|
||||
|
||||
Returns an SVG file (also available as PNG via the export button in the UI).
|
||||
```
|
||||
|
||||
### Startup Wiring
|
||||
|
||||
In `server/src/index.ts`, the existing startup block already calls `getSkillRegistryDb()` as fire-and-forget. Extend that block to also call `fetchAllSources()` with `BUILT_IN_SOURCES` immediately after DB init so content skills are seeded on every cold start:
|
||||
|
||||
```typescript
|
||||
// [nexus] Initialize skill registry and seed content skills
|
||||
void (async () => {
|
||||
try {
|
||||
const { getSkillRegistryDb } = await import("./services/skill-registry-db.js");
|
||||
await getSkillRegistryDb(); // creates tables + builtin groups
|
||||
const { skillRegistryService } = await import("./services/skill-registry.js");
|
||||
await skillRegistryService().fetchAll(); // seeds local-nexus-content skills
|
||||
logger.info("skill registry database initialized and content skills seeded");
|
||||
} catch (err) {
|
||||
logger.error({ err }, "skill registry init failed");
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
|
||||
- **Seeding Creative group members before the skills table rows exist:** `skill_group_members.skill_id` has no FK constraint, so INSERT succeeds even if the skill row is absent — but `resolveEffectiveSkills` then returns orphaned IDs. Always seed skill rows first, group members second.
|
||||
- **Calling `fetchAll()` before `getSkillRegistryDb()`:** DB tables must exist before the local fetcher tries to upsert.
|
||||
- **Using mutable remote SHA for local file versioning:** Local files don't have a git SHA. Use a SHA-1 of the file content instead — deterministic and cheap.
|
||||
- **Blocking HTTP on `fetchAll()` at startup:** Keep as fire-and-forget; the external github-tree fetches can be slow. Content skills (local path) are synchronous but the remote sources may timeout.
|
||||
|
||||
---
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| Skill install/uninstall | Custom file copy logic | `skillRegistryService().install()` / `uninstall()` | Already handles version tracking, cache dirs, agentSkills table |
|
||||
| Group assignment | Direct INSERT into agentSkillGroups | `skillGroupService().assignGroup()` | Handles effective skill resolution, per-skill install, dedup |
|
||||
| Skill Browser display | New UI component | Existing SkillBrowser page at `/skills` | Already renders all registered skills with install/remove actions |
|
||||
| Group membership seeding | New service | `INSERT OR IGNORE INTO skill_group_members` on DB init | Simpler than adding a new route; idempotent |
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Creative group populated but skills not yet in registry
|
||||
**What goes wrong:** `pendingSkillGroups` reconciler runs at startup and calls `assignGroup("builtin/creative", agentId, skillsDir)`. `resolveEffectiveSkills` returns the 9 skill IDs, but `svc.install()` fails with "Skill not found" because local skills haven't been fetched yet.
|
||||
**Why it happens:** The skill-registry init and the `pendingSkillGroups` reconciler are both fire-and-forget; their order is non-deterministic.
|
||||
**How to avoid:** Seed creative group members only after `fetchAll()` completes (not at DB init time). Alternatively, sequence the two startup blocks: skill init → fetchAll → then let the reconciler run.
|
||||
**Warning signs:** `skipped` array non-empty in `assignGroup` result; Generalist agent has group assigned but no SKILL.md files on disk.
|
||||
|
||||
### Pitfall 2: Local source dir resolution broken in production
|
||||
**What goes wrong:** `path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills", "content")` resolves to `server/src/skills/content` in dev but points to a non-existent path in a compiled/packaged build.
|
||||
**Why it happens:** `import.meta.url` refers to the compiled output location.
|
||||
**How to avoid:** In `tsconfig.json`, include `server/src/skills/content/*.SKILL.md` in the assets copied to dist, OR read the source dir via `process.cwd()` with a known repo-relative path. Check that the resolved dir exists before iterating; log a clear error if absent.
|
||||
|
||||
### Pitfall 3: SKILL.md frontmatter parse returns undefined name
|
||||
**What goes wrong:** `parseSkillFrontmatter` fails silently on a multi-line YAML description block (`description: >\n line1\n line2`). The `name` field parses correctly but `description` is truncated to `>` literal.
|
||||
**Why it happens:** `parseSkillFrontmatter` uses a single-line regex (`^description:\s*(.+)$`) — it does not handle YAML block scalars.
|
||||
**How to avoid:** For the `name` field (used for registry display), keep it on a single line. For `description` use a single-line string or expand the frontmatter parser to handle folded scalars. Alternatively, accept the `>` literal as description — it won't break anything.
|
||||
|
||||
### Pitfall 4: Agent SKILL.md dir wrong for Generalist agent
|
||||
**What goes wrong:** `assignGroup` copies skills to `agentSkillsDir` but the Generalist agent's adapter is `claude_local` — the skills dir is resolved by `resolveAdapterSkillConfig("claude_local").skillDir`. If the agent's workspace does not exist yet (new onboard), `cp` fails.
|
||||
**Why it happens:** The workspace dir is created lazily when the agent first runs.
|
||||
**How to avoid:** In `assignGroup`, `mkdir(agentSkillsDir, { recursive: true })` before attempting the copy. Inspect the existing code path — if this `mkdir` is already there, no action needed.
|
||||
|
||||
### Pitfall 5: Duplicate registration on restarts
|
||||
**What goes wrong:** Every server restart calls `fetchAll()`, which re-registers all 9 local skills. Without idempotency guards, this creates duplicate `skill_versions` and `skill_files` rows.
|
||||
**Why it happens:** The content hash SHA is deterministic, so `versionExists()` returns true and `cacheSkillVersion` is skipped. But `upsertSkill` uses `onConflictDoUpdate` — the name/description are updated but no duplicate is created.
|
||||
**How to avoid:** The existing `versionExists()` check handles this. Confirm the content-hash versionId format matches: `${skillId}@${sha}`.
|
||||
|
||||
---
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Registering skills from disk (local-nexus-content handler)
|
||||
|
||||
```typescript
|
||||
// Source: server/src/services/skill-registry-fetcher.ts (new function)
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
async function fetchLocalNexusContent(
|
||||
source: Extract<SkillSourceConfig, { type: "local-nexus-content" }>,
|
||||
db: SkillRegistryDb,
|
||||
): Promise<number> {
|
||||
let entries: Awaited<ReturnType<typeof readdir>>;
|
||||
try {
|
||||
entries = await readdir(source.dir, { withFileTypes: true });
|
||||
} catch {
|
||||
// Directory doesn't exist (e.g. missing after a build step) — log and skip
|
||||
return 0;
|
||||
}
|
||||
let fetched = 0;
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || !entry.name.endsWith(".SKILL.md")) continue;
|
||||
const slug = entry.name.replace(/\.SKILL\.md$/, "");
|
||||
const skillId = `${source.id}/${slug}`;
|
||||
const content = await readFile(path.join(source.dir, entry.name), "utf-8");
|
||||
const sha = crypto.createHash("sha1").update(content).digest("hex");
|
||||
const { name, description } = parseSkillFrontmatter(content);
|
||||
await upsertSkill(db, {
|
||||
skillId, sourceId: source.id,
|
||||
name: name ?? slug, description,
|
||||
sourceUrl: "",
|
||||
});
|
||||
await cacheSkillVersion(db, { skillId, sha, skillMdContent: content, skillMdUrl: "" });
|
||||
await upsertCommunityRatingsStub(db, skillId, source.id);
|
||||
fetched++;
|
||||
}
|
||||
return fetched;
|
||||
}
|
||||
```
|
||||
|
||||
### Seeding Creative group membership after fetchAll
|
||||
|
||||
```typescript
|
||||
// In skill-registry-db.ts — called after seedBuiltinGroups
|
||||
async function seedCreativeGroupMembers(client: LibSQLClient): Promise<void> {
|
||||
const now = Date.now();
|
||||
const SKILLS = [
|
||||
"nexus-content/diagram", "nexus-content/icon-set", "nexus-content/theme-palette",
|
||||
"nexus-content/wallpaper", "nexus-content/social-post", "nexus-content/convert",
|
||||
"nexus-content/pdf-document", "nexus-content/brand-kit", "nexus-content/presentation",
|
||||
];
|
||||
for (const skillId of SKILLS) {
|
||||
await client.execute({
|
||||
sql: `INSERT OR IGNORE INTO skill_group_members (group_id, skill_id, added_at) VALUES (?, ?, ?)`,
|
||||
args: ["builtin/creative", skillId, now],
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ensuring Creative is assigned to a new Generalist agent
|
||||
|
||||
```typescript
|
||||
// The existing path in index.ts (already present, no change needed):
|
||||
await agentSvc.create(company.id, {
|
||||
name: "Generalist",
|
||||
role: "general",
|
||||
adapterType: "claude_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
metadata: { pendingSkillGroups: ["Creative"], backfilled: true },
|
||||
});
|
||||
// The reconciler at server startup calls svc.assignGroup("builtin/creative", agentId, skillsDir)
|
||||
// which installs all Creative group members to the agent's .claude/skills/ directory.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| Skills only from remote GitHub repos | Skills can come from local filesystem too (new local-nexus-content type) | Phase 45 | Nexus-specific content skills don't need a public GitHub repo |
|
||||
| Creative group is empty (seed only creates the group row) | Creative group has 9 content skill members after server init | Phase 45 | Generalist agents automatically get all content tools on first run |
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Should `cacheSkillVersion` write files to a tmpdir for local skills?**
|
||||
- What we know: `cacheSkillVersion` writes `SKILL.md` to `~/.paperclip/instances/default/skills/cache/nexus-content/<slug>/<sha>/SKILL.md`, then `svc.install()` copies from that cache dir to the agent's skills dir.
|
||||
- What's unclear: For local skills, caching to disk before installing is redundant — the source file is already on disk.
|
||||
- Recommendation: Keep the cache write for consistency (install always reads from cache); the file is tiny. If performance matters, skip caching and have install copy the source file directly.
|
||||
|
||||
2. **Where exactly is the `server/src/skills/content/` dir resolved in compiled builds?**
|
||||
- What we know: The server compiles TypeScript but does not currently bundle assets — source files are referenced at runtime via `import.meta.url`.
|
||||
- What's unclear: Whether `.SKILL.md` files are included in the dist output.
|
||||
- Recommendation: Add `server/src/skills/content/` to the list of directories copied during build (check the server's build script), or use `process.env.SKILL_CONTENT_DIR` as an override for production.
|
||||
|
||||
3. **Should the Skill Aggregator UI show a "Creative" filter tab?**
|
||||
- What we know: `SkillBrowser.tsx` filters by adapter compatibility; the "Groups" tab shows group membership via `skillGroupsApi`. Content skills installed via the registry appear in the installed tab.
|
||||
- What's unclear: Whether the browse experience needs a dedicated "Creative" section or if the existing group-based filtering is sufficient.
|
||||
- Recommendation: No UI changes needed for SKILL-03. The existing SkillBrowser already supports install/remove for any registered skill. Adding a dedicated Creative tab is a future enhancement.
|
||||
|
||||
---
|
||||
|
||||
## Environment Availability
|
||||
|
||||
Step 2.6: SKIPPED (no new external dependencies — local filesystem only)
|
||||
|
||||
---
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
### Test Framework
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Framework | Vitest (node environment) |
|
||||
| Config file | `server/vitest.config.ts` |
|
||||
| Quick run command | `cd /opt/nexus/server && npx vitest run --reporter=verbose src/__tests__/skill-registry-content-skills.test.ts` |
|
||||
| Full suite command | `cd /opt/nexus/server && npx vitest run --reporter=verbose` |
|
||||
|
||||
### Phase Requirements → Test Map
|
||||
|
||||
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||
|--------|----------|-----------|-------------------|-------------|
|
||||
| SKILL-01 | 9 SKILL.md files are seeded into registry DB via local-nexus-content source | unit | `npx vitest run src/__tests__/skill-registry-content-skills.test.ts -t "seeds content skills"` | ❌ Wave 0 |
|
||||
| SKILL-01 | Each skill is installable to an agent skills dir | unit | `npx vitest run src/__tests__/skill-registry-content-skills.test.ts -t "installs content skill"` | ❌ Wave 0 |
|
||||
| SKILL-02 | Creative group has 9 members after DB init | unit | `npx vitest run src/__tests__/skill-registry-content-skills.test.ts -t "Creative group members"` | ❌ Wave 0 |
|
||||
| SKILL-03 | Content skills appear in registry list | unit | `npx vitest run src/__tests__/skill-registry-content-skills.test.ts -t "list includes content skills"` | ❌ Wave 0 |
|
||||
|
||||
### Sampling Rate
|
||||
|
||||
- **Per task commit:** `cd /opt/nexus/server && npx vitest run src/__tests__/skill-registry-content-skills.test.ts`
|
||||
- **Per wave merge:** `cd /opt/nexus/server && npx vitest run`
|
||||
- **Phase gate:** Full suite green before `/gsd:verify-work`
|
||||
|
||||
### Wave 0 Gaps
|
||||
|
||||
- [ ] `server/src/__tests__/skill-registry-content-skills.test.ts` — covers SKILL-01, SKILL-02, SKILL-03
|
||||
- [ ] `server/src/skills/content/*.SKILL.md` — 9 skill files (authored, not test infra)
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
|
||||
- Direct source code inspection — `server/src/services/skill-registry-fetcher.ts`, `skill-registry-db.ts`, `skill-registry-groups.ts`, `skill-registry.ts`
|
||||
- Direct source code inspection — `server/src/index.ts` lines 256-279 (ensureGeneralistAgents), 628-658 (pendingSkillGroups reconciler)
|
||||
- Direct source code inspection — `server/src/adapters/registry.ts`, `server/src/routes/skill-registry.ts`, `server/src/routes/skill-registry-groups.ts`
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
|
||||
- Existing test patterns from `server/src/__tests__/skill-registry-install.test.ts` and `hermes-dual-source.test.ts` — confirms vitest + real temp dirs as test pattern
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH — all libraries already installed; no new deps
|
||||
- Architecture: HIGH — local source type follows identical pattern to existing github-tree handler
|
||||
- Pitfalls: HIGH — race conditions and dir resolution issues verified against actual startup code
|
||||
|
||||
**Research date:** 2026-04-04
|
||||
**Valid until:** 2026-05-04 (stable codebase)
|
||||
258
server/src/__tests__/skill-registry-content-skills.test.ts
Normal file
258
server/src/__tests__/skill-registry-content-skills.test.ts
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
import { mkdtemp, rm, mkdir, writeFile, readFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function mockSkillMd(name: string, description: string, jobType: string): string {
|
||||
return `---
|
||||
name: ${name}
|
||||
description: ${description}
|
||||
---
|
||||
|
||||
# ${name}
|
||||
|
||||
## Usage
|
||||
|
||||
POST /api/content-jobs with jobType: "${jobType}"
|
||||
`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests — SKILL.md files on disk
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("content skill files", () => {
|
||||
const SKILL_TYPES = [
|
||||
"diagram",
|
||||
"icon-set",
|
||||
"theme-palette",
|
||||
"wallpaper",
|
||||
"social-post",
|
||||
"convert",
|
||||
"pdf-document",
|
||||
"brand-kit",
|
||||
"presentation",
|
||||
];
|
||||
|
||||
const contentDir = path.resolve(
|
||||
new URL("../skills/content", import.meta.url).pathname,
|
||||
);
|
||||
|
||||
it("Test 1: 9 .SKILL.md files exist in server/src/skills/content/", async () => {
|
||||
const { readdir } = await import("node:fs/promises");
|
||||
const entries = await readdir(contentDir);
|
||||
const skillFiles = entries.filter((e) => e.endsWith(".SKILL.md"));
|
||||
expect(skillFiles).toHaveLength(9);
|
||||
});
|
||||
|
||||
it("Test 2: Each SKILL.md has parseable frontmatter with name and description", async () => {
|
||||
const { parseSkillFrontmatter } = await import("../services/skill-registry-fetcher.js");
|
||||
|
||||
for (const skillType of SKILL_TYPES) {
|
||||
const filePath = path.join(contentDir, `${skillType}.SKILL.md`);
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
const { name, description } = parseSkillFrontmatter(content);
|
||||
expect(name, `${skillType}.SKILL.md missing name`).toBeTruthy();
|
||||
expect(description, `${skillType}.SKILL.md missing description`).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it("Test 3: Each SKILL.md contains its jobType string", async () => {
|
||||
for (const skillType of SKILL_TYPES) {
|
||||
const filePath = path.join(contentDir, `${skillType}.SKILL.md`);
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
expect(content, `${skillType}.SKILL.md missing jobType reference`).toContain(`jobType`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests — fetchLocalNexusContent via fetchAllSources
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("fetchLocalNexusContent", () => {
|
||||
let tmpDir: string;
|
||||
let tmpContentDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = await mkdtemp(path.join(os.tmpdir(), "nexus-content-skills-test-"));
|
||||
tmpContentDir = path.join(tmpDir, "skills", "content");
|
||||
await mkdir(tmpContentDir, { recursive: true });
|
||||
process.env.PAPERCLIP_HOME = tmpDir;
|
||||
|
||||
const { resetSkillRegistryDb } = await import("../services/skill-registry-db.js");
|
||||
resetSkillRegistryDb();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const { resetSkillRegistryDb } = await import("../services/skill-registry-db.js");
|
||||
resetSkillRegistryDb();
|
||||
delete process.env.PAPERCLIP_HOME;
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("Test 4: fetchAllSources with local-nexus-content returns fetched=9 for 9 mock SKILL.md files", async () => {
|
||||
// Create 9 mock SKILL.md files
|
||||
const types = ["diagram", "icon-set", "theme-palette", "wallpaper", "social-post", "convert", "pdf-document", "brand-kit", "presentation"];
|
||||
for (const t of types) {
|
||||
await writeFile(
|
||||
path.join(tmpContentDir, `${t}.SKILL.md`),
|
||||
mockSkillMd(`${t} Skill`, `Does ${t} stuff`, t),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
|
||||
const { fetchAllSources } = await import("../services/skill-registry-fetcher.js");
|
||||
const localSource = {
|
||||
id: "nexus-content",
|
||||
type: "local-nexus-content" as const,
|
||||
dir: tmpContentDir,
|
||||
label: "Nexus Content Tools",
|
||||
};
|
||||
|
||||
const result = await fetchAllSources([localSource]);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
expect(result.fetched).toBe(9);
|
||||
});
|
||||
|
||||
it("Test 5: Skills are upserted into the DB with correct sourceId and skillId format", async () => {
|
||||
await writeFile(
|
||||
path.join(tmpContentDir, "diagram.SKILL.md"),
|
||||
mockSkillMd("Diagram Generator", "Generates diagrams", "diagram"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { fetchAllSources } = await import("../services/skill-registry-fetcher.js");
|
||||
const { getSkillRegistryDb } = await import("../services/skill-registry-db.js");
|
||||
const { skills } = await import("../services/skill-registry-schema.js");
|
||||
|
||||
const localSource = {
|
||||
id: "nexus-content",
|
||||
type: "local-nexus-content" as const,
|
||||
dir: tmpContentDir,
|
||||
label: "Nexus Content Tools",
|
||||
};
|
||||
|
||||
await fetchAllSources([localSource]);
|
||||
const db = await getSkillRegistryDb();
|
||||
const rows = await db.select().from(skills);
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]!.id).toBe("nexus-content/diagram");
|
||||
expect(rows[0]!.sourceId).toBe("nexus-content");
|
||||
expect(rows[0]!.name).toBe("Diagram Generator");
|
||||
});
|
||||
|
||||
it("Test 6: Duplicate fetch (restart) does not create duplicate rows — idempotent", async () => {
|
||||
await writeFile(
|
||||
path.join(tmpContentDir, "diagram.SKILL.md"),
|
||||
mockSkillMd("Diagram Generator", "Generates diagrams", "diagram"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { fetchAllSources } = await import("../services/skill-registry-fetcher.js");
|
||||
const { getSkillRegistryDb } = await import("../services/skill-registry-db.js");
|
||||
const { skills, skillVersions } = await import("../services/skill-registry-schema.js");
|
||||
|
||||
const localSource = {
|
||||
id: "nexus-content",
|
||||
type: "local-nexus-content" as const,
|
||||
dir: tmpContentDir,
|
||||
label: "Nexus Content Tools",
|
||||
};
|
||||
|
||||
await fetchAllSources([localSource]);
|
||||
await fetchAllSources([localSource]);
|
||||
|
||||
const db = await getSkillRegistryDb();
|
||||
const skillRows = await db.select().from(skills);
|
||||
const versionRows = await db.select().from(skillVersions);
|
||||
|
||||
// Should be exactly 1 skill and 1 version (no duplicates)
|
||||
expect(skillRows).toHaveLength(1);
|
||||
expect(versionRows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("Test 7: Missing directory returns fetched=0 with no errors", async () => {
|
||||
const { fetchAllSources } = await import("../services/skill-registry-fetcher.js");
|
||||
const localSource = {
|
||||
id: "nexus-content",
|
||||
type: "local-nexus-content" as const,
|
||||
dir: "/nonexistent/path/that/does/not/exist",
|
||||
label: "Nexus Content Tools",
|
||||
};
|
||||
|
||||
const result = await fetchAllSources([localSource]);
|
||||
expect(result.fetched).toBe(0);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests — Creative group members (placeholder — filled in by Task 2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("Creative group members", () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = await mkdtemp(path.join(os.tmpdir(), "nexus-creative-group-test-"));
|
||||
process.env.PAPERCLIP_HOME = tmpDir;
|
||||
|
||||
const { resetSkillRegistryDb } = await import("../services/skill-registry-db.js");
|
||||
resetSkillRegistryDb();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const { resetSkillRegistryDb } = await import("../services/skill-registry-db.js");
|
||||
resetSkillRegistryDb();
|
||||
delete process.env.PAPERCLIP_HOME;
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("Test 8: seedCreativeGroupMembers inserts 9 rows for builtin/creative group", async () => {
|
||||
const { getSkillRegistryDb, seedCreativeGroupMembers } = await import("../services/skill-registry-db.js");
|
||||
|
||||
// Initialize DB (creates tables and builtin groups)
|
||||
await getSkillRegistryDb();
|
||||
|
||||
// Seed the group members
|
||||
await seedCreativeGroupMembers();
|
||||
|
||||
// Query directly
|
||||
const { getRawClient } = await import("../services/skill-registry-db.js");
|
||||
const client = getRawClient();
|
||||
const result = await client.execute({
|
||||
sql: `SELECT skill_id FROM skill_group_members WHERE group_id = ?`,
|
||||
args: ["builtin/creative"],
|
||||
});
|
||||
|
||||
expect(result.rows).toHaveLength(9);
|
||||
const skillIds = result.rows.map((r) => r[0] as string);
|
||||
expect(skillIds).toContain("nexus-content/diagram");
|
||||
expect(skillIds).toContain("nexus-content/presentation");
|
||||
});
|
||||
|
||||
it("Test 9: seedCreativeGroupMembers is idempotent — calling twice does not duplicate rows", async () => {
|
||||
const { getSkillRegistryDb, seedCreativeGroupMembers } = await import("../services/skill-registry-db.js");
|
||||
|
||||
await getSkillRegistryDb();
|
||||
await seedCreativeGroupMembers();
|
||||
await seedCreativeGroupMembers();
|
||||
|
||||
const { getRawClient } = await import("../services/skill-registry-db.js");
|
||||
const client = getRawClient();
|
||||
const result = await client.execute({
|
||||
sql: `SELECT skill_id FROM skill_group_members WHERE group_id = ?`,
|
||||
args: ["builtin/creative"],
|
||||
});
|
||||
|
||||
// Still exactly 9, not 18
|
||||
expect(result.rows).toHaveLength(9);
|
||||
});
|
||||
});
|
||||
|
|
@ -381,24 +381,28 @@ describe("skill-registry-fetch", () => {
|
|||
// -------------------------------------------------------------------------
|
||||
// Test 7: BUILT_IN_SOURCES has exactly 3 entries
|
||||
// -------------------------------------------------------------------------
|
||||
it("Test 7: BUILT_IN_SOURCES contains 3 entries (anthropic-official, schwepps-skills, daymade-skills)", async () => {
|
||||
it("Test 7: BUILT_IN_SOURCES contains 4 entries (anthropic-official, schwepps-skills, daymade-skills, nexus-content)", async () => {
|
||||
const { BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js");
|
||||
|
||||
expect(BUILT_IN_SOURCES).toHaveLength(3);
|
||||
expect(BUILT_IN_SOURCES).toHaveLength(4);
|
||||
|
||||
const ids = BUILT_IN_SOURCES.map((s) => s.id);
|
||||
expect(ids).toContain("anthropic-official");
|
||||
expect(ids).toContain("schwepps-skills");
|
||||
expect(ids).toContain("daymade-skills");
|
||||
expect(ids).toContain("nexus-content");
|
||||
|
||||
// Verify all sources have required fields
|
||||
for (const source of BUILT_IN_SOURCES) {
|
||||
expect(source.id).toBeTruthy();
|
||||
expect(source.type).toMatch(/^(anthropic-marketplace|github-tree)$/);
|
||||
expect(source.owner).toBeTruthy();
|
||||
expect(source.repo).toBeTruthy();
|
||||
expect(source.ref).toBeTruthy();
|
||||
expect(source.label).toBeTruthy();
|
||||
// local-nexus-content does not have owner/repo/ref
|
||||
if (source.type !== "local-nexus-content") {
|
||||
expect(source.type).toMatch(/^(anthropic-marketplace|github-tree)$/);
|
||||
expect(source.owner).toBeTruthy();
|
||||
expect(source.repo).toBeTruthy();
|
||||
expect(source.ref).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -614,19 +614,21 @@ export async function startServer(): Promise<StartedServer> {
|
|||
logger.error({ err }, "startup reconciliation of persisted runtime services failed");
|
||||
});
|
||||
|
||||
// [nexus] Initialize skill registry database (fire-and-forget)
|
||||
// [nexus] Initialize skill registry, seed content skills, populate Creative group, reconcile pending groups
|
||||
// Sequence: DB init -> fetchAll (registers local skills) -> seed Creative group members -> reconcile pending
|
||||
void (async () => {
|
||||
try {
|
||||
const { getSkillRegistryDb } = await import("./services/skill-registry-db.js");
|
||||
const { getSkillRegistryDb, seedCreativeGroupMembers } = await import("./services/skill-registry-db.js");
|
||||
await getSkillRegistryDb();
|
||||
logger.info("skill registry database initialized");
|
||||
const { skillRegistryService } = await import("./services/skill-registry.js");
|
||||
await skillRegistryService().fetchAll();
|
||||
await seedCreativeGroupMembers();
|
||||
logger.info("skill registry initialized, content skills seeded, Creative group populated");
|
||||
} catch (err) {
|
||||
logger.error({ err }, "skill registry init failed");
|
||||
}
|
||||
})();
|
||||
|
||||
// [nexus] Reconcile pendingSkillGroups metadata on agents (fire-and-forget)
|
||||
void (async () => {
|
||||
// Reconcile pendingSkillGroups metadata on agents — runs after Creative group is seeded
|
||||
try {
|
||||
const { join } = await import("node:path");
|
||||
const { skillGroupService } = await import("./services/skill-registry-groups.js");
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ async function runSingleFileUpload(
|
|||
});
|
||||
}
|
||||
|
||||
export function convertRoutes(db: Db, _storage: StorageService) {
|
||||
export function convertRoutes(db: Db, storage: StorageService) {
|
||||
const router = Router();
|
||||
|
||||
// POST /companies/:companyId/convert — multipart upload + MIME validation + job dispatch
|
||||
|
|
@ -139,7 +139,7 @@ export function convertRoutes(db: Db, _storage: StorageService) {
|
|||
sourceTaskId: typeof req.body.sourceTaskId === "string" ? req.body.sourceTaskId : null,
|
||||
});
|
||||
|
||||
void contentJobRunner.dispatch(db, {} as StorageService, job!);
|
||||
void contentJobRunner.dispatch(db, storage, job!);
|
||||
|
||||
res.status(202).json({
|
||||
jobId: job!.id,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { resolveSkillRegistryDbPath } from "../home-paths.js";
|
|||
export type SkillRegistryDb = ReturnType<typeof drizzle<typeof schema>>;
|
||||
|
||||
let _db: SkillRegistryDb | null = null;
|
||||
let _client: LibSQLClient | null = null;
|
||||
|
||||
const CREATE_SKILLS_TABLE = `
|
||||
CREATE TABLE IF NOT EXISTS skills (
|
||||
|
|
@ -147,6 +148,7 @@ export async function getSkillRegistryDb(): Promise<SkillRegistryDb> {
|
|||
await mkdir(dirname(dbPath), { recursive: true });
|
||||
|
||||
const client = createClient({ url: `file:${dbPath}` });
|
||||
_client = client;
|
||||
_db = drizzle({ client, schema });
|
||||
|
||||
await client.execute(CREATE_SKILLS_TABLE);
|
||||
|
|
@ -181,7 +183,47 @@ export async function getSkillRegistryDb(): Promise<SkillRegistryDb> {
|
|||
return _db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the raw LibSQL client singleton.
|
||||
* Must be called after getSkillRegistryDb() to ensure the client is initialized.
|
||||
*/
|
||||
export function getRawClient(): LibSQLClient {
|
||||
if (_client === null) {
|
||||
throw new Error("Skill registry DB not initialized — call getSkillRegistryDb() first");
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
const NEXUS_CONTENT_SKILL_IDS = [
|
||||
"nexus-content/diagram",
|
||||
"nexus-content/icon-set",
|
||||
"nexus-content/theme-palette",
|
||||
"nexus-content/wallpaper",
|
||||
"nexus-content/social-post",
|
||||
"nexus-content/convert",
|
||||
"nexus-content/pdf-document",
|
||||
"nexus-content/brand-kit",
|
||||
"nexus-content/presentation",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Seed the builtin/creative skill group with all 9 Nexus content skill IDs.
|
||||
* Uses INSERT OR IGNORE for idempotency — safe to call multiple times.
|
||||
* Must be called AFTER skillRegistryService().fetchAll() so skill rows exist.
|
||||
*/
|
||||
export async function seedCreativeGroupMembers(): Promise<void> {
|
||||
const client = getRawClient();
|
||||
const now = Date.now();
|
||||
for (const skillId of NEXUS_CONTENT_SKILL_IDS) {
|
||||
await client.execute({
|
||||
sql: `INSERT OR IGNORE INTO skill_group_members (group_id, skill_id, added_at) VALUES (?, ?, ?)`,
|
||||
args: ["builtin/creative", skillId, now],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Reset the singleton — used for test cleanup */
|
||||
export function resetSkillRegistryDb(): void {
|
||||
_db = null;
|
||||
_client = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import crypto from "node:crypto";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { mkdir, writeFile, readdir, readFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getSkillRegistryDb, type SkillRegistryDb } from "./skill-registry-db.js";
|
||||
import { skills, skillVersions, skillFiles, communityRatings } from "./skill-registry-schema.js";
|
||||
|
|
@ -17,14 +18,10 @@ import { resolveSkillCacheDir } from "../home-paths.js";
|
|||
// Source config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type SkillSourceConfig = {
|
||||
id: string;
|
||||
type: "anthropic-marketplace" | "github-tree";
|
||||
owner: string;
|
||||
repo: string;
|
||||
ref: string;
|
||||
label: string;
|
||||
};
|
||||
export type SkillSourceConfig =
|
||||
| { id: string; type: "anthropic-marketplace"; owner: string; repo: string; ref: string; label: string }
|
||||
| { id: string; type: "github-tree"; owner: string; repo: string; ref: string; label: string }
|
||||
| { id: string; type: "local-nexus-content"; dir: string; label: string };
|
||||
|
||||
export const BUILT_IN_SOURCES: SkillSourceConfig[] = [
|
||||
{
|
||||
|
|
@ -51,6 +48,12 @@ export const BUILT_IN_SOURCES: SkillSourceConfig[] = [
|
|||
ref: "main",
|
||||
label: "Daymade Community",
|
||||
},
|
||||
{
|
||||
id: "nexus-content",
|
||||
type: "local-nexus-content",
|
||||
dir: path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills", "content"),
|
||||
label: "Nexus Content Tools",
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -244,7 +247,7 @@ async function cacheSkillVersion(
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function fetchAnthropicMarketplace(
|
||||
source: SkillSourceConfig,
|
||||
source: Extract<SkillSourceConfig, { type: "anthropic-marketplace" }>,
|
||||
db: SkillRegistryDb,
|
||||
): Promise<number> {
|
||||
const marketplaceUrl = resolveRawGitHubUrl(
|
||||
|
|
@ -308,7 +311,7 @@ async function fetchAnthropicMarketplace(
|
|||
}
|
||||
|
||||
async function fetchGitHubTree(
|
||||
source: SkillSourceConfig,
|
||||
source: Extract<SkillSourceConfig, { type: "github-tree" }>,
|
||||
db: SkillRegistryDb,
|
||||
): Promise<number> {
|
||||
const treeUrl = `https://api.github.com/repos/${source.owner}/${source.repo}/git/trees/${encodeURIComponent(source.ref)}?recursive=1`;
|
||||
|
|
@ -372,6 +375,77 @@ async function fetchGitHubTree(
|
|||
return fetched;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Local filesystem source handler
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function fetchLocalNexusContent(
|
||||
source: Extract<SkillSourceConfig, { type: "local-nexus-content" }>,
|
||||
db: SkillRegistryDb,
|
||||
): Promise<number> {
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(source.dir, { withFileTypes: true });
|
||||
} catch {
|
||||
// Directory missing or unreadable — return 0, no error
|
||||
return 0;
|
||||
}
|
||||
|
||||
const skillMdFiles = entries.filter(
|
||||
(e) => e.isFile() && e.name.endsWith(".SKILL.md"),
|
||||
);
|
||||
|
||||
let fetched = 0;
|
||||
|
||||
for (const entry of skillMdFiles) {
|
||||
// Derive slug from filename: "diagram.SKILL.md" → "diagram"
|
||||
const slug = entry.name.replace(/\.SKILL\.md$/, "");
|
||||
const skillId = `${source.id}/${slug}`;
|
||||
const filePath = path.join(source.dir, entry.name);
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
content = await readFile(filePath, "utf-8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compute a content-based SHA-1 hash for idempotency
|
||||
const sha = crypto.createHash("sha1").update(content).digest("hex");
|
||||
const versionId = `${skillId}@${sha}`;
|
||||
|
||||
// Idempotency check — skip DB writes if version already cached
|
||||
if (await versionExists(db, versionId)) {
|
||||
fetched++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const { name, description } = parseSkillFrontmatter(content);
|
||||
const sourceUrl = `file://${filePath}`;
|
||||
|
||||
await upsertSkill(db, {
|
||||
skillId,
|
||||
sourceId: source.id,
|
||||
name: name ?? slug,
|
||||
description,
|
||||
sourceUrl,
|
||||
});
|
||||
|
||||
await cacheSkillVersion(db, {
|
||||
skillId,
|
||||
sha,
|
||||
skillMdContent: content,
|
||||
skillMdUrl: sourceUrl,
|
||||
});
|
||||
|
||||
await upsertCommunityRatingsStub(db, skillId, source.id);
|
||||
|
||||
fetched++;
|
||||
}
|
||||
|
||||
return fetched;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -398,8 +472,8 @@ export async function fetchAllSources(
|
|||
fetched += await fetchAnthropicMarketplace(source, db);
|
||||
} else if (source.type === "github-tree") {
|
||||
fetched += await fetchGitHubTree(source, db);
|
||||
} else {
|
||||
errors.push(`Unknown source type for ${source.id}`);
|
||||
} else if (source.type === "local-nexus-content") {
|
||||
fetched += await fetchLocalNexusContent(source, db);
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
|
|
|
|||
36
server/src/skills/content/brand-kit.SKILL.md
Normal file
36
server/src/skills/content/brand-kit.SKILL.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
name: Brand Kit Generator
|
||||
description: Generate a complete brand identity kit including logo, color palette, and social assets via the Nexus content job API
|
||||
---
|
||||
|
||||
# Brand Kit Generator
|
||||
|
||||
Generate a cohesive brand identity kit from a company name and description. Produces logo SVG, color palette, typography recommendations, and social media image templates.
|
||||
|
||||
## Usage
|
||||
|
||||
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
|
||||
|
||||
```json
|
||||
{
|
||||
"jobType": "brand-kit",
|
||||
"input": {
|
||||
"companyName": "Acme Corp",
|
||||
"description": "A modern SaaS platform for project management",
|
||||
"seedColor": "#2563eb"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `companyName` (required): The company or brand name
|
||||
- `description` (required): Brief description of the company and its focus
|
||||
- `seedColor` (optional): Hex color to anchor the brand palette
|
||||
|
||||
## Output
|
||||
|
||||
The job returns a `BrandKitBundle` with:
|
||||
- `logoSvg`: Primary logo as SVG
|
||||
- `palette`: Color tokens (primary, secondary, accent, neutrals)
|
||||
- `socialImages`: Array of platform-sized social image templates
|
||||
- `typography`: Recommended font pairings
|
||||
33
server/src/skills/content/convert.SKILL.md
Normal file
33
server/src/skills/content/convert.SKILL.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
name: Format Converter
|
||||
description: Convert files between formats using direct conversion or AI-bridged fallback via the Nexus content job API
|
||||
---
|
||||
|
||||
# Format Converter
|
||||
|
||||
Convert an existing asset to a different file format. Supports direct converter paths (ffmpeg, sharp, pandoc) with AI-bridged fallback for all format pairs.
|
||||
|
||||
## Usage
|
||||
|
||||
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
|
||||
|
||||
```json
|
||||
{
|
||||
"jobType": "convert",
|
||||
"input": {
|
||||
"sourceAssetId": "asset-uuid-here",
|
||||
"targetFormat": "pdf"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `sourceAssetId` (required): UUID of the source asset already stored in Nexus
|
||||
- `targetFormat` (required): Target format extension — e.g. `pdf`, `png`, `mp4`, `docx`, `svg`
|
||||
|
||||
## Output
|
||||
|
||||
The job returns a `ConvertBundle` with:
|
||||
- `assetId`: ID of the converted output asset
|
||||
- `targetFormat`: Format that was applied
|
||||
- `method`: Conversion path used — `direct` or `ai-bridge`
|
||||
32
server/src/skills/content/diagram.SKILL.md
Normal file
32
server/src/skills/content/diagram.SKILL.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
name: Diagram Generator
|
||||
description: Generate Mermaid diagrams as SVG via the Nexus content job API
|
||||
---
|
||||
|
||||
# Diagram Generator
|
||||
|
||||
Generate diagrams from natural language descriptions. Supports flowcharts, sequence diagrams, ER diagrams, Gantt charts, and more using Mermaid syntax rendered to SVG.
|
||||
|
||||
## Usage
|
||||
|
||||
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
|
||||
|
||||
```json
|
||||
{
|
||||
"jobType": "diagram",
|
||||
"input": {
|
||||
"prompt": "A flowchart showing user login flow with success and failure paths",
|
||||
"diagramType": "flowchart"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `prompt` (required): Natural language description of the diagram
|
||||
- `diagramType` (optional): Hint for diagram type — `flowchart`, `sequence`, `er`, `gantt`, `class`, `pie`
|
||||
|
||||
## Output
|
||||
|
||||
The job returns a `DiagramBundle` with:
|
||||
- `svg`: Rendered SVG string
|
||||
- `mermaidSource`: Raw Mermaid source code
|
||||
34
server/src/skills/content/icon-set.SKILL.md
Normal file
34
server/src/skills/content/icon-set.SKILL.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
name: Icon Set Generator
|
||||
description: Generate cohesive SVG icon sets with consistent style via the Nexus content job API
|
||||
---
|
||||
|
||||
# Icon Set Generator
|
||||
|
||||
Generate a set of SVG icons with a consistent visual style. Icons are produced as clean, scalable SVG suitable for UI, documentation, or branding use.
|
||||
|
||||
## Usage
|
||||
|
||||
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
|
||||
|
||||
```json
|
||||
{
|
||||
"jobType": "icon-set",
|
||||
"input": {
|
||||
"description": "Navigation icons for a dashboard app",
|
||||
"style": "outline",
|
||||
"count": 6
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `description` (required): What the icons represent or their purpose
|
||||
- `style` (optional): Visual style — `outline`, `filled`, `duotone` (default: `outline`)
|
||||
- `count` (optional): Number of icons to generate (default: 6, max: 12)
|
||||
|
||||
## Output
|
||||
|
||||
The job returns an `IconSetBundle` with:
|
||||
- `icons`: Array of `{ name, svg }` objects
|
||||
- `style`: The style applied
|
||||
33
server/src/skills/content/pdf-document.SKILL.md
Normal file
33
server/src/skills/content/pdf-document.SKILL.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
name: PDF Document Generator
|
||||
description: Generate styled PDF documents from structured content via the Nexus content job API
|
||||
---
|
||||
|
||||
# PDF Document Generator
|
||||
|
||||
Generate professional PDF documents from Markdown or structured content. Supports multiple document types with appropriate layouts and styling.
|
||||
|
||||
## Usage
|
||||
|
||||
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
|
||||
|
||||
```json
|
||||
{
|
||||
"jobType": "pdf-document",
|
||||
"input": {
|
||||
"content": "# Q1 Report\n\nRevenue grew 24% year-over-year...",
|
||||
"docType": "report"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `content` (required): Document body as Markdown or plain text
|
||||
- `docType` (optional): Layout template — `report`, `invoice`, `api-docs`, `one-pager` (default: `report`)
|
||||
|
||||
## Output
|
||||
|
||||
The job returns a `PdfBundle` with:
|
||||
- `assetId`: ID of the stored PDF asset
|
||||
- `pageCount`: Number of pages in the output
|
||||
- `docType`: Document type template applied
|
||||
35
server/src/skills/content/presentation.SKILL.md
Normal file
35
server/src/skills/content/presentation.SKILL.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
name: Presentation Generator
|
||||
description: Generate animated video presentations with LLM-authored slides via the Nexus content job API
|
||||
---
|
||||
|
||||
# Presentation Generator
|
||||
|
||||
Generate a video presentation from a topic. Uses LLM to author slide content and Remotion to render an MP4 with animations and transitions.
|
||||
|
||||
## Usage
|
||||
|
||||
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
|
||||
|
||||
```json
|
||||
{
|
||||
"jobType": "presentation",
|
||||
"input": {
|
||||
"topic": "Introduction to machine learning for product managers",
|
||||
"slideCount": 8,
|
||||
"style": "professional"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `topic` (required): The subject of the presentation
|
||||
- `slideCount` (optional): Number of slides to generate (default: 6, max: 20)
|
||||
- `style` (optional): Visual style — `professional`, `creative`, `minimal` (default: `professional`)
|
||||
|
||||
## Output
|
||||
|
||||
The job returns a `PresentationBundle` with:
|
||||
- `assetId`: ID of the rendered MP4 asset
|
||||
- `slides`: Array of slide data (title, bullets, speakerNotes)
|
||||
- `durationSeconds`: Approximate video duration
|
||||
34
server/src/skills/content/social-post.SKILL.md
Normal file
34
server/src/skills/content/social-post.SKILL.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
name: Social Post Generator
|
||||
description: Generate platform-optimized social media posts with optional images via the Nexus content job API
|
||||
---
|
||||
|
||||
# Social Post Generator
|
||||
|
||||
Generate social media post copy and optional image assets sized for specific platforms. Respects character limits and platform conventions.
|
||||
|
||||
## Usage
|
||||
|
||||
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
|
||||
|
||||
```json
|
||||
{
|
||||
"jobType": "social-post",
|
||||
"input": {
|
||||
"prompt": "Launch announcement for our new AI assistant feature",
|
||||
"platform": "twitter-x"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `prompt` (required): The subject or message to communicate
|
||||
- `platform` (optional): Target platform — `twitter-x`, `linkedin`, `instagram-caption`, `instagram-carousel` (default: `twitter-x`)
|
||||
|
||||
## Output
|
||||
|
||||
The job returns a `SocialPostBundle` with:
|
||||
- `copy`: Post text body
|
||||
- `hashtags`: Array of suggested hashtags
|
||||
- `slides`: Optional carousel slide content array
|
||||
- `platform`: The target platform applied
|
||||
33
server/src/skills/content/theme-palette.SKILL.md
Normal file
33
server/src/skills/content/theme-palette.SKILL.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
name: Theme Palette Generator
|
||||
description: Generate accessible OKLCH color palettes for UI theming via the Nexus content job API
|
||||
---
|
||||
|
||||
# Theme Palette Generator
|
||||
|
||||
Generate a complete UI color palette from a seed color. Uses OKLCH color space for perceptually uniform, accessible color scales with WCAG contrast ratios.
|
||||
|
||||
## Usage
|
||||
|
||||
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
|
||||
|
||||
```json
|
||||
{
|
||||
"jobType": "theme-palette",
|
||||
"input": {
|
||||
"seedColor": "#6366f1",
|
||||
"name": "Indigo Theme"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `seedColor` (required): Hex color used as the primary brand color
|
||||
- `name` (optional): Human-readable name for the theme
|
||||
|
||||
## Output
|
||||
|
||||
The job returns a `ThemePaletteBundle` with:
|
||||
- `colors`: Named color tokens (primary, surface, accent, text, border scales)
|
||||
- `wcagReport`: Contrast ratios for foreground/background pairs
|
||||
- `cssVars`: Ready-to-use CSS custom property declarations
|
||||
32
server/src/skills/content/wallpaper.SKILL.md
Normal file
32
server/src/skills/content/wallpaper.SKILL.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
name: Wallpaper Generator
|
||||
description: Generate desktop wallpapers as high-resolution PNG via the Nexus content job API
|
||||
---
|
||||
|
||||
# Wallpaper Generator
|
||||
|
||||
Generate desktop wallpapers from a description. Produces high-resolution PNG images suitable for desktop backgrounds, lock screens, or presentation backdrops.
|
||||
|
||||
## Usage
|
||||
|
||||
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
|
||||
|
||||
```json
|
||||
{
|
||||
"jobType": "wallpaper",
|
||||
"input": {
|
||||
"prompt": "Abstract geometric pattern with deep blue and gold tones",
|
||||
"platform": "desktop-fhd"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `prompt` (required): Visual description of the desired wallpaper
|
||||
- `platform` (optional): Target platform — `desktop-hd`, `desktop-fhd`, `desktop-4k`, `mobile-portrait`, `mobile-landscape`, `og-image`, `twitter-card`, `instagram-post`, `instagram-banner`, `linkedin-banner`, `app-icon`, `favicon` (default: `desktop-fhd`)
|
||||
|
||||
## Output
|
||||
|
||||
The job returns a `WallpaperBundle` with:
|
||||
- `assetId`: ID of the stored PNG asset
|
||||
- `resolution`: Final output dimensions
|
||||
|
|
@ -8,18 +8,32 @@ import { DocumentGeneratePanel } from "../components/DocumentGeneratePanel";
|
|||
import { BrandKitPanel } from "../components/BrandKitPanel";
|
||||
import { PresentationPanel } from "../components/PresentationPanel";
|
||||
import { ThemeSeedInput } from "../components/ThemeSeedInput";
|
||||
import { ThemePaletteGrid } from "../components/ThemePaletteGrid";
|
||||
import { ThemePaletteGrid, type PaletteRole } from "../components/ThemePaletteGrid";
|
||||
import { ThemePreviewPanel } from "../components/ThemePreviewPanel";
|
||||
import { ThemeExportTabs } from "../components/ThemeExportTabs";
|
||||
import { ThemeApplyConfirmDialog } from "../components/ThemeApplyConfirmDialog";
|
||||
import { useContentJob } from "../hooks/useContentJob";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export function ContentStudio() {
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const companyId = selectedCompanyId ?? "";
|
||||
const themeJob = useContentJob(companyId);
|
||||
const [showApplyDialog, setShowApplyDialog] = useState(false);
|
||||
const [seedColor, setSeedColor] = useState("#4f46e5");
|
||||
const [themeBundle, setThemeBundle] = useState<{
|
||||
palette: PaletteRole[];
|
||||
exports: { css: string; tailwind: string; vscode: string; json: string };
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (themeJob.status === "done" && themeJob.resultAssetId && companyId) {
|
||||
fetch(`/api/companies/${companyId}/assets/${themeJob.resultAssetId}`)
|
||||
.then((r) => r.json())
|
||||
.then((data) => setThemeBundle(data as typeof themeBundle))
|
||||
.catch(() => {});
|
||||
}
|
||||
}, [themeJob.status, themeJob.resultAssetId, companyId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
|
|
@ -57,17 +71,24 @@ export function ContentStudio() {
|
|||
{companyId ? (
|
||||
<div className="flex flex-col gap-6">
|
||||
<ThemeSeedInput
|
||||
companyId={companyId}
|
||||
onSubmit={(seedColor) => {
|
||||
value={seedColor}
|
||||
onChange={setSeedColor}
|
||||
/>
|
||||
<button
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
disabled={themeJob.status === "running" || themeJob.status === "queued"}
|
||||
onClick={() => {
|
||||
setThemeBundle(null);
|
||||
themeJob.submit("theme-palette", { seedColor });
|
||||
}}
|
||||
isLoading={themeJob.status === "running" || themeJob.status === "queued"}
|
||||
/>
|
||||
{themeJob.bundle && (
|
||||
>
|
||||
{themeJob.status === "running" || themeJob.status === "queued" ? "Generating..." : "Generate Palette"}
|
||||
</button>
|
||||
{themeBundle && (
|
||||
<>
|
||||
<ThemePaletteGrid palette={(themeJob.bundle as Record<string, unknown>).palette as Array<Record<string, unknown>>} />
|
||||
<ThemePreviewPanel palette={(themeJob.bundle as Record<string, unknown>).palette as Array<Record<string, unknown>>} />
|
||||
<ThemeExportTabs exports={(themeJob.bundle as Record<string, unknown>).exports as Record<string, string>} />
|
||||
<ThemePaletteGrid palette={themeBundle.palette} />
|
||||
<ThemePreviewPanel palette={themeBundle.palette} variant="light" />
|
||||
<ThemeExportTabs exports={themeBundle.exports} />
|
||||
<button
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
onClick={() => setShowApplyDialog(true)}
|
||||
|
|
@ -76,10 +97,10 @@ export function ContentStudio() {
|
|||
</button>
|
||||
<ThemeApplyConfirmDialog
|
||||
open={showApplyDialog}
|
||||
onOpenChange={setShowApplyDialog}
|
||||
onConfirm={() => {
|
||||
setShowApplyDialog(false);
|
||||
}}
|
||||
onCancel={() => setShowApplyDialog(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue