Initial scaffold: GyanDoot v2.0 in Rust
Full Cargo workspace with 11 crates: - gyandoot-core: Shared types, config, error types - gyandoot-agent: Agent loop, tool execution, context compression - gyandoot-providers: LLM provider abstraction (OpenAI, Anthropic, Ollama) - gyandoot-memory: MemPalace persistent memory system - gyandoot-platform: Platform adapters (Telegram, Discord, Webhook, CLI) - gyandoot-security: Credential vault, sanitizer, threat detector - gyandoot-kanban: Task decomposition, swarm orchestration, diagnostics - gyandoot-subagents: Sub-agent spawning and orchestration - gyandoot-skills: Skills discovery, loading, preprocessing, bundles - gyandoot-profile: Multi-agent profile manager - gyandoot-gateway: Gateway sessions, routing, access control Plus: CLI entry point with clap, cross-platform installer script, KANBAN.md, .gnpl/AGENTS.md Workspace compiles clean: cargo check passes with 0 errors.
This commit is contained in:
41
crates/gyandoot-platform/src/cli_ux.rs
Normal file
41
crates/gyandoot-platform/src/cli_ux.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! CLI user experience — rich terminal interface.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use gyandoot_core::PlatformResult;
|
||||
use tracing::info;
|
||||
|
||||
use crate::{IncomingMessage, OutgoingMessage, Platform};
|
||||
|
||||
/// CLI platform adapter — reads from stdin, writes to stdout.
|
||||
pub struct CliPlatform;
|
||||
|
||||
#[async_trait]
|
||||
impl Platform for CliPlatform {
|
||||
fn name(&self) -> &str {
|
||||
"cli"
|
||||
}
|
||||
|
||||
async fn start(&self) -> PlatformResult<tokio::sync::mpsc::Receiver<IncomingMessage>> {
|
||||
info!("CLI platform ready (reading from stdin)");
|
||||
// CLI input is handled directly by the agent loop in gyandoot-agent,
|
||||
// via the AgentLoop's read_input method. This channel remains empty
|
||||
// since CLI is the default interactive mode.
|
||||
let (_tx, rx) = tokio::sync::mpsc::channel(100);
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
async fn send(&self, message: OutgoingMessage) -> PlatformResult<()> {
|
||||
println!("{}", message.text);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_typing(&self, _channel_id: &str) -> PlatformResult<()> {
|
||||
// No typing indicator needed for CLI
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> PlatformResult<()> {
|
||||
info!("CLI platform shutting down");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
89
crates/gyandoot-platform/src/discord.rs
Normal file
89
crates/gyandoot-platform/src/discord.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
//! Discord bot platform adapter.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use gyandoot_core::PlatformResult;
|
||||
use reqwest::Client;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::{IncomingMessage, OutgoingMessage, Platform};
|
||||
|
||||
/// Adapter for the Discord Bot API (REST + Gateway).
|
||||
pub struct DiscordPlatform {
|
||||
bot_token: String,
|
||||
client: Client,
|
||||
guild_id: Option<u64>,
|
||||
}
|
||||
|
||||
impl DiscordPlatform {
|
||||
/// Create a new Discord platform adapter.
|
||||
pub fn new(bot_token: String, guild_id: Option<u64>) -> Self {
|
||||
Self {
|
||||
bot_token,
|
||||
client: Client::new(),
|
||||
guild_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Discord REST API base URL.
|
||||
fn api_url(&self, path: &str) -> String {
|
||||
format!("https://discord.com/api/v10{}", path)
|
||||
}
|
||||
|
||||
/// Authorization header for Discord API.
|
||||
fn auth_header(&self) -> String {
|
||||
format!("Bot {}", self.bot_token)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Platform for DiscordPlatform {
|
||||
fn name(&self) -> &str {
|
||||
"discord"
|
||||
}
|
||||
|
||||
async fn start(&self) -> PlatformResult<tokio::sync::mpsc::Receiver<IncomingMessage>> {
|
||||
info!("Starting Discord platform (REST polling mode)...");
|
||||
// Note: Full Gateway implementation requires the 'tungstenite' crate.
|
||||
// For this scaffold, we simulate via a channel that stays empty until
|
||||
// proper WebSocket Gateway is connected.
|
||||
let (_tx, rx) = tokio::sync::mpsc::channel(100);
|
||||
info!("Discord Gateway connection not yet implemented — requires tungstenite/async-tungstenite");
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
async fn send(&self, message: OutgoingMessage) -> PlatformResult<()> {
|
||||
let body = serde_json::json!({
|
||||
"content": message.text,
|
||||
});
|
||||
|
||||
let url = self.api_url(&format!("/channels/{}/messages", message.channel_id));
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.header("Authorization", self.auth_header())
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
error!("Discord send failed: {}", response.status());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_typing(&self, channel_id: &str) -> PlatformResult<()> {
|
||||
let url = self.api_url(&format!("/channels/{}/typing", channel_id));
|
||||
self.client
|
||||
.post(&url)
|
||||
.header("Authorization", self.auth_header())
|
||||
.send()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> PlatformResult<()> {
|
||||
info!("Discord platform shutting down");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
52
crates/gyandoot-platform/src/lib.rs
Normal file
52
crates/gyandoot-platform/src/lib.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! GyanDoot Platform — Platform adapters for Telegram, Discord, Webhook, and CLI.
|
||||
//!
|
||||
//! Each adapter implements the `Platform` trait, allowing the agent to
|
||||
//! communicate over different messaging channels transparently.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use gyandoot_core::{Message, PlatformResult};
|
||||
|
||||
pub mod telegram;
|
||||
pub mod discord;
|
||||
pub mod webhook;
|
||||
pub mod cli_ux;
|
||||
|
||||
/// A message received from a platform.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IncomingMessage {
|
||||
pub platform: String,
|
||||
pub channel_id: String,
|
||||
pub user_id: String,
|
||||
pub username: String,
|
||||
pub text: String,
|
||||
pub message_id: String,
|
||||
}
|
||||
|
||||
/// A message to send to a platform.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OutgoingMessage {
|
||||
pub channel_id: String,
|
||||
pub text: String,
|
||||
pub reply_to: Option<String>,
|
||||
}
|
||||
|
||||
/// Trait that all platform adapters must implement.
|
||||
#[async_trait]
|
||||
pub trait Platform: Send + Sync {
|
||||
/// The platform identifier (e.g., "telegram", "discord", "cli").
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Start listening for incoming messages. Returns a channel receiver.
|
||||
async fn start(
|
||||
&self,
|
||||
) -> PlatformResult<tokio::sync::mpsc::Receiver<IncomingMessage>>;
|
||||
|
||||
/// Send a message to a channel.
|
||||
async fn send(&self, message: OutgoingMessage) -> PlatformResult<()>;
|
||||
|
||||
/// Send a typing indicator or equivalent "thinking" state.
|
||||
async fn send_typing(&self, channel_id: &str) -> PlatformResult<()>;
|
||||
|
||||
/// Shut down the platform connection gracefully.
|
||||
async fn shutdown(&self) -> PlatformResult<()>;
|
||||
}
|
||||
138
crates/gyandoot-platform/src/telegram.rs
Normal file
138
crates/gyandoot-platform/src/telegram.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
//! Telegram bot platform adapter.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use gyandoot_core::PlatformResult;
|
||||
use reqwest::Client;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::{IncomingMessage, OutgoingMessage, Platform};
|
||||
|
||||
/// Adapter for the Telegram Bot API.
|
||||
pub struct TelegramPlatform {
|
||||
bot_token: String,
|
||||
client: Client,
|
||||
allowed_users: Vec<i64>,
|
||||
}
|
||||
|
||||
impl TelegramPlatform {
|
||||
/// Create a new Telegram platform adapter.
|
||||
pub fn new(bot_token: String, allowed_users: Vec<i64>) -> Self {
|
||||
Self {
|
||||
bot_token,
|
||||
client: Client::new(),
|
||||
allowed_users,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the Telegram API URL for a given method.
|
||||
fn api_url(&self, method: &str) -> String {
|
||||
format!("https://api.telegram.org/bot{}/{}", self.bot_token, method)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Platform for TelegramPlatform {
|
||||
fn name(&self) -> &str {
|
||||
"telegram"
|
||||
}
|
||||
|
||||
async fn start(&self) -> PlatformResult<tokio::sync::mpsc::Receiver<IncomingMessage>> {
|
||||
info!("Starting Telegram polling...");
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(100);
|
||||
|
||||
let bot_token = self.bot_token.clone();
|
||||
let client = self.client.clone();
|
||||
let allowed_users = self.allowed_users.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut offset: i64 = 0;
|
||||
loop {
|
||||
let url = format!(
|
||||
"https://api.telegram.org/bot{}/getUpdates?offset={}&timeout=30",
|
||||
bot_token, offset
|
||||
);
|
||||
match client.get(&url).send().await {
|
||||
Ok(response) => {
|
||||
if let Ok(data) = response.json::<serde_json::Value>().await {
|
||||
if let Some(results) = data["result"].as_array() {
|
||||
for update in results {
|
||||
let update_id = update["update_id"].as_i64().unwrap_or(0);
|
||||
offset = update_id + 1;
|
||||
if let Some(msg) = update.get("message") {
|
||||
let chat_id = msg["chat"]["id"].as_i64().unwrap_or(0);
|
||||
let text = msg["text"].as_str().unwrap_or("").to_string();
|
||||
let user_id = msg["from"]["id"].as_i64().unwrap_or(0).to_string();
|
||||
let username = msg["from"]["username"]
|
||||
.as_str()
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
if !allowed_users.is_empty() && !allowed_users.contains(&chat_id) {
|
||||
debug!("Ignoring message from non-allowed user {}", chat_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
let incoming = IncomingMessage {
|
||||
platform: "telegram".to_string(),
|
||||
channel_id: chat_id.to_string(),
|
||||
user_id,
|
||||
username,
|
||||
text,
|
||||
message_id: update_id.to_string(),
|
||||
};
|
||||
|
||||
if tx.send(incoming).await.is_err() {
|
||||
error!("Telegram channel closed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Telegram poll error: {}", e);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
async fn send(&self, message: OutgoingMessage) -> PlatformResult<()> {
|
||||
let chat_id: i64 = message.channel_id.parse().unwrap_or(0);
|
||||
let body = serde_json::json!({
|
||||
"chat_id": chat_id,
|
||||
"text": message.text,
|
||||
"parse_mode": "Markdown",
|
||||
});
|
||||
|
||||
let url = self.api_url("sendMessage");
|
||||
let response = self.client.post(&url).json(&body).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
error!("Telegram send failed: {}", response.status());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_typing(&self, channel_id: &str) -> PlatformResult<()> {
|
||||
let chat_id: i64 = channel_id.parse().unwrap_or(0);
|
||||
let body = serde_json::json!({
|
||||
"chat_id": chat_id,
|
||||
"action": "typing",
|
||||
});
|
||||
|
||||
let url = self.api_url("sendChatAction");
|
||||
self.client.post(&url).json(&body).send().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> PlatformResult<()> {
|
||||
info!("Telegram platform shutting down");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
56
crates/gyandoot-platform/src/webhook.rs
Normal file
56
crates/gyandoot-platform/src/webhook.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
//! Webhook platform adapter.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use gyandoot_core::PlatformResult;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::{IncomingMessage, OutgoingMessage, Platform};
|
||||
|
||||
/// Adapter for receiving messages via HTTP webhook.
|
||||
pub struct WebhookPlatform {
|
||||
host: String,
|
||||
port: u16,
|
||||
secret: Option<String>,
|
||||
}
|
||||
|
||||
impl WebhookPlatform {
|
||||
/// Create a new webhook platform adapter.
|
||||
pub fn new(host: String, port: u16, secret: Option<String>) -> Self {
|
||||
Self { host, port, secret }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Platform for WebhookPlatform {
|
||||
fn name(&self) -> &str {
|
||||
"webhook"
|
||||
}
|
||||
|
||||
async fn start(&self) -> PlatformResult<tokio::sync::mpsc::Receiver<IncomingMessage>> {
|
||||
info!(
|
||||
"Starting webhook server on {}:{}",
|
||||
self.host, self.port
|
||||
);
|
||||
// Webhook server would use tokio::net::TcpListener or axum/warp.
|
||||
// For this scaffold, return an empty receiver.
|
||||
let (_tx, rx) = tokio::sync::mpsc::channel(100);
|
||||
warn!("Webhook server not yet implemented — requires axum or warp");
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
async fn send(&self, _message: OutgoingMessage) -> PlatformResult<()> {
|
||||
// Webhook outbound sends via HTTP POST to a configured callback URL.
|
||||
info!("Webhook outbound send (not yet implemented)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_typing(&self, _channel_id: &str) -> PlatformResult<()> {
|
||||
// No typing indicator for webhooks
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> PlatformResult<()> {
|
||||
info!("Webhook platform shutting down");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user