Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ members = [
"crates/tools",
"crates/tui",
"crates/whaleflow",
"crates/codewhale-extension",
"crates/tn-extension",

]
default-members = ["crates/cli", "crates/app-server", "crates/tui"]
resolver = "2"
Expand Down Expand Up @@ -55,3 +58,7 @@ tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
uuid = { version = "1.11", features = ["v4"] }




20 changes: 20 additions & 0 deletions crates/codewhale-extension/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "codewhale-extension"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Runtime extension API for CodeWhale — lifecycle hooks, tool registration, MCP server auto-config, slash commands, keyboard shortcuts"

[dependencies]
serde.workspace = true
serde_json.workspace = true
uuid.workspace = true
chrono.workspace = true
anyhow.workspace = true
tokio.workspace = true
async-trait.workspace = true

[dev-dependencies]
tempfile.workspace = true
209 changes: 209 additions & 0 deletions crates/codewhale-extension/src/extension.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
//! Extension API for agent lifecycle hooks.
//!
//! Mirrors the Pi Coding Agent's `ExtensionAPI` pattern:
//! extensions register callbacks for lifecycle events (session start,
//! tool call, turn end, etc.) and the runtime dispatches to them.

use async_trait::async_trait;
use serde_json::Value;

/// Data available at each hook point.
#[derive(Debug, Clone)]
pub enum HookEvent {
/// A new session has started.
SessionStart {
session_id: String,
reason: String,
},
/// Before the agent sends a prompt to the LLM.
BeforeAgentStart {
system_prompt: String,
prompt: String,
is_first_turn: bool,
},
/// A tool is about to be called.
ToolCall {
tool_name: String,
args: Value,
turn_id: String,
},
/// A tool has returned a result.
ToolResult {
tool_name: String,
args: Value,
result: Value,
is_error: bool,
turn_id: String,
},
/// The current turn has ended.
TurnEnd {
turn_id: String,
message_count: usize,
tool_count: usize,
},
/// User input is being processed (for @memory:key expansion).
Input {
text: String,
},
/// User ran a shell command.
UserBash {
command: String,
},
/// Model was selected.
ModelSelect {
model: String,
provider: String,
},
/// Before session compaction.
SessionBeforeCompact {
session_id: String,
entry_count: usize,
},
/// After session compaction.
SessionCompact {
session_id: String,
summary: String,
},
/// Session is shutting down.
SessionShutdown {
session_id: String,
},
}

/// Result returned from a hook, possibly modifying the event.
#[derive(Debug, Clone, Default)]
pub struct HookResult {
/// Modified system prompt (for BeforeAgentStart).
pub system_prompt: Option<String>,
/// Modified user prompt (for Input / BeforeAgentStart).
pub prompt: Option<String>,
/// Whether to suppress the default behavior.
pub suppress: bool,
}

/// A single extension that hooks into the agent lifecycle.
#[async_trait]
pub trait Extension: Send + Sync {
/// Name of this extension (e.g. "example").
fn name(&self) -> &str;

/// Called for each lifecycle event.
async fn on_event(&self, event: &HookEvent) -> HookResult {
let _ = event;
HookResult::default()
}

/// Register MCP servers this extension provides.
fn mcp_servers(&self) -> Vec<(String, McpServerDef)> {
vec![]
}

/// Register slash commands this extension provides.
fn slash_commands(&self) -> Vec<SlashCommandDef> {
vec![]
}

/// Register keyboard shortcuts this extension provides.
fn shortcuts(&self) -> Vec<ShortcutDef> {
vec![]
}

/// Custom tool definitions this extension provides.
fn tools(&self) -> Vec<ToolDef> {
vec![]
}
}

#[derive(Debug, Clone)]
pub struct McpServerDef {
pub command: String,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
}

#[derive(Debug, Clone)]
pub struct SlashCommandDef {
pub name: String,
pub description: String,
pub handler: String, // e.g. "mcp_example_my_tool"
}

#[derive(Debug, Clone)]
pub struct ShortcutDef {
pub keys: String, // e.g. "ctrl+shift+m"
pub description: String,
pub action: String, // e.g. "tn-memory-store"
}

#[derive(Debug, Clone)]
pub struct ToolDef {
pub name: String,
pub description: String,
pub parameters: Option<Value>,
}

/// Manages a collection of extensions and dispatches events to all of them.
#[derive(Default)]
pub struct ExtensionManager {
extensions: Vec<Box<dyn Extension>>,
}

impl ExtensionManager {
pub fn new() -> Self {
Self { extensions: Vec::new() }
}

pub fn register(&mut self, ext: Box<dyn Extension>) {
self.extensions.push(ext);
}

pub fn extensions(&self) -> &[Box<dyn Extension>] {
&self.extensions
}

/// Dispatch an event to all registered extensions and aggregate results.
pub async fn dispatch(&self, event: &HookEvent) -> Vec<HookResult> {
let mut results = Vec::new();
for ext in &self.extensions {
results.push(ext.on_event(event).await);
}
results
}

/// Collect all MCP server definitions from all extensions.
pub fn collect_mcp_servers(&self) -> Vec<(String, McpServerDef)> {
let mut servers = Vec::new();
for ext in &self.extensions {
servers.extend(ext.mcp_servers());
}
servers
}

/// Collect all slash commands from all extensions.
pub fn collect_slash_commands(&self) -> Vec<SlashCommandDef> {
let mut cmds = Vec::new();
for ext in &self.extensions {
cmds.extend(ext.slash_commands());
}
cmds
}

/// Collect all shortcuts from all extensions.
pub fn collect_shortcuts(&self) -> Vec<ShortcutDef> {
let mut shortcuts = Vec::new();
for ext in &self.extensions {
shortcuts.extend(ext.shortcuts());
}
shortcuts
}

/// Collect all tools from all extensions.
pub fn collect_tools(&self) -> Vec<ToolDef> {
let mut tools = Vec::new();
for ext in &self.extensions {
tools.extend(ext.tools());
}
tools
}
}

52 changes: 52 additions & 0 deletions crates/codewhale-extension/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//! # CodeWhale Runtime Extension API
//!
//! A generic runtime extension system for CodeWhale that mirrors the
//! Pi Coding Agent's `ExtensionAPI` pattern. Extensions can register
//! lifecycle hooks (session start, tool call, turn end, etc.), define
//! MCP servers, slash commands, keyboard shortcuts, and custom tools.
//!
//! ## Architecture
//!
//! - [`Extension`] trait — implement this to create an extension
//! - [`ExtensionManager`] — registers extensions and dispatches events
//! - [`HookEvent`] — all lifecycle events extensions can hook into
//! - [`HookResult`] — result from a hook that may modify the runtime
//!
//! ## Usage
//!
//! ```ignore
//! use codewhale_extension::prelude::*;
//!
//! struct MyExtension;
//! #[async_trait]
//! impl Extension for MyExtension {
//! fn name(&self) -> &str { "my-ext" }
//!
//! async fn on_event(&self, event: &HookEvent) -> HookResult {
//! match event {
//! HookEvent::SessionStart { session_id, .. } => {
//! println!("Session started: {session_id}");
//! }
//! _ => {}
//! }
//! HookResult::default()
//! }
//!
//! fn mcp_servers(&self) -> Vec<(String, McpServerDef)> {
//! vec![("my-server".into(), McpServerDef {
//! command: "my-mcp-server".into(),
//! args: vec![],
//! env: vec![],
//! })]
//! }
//! }
//! ```

pub mod extension;
pub mod prelude;

// Re-export key types at crate root
pub use extension::{
Extension, ExtensionManager, HookEvent, HookResult,
McpServerDef, ShortcutDef, SlashCommandDef, ToolDef,
};
5 changes: 5 additions & 0 deletions crates/codewhale-extension/src/prelude.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub use crate::extension::{
Extension, ExtensionManager, HookEvent, HookResult,
McpServerDef, ShortcutDef, SlashCommandDef, ToolDef,
};
pub use async_trait::async_trait;
16 changes: 16 additions & 0 deletions crates/tn-extension/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "codewhale-tn-extension"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "TormentNexus extension adapter for CodeWhale — hooks into the extension API"

[dependencies]
codewhale-extension = { path = "../codewhale-extension" }
serde.workspace = true
serde_json.workspace = true
chrono.workspace = true
async-trait.workspace = true
reqwest.workspace = true
Loading