ワンクリックで
extension-building
Build library extensions for systemprompt.io with Rust - architecture, traits, schemas, API routes, jobs, and code review
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Build library extensions for systemprompt.io with Rust - architecture, traits, schemas, API routes, jobs, and code review
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Maintains the living marketing strategy doc at /var/www/html/systemprompt-web/reports/marketing/marketing-strategy-master.md. Never rewrites wholesale — writes diffs. Contains objectives, 30/60/90 targets, channel hypotheses in flight, dead hypotheses, winning tactics, and funnel snapshots. Load marketing-identity first.
Deterministically audit and optimise a published documentation page. Runs a 10-section quality audit, applies 5 rewrite rules for structure, terminology, link health, and completeness. Reads website analytics and GSC data per doc URL, produces a 75-point score delta (100 with analytics), commits changes, and updates the per-doc report. Load identity and brand-voice first.
Deterministically audit and optimise a published feature page on the register ladder: struggling-moment heroes with zero implementation jargon, mechanism evidence relocated to section bodies and dropdowns in the HashiCorp / Stripe / Tailscale register. Runs an 11-section quality audit plus register-ladder checks (hero jargon count, homepage narrative echo, struggling-moment opener, identifier-enumeration, established-not-indie voice) and applies 6 rewrite rules: claim verification, conversion clarity, brand discipline, and Technical-Marketing Synthesis (ten sub-checks: outcome headlines, jargon payoff scoped to sections/dropdowns, numbers with context, feature-to-outcome binding, narrative-vs-reference separation, skeptic test, named surfaces over coined metaphors, dropdown alignment, no Rust internals in narrative, dense sentences no filler). Penalises metaphor-stacking and marketing adjectives. Reads website analytics and GSC data per feature URL, produces a 90-point score delta (115 with analytics), commit
Deterministically audit and optimise a published guide. Runs a 14-section quality audit, applies 7 rewrite rules for value density, brand discipline, search-intent alignment, and CTR. Reads 28-day GSC query data per URL, produces a 100-point score delta across 11 dimensions, commits changes, and updates the per-guide report. Handles guides without GSC data. Load identity and brand-voice first.
Daily CRM and funnel measurement. Pulls GitHub Traffic API for systemprompt-core and systemprompt-template (14d retention, MUST run daily), website analytics via systemprompt CLI, and external feedback signals. Emits 1d/7d/31d funnel deltas and a dated report. Source of truth for every hypothesis metric.
Daily SEO briefing. Analyses content traffic, engagement, and search performance across published guides using internal analytics and Google Search Console. Generates actionable reports with S-### hypotheses. Designed for daily /loop. Load identity first.
| name | extension-building |
| description | Build library extensions for systemprompt.io with Rust - architecture, traits, schemas, API routes, jobs, and code review |
Build library extensions for systemprompt.io. Reference implementation: extensions/web/.
If it's Rust code, it's an extension. If it's YAML/Markdown, it's a service.
| Category | Format | Location |
|---|---|---|
| Extensions | .rs | /extensions/ |
| Services | YAML/Markdown | /services/ |
The core/ directory is a git submodule. Never modify it.
systemprompt-template/
├── core/ # READ-ONLY submodule
├── extensions/ # ALL Rust code
│ ├── web/ # Reference implementation
│ ├── cli/ # CLI extensions
│ └── mcp/ # MCP servers
├── services/ # YAML/Markdown only
│ ├── agents/ # Agent definitions
│ ├── config/ # Configuration
│ ├── content/ # Markdown content
│ ├── skills/ # Skills
│ └── web/ # Theme config
├── profiles/ # Environment configs
└── src/main.rs # Server entry
┌─────────────────────────────────────────────┐
│ src/main.rs │
│ Loads config, connects DB, mounts routers │
└──────────────────────┬──────────────────────┘
│
┌──────────────────────▼──────────────────────┐
│ extensions/ │
│ Schemas, Models, Repos, Services, API, Jobs │
└──────────────────────┬──────────────────────┘
│
┌──────────────────────▼──────────────────────┐
│ services/ (config) │
│ Agent YAML, AI config, schedules, content │
└──────────────────────┬──────────────────────┘
│ imports
┌──────────────────────▼──────────────────────┐
│ core/ (read-only) │
│ Traits, Models, IDs, DB, Logging, Security │
└─────────────────────────────────────────────┘
┌─────────────────────────────┐
│ API (handlers) │ HTTP requests
└─────────────┬───────────────┘
│ calls
┌─────────────▼───────────────┐
│ Services │ Business logic
└─────────────┬───────────────┘
│ calls
┌─────────────▼───────────────┐
│ Repository │ SQL queries
└─────────────┬───────────────┘
│ uses
┌─────────────▼───────────────┐
│ Models │ Domain types
└─────────────────────────────┘
Rules:
extensions/my-extension/
├── Cargo.toml
├── build.rs
├── schema/
│ ├── 001_tables.sql
│ └── migrations/
│ └── 001_add_status.sql
└── src/
├── lib.rs
├── extension.rs
├── error.rs
├── models/
├── repository/
├── services/
├── api/
└── jobs/
[package]
name = "my-extension"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["rlib"]
[dependencies]
systemprompt = { workspace = true }
axum = { workspace = true }
tokio = { workspace = true }
sqlx = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
async-trait = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
# Required only if the extension has schema/migrations/ files.
[build-dependencies]
systemprompt-extension = { workspace = true }
mod error;
mod extension;
mod models;
mod repository;
mod services;
mod api;
mod jobs;
pub use error::MyExtensionError;
pub use extension::MyExtension;
pub const PREFIX: &str = "my-extension";
use std::sync::Arc;
use systemprompt::extension::prelude::*;
use crate::api;
use crate::jobs::CleanupJob;
#[derive(Debug, Default)]
pub struct MyExtension;
impl Extension for MyExtension {
fn metadata(&self) -> ExtensionMetadata {
ExtensionMetadata {
id: "my-extension",
name: "My Extension",
version: env!("CARGO_PKG_VERSION"),
}
}
fn priority(&self) -> u32 {
50
}
fn dependencies(&self) -> Vec<&'static str> {
vec!["users"]
}
fn schemas(&self) -> Vec<SchemaDefinition> {
vec![
SchemaDefinition::inline("my_tables", include_str!("../schema/001_tables.sql")),
]
}
fn migration_weight(&self) -> u32 {
50
}
fn router(&self, ctx: &dyn ExtensionContext) -> Option<ExtensionRouter> {
let db = ctx.database();
let pool = db.as_any().downcast_ref::<Database>()?.pool()?;
Some(ExtensionRouter::new(api::router(pool), "/api/v1/my-extension"))
}
fn jobs(&self) -> Vec<Arc<dyn Job>> {
vec![Arc::new(CleanupJob)]
}
}
register_extension!(MyExtension);
use axum::http::StatusCode;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyExtensionError {
#[error("Not found: {0}")]
NotFound(String),
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("Validation error: {0}")]
Validation(String),
}
impl MyExtensionError {
pub fn code(&self) -> &'static str {
match self {
Self::NotFound(_) => "NOT_FOUND",
Self::Database(_) => "DATABASE_ERROR",
Self::Validation(_) => "VALIDATION_ERROR",
}
}
pub fn status(&self) -> StatusCode {
match self {
Self::NotFound(_) => StatusCode::NOT_FOUND,
Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Validation(_) => StatusCode::BAD_REQUEST,
}
}
}
impl axum::response::IntoResponse for MyExtensionError {
fn into_response(self) -> axum::response::Response {
// JSON: API boundary -- constructing fixed-shape error response
let body = serde_json::json!({
"error": {
"code": self.code(),
"message": self.to_string(),
}
});
(self.status(), axum::Json(body)).into_response()
}
}
CREATE TABLE IF NOT EXISTS my_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_my_items_name ON my_items(name);
CREATE INDEX IF NOT EXISTS idx_my_items_created ON my_items(created_at DESC);
fn schemas(&self) -> Vec<SchemaDefinition> {
vec![
SchemaDefinition::inline("my_items", include_str!("../schema/001_tables.sql")),
]
}
fn migration_weight(&self) -> u32 {
50
}
Migration SQL lives in schema/migrations/NNN_<name>.sql files — never as a Rust
string literal. The crate's build.rs discovers them; extension_migrations!()
returns them. The version (NNN) and name come from the filename, so adding a
migration is adding one file with no Rust edit.
// build.rs
fn main() {
systemprompt_extension::build::emit_migrations();
}
// src/extension.rs
fn migrations(&self) -> Vec<Migration> {
extension_migrations!()
}
-- schema/migrations/001_add_status.sql
ALTER TABLE my_items ADD COLUMN IF NOT EXISTS status TEXT DEFAULT 'active';
Inline SQL strings and hand-written Migration::new(...) lists are rejected by
just lint-extensions. Add build.rs to the package include list so it ships
on publish.
Use companion tables, not ALTER:
CREATE TABLE IF NOT EXISTS content_metadata (
id TEXT PRIMARY KEY,
content_id TEXT NOT NULL UNIQUE,
custom_field JSONB DEFAULT '{}',
CONSTRAINT fk_content
FOREIGN KEY (content_id) REFERENCES markdown_content(id) ON DELETE CASCADE
);
use axum::{Router, routing::{get, post, put, delete}};
use std::sync::Arc;
use sqlx::PgPool;
mod handlers;
#[derive(Clone)]
pub struct AppState {
pub pool: Arc<PgPool>,
}
pub fn router(pool: Arc<PgPool>) -> Router {
let state = AppState { pool };
Router::new()
.route("/items", get(handlers::list).post(handlers::create))
.route("/items/:id", get(handlers::get).put(handlers::update).delete(handlers::delete))
.with_state(state)
}
use axum::{extract::{Path, State, Json}, http::StatusCode};
use uuid::Uuid;
pub async fn list(
State(state): State<AppState>,
) -> Result<Json<Vec<Item>>, MyExtensionError> {
let items = sqlx::query_as!(Item, "SELECT * FROM my_items ORDER BY created_at DESC")
.fetch_all(state.pool.as_ref())
.await?;
Ok(Json(items))
}
pub async fn get(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<Json<Item>, MyExtensionError> {
let item = sqlx::query_as!(Item, "SELECT * FROM my_items WHERE id = $1", id)
.fetch_optional(state.pool.as_ref())
.await?
.ok_or_else(|| MyExtensionError::NotFound(id.to_string()))?;
Ok(Json(item))
}
pub async fn create(
State(state): State<AppState>,
Json(input): Json<CreateInput>,
) -> Result<(StatusCode, Json<Item>), MyExtensionError> {
let item = sqlx::query_as!(
Item,
"INSERT INTO my_items (name, description) VALUES ($1, $2) RETURNING *",
input.name,
input.description
)
.fetch_one(state.pool.as_ref())
.await?;
Ok((StatusCode::CREATED, Json(item)))
}
use systemprompt_provider_contracts::{Job, JobContext, JobResult};
#[derive(Debug, Clone, Copy, Default)]
pub struct CleanupJob;
#[async_trait::async_trait]
impl Job for CleanupJob {
fn name(&self) -> &'static str {
"my-extension-cleanup"
}
fn description(&self) -> &'static str {
"Clean up expired items"
}
fn schedule(&self) -> &'static str {
"0 0 * * * *"
}
async fn execute(&self, ctx: &JobContext) -> anyhow::Result<JobResult> {
let pool = ctx.db_pool::<Arc<PgPool>>()
.ok_or_else(|| anyhow::anyhow!("Database not available"))?;
let deleted = sqlx::query!(
"DELETE FROM my_items WHERE created_at < NOW() - INTERVAL '30 days'"
)
.execute(&*pool)
.await?
.rows_affected();
tracing::info!(deleted = deleted, "Cleanup completed");
Ok(JobResult::success()
.with_stats(deleted, 0)
.with_message(format!("Deleted {} items", deleted)))
}
}
systemprompt::traits::submit_job!(&CleanupJob);
| Schedule | Meaning |
|---|---|
0 0 * * * * | Every hour |
0 */15 * * * * | Every 15 minutes |
0 0 0 * * * | Daily at midnight |
0 30 2 * * * | Daily at 2:30 AM |
"" | Manual only |
fn jobs(&self) -> Vec<Arc<dyn Job>> {
vec![Arc::new(CleanupJob)]
}
[workspace]
members = [
"extensions/my-extension",
]
[dependencies]
my-extension = { path = "extensions/my-extension" }
pub use my_extension as my_ext;
pub fn __force_extension_link() {
let _ = core::hint::black_box(&my_ext::PREFIX);
}
let name = request.name.as_deref().map(str::trim);
let value = opt.unwrap_or_else(|| compute_default());
let result = input.ok_or_else(|| Error::Missing)?;
let valid_items: Vec<_> = items
.iter()
.filter(|item| item.is_active())
.map(|item| item.to_dto())
.collect();
| Construct | Resolution |
|---|---|
unsafe | Remove - forbidden |
unwrap() | Use ?, ok_or_else(), or expect() with message |
panic!() / todo!() | Return Result or implement |
Inline comments (//) | Delete - code documents itself |
Doc comments (///) | Delete - no rustdoc |
| Tests in source files | Move to separate test crate |
serde_json::Value | Define typed structs with #[derive(Deserialize)]. Allowed only at protocol/trait boundaries with justification comment |
ContentId, UserId, etc. from systemprompt_identifierstracing. No println! in library codequery!(), query_as!(), query_scalar!() - compile-time verifiedthiserrorDateTime<Utc> in Rust, TIMESTAMPTZ in PostgreSQL| Prefix | Returns |
|---|---|
get_ | Result<T> - fails if missing |
find_ | Result<Option<T>> - may not exist |
list_ | Result<Vec<T>> |
create_ | Result<T> or Result<Id> |
update_ | Result<T> or Result<()> |
delete_ | Result<()> |
| Metric | Limit |
|---|---|
| Source file length | 300 lines |
| Cognitive complexity | 15 |
| Function length | 75 lines |
| Parameters | 5 |
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
let pg_pool = db_pool.pool().ok_or_else(|| {
McpError::internal_error("Database pool not available", None)
})?;
let repo = ContentRepository::new(pg_pool);
let db_pool = ctx.db_pool::<DbPool>()
.ok_or_else(|| anyhow::anyhow!("Database not available"))?;
let repo = ContentRepository::new(db_pool.pool().unwrap());
// JSON: required by PageDataProvider trait contract
let Some(db) = ctx.db_pool::<Arc<Database>>() else {
return Ok(json!({ "data": "" }));
};
let Some(pool) = db.pool() else {
return Ok(json!({ "data": "" }));
};
systemprompt-models = { git = "..." }
systemprompt-identifiers = { git = "..." }
systemprompt-traits = { git = "..." }
systemprompt-core-database = { git = "..." }
systemprompt-blog-extension = { path = "../blog" }
systemprompt-core-api = { git = "..." } # FORBIDDEN
systemprompt-core-scheduler = { git = "..." } # FORBIDDEN
Use a single dependency instead of multiple internal crates:
[dependencies]
systemprompt = { version = "0.1", features = ["api"] }
| Feature | Use Case |
|---|---|
core | Minimal extensions, CLI tools |
database | Extensions with database tables |
mcp | MCP servers with proc macros |
api | Full HTTP API extensions |
full | Complete applications |
Cargo.toml with crate-type = ["rlib"] and workspace dependenciessrc/lib.rs exports public API and PREFIX constantsrc/extension.rs implements Extension traitregister_extension! macro calledsrc/error.rs implements error types with thiserrorIF NOT EXISTS patternssrc/lib.rs via __force_extension_link()Cargo.tomlcargo clippy -p {crate} -- -D warnings passescargo fmt -p {crate} -- --check passesReview extensions as world-class idiomatic Rust. Generate status.md with:
.rs filesfile:line + type| Task | Command |
|---|---|
| Build | cargo build -p systemprompt-{name}-extension |
| Test | cargo test -p systemprompt-{name}-extension |
| Lint | cargo clippy -p systemprompt-{name}-extension -- -D warnings |
| Format | cargo fmt --all |
| Clean build | cargo clean && cargo build |
| Verify | cargo run -- extensions list |
| Run migrations | systemprompt infra db migrate |
| Run job | systemprompt infra jobs run <name> |
| Concept | Location |
|---|---|
| Extension trait | extensions/web/src/extension.rs |
| ExtensionError | extensions/web/src/error.rs |
| Repository | extensions/web/src/repository/ |
| Service | extensions/web/src/services/ |
| API | extensions/web/src/api/ |
| Jobs | extensions/web/src/jobs/ |