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:
2026-06-11 20:04:12 +05:30
commit 851c838ca5
53 changed files with 8037 additions and 0 deletions

View 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(())
}
}

View 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(())
}
}

View 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<()>;
}

View 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(())
}
}

View 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(())
}
}