Renames the config directory and updates 514 references across 42 Rust source files, plus CLAUDE.md, .gitignore, Makefile, script/release, and .mcp.json files. All 1205 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
216 lines
5.7 KiB
Rust
216 lines
5.7 KiB
Rust
pub mod agents;
|
|
pub mod agents_sse;
|
|
pub mod anthropic;
|
|
pub mod assets;
|
|
pub mod chat;
|
|
pub mod context;
|
|
pub mod health;
|
|
pub mod io;
|
|
pub mod mcp;
|
|
pub mod model;
|
|
pub mod settings;
|
|
pub mod workflow;
|
|
|
|
pub mod project;
|
|
pub mod ws;
|
|
|
|
use agents::AgentsApi;
|
|
use anthropic::AnthropicApi;
|
|
use chat::ChatApi;
|
|
use context::AppContext;
|
|
use health::HealthApi;
|
|
use io::IoApi;
|
|
use model::ModelApi;
|
|
use poem::EndpointExt;
|
|
use poem::{Route, get, post};
|
|
use poem_openapi::OpenApiService;
|
|
use project::ProjectApi;
|
|
use settings::SettingsApi;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::Arc;
|
|
|
|
use crate::slack::SlackWebhookContext;
|
|
use crate::whatsapp::WhatsAppWebhookContext;
|
|
|
|
const DEFAULT_PORT: u16 = 3001;
|
|
|
|
pub fn parse_port(value: Option<String>) -> u16 {
|
|
value
|
|
.and_then(|v| v.parse::<u16>().ok())
|
|
.unwrap_or(DEFAULT_PORT)
|
|
}
|
|
|
|
pub fn resolve_port() -> u16 {
|
|
parse_port(std::env::var("STORYKIT_PORT").ok())
|
|
}
|
|
|
|
pub fn write_port_file(dir: &Path, port: u16) -> Option<PathBuf> {
|
|
let path = dir.join(".storkit_port");
|
|
std::fs::write(&path, port.to_string()).ok()?;
|
|
Some(path)
|
|
}
|
|
|
|
pub fn remove_port_file(path: &Path) {
|
|
let _ = std::fs::remove_file(path);
|
|
}
|
|
|
|
pub fn build_routes(
|
|
ctx: AppContext,
|
|
whatsapp_ctx: Option<Arc<WhatsAppWebhookContext>>,
|
|
slack_ctx: Option<Arc<SlackWebhookContext>>,
|
|
) -> impl poem::Endpoint {
|
|
let ctx_arc = std::sync::Arc::new(ctx);
|
|
|
|
let (api_service, docs_service) = build_openapi_service(ctx_arc.clone());
|
|
|
|
let mut route = Route::new()
|
|
.nest("/api", api_service)
|
|
.nest("/docs", docs_service.swagger_ui())
|
|
.at("/ws", get(ws::ws_handler))
|
|
.at(
|
|
"/agents/:story_id/:agent_name/stream",
|
|
get(agents_sse::agent_stream),
|
|
)
|
|
.at(
|
|
"/mcp",
|
|
post(mcp::mcp_post_handler).get(mcp::mcp_get_handler),
|
|
)
|
|
.at("/health", get(health::health))
|
|
.at("/assets/*path", get(assets::embedded_asset))
|
|
.at("/", get(assets::embedded_index))
|
|
.at("/*path", get(assets::embedded_file));
|
|
|
|
if let Some(wa_ctx) = whatsapp_ctx {
|
|
route = route.at(
|
|
"/webhook/whatsapp",
|
|
get(crate::whatsapp::webhook_verify)
|
|
.post(crate::whatsapp::webhook_receive)
|
|
.data(wa_ctx),
|
|
);
|
|
}
|
|
|
|
if let Some(sl_ctx) = slack_ctx {
|
|
route = route
|
|
.at(
|
|
"/webhook/slack",
|
|
post(crate::slack::webhook_receive).data(sl_ctx.clone()),
|
|
)
|
|
.at(
|
|
"/webhook/slack/command",
|
|
post(crate::slack::slash_command_receive).data(sl_ctx),
|
|
);
|
|
}
|
|
|
|
route.data(ctx_arc)
|
|
}
|
|
|
|
type ApiTuple = (
|
|
ProjectApi,
|
|
ModelApi,
|
|
AnthropicApi,
|
|
IoApi,
|
|
ChatApi,
|
|
AgentsApi,
|
|
SettingsApi,
|
|
HealthApi,
|
|
);
|
|
|
|
type ApiService = OpenApiService<ApiTuple, ()>;
|
|
|
|
/// All HTTP methods are documented by OpenAPI at /docs
|
|
pub fn build_openapi_service(ctx: Arc<AppContext>) -> (ApiService, ApiService) {
|
|
let api = (
|
|
ProjectApi { ctx: ctx.clone() },
|
|
ModelApi { ctx: ctx.clone() },
|
|
AnthropicApi::new(ctx.clone()),
|
|
IoApi { ctx: ctx.clone() },
|
|
ChatApi { ctx: ctx.clone() },
|
|
AgentsApi { ctx: ctx.clone() },
|
|
SettingsApi { ctx: ctx.clone() },
|
|
HealthApi,
|
|
);
|
|
|
|
let api_service =
|
|
OpenApiService::new(api, "Storkit API", "1.0").server("http://127.0.0.1:3001/api");
|
|
|
|
let docs_api = (
|
|
ProjectApi { ctx: ctx.clone() },
|
|
ModelApi { ctx: ctx.clone() },
|
|
AnthropicApi::new(ctx.clone()),
|
|
IoApi { ctx: ctx.clone() },
|
|
ChatApi { ctx: ctx.clone() },
|
|
AgentsApi { ctx: ctx.clone() },
|
|
SettingsApi { ctx },
|
|
HealthApi,
|
|
);
|
|
|
|
let docs_service =
|
|
OpenApiService::new(docs_api, "Storkit API", "1.0").server("http://127.0.0.1:3001/api");
|
|
|
|
(api_service, docs_service)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parse_port_defaults_to_3001() {
|
|
assert_eq!(parse_port(None), 3001);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_port_reads_valid_value() {
|
|
assert_eq!(parse_port(Some("4200".to_string())), 4200);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_port_ignores_invalid_value() {
|
|
assert_eq!(parse_port(Some("not_a_number".to_string())), 3001);
|
|
}
|
|
|
|
#[test]
|
|
fn write_and_remove_port_file() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
let path = write_port_file(tmp.path(), 4567).expect("should write port file");
|
|
assert_eq!(std::fs::read_to_string(&path).unwrap(), "4567");
|
|
|
|
remove_port_file(&path);
|
|
assert!(!path.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn write_port_file_returns_none_on_nonexistent_dir() {
|
|
let bad = std::path::Path::new("/this_dir_does_not_exist_storykit_test_xyz");
|
|
assert!(write_port_file(bad, 1234).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn remove_port_file_does_not_panic_for_missing_file() {
|
|
let path = std::path::Path::new("/tmp/nonexistent_storykit_port_test_xyz_999");
|
|
remove_port_file(path);
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_port_returns_a_valid_port() {
|
|
// Exercises the resolve_port code path (reads STORYKIT_PORT env var or defaults).
|
|
let port = resolve_port();
|
|
assert!(port > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn build_openapi_service_constructs_without_panic() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let ctx = Arc::new(context::AppContext::new_test(tmp.path().to_path_buf()));
|
|
let (_api_service, _docs_service) = build_openapi_service(ctx);
|
|
}
|
|
|
|
#[test]
|
|
fn build_routes_constructs_without_panic() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let ctx = context::AppContext::new_test(tmp.path().to_path_buf());
|
|
let _endpoint = build_routes(ctx, None, None);
|
|
}
|
|
}
|