Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ea3d361b1 | ||
|
|
271721694b | ||
|
|
171597ca05 | ||
|
|
7b0b53671f | ||
|
|
884b19ccb5 | ||
|
|
6db00b2266 | ||
|
|
7f64423615 | ||
|
|
6d3f4918bf |
No files matched your search
@@ -0,0 +1,26 @@
|
||||
# Rust build artifacts
|
||||
target/
|
||||
|
||||
# VCS
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Local/secret files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Editor
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
|
||||
# Nix
|
||||
result
|
||||
result-*
|
||||
|
||||
# Kilo metadata
|
||||
.kilo/
|
||||
|
||||
# Docs lessons
|
||||
docs/lesson/
|
||||
@@ -21,11 +21,11 @@ jobs:
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
- name: Build workspace
|
||||
run: cargo build --release --workspace
|
||||
|
||||
- name: Test workspace
|
||||
run: cargo test --workspace
|
||||
- name: Format check
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Clippy workspace
|
||||
run: cargo clippy --workspace -- -D warnings
|
||||
run: cargo clippy --workspace --all-targets -- -D warnings
|
||||
|
||||
- name: Test workspace
|
||||
run: cargo test --workspace
|
||||
@@ -1,3 +1,20 @@
|
||||
## [1.18.4](https://github.com/asepharyana/zesdex/compare/v1.18.3...v1.18.4) (2026-08-27)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **tui:** render streaming token secara inkremental + kurangi redraw sia-sia ([2717216](https://github.com/asepharyana/zesdex/commit/271721694beb62e9fa5ff932312b293aa1d56823))
|
||||
|
||||
## [1.18.3](https://github.com/asepharyana/zesdex/compare/v1.18.2...v1.18.3) (2026-08-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** cegah race condition pada register users.json (TOCTOU) ([884b19c](https://github.com/asepharyana/zesdex/commit/884b19ccb5fbcaa6b29cb41dc978386cf7b1b3f9))
|
||||
* **api:** perbaiki keamanan auth & WebSocket, tambah rate limiting ([6db00b2](https://github.com/asepharyana/zesdex/commit/6db00b22663839a6a975ae978058b894900c71de))
|
||||
* **build:** perbaiki referensi paket zesdex-gateway dan sinkronisasi versi nix ([6d3f491](https://github.com/asepharyana/zesdex/commit/6d3f4918bfea06b106d4fe75adf58e6a29aca2a5))
|
||||
* **build:** perbaiki referensi paket zesdex-gateway di Dockerfile & default.nix ([7f64423](https://github.com/asepharyana/zesdex/commit/7f644236158f66fcc106ec55e437ebf28ee1cca1))
|
||||
|
||||
## [1.18.2](https://github.com/asepharyana/zesdex/compare/v1.18.1...v1.18.2) (2026-08-20)
|
||||
|
||||
|
||||
|
||||
Generated
+11
-11
@@ -4895,7 +4895,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-api"
|
||||
version = "1.18.0"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -4918,7 +4918,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-application"
|
||||
version = "1.18.0"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -4935,7 +4935,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-bootstrap"
|
||||
version = "1.18.0"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -4952,7 +4952,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-daemon"
|
||||
version = "1.18.0"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -4976,7 +4976,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-domain"
|
||||
version = "1.18.0"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -4992,7 +4992,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-gateway"
|
||||
version = "1.18.0"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -5019,7 +5019,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-grpc"
|
||||
version = "1.18.0"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -5036,7 +5036,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-infrastructure"
|
||||
version = "1.18.0"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -5085,7 +5085,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-tui"
|
||||
version = "1.18.0"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -5111,7 +5111,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-web"
|
||||
version = "1.18.0"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -5131,7 +5131,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zesdex-ws"
|
||||
version = "1.18.0"
|
||||
version = "1.18.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.18.2"
|
||||
version = "1.18.4"
|
||||
edition = "2021"
|
||||
authors = ["asepharyana <superaseph@gmail.com>"]
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# Build with release profile (treats warnings as errors via lints)
|
||||
RUN cargo build --release -p zesdex-backend --bin zesdex
|
||||
RUN cargo build --release -p zesdex-gateway --bin zesdex
|
||||
|
||||
# Stage 2: Minimal runtime image
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
@@ -16,10 +16,7 @@ pub trait ToolExecutor: Send + Sync {
|
||||
/// Service for running agent turns asynchronously.
|
||||
pub trait AgentTurnService: Send + Sync {
|
||||
/// Run a full agent turn loop asynchronously.
|
||||
fn run_turn(
|
||||
&self,
|
||||
params: AgentTurnParams,
|
||||
) -> impl Future<Output = Result<()>> + Send;
|
||||
fn run_turn(&self, params: AgentTurnParams) -> impl Future<Output = Result<()>> + Send;
|
||||
}
|
||||
|
||||
pub mod explore;
|
||||
|
||||
@@ -7,8 +7,8 @@ use zesdex_domain::agent::{AgentTurnParams, TurnEvent};
|
||||
use zesdex_domain::core::{ChatMessage, StreamEvent, ToolDef};
|
||||
use zesdex_domain::main_agent_prompt;
|
||||
|
||||
use crate::ports::ProviderService;
|
||||
use super::{ExploreService, ToolExecutor};
|
||||
use crate::ports::ProviderService;
|
||||
|
||||
/// Maximum tool-call iterations per agent turn before forcing termination.
|
||||
const MAX_TURN_ITERATIONS: u32 = 50;
|
||||
@@ -124,11 +124,7 @@ pub struct AgentTurnServiceImpl<P: ProviderService, T: ToolExecutor> {
|
||||
}
|
||||
|
||||
impl<P: ProviderService, T: ToolExecutor> AgentTurnServiceImpl<P, T> {
|
||||
pub fn new(
|
||||
provider: Arc<P>,
|
||||
tool_executor: Arc<T>,
|
||||
tool_defs: Vec<ToolDef>,
|
||||
) -> Self {
|
||||
pub fn new(provider: Arc<P>, tool_executor: Arc<T>, tool_defs: Vec<ToolDef>) -> Self {
|
||||
Self {
|
||||
provider,
|
||||
tool_executor,
|
||||
@@ -203,7 +199,10 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
|
||||
},
|
||||
);
|
||||
|
||||
match explorer.explore(&user_query, &workspace_root, ¶ms.turn_events).await {
|
||||
match explorer
|
||||
.explore(&user_query, &workspace_root, ¶ms.turn_events)
|
||||
.await
|
||||
{
|
||||
Ok(output) => {
|
||||
// Insert each context message as a system message.
|
||||
// They go at index 0 and are removed after the turn
|
||||
@@ -236,7 +235,9 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
|
||||
// entire turn, avoiding per-iteration clones of the full message list.
|
||||
// It is removed before emitting the Compacted event so persistence
|
||||
// does not store the prompt redundantly.
|
||||
params.messages.insert(0, ChatMessage::system(main_agent_prompt()));
|
||||
params
|
||||
.messages
|
||||
.insert(0, ChatMessage::system(main_agent_prompt()));
|
||||
let original_count = params.messages.len();
|
||||
|
||||
for iteration in 0..MAX_TURN_ITERATIONS {
|
||||
@@ -287,7 +288,8 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
|
||||
// ── Execute each tool call ──────────────────────────
|
||||
for tc in &tool_calls {
|
||||
let output =
|
||||
execute_tool_call(self.tool_executor.as_ref(), ¶ms.turn_events, tc).await;
|
||||
execute_tool_call(self.tool_executor.as_ref(), ¶ms.turn_events, tc)
|
||||
.await;
|
||||
params
|
||||
.messages
|
||||
.push(ChatMessage::tool(tc.id.clone(), output));
|
||||
@@ -295,10 +297,7 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("{e}");
|
||||
push_event(
|
||||
¶ms.turn_events,
|
||||
TurnEvent::Error(e),
|
||||
);
|
||||
push_event(¶ms.turn_events, TurnEvent::Error(e));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -307,10 +306,7 @@ impl<P: ProviderService, T: ToolExecutor> super::AgentTurnService for AgentTurnS
|
||||
// Remove the synthetic sys_msg before shipping events to the TUI
|
||||
// so the transcript shows only the actual user/assistant/tool exchange.
|
||||
let compacted: Vec<ChatMessage> = params.messages.drain(original_count - 1..).collect();
|
||||
push_event(
|
||||
¶ms.turn_events,
|
||||
TurnEvent::Compacted(compacted),
|
||||
);
|
||||
push_event(¶ms.turn_events, TurnEvent::Compacted(compacted));
|
||||
push_event(¶ms.turn_events, TurnEvent::Done);
|
||||
params.in_flight.store(false, Ordering::SeqCst);
|
||||
|
||||
@@ -341,15 +337,16 @@ pub async fn compact_messages_with_ai<P: ProviderService>(
|
||||
let split_idx = messages.len() - COMPACT_KEEP_TAIL;
|
||||
let evicted: Vec<_> = messages.drain(..split_idx).collect();
|
||||
|
||||
let mut summary_prompt = vec![
|
||||
ChatMessage::system(zesdex_domain::compaction_prompt()),
|
||||
];
|
||||
let mut summary_prompt = vec![ChatMessage::system(zesdex_domain::compaction_prompt())];
|
||||
summary_prompt.extend(evicted);
|
||||
summary_prompt.push(ChatMessage::user(
|
||||
"Please summarise our previous conversation above for context continuity.".to_string(),
|
||||
));
|
||||
|
||||
match provider.chat(&summary_prompt, None, Some(1024), Some(0.3)).await {
|
||||
match provider
|
||||
.chat(&summary_prompt, None, Some(1024), Some(0.3))
|
||||
.await
|
||||
{
|
||||
Ok((summary_msg, _)) => {
|
||||
let summary_text = summary_msg
|
||||
.content
|
||||
@@ -373,5 +370,3 @@ pub async fn compact_messages_with_ai<P: ProviderService>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,11 +45,7 @@ use sha2::{Digest, Sha256};
|
||||
/// and clear them after a successful (or failed) flow completion.
|
||||
pub trait OAuthFlowStore: Send + Sync {
|
||||
/// Persist the PKCE code verifier and CSRF state token.
|
||||
fn save_flow_state(
|
||||
&self,
|
||||
verifier: &str,
|
||||
state: &str,
|
||||
) -> Result<(), ServiceError>;
|
||||
fn save_flow_state(&self, verifier: &str, state: &str) -> Result<(), ServiceError>;
|
||||
|
||||
/// Load the stored PKCE code verifier.
|
||||
fn load_verifier(&self) -> Result<String, ServiceError>;
|
||||
@@ -141,12 +137,7 @@ pub struct OAuthUseCase<R, S, E> {
|
||||
|
||||
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> OAuthUseCase<R, S, E> {
|
||||
/// Create a new OAuth use-case.
|
||||
pub fn new(
|
||||
token_repo: R,
|
||||
flow_store: S,
|
||||
token_exchanger: E,
|
||||
token_path: PathBuf,
|
||||
) -> Self {
|
||||
pub fn new(token_repo: R, flow_store: S, token_exchanger: E, token_path: PathBuf) -> Self {
|
||||
OAuthUseCase {
|
||||
token_repo,
|
||||
flow_store,
|
||||
@@ -156,8 +147,8 @@ impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> OAuthUseCase<R, S
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger>
|
||||
zesdex_domain::auth::OAuthService for OAuthUseCase<R, S, E>
|
||||
impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger> zesdex_domain::auth::OAuthService
|
||||
for OAuthUseCase<R, S, E>
|
||||
{
|
||||
fn start_flow(
|
||||
&self,
|
||||
@@ -183,13 +174,9 @@ impl<R: OAuthRepository, S: OAuthFlowStore, E: TokenExchanger>
|
||||
"starting OAuth flow",
|
||||
);
|
||||
|
||||
let mut url = url::Url::parse(&config.auth_url)
|
||||
.map_err(|e| {
|
||||
ServiceError::InvalidConfig(format!(
|
||||
"invalid auth_url '{}': {e}",
|
||||
config.auth_url
|
||||
))
|
||||
})?;
|
||||
let mut url = url::Url::parse(&config.auth_url).map_err(|e| {
|
||||
ServiceError::InvalidConfig(format!("invalid auth_url '{}': {e}", config.auth_url))
|
||||
})?;
|
||||
|
||||
url.query_pairs_mut()
|
||||
.append_pair("response_type", "code")
|
||||
|
||||
@@ -46,12 +46,11 @@ impl<R: SessionRepository, L: SessionLockRepository> SessionServiceImpl<R, L> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: SessionRepository, L: SessionLockRepository>
|
||||
zesdex_domain::auth::SessionService for SessionServiceImpl<R, L>
|
||||
impl<R: SessionRepository, L: SessionLockRepository> zesdex_domain::auth::SessionService
|
||||
for SessionServiceImpl<R, L>
|
||||
{
|
||||
fn create_session(&self, title: &str) -> Result<Session, ServiceError> {
|
||||
let id = SessionId::new(&Uuid::new_v4().to_string())
|
||||
.map_err(ServiceError::Other)?;
|
||||
let id = SessionId::new(&Uuid::new_v4().to_string()).map_err(ServiceError::Other)?;
|
||||
let title_owned = if title.is_empty() {
|
||||
"New Session".to_string()
|
||||
} else {
|
||||
@@ -59,8 +58,7 @@ impl<R: SessionRepository, L: SessionLockRepository>
|
||||
};
|
||||
let session = Session::new(id.into_string(), title_owned);
|
||||
tracing::debug!(session_id = %session.id, title = %session.title, "creating new session");
|
||||
self.session_repo
|
||||
.save_session(&self.base_dir, &session)?;
|
||||
self.session_repo.save_session(&self.base_dir, &session)?;
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
@@ -73,17 +71,14 @@ impl<R: SessionRepository, L: SessionLockRepository>
|
||||
|
||||
fn archive_session(&self, id: SessionId) -> Result<(), ServiceError> {
|
||||
tracing::debug!(session_id = %id, "archiving session");
|
||||
let mut session = self
|
||||
.session_repo
|
||||
.load_session(&self.base_dir, &id)?;
|
||||
let mut session = self.session_repo.load_session(&self.base_dir, &id)?;
|
||||
session.archived = true;
|
||||
let millis = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis();
|
||||
session.updated_at = i64::try_from(millis).unwrap_or(i64::MAX);
|
||||
self.session_repo
|
||||
.save_session(&self.base_dir, &session)?;
|
||||
self.session_repo.save_session(&self.base_dir, &session)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -58,11 +58,7 @@ impl<R: ConversationRepository> zesdex_domain::cms::ConversationService
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_message(
|
||||
&self,
|
||||
conv: &mut Conversation,
|
||||
msg: ChatMessage,
|
||||
) -> Result<(), ServiceError> {
|
||||
fn add_message(&self, conv: &mut Conversation, msg: ChatMessage) -> Result<(), ServiceError> {
|
||||
tracing::debug!("adding message to session {}", conv.session_id);
|
||||
conv.push(msg);
|
||||
let dir = self.session_dir(&conv.session_id);
|
||||
|
||||
@@ -14,8 +14,7 @@ use std::path::PathBuf;
|
||||
use tracing;
|
||||
|
||||
use zesdex_domain::cms::{
|
||||
AppConfig, AppConfigRepository, ProviderConfig, ServiceError, Settings,
|
||||
SettingsRepository,
|
||||
AppConfig, AppConfigRepository, ProviderConfig, ServiceError, Settings, SettingsRepository,
|
||||
};
|
||||
|
||||
/// Service implementation for settings and app-config operations.
|
||||
@@ -30,11 +29,7 @@ pub struct SettingsServiceImpl<S, C> {
|
||||
|
||||
impl<S: SettingsRepository, C: AppConfigRepository> SettingsServiceImpl<S, C> {
|
||||
/// Create a new service with the given repositories and base directory.
|
||||
pub fn new(
|
||||
settings_repo: S,
|
||||
app_config_repo: C,
|
||||
base_dir: impl Into<PathBuf>,
|
||||
) -> Self {
|
||||
pub fn new(settings_repo: S, app_config_repo: C, base_dir: impl Into<PathBuf>) -> Self {
|
||||
tracing::debug!("creating SettingsServiceImpl");
|
||||
Self {
|
||||
settings_repo,
|
||||
@@ -44,8 +39,8 @@ impl<S: SettingsRepository, C: AppConfigRepository> SettingsServiceImpl<S, C> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: SettingsRepository, C: AppConfigRepository>
|
||||
zesdex_domain::cms::SettingsService for SettingsServiceImpl<S, C>
|
||||
impl<S: SettingsRepository, C: AppConfigRepository> zesdex_domain::cms::SettingsService
|
||||
for SettingsServiceImpl<S, C>
|
||||
{
|
||||
fn load_settings(&self) -> Result<Settings, ServiceError> {
|
||||
tracing::debug!("loading settings");
|
||||
@@ -60,11 +55,7 @@ impl<S: SettingsRepository, C: AppConfigRepository>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_provider(
|
||||
&self,
|
||||
name: &str,
|
||||
config: &ProviderConfig,
|
||||
) -> Result<(), ServiceError> {
|
||||
fn update_provider(&self, name: &str, config: &ProviderConfig) -> Result<(), ServiceError> {
|
||||
tracing::debug!("updating provider '{name}'");
|
||||
let mut app_config: AppConfig = self.app_config_repo.load(&self.base_dir)?;
|
||||
app_config
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
//! the use-case logic independent of any specific persistence or infrastructure
|
||||
//! technology.
|
||||
|
||||
pub mod agent;
|
||||
pub mod auth;
|
||||
pub mod cms;
|
||||
pub mod ports;
|
||||
pub mod agent;
|
||||
|
||||
// Re-export port traits for ergonomic access.
|
||||
pub use ports::*;
|
||||
@@ -46,12 +46,11 @@ pub use auth::{
|
||||
|
||||
// Re-export CMS use-cases.
|
||||
pub use cms::{
|
||||
conversation_service::ConversationServiceImpl,
|
||||
memory_service::MemoryServiceImpl,
|
||||
conversation_service::ConversationServiceImpl, memory_service::MemoryServiceImpl,
|
||||
settings_service::SettingsServiceImpl,
|
||||
};
|
||||
|
||||
pub use agent::{
|
||||
turn_service::{compact_messages_with_ai, AgentTurnServiceImpl},
|
||||
AgentTurnService, ExploreOutput, ExploreService, ToolExecutor,
|
||||
turn_service::{AgentTurnServiceImpl, compact_messages_with_ai},
|
||||
};
|
||||
@@ -22,11 +22,8 @@ pub trait AuthService: Send + Sync {
|
||||
/// Authenticate a user by verifying a password against a stored hash.
|
||||
///
|
||||
/// Returns `true` if the password matches, `false` otherwise.
|
||||
fn authenticate(
|
||||
&self,
|
||||
password: &str,
|
||||
hash: &str,
|
||||
) -> impl Future<Output = Result<bool>> + Send;
|
||||
fn authenticate(&self, password: &str, hash: &str)
|
||||
-> impl Future<Output = Result<bool>> + Send;
|
||||
|
||||
/// Issue a new access + refresh token pair for the given subject.
|
||||
///
|
||||
|
||||
@@ -7,8 +7,8 @@ use std::path::PathBuf;
|
||||
use crate::core::{ChatMessage, ToolCallResult, UsageStats};
|
||||
|
||||
pub mod defaults;
|
||||
pub mod prompt;
|
||||
pub mod progress;
|
||||
pub mod prompt;
|
||||
|
||||
/// Which kind of caller (main agent vs. subagent vs. reviewer) is
|
||||
/// invoking a tool, used to scope permissions and tag log/output paths.
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
//! - `ChatMessage` — a single message with role, content, and tool metadata
|
||||
//! - `Role` — message role enum (User, Assistant, System, Tool)
|
||||
|
||||
pub use crate::core::message::{ChatMessage, Role};
|
||||
pub use crate::core::conversation::Conversation;
|
||||
pub use crate::core::message::{ChatMessage, Role};
|
||||
@@ -32,6 +32,7 @@ pub mod settings;
|
||||
pub use app_config::AppConfig;
|
||||
pub use app_config::ModelRole;
|
||||
pub use app_config::ProviderConfig;
|
||||
pub use commands::{NewMemory, SettingsPatch};
|
||||
pub use conversation::Conversation;
|
||||
pub use edit_log::EditLog;
|
||||
pub use edit_log::EditLogEntry;
|
||||
@@ -46,7 +47,6 @@ pub use repository::SettingsRepository;
|
||||
pub use service::ConversationService;
|
||||
pub use service::MemoryService;
|
||||
pub use service::SettingsService;
|
||||
pub use commands::{NewMemory, SettingsPatch};
|
||||
pub use settings::InternetMode;
|
||||
pub use settings::Settings;
|
||||
pub use settings::SettingsFlags;
|
||||
@@ -56,11 +56,7 @@ pub trait ConversationRepository {
|
||||
fn load(&self, session_dir: &Path) -> Result<Conversation, RepositoryError>;
|
||||
|
||||
/// Persist a `Conversation` to the given session directory.
|
||||
fn save(
|
||||
&self,
|
||||
session_dir: &Path,
|
||||
conversation: &Conversation,
|
||||
) -> Result<(), RepositoryError>;
|
||||
fn save(&self, session_dir: &Path, conversation: &Conversation) -> Result<(), RepositoryError>;
|
||||
}
|
||||
|
||||
/// Persistence contract for `Memory` (long-term agent memory entries).
|
||||
|
||||
@@ -44,11 +44,7 @@ pub trait ConversationService {
|
||||
fn save_conversation(&self, conv: &Conversation) -> Result<(), ServiceError>;
|
||||
|
||||
/// Append a single `ChatMessage` to the conversation and persist.
|
||||
fn add_message(
|
||||
&self,
|
||||
conv: &mut Conversation,
|
||||
msg: ChatMessage,
|
||||
) -> Result<(), ServiceError>;
|
||||
fn add_message(&self, conv: &mut Conversation, msg: ChatMessage) -> Result<(), ServiceError>;
|
||||
}
|
||||
|
||||
/// Use-cases for long-term memory management.
|
||||
|
||||
@@ -335,16 +335,16 @@ impl SseParser {
|
||||
{
|
||||
const MAX_TOOL_CALLS: usize = 64;
|
||||
for tc in tool_calls {
|
||||
let raw_index =
|
||||
tc.get("index").and_then(Value::as_u64).unwrap_or_else(
|
||||
|| {
|
||||
tracing::warn!(
|
||||
"[stream] tool call delta missing index, \
|
||||
let raw_index = tc
|
||||
.get("index")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or_else(|| {
|
||||
tracing::warn!(
|
||||
"[stream] tool call delta missing index, \
|
||||
defaulting to 0"
|
||||
);
|
||||
0
|
||||
},
|
||||
);
|
||||
);
|
||||
0
|
||||
});
|
||||
// Clamp index to prevent out-of-bounds / memory exhaustion
|
||||
let index = usize::try_from(raw_index)
|
||||
.unwrap_or(0)
|
||||
@@ -395,8 +395,7 @@ impl SseParser {
|
||||
if let Some(content) = delta.get("text").and_then(|c| c.as_str()) {
|
||||
d_events.push(StreamEvent::Token(content.to_string()));
|
||||
}
|
||||
if let Some(reasoning) =
|
||||
delta.get("reasoning_content").and_then(|r| r.as_str())
|
||||
if let Some(reasoning) = delta.get("reasoning_content").and_then(|r| r.as_str())
|
||||
{
|
||||
d_events.push(StreamEvent::Reasoning(reasoning.to_string()));
|
||||
}
|
||||
|
||||
@@ -40,9 +40,11 @@ impl Store {
|
||||
///
|
||||
/// Why: paths are computed, not created — call `ensure_dirs` before use.
|
||||
pub fn new() -> Self {
|
||||
let base = if let Some(data_dir) = std::env::var("XDG_DATA_HOME").ok()
|
||||
.or_else(|| std::env::var("HOME").ok().map(|h| format!("{h}/.local/share")))
|
||||
{
|
||||
let base = if let Some(data_dir) = std::env::var("XDG_DATA_HOME").ok().or_else(|| {
|
||||
std::env::var("HOME")
|
||||
.ok()
|
||||
.map(|h| format!("{h}/.local/share"))
|
||||
}) {
|
||||
PathBuf::from(data_dir).join("zesdex")
|
||||
} else {
|
||||
PathBuf::from(".local/share/zesdex")
|
||||
|
||||
+14
-15
@@ -24,33 +24,32 @@
|
||||
//! no framework imports, no side effects. All persistence is expressed
|
||||
//! through repository traits that infrastructure adapters implement.
|
||||
|
||||
pub mod agent;
|
||||
pub mod auth;
|
||||
pub mod cms;
|
||||
pub mod core;
|
||||
pub mod error;
|
||||
pub mod agent;
|
||||
pub mod workflow;
|
||||
pub mod subagent;
|
||||
pub mod workflow;
|
||||
|
||||
// Re-export all public items from each module for ergonomic imports.
|
||||
// Consumers can do `use zesdex_domain::*` for common types.
|
||||
pub use auth::{
|
||||
IamSession, NewSession, OAuthConfig, OAuthToken, OAuthRepository, OAuthService,
|
||||
RepositoryError as AuthRepositoryError, ServiceError as AuthServiceError, Session,
|
||||
SessionId, SessionLock, SessionLockRepository, SessionRepository, SessionService,
|
||||
IamSession, NewSession, OAuthConfig, OAuthRepository, OAuthService, OAuthToken,
|
||||
RepositoryError as AuthRepositoryError, ServiceError as AuthServiceError, Session, SessionId,
|
||||
SessionLock, SessionLockRepository, SessionRepository, SessionService,
|
||||
};
|
||||
pub use cms::{
|
||||
AppConfig, AppConfigRepository, Conversation as CmsConversation,
|
||||
ConversationRepository, ConversationService, EditLog, EditLogEntry,
|
||||
EditLogRepository, InternetMode, Memory, MemoryRepository, MemoryService,
|
||||
ModelRole, NewMemory, ProviderConfig, RepositoryError as CmsRepositoryError,
|
||||
ServiceError as CmsServiceError, Settings, SettingsFlags, SettingsPatch,
|
||||
SettingsRepository, SettingsService,
|
||||
AppConfig, AppConfigRepository, Conversation as CmsConversation, ConversationRepository,
|
||||
ConversationService, EditLog, EditLogEntry, EditLogRepository, InternetMode, Memory,
|
||||
MemoryRepository, MemoryService, ModelRole, NewMemory, ProviderConfig,
|
||||
RepositoryError as CmsRepositoryError, ServiceError as CmsServiceError, Settings,
|
||||
SettingsFlags, SettingsPatch, SettingsRepository, SettingsService,
|
||||
};
|
||||
pub use core::{
|
||||
ChatMessage, ChatRequest, ChatResponse, Choice, Conversation, Delta, Role,
|
||||
SseParser, StreamEvent, StreamOptions, Store, TokenUsage, ToolCall,
|
||||
ToolCallResult, ToolDef, ToolFunction, ToolFunctionDef, UsageStats,
|
||||
ChatMessage, ChatRequest, ChatResponse, Choice, Conversation, Delta, Role, SseParser, Store,
|
||||
StreamEvent, StreamOptions, TokenUsage, ToolCall, ToolCallResult, ToolDef, ToolFunction,
|
||||
ToolFunctionDef, UsageStats,
|
||||
};
|
||||
pub use error::DomainError;
|
||||
|
||||
@@ -60,5 +59,5 @@ pub use agent::*;
|
||||
pub use agent::defaults::*;
|
||||
pub use agent::progress::AgentProgress;
|
||||
pub use agent::prompt::{compaction_prompt, main_agent_prompt, subagent_directive};
|
||||
pub use workflow::*;
|
||||
pub use subagent::*;
|
||||
pub use workflow::*;
|
||||
@@ -4,7 +4,6 @@
|
||||
//! configuration files plus a seed session for development/testing.
|
||||
//! Invoked as `cargo run --bin seed`.
|
||||
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let store = zesdex_domain::core::Store::new();
|
||||
store.ensure_dirs()?;
|
||||
@@ -46,13 +45,11 @@ fn main() -> anyhow::Result<()> {
|
||||
|
||||
// Create a seed session
|
||||
let session_id = uuid::Uuid::new_v4().to_string();
|
||||
let session = zesdex_domain::auth::Session::new(
|
||||
session_id.clone(),
|
||||
"Seed Session".to_string(),
|
||||
);
|
||||
let session = zesdex_domain::auth::Session::new(session_id.clone(), "Seed Session".to_string());
|
||||
// Persist via the session repository
|
||||
use zesdex_domain::SessionRepository;
|
||||
let repo = zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository::new();
|
||||
let repo =
|
||||
zesdex_infrastructure::persistence::iam::session_repo::FileSystemSessionRepository::new();
|
||||
repo.save_session(&store.base_dir, &session)?;
|
||||
tracing::info!("Seed session created: id={session_id}");
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ fn main() {
|
||||
let base_dir = dirs::home_dir().unwrap().join(".local/share/zesdex");
|
||||
let repo = JsonAppConfigRepository::new();
|
||||
let config = repo.load(&base_dir).unwrap();
|
||||
|
||||
|
||||
println!("Providers:");
|
||||
for (k, v) in &config.providers {
|
||||
println!(" - {} (default model: {:?})", k, v.default_model);
|
||||
}
|
||||
|
||||
|
||||
println!("Default provider: {}", config.default_provider);
|
||||
println!("Default model: {}", config.default_model);
|
||||
println!("Model roles:");
|
||||
|
||||
@@ -16,7 +16,10 @@ struct ClaudeSettings {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path = dirs::home_dir().unwrap().join(".claude").join("settings.json");
|
||||
let path = dirs::home_dir()
|
||||
.unwrap()
|
||||
.join(".claude")
|
||||
.join("settings.json");
|
||||
println!("Path: {:?}", path);
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => {
|
||||
|
||||
@@ -12,7 +12,11 @@ use clap::Parser;
|
||||
|
||||
/// Zesdex — autonomous AI coding agent.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "zesdex", version, about = "Autonomous AI coding agent with TUI")]
|
||||
#[command(
|
||||
name = "zesdex",
|
||||
version,
|
||||
about = "Autonomous AI coding agent with TUI"
|
||||
)]
|
||||
struct Cli {
|
||||
/// Run as background daemon with IPC socket
|
||||
#[arg(long)]
|
||||
|
||||
@@ -8,12 +8,27 @@ pub struct JwtClaims {
|
||||
pub sub: String,
|
||||
pub exp: u64,
|
||||
pub iat: u64,
|
||||
/// Token purpose: `"access"` or `"refresh"`.
|
||||
///
|
||||
/// Prevents an access token from being replayed as a refresh token
|
||||
/// (which would otherwise extend a short-lived credential into the
|
||||
/// 7-day refresh window).
|
||||
#[serde(rename = "typ")]
|
||||
pub token_type: TokenType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
/// JWT token purpose.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TokenType {
|
||||
Access,
|
||||
Refresh,
|
||||
}
|
||||
|
||||
impl JwtClaims {
|
||||
pub fn new(sub: String, exp: u64, session_id: Option<String>) -> Self {
|
||||
pub fn new(sub: String, exp: u64, token_type: TokenType, session_id: Option<String>) -> Self {
|
||||
let iat = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
@@ -22,6 +37,7 @@ impl JwtClaims {
|
||||
sub,
|
||||
exp,
|
||||
iat,
|
||||
token_type,
|
||||
session_id,
|
||||
}
|
||||
}
|
||||
@@ -48,3 +64,57 @@ pub fn verify_token(secret: &str, token: &str) -> anyhow::Result<JwtClaims> {
|
||||
let token_data = jsonwebtoken::decode::<JwtClaims>(token, &key, &validation)?;
|
||||
Ok(token_data.claims)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn claims(exp_secs_from_now: u64, token_type: TokenType) -> JwtClaims {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
JwtClaims::new(
|
||||
"user-1".to_string(),
|
||||
now + exp_secs_from_now,
|
||||
token_type,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn access_and_refresh_tokens_roundtrip() {
|
||||
let secret = "test-secret";
|
||||
let access = create_token(secret, claims(3600, TokenType::Access)).unwrap();
|
||||
let refresh = create_token(secret, claims(604800, TokenType::Refresh)).unwrap();
|
||||
|
||||
let acc = verify_token(secret, &access).unwrap();
|
||||
assert_eq!(acc.token_type, TokenType::Access);
|
||||
|
||||
let refr = verify_token(secret, &refresh).unwrap();
|
||||
assert_eq!(refr.token_type, TokenType::Refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_type_is_distinct() {
|
||||
let secret = "test-secret";
|
||||
let access = create_token(secret, claims(3600, TokenType::Access)).unwrap();
|
||||
let claims = verify_token(secret, &access).unwrap();
|
||||
assert_ne!(claims.token_type, TokenType::Refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expired_token_is_rejected() {
|
||||
let secret = "test-secret";
|
||||
// exp well in the past (beyond the library's default 60s leeway) →
|
||||
// verification must fail.
|
||||
let past = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
.saturating_sub(120);
|
||||
let expired = JwtClaims::new("user-1".to_string(), past, TokenType::Access, None);
|
||||
let token = create_token(secret, expired).unwrap();
|
||||
assert!(verify_token(secret, &token).is_err());
|
||||
}
|
||||
}
|
||||
@@ -21,20 +21,13 @@ impl LoopbackServer {
|
||||
format!("http://127.0.0.1:{}/callback", self.port)
|
||||
}
|
||||
|
||||
pub fn wait_for_code(
|
||||
&self,
|
||||
timeout_ms: u64,
|
||||
expected_state: &str,
|
||||
) -> std::io::Result<String> {
|
||||
pub fn wait_for_code(&self, timeout_ms: u64, expected_state: &str) -> std::io::Result<String> {
|
||||
let (mut stream, _) = self.listener.accept()?;
|
||||
stream.set_read_timeout(Some(std::time::Duration::from_millis(timeout_ms)))?;
|
||||
Self::read_callback(&mut stream, expected_state)
|
||||
}
|
||||
|
||||
fn read_callback(
|
||||
stream: &mut TcpStream,
|
||||
expected_state: &str,
|
||||
) -> std::io::Result<String> {
|
||||
fn read_callback(stream: &mut TcpStream, expected_state: &str) -> std::io::Result<String> {
|
||||
let mut buf = [0u8; 4096];
|
||||
let n = stream.read(&mut buf)?;
|
||||
let request = String::from_utf8_lossy(&buf[..n]);
|
||||
@@ -68,7 +61,10 @@ impl LoopbackServer {
|
||||
));
|
||||
}
|
||||
code.ok_or_else(|| {
|
||||
std::io::Error::new(std::io::ErrorKind::InvalidData, "code not found in callback")
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"code not found in callback",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -59,17 +59,25 @@ pub struct AuditReport {
|
||||
impl AuditReport {
|
||||
/// True if any ERROR-level violations exist.
|
||||
pub fn has_errors(&self) -> bool {
|
||||
self.violations.iter().any(|v| v.severity == Severity::Error)
|
||||
self.violations
|
||||
.iter()
|
||||
.any(|v| v.severity == Severity::Error)
|
||||
}
|
||||
|
||||
/// Number of errors.
|
||||
pub fn error_count(&self) -> usize {
|
||||
self.violations.iter().filter(|v| v.severity == Severity::Error).count()
|
||||
self.violations
|
||||
.iter()
|
||||
.filter(|v| v.severity == Severity::Error)
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Number of warnings.
|
||||
pub fn warning_count(&self) -> usize {
|
||||
self.violations.iter().filter(|v| v.severity == Severity::Warning).count()
|
||||
self.violations
|
||||
.iter()
|
||||
.filter(|v| v.severity == Severity::Warning)
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,11 +160,7 @@ fn forbidden_imports(layer: &str) -> &'static [&'static str] {
|
||||
}
|
||||
|
||||
/// Scan a single Rust source file for forbidden imports.
|
||||
fn scan_file(
|
||||
file_path: &Path,
|
||||
layer: &'static str,
|
||||
root: &Path,
|
||||
) -> Vec<Violation> {
|
||||
fn scan_file(file_path: &Path, layer: &'static str, root: &Path) -> Vec<Violation> {
|
||||
let mut violations = Vec::new();
|
||||
let content = match std::fs::read_to_string(file_path) {
|
||||
Ok(c) => c,
|
||||
@@ -185,10 +189,12 @@ fn scan_file(
|
||||
// `use crate::` in domain could reference domain-only items — skip.
|
||||
continue;
|
||||
}
|
||||
if trimmed.starts_with(&pattern) || trimmed.starts_with(&format!("use {forbidden}::")) {
|
||||
if trimmed.starts_with(&pattern)
|
||||
|| trimmed.starts_with(&format!("use {forbidden}::"))
|
||||
{
|
||||
// Skip test code — test modules commonly import outer layers.
|
||||
let is_test = content[..content.len().saturating_sub(1)]
|
||||
.contains("#[cfg(test)]");
|
||||
let is_test =
|
||||
content[..content.len().saturating_sub(1)].contains("#[cfg(test)]");
|
||||
if is_test {
|
||||
continue;
|
||||
}
|
||||
@@ -243,10 +249,7 @@ pub fn audit_layering(root: &Path) -> Result<AuditReport> {
|
||||
}
|
||||
|
||||
// Determine which crate this file belongs to by walking up.
|
||||
let layer = path
|
||||
.ancestors()
|
||||
.skip(1)
|
||||
.find_map(|p| classify_layer(p));
|
||||
let layer = path.ancestors().skip(1).find_map(|p| classify_layer(p));
|
||||
|
||||
if let Some(layer) = layer {
|
||||
files_scanned += 1;
|
||||
@@ -371,7 +374,10 @@ mod tests {
|
||||
fn function_metrics_short_function_ok() {
|
||||
let content = "fn ok() {\n let x = 1;\n}\n";
|
||||
let violations = check_function_metrics(content);
|
||||
let long: Vec<_> = violations.iter().filter(|v| v.message.contains("Function too long")).collect();
|
||||
let long: Vec<_> = violations
|
||||
.iter()
|
||||
.filter(|v| v.message.contains("Function too long"))
|
||||
.collect();
|
||||
assert!(long.is_empty(), "short function should not trigger");
|
||||
}
|
||||
|
||||
@@ -383,7 +389,10 @@ mod tests {
|
||||
}
|
||||
lines.push_str("}\n");
|
||||
let violations = check_function_metrics(&lines);
|
||||
let long: Vec<_> = violations.iter().filter(|v| v.message.contains("Function too long")).collect();
|
||||
let long: Vec<_> = violations
|
||||
.iter()
|
||||
.filter(|v| v.message.contains("Function too long"))
|
||||
.collect();
|
||||
assert!(!long.is_empty(), "long function should trigger warning");
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,9 @@ pub struct CodeQualityReport {
|
||||
|
||||
impl CodeQualityReport {
|
||||
pub fn has_errors(&self) -> bool {
|
||||
self.findings.iter().any(|f| f.severity == super::arch_audit::Severity::Error)
|
||||
self.findings
|
||||
.iter()
|
||||
.any(|f| f.severity == super::arch_audit::Severity::Error)
|
||||
}
|
||||
pub fn count_by_rule(&self) -> Vec<(&'static str, usize)> {
|
||||
let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
|
||||
@@ -147,7 +149,8 @@ pub fn scan_quality_file(file_path: &Path, root: &Path) -> Vec<Finding> {
|
||||
|
||||
// ── Rule: Missing doc comments on pub items ────────────────────
|
||||
if (trimmed.starts_with("pub ") || trimmed.starts_with("pub("))
|
||||
&& !prev_line_doc && !prev_line_empty
|
||||
&& !prev_line_doc
|
||||
&& !prev_line_empty
|
||||
{
|
||||
// Check it's a struct/enum/fn/trait/type/const/mod
|
||||
let is_item = trimmed.starts_with("pub fn ")
|
||||
@@ -246,7 +249,10 @@ mod tests {
|
||||
std::fs::write(&file, "fn x() { let y = foo.unwrap(); }\n").unwrap();
|
||||
|
||||
let findings = scan_quality_file(&file, &dir);
|
||||
let unwrap_findings: Vec<_> = findings.iter().filter(|f| f.rule == "unwrap-in-production").collect();
|
||||
let unwrap_findings: Vec<_> = findings
|
||||
.iter()
|
||||
.filter(|f| f.rule == "unwrap-in-production")
|
||||
.collect();
|
||||
assert!(!unwrap_findings.is_empty(), "should detect unwrap");
|
||||
}
|
||||
|
||||
@@ -262,8 +268,14 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let findings = scan_quality_file(&file, &dir);
|
||||
let unwrap_findings: Vec<_> = findings.iter().filter(|f| f.rule == "unwrap-in-production").collect();
|
||||
assert!(unwrap_findings.is_empty(), "should skip unwrap in test blocks");
|
||||
let unwrap_findings: Vec<_> = findings
|
||||
.iter()
|
||||
.filter(|f| f.rule == "unwrap-in-production")
|
||||
.collect();
|
||||
assert!(
|
||||
unwrap_findings.is_empty(),
|
||||
"should skip unwrap in test blocks"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -274,7 +286,13 @@ mod tests {
|
||||
std::fs::write(&file, "#[allow(clippy::too_many_arguments)]\nfn x() {}\n").unwrap();
|
||||
|
||||
let findings = scan_quality_file(&file, &dir);
|
||||
let bypass_findings: Vec<_> = findings.iter().filter(|f| f.rule == "compiler-bypass").collect();
|
||||
assert!(!bypass_findings.is_empty(), "should detect allow attributes");
|
||||
let bypass_findings: Vec<_> = findings
|
||||
.iter()
|
||||
.filter(|f| f.rule == "compiler-bypass")
|
||||
.collect();
|
||||
assert!(
|
||||
!bypass_findings.is_empty(),
|
||||
"should detect allow attributes"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -50,10 +50,9 @@ pub fn validate_commit_message(msg: &str) -> Result<(), Vec<String>> {
|
||||
}
|
||||
|
||||
// Parse: `type(scope): description` or `type!: description` or `type: description`
|
||||
let re = Regex::new(
|
||||
r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$",
|
||||
)
|
||||
.expect("valid regex for commit parsing");
|
||||
let re =
|
||||
Regex::new(r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$")
|
||||
.expect("valid regex for commit parsing");
|
||||
|
||||
match re.captures(subject) {
|
||||
None => {
|
||||
@@ -94,21 +93,19 @@ pub fn validate_commit_message(msg: &str) -> Result<(), Vec<String>> {
|
||||
} else {
|
||||
let first_char = desc.chars().next().unwrap_or(' ');
|
||||
if first_char.is_uppercase() {
|
||||
errors.push(format!(
|
||||
"Description must start with lowercase: '{desc}'"
|
||||
));
|
||||
errors.push(format!("Description must start with lowercase: '{desc}'"));
|
||||
}
|
||||
if desc.ends_with('.') {
|
||||
errors.push(format!(
|
||||
"Description must not end with a period: '{desc}'"
|
||||
));
|
||||
errors.push(format!("Description must not end with a period: '{desc}'"));
|
||||
}
|
||||
}
|
||||
|
||||
// Type-specific rules.
|
||||
match type_ {
|
||||
"chore" | "docs" | "refactor" | "test" | "style" | "perf" | "ci" | "build"
|
||||
| "revert" if scope.is_some() => {
|
||||
| "revert"
|
||||
if scope.is_some() =>
|
||||
{
|
||||
errors.push(format!(
|
||||
"'{type_}' commits should not use a scope. \
|
||||
Only 'feat' and 'fix' require scopes."
|
||||
@@ -140,10 +137,9 @@ pub fn parse_commit_message(msg: &str) -> Option<CommitInfo> {
|
||||
let msg = msg.trim();
|
||||
let subject = msg.lines().next()?;
|
||||
|
||||
let re = Regex::new(
|
||||
r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$",
|
||||
)
|
||||
.expect("valid regex");
|
||||
let re =
|
||||
Regex::new(r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$")
|
||||
.expect("valid regex");
|
||||
|
||||
let caps = re.captures(subject)?;
|
||||
|
||||
@@ -155,10 +151,18 @@ pub fn parse_commit_message(msg: &str) -> Option<CommitInfo> {
|
||||
};
|
||||
|
||||
Some(CommitInfo {
|
||||
type_: caps.name("type").map(|m| m.as_str()).unwrap_or("").to_string(),
|
||||
type_: caps
|
||||
.name("type")
|
||||
.map(|m| m.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
scope: caps.name("scope").map(|m| m.as_str().to_string()),
|
||||
breaking: caps.name("breaking").is_some(),
|
||||
description: caps.name("desc").map(|m| m.as_str()).unwrap_or("").to_string(),
|
||||
description: caps
|
||||
.name("desc")
|
||||
.map(|m| m.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
body,
|
||||
})
|
||||
}
|
||||
@@ -232,7 +236,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_valid_commit() {
|
||||
let parsed = parse_commit_message("feat(agent): add parallel execution\n\nWith cycle orchestration.").unwrap();
|
||||
let parsed = parse_commit_message(
|
||||
"feat(agent): add parallel execution\n\nWith cycle orchestration.",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(parsed.type_, "feat");
|
||||
assert_eq!(parsed.scope, Some("agent".to_string()));
|
||||
assert!(!parsed.breaking);
|
||||
|
||||
@@ -51,7 +51,6 @@ const EXPLORE_DIRECTIVES: [&str; 3] = [
|
||||
4. Identify main entry points (main.rs, main.py, index.ts, etc.).\n\
|
||||
5. Count files by extension type.\n\
|
||||
Use the ls_dir, read, grep, and glob tools. Be concise.",
|
||||
|
||||
// Agent 1: Symbol Index
|
||||
"You are a symbol index explorer.\n\
|
||||
1. Call the 'rebuild_index' tool to rebuild the symbol index.\n\
|
||||
@@ -59,7 +58,6 @@ const EXPLORE_DIRECTIVES: [&str; 3] = [
|
||||
3. Identify public APIs, entry points, and key types.\n\
|
||||
4. Group symbols by language and kind.\n\
|
||||
Be concise. Report what symbols exist and where they live.",
|
||||
|
||||
// Agent 2: Semantic Context
|
||||
"You are a semantic context explorer.\n\
|
||||
1. Call the 'rebuild_index' tool to ensure the index is fresh.\n\
|
||||
@@ -231,8 +229,8 @@ async fn run_explore_phase(
|
||||
let tc = tool_ctx.clone();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new()
|
||||
.context("create explore subagent tokio runtime")?;
|
||||
let rt =
|
||||
tokio::runtime::Runtime::new().context("create explore subagent tokio runtime")?;
|
||||
rt.block_on(run_agent(ctx, &directive, AccessTier::Read, tc))
|
||||
});
|
||||
|
||||
@@ -252,12 +250,22 @@ async fn run_explore_phase(
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
warn!(agent = i, error = %e, "explore subagent failed");
|
||||
emit_failed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i], &e.to_string());
|
||||
emit_failed(
|
||||
&turn_events_clone,
|
||||
EXPLORE_IDS[i],
|
||||
EXPLORE_LABELS[i],
|
||||
&e.to_string(),
|
||||
);
|
||||
(i, format!("Error: {e}"), false)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(agent = i, error = ?e, "explore subagent panicked");
|
||||
emit_failed(&turn_events_clone, EXPLORE_IDS[i], EXPLORE_LABELS[i], "thread panicked");
|
||||
emit_failed(
|
||||
&turn_events_clone,
|
||||
EXPLORE_IDS[i],
|
||||
EXPLORE_LABELS[i],
|
||||
"thread panicked",
|
||||
);
|
||||
(i, format!("Thread panic: {e:?}"), false)
|
||||
}
|
||||
};
|
||||
@@ -281,9 +289,7 @@ fn build_explore_context(results: &[(usize, String, bool)]) -> String {
|
||||
let success_count = results.iter().filter(|r| r.2).count();
|
||||
let total = results.len();
|
||||
|
||||
let mut msg = format!(
|
||||
"[Explore Phase — {success_count}/{total} agents succeeded]\n\n"
|
||||
);
|
||||
let mut msg = format!("[Explore Phase — {success_count}/{total} agents succeeded]\n\n");
|
||||
|
||||
for (i, output, success) in results {
|
||||
let label = EXPLORE_LABELS.get(*i).unwrap_or(&"❓ Unknown");
|
||||
|
||||
@@ -107,10 +107,7 @@ impl BestPracticeEngine {
|
||||
let layering = self.audit_layering(workspace_root)?;
|
||||
let quality = self.scan_quality(workspace_root)?;
|
||||
|
||||
Ok(CombinedAuditReport {
|
||||
layering,
|
||||
quality,
|
||||
})
|
||||
Ok(CombinedAuditReport { layering, quality })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,8 @@ impl BashJob {
|
||||
let Ok(mut guard) = self.process.lock() else {
|
||||
return false;
|
||||
};
|
||||
guard.as_mut().is_some_and(|c| matches!(c.try_wait(), Ok(None)))
|
||||
guard
|
||||
.as_mut()
|
||||
.is_some_and(|c| matches!(c.try_wait(), Ok(None)))
|
||||
}
|
||||
}
|
||||
@@ -7,24 +7,22 @@
|
||||
pub fn check_dangerous_pattern(tool_name: &str, args: &serde_json::Value) -> Option<String> {
|
||||
match tool_name {
|
||||
"bash" => {
|
||||
let cmd = args
|
||||
.get("command")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let cmd = args.get("command").and_then(|v| v.as_str()).unwrap_or("");
|
||||
// Detect git push with --force
|
||||
if cmd.contains("git push") && cmd.contains("--force") {
|
||||
return Some("Force-pushing to git is destructive and may lose history".to_string());
|
||||
return Some(
|
||||
"Force-pushing to git is destructive and may lose history".to_string(),
|
||||
);
|
||||
}
|
||||
// Detect rm -rf /
|
||||
if cmd.contains("rm -rf /") || cmd.contains("rm -rf /*") {
|
||||
return Some("Recursive deletion of the root filesystem is never allowed".to_string());
|
||||
return Some(
|
||||
"Recursive deletion of the root filesystem is never allowed".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
"delete" => {
|
||||
let path = args
|
||||
.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if path == "/" || path.starts_with("/etc") {
|
||||
return Some(format!("Deleting '{}' is too dangerous", path));
|
||||
}
|
||||
|
||||
@@ -76,10 +76,7 @@ pub struct StatePayload {
|
||||
pub enum DaemonFrame {
|
||||
StateUpdate(Box<StatePayload>),
|
||||
StreamToken(String),
|
||||
SystemNote {
|
||||
kind: String,
|
||||
message: String,
|
||||
},
|
||||
SystemNote { kind: String, message: String },
|
||||
ClipboardCopy(String),
|
||||
Closed,
|
||||
}
|
||||
@@ -67,8 +67,7 @@ use std::sync::Arc;
|
||||
pub use tools::{GraduatedCheck, Tool, ToolCtx, ToolCtxBuilder};
|
||||
|
||||
// Re-export commonly needed types at the crate root
|
||||
pub use zesdex_domain::core::{ChatMessage, Role, Store, UsageStats, ToolCallResult};
|
||||
|
||||
pub use zesdex_domain::core::{ChatMessage, Role, Store, ToolCallResult, UsageStats};
|
||||
|
||||
/// A shared, async-writable cache of directory entries, used to avoid
|
||||
/// re-reading a directory every render frame.
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
use rand_core::RngCore;
|
||||
use std::time::Duration;
|
||||
|
||||
use zesdex_application::ports::ProviderService;
|
||||
use zesdex_domain::core::{
|
||||
ChatMessage, ChatRequest, ChatResponse, SseParser, StreamEvent, StreamOptions, ToolDef,
|
||||
};
|
||||
use zesdex_application::ports::ProviderService;
|
||||
|
||||
use zesdex_domain::agent::defaults::{DEFAULT_API_BASE, DEFAULT_MODEL};
|
||||
const DEFAULT_BASE_URL: &str = DEFAULT_API_BASE;
|
||||
@@ -82,10 +82,7 @@ impl LlmClient {
|
||||
retrying without connect timeout",
|
||||
e,
|
||||
);
|
||||
match reqwest::Client::builder()
|
||||
.timeout(REQUEST_TIMEOUT)
|
||||
.build()
|
||||
{
|
||||
match reqwest::Client::builder().timeout(REQUEST_TIMEOUT).build() {
|
||||
Ok(c) => c,
|
||||
Err(e2) => {
|
||||
tracing::warn!("also failed: {e2}. using default client");
|
||||
@@ -115,8 +112,7 @@ impl LlmClient {
|
||||
.post(url)
|
||||
.header("Content-Type", "application/json");
|
||||
if !self.api_key.is_empty() {
|
||||
http_req =
|
||||
http_req.header("Authorization", format!("Bearer {}", self.api_key));
|
||||
http_req = http_req.header("Authorization", format!("Bearer {}", self.api_key));
|
||||
}
|
||||
|
||||
let mut resp = http_req.json(req).send().await.map_err(|e| {
|
||||
@@ -182,7 +178,7 @@ impl LlmClient {
|
||||
}
|
||||
|
||||
let tc = &mut self.tool_calls[index];
|
||||
|
||||
|
||||
if let Some(ref id_val) = id {
|
||||
tc.id = id_val.clone();
|
||||
}
|
||||
@@ -237,8 +233,7 @@ impl LlmClient {
|
||||
n
|
||||
}
|
||||
};
|
||||
let text =
|
||||
String::from_utf8_lossy(&byte_buf[..valid_len]).into_owned();
|
||||
let text = String::from_utf8_lossy(&byte_buf[..valid_len]).into_owned();
|
||||
byte_buf.drain(..valid_len);
|
||||
|
||||
for event in parser.feed(&text) {
|
||||
@@ -306,8 +301,7 @@ impl ProviderService for LlmClient {
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
if !self.api_key.is_empty() {
|
||||
http_req =
|
||||
http_req.header("Authorization", format!("Bearer {}", self.api_key));
|
||||
http_req = http_req.header("Authorization", format!("Bearer {}", self.api_key));
|
||||
}
|
||||
|
||||
let result = async {
|
||||
@@ -335,9 +329,9 @@ impl ProviderService for LlmClient {
|
||||
}
|
||||
|
||||
let data: ChatResponse = resp.json().await?;
|
||||
let usage = data.usage.map(|u| {
|
||||
(u64::from(u.prompt_tokens), u64::from(u.completion_tokens))
|
||||
});
|
||||
let usage = data
|
||||
.usage
|
||||
.map(|u| (u64::from(u.prompt_tokens), u64::from(u.completion_tokens)));
|
||||
let message = data
|
||||
.choices
|
||||
.into_iter()
|
||||
@@ -345,7 +339,8 @@ impl ProviderService for LlmClient {
|
||||
.and_then(|c| c.message)
|
||||
.ok_or_else(|| anyhow::anyhow!("API response had no choices"))?;
|
||||
Ok((message, usage))
|
||||
}.await;
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok((msg, usage)) => return Ok((msg, usage)),
|
||||
@@ -393,14 +388,16 @@ impl ProviderService for LlmClient {
|
||||
let mut captured_content = false;
|
||||
let mut wrapped = |event: &StreamEvent| -> bool {
|
||||
match event {
|
||||
StreamEvent::Token(_) | StreamEvent::Reasoning(_) | StreamEvent::ToolCallDelta { .. } => {
|
||||
StreamEvent::Token(_)
|
||||
| StreamEvent::Reasoning(_)
|
||||
| StreamEvent::ToolCallDelta { .. } => {
|
||||
captured_content = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
on_event(event)
|
||||
};
|
||||
|
||||
|
||||
match self.try_stream_once(&req, &url, &mut wrapped).await {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(e) => {
|
||||
@@ -425,11 +422,7 @@ pub fn resolve_api_key(
|
||||
) -> String {
|
||||
let provider = &settings.provider;
|
||||
|
||||
let mut api_key = settings
|
||||
.api_keys
|
||||
.get(provider)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let mut api_key = settings.api_keys.get(provider).cloned().unwrap_or_default();
|
||||
|
||||
if api_key.is_empty() {
|
||||
if let Some(provider_cfg) = app_config.providers.get(provider) {
|
||||
|
||||
@@ -32,8 +32,16 @@ impl LspClient {
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child.stdin.take().ok_or_else(|| anyhow::anyhow!("no stdin on LSP process"))?;
|
||||
let stdout = BufReader::new(child.stdout.take().ok_or_else(|| anyhow::anyhow!("no stdout on LSP process"))?);
|
||||
let stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("no stdin on LSP process"))?;
|
||||
let stdout = BufReader::new(
|
||||
child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("no stdout on LSP process"))?,
|
||||
);
|
||||
|
||||
info!("LSP client spawned: {command}");
|
||||
Ok(LspClient {
|
||||
|
||||
@@ -28,7 +28,6 @@ impl Default for LspManager {
|
||||
}
|
||||
|
||||
impl LspManager {
|
||||
|
||||
pub fn start(&mut self, language: &str, command: &str, args: &[String]) -> anyhow::Result<()> {
|
||||
let client = LspClient::start(command, args)?;
|
||||
self.clients.insert(language.to_string(), client);
|
||||
|
||||
@@ -9,8 +9,14 @@ fn known_configs() -> HashMap<&'static str, (&'static str, Vec<&'static str>)> {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("rust", ("rust-analyzer", vec![]));
|
||||
m.insert("python", ("pyright-langserver", vec!["--stdio"]));
|
||||
m.insert("typescript", ("typescript-language-server", vec!["--stdio"]));
|
||||
m.insert("javascript", ("typescript-language-server", vec!["--stdio"]));
|
||||
m.insert(
|
||||
"typescript",
|
||||
("typescript-language-server", vec!["--stdio"]),
|
||||
);
|
||||
m.insert(
|
||||
"javascript",
|
||||
("typescript-language-server", vec!["--stdio"]),
|
||||
);
|
||||
m.insert("go", ("gopls", vec![]));
|
||||
m
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ pub fn install_language_server(language: &str) -> anyhow::Result<String> {
|
||||
if output.status.success() {
|
||||
Ok("rust-analyzer installed via rustup".to_string())
|
||||
} else {
|
||||
anyhow::bail!("failed to install rust-analyzer: {}", String::from_utf8_lossy(&output.stderr))
|
||||
anyhow::bail!(
|
||||
"failed to install rust-analyzer: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
}
|
||||
}
|
||||
"python" => {
|
||||
@@ -24,7 +27,10 @@ pub fn install_language_server(language: &str) -> anyhow::Result<String> {
|
||||
if output.status.success() {
|
||||
Ok("pyright installed via npm".to_string())
|
||||
} else {
|
||||
anyhow::bail!("failed to install pyright: {}", String::from_utf8_lossy(&output.stderr))
|
||||
anyhow::bail!(
|
||||
"failed to install pyright: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
}
|
||||
}
|
||||
lang => anyhow::bail!("no install method known for language '{lang}'"),
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
//! High-level manager that discovers, installs (if needed), and starts
|
||||
//! LSP servers.
|
||||
|
||||
use crate::lsp::manager::LspManager;
|
||||
use super::discovery::discover_installed;
|
||||
use super::install::install_language_server;
|
||||
use crate::lsp::manager::LspManager;
|
||||
|
||||
/// Auto-provision language servers for the given list of languages.
|
||||
///
|
||||
/// Flow: discover already-installed servers → for each requested language
|
||||
/// not yet available, attempt auto-install → start each server.
|
||||
pub fn auto_provision(
|
||||
lsp_manager: &mut LspManager,
|
||||
languages: &[String],
|
||||
) -> Vec<String> {
|
||||
pub fn auto_provision(lsp_manager: &mut LspManager, languages: &[String]) -> Vec<String> {
|
||||
let mut started = Vec::new();
|
||||
let installed = discover_installed();
|
||||
let mut installed_map: std::collections::HashMap<&str, &crate::lsp::provisioner::config::LspProvisionerConfig> = std::collections::HashMap::new();
|
||||
let mut installed_map: std::collections::HashMap<
|
||||
&str,
|
||||
&crate::lsp::provisioner::config::LspProvisionerConfig,
|
||||
> = std::collections::HashMap::new();
|
||||
for cfg in &installed {
|
||||
installed_map.insert(cfg.language.as_str(), cfg);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ impl Default for McpManager {
|
||||
}
|
||||
|
||||
impl McpManager {
|
||||
|
||||
/// Register an MCP server by name and transport string.
|
||||
///
|
||||
/// Returns an error if a server with the same name is already registered.
|
||||
|
||||
@@ -114,7 +114,11 @@ where
|
||||
}
|
||||
|
||||
Box::pin(async move {
|
||||
Ok((StatusCode::UNAUTHORIZED, "missing or invalid X-Session-Id header").into_response())
|
||||
Ok((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"missing or invalid X-Session-Id header",
|
||||
)
|
||||
.into_response())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
//! CORS layer factory for the daemon HTTP server.
|
||||
|
||||
use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer};
|
||||
|
||||
/// Return a permissive CorsLayer for local daemon IPC.
|
||||
///
|
||||
/// All method and header names are static strings guaranteed to be valid
|
||||
/// HTTP tokens — `.parse()` is infallible here.
|
||||
pub fn default_cors_layer() -> CorsLayer {
|
||||
CorsLayer::new()
|
||||
.allow_origin(AllowOrigin::any())
|
||||
.allow_methods([
|
||||
"GET".parse().expect("static HTTP method"),
|
||||
"POST".parse().expect("static HTTP method"),
|
||||
"PUT".parse().expect("static HTTP method"),
|
||||
"DELETE".parse().expect("static HTTP method"),
|
||||
"PATCH".parse().expect("static HTTP method"),
|
||||
"OPTIONS".parse().expect("static HTTP method"),
|
||||
])
|
||||
.allow_headers(AllowHeaders::any())
|
||||
.expose_headers([
|
||||
"Content-Type".parse().expect("static HTTP header"),
|
||||
"X-Session-Id".parse().expect("static HTTP header"),
|
||||
"X-Request-Id".parse().expect("static HTTP header"),
|
||||
])
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Axum middleware tower for the HTTP API layer.
|
||||
|
||||
pub mod auth;
|
||||
pub mod cors;
|
||||
pub mod rate_limit;
|
||||
@@ -28,11 +28,14 @@ impl RateLimiter {
|
||||
.as_secs() as i64;
|
||||
|
||||
let cutoff = now.saturating_sub(window_secs as i64);
|
||||
let mut windows = self.windows.lock().map_err(|e| {
|
||||
anyhow::anyhow!("rate limiter lock poisoned: {e}")
|
||||
})?;
|
||||
let mut windows = self
|
||||
.windows
|
||||
.lock()
|
||||
.map_err(|e| anyhow::anyhow!("rate limiter lock poisoned: {e}"))?;
|
||||
|
||||
let timestamps = windows.entry(client_id.to_string()).or_insert_with(Vec::new);
|
||||
let timestamps = windows
|
||||
.entry(client_id.to_string())
|
||||
.or_insert_with(Vec::new);
|
||||
timestamps.retain(|&ts| ts >= cutoff);
|
||||
|
||||
if timestamps.len() >= max_requests as usize {
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
use std::path::Path;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zesdex_domain::cms::{AppConfig, AppConfigRepository, ModelRole, ProviderConfig, RepositoryError};
|
||||
use zesdex_domain::cms::{
|
||||
AppConfig, AppConfigRepository, ModelRole, ProviderConfig, RepositoryError,
|
||||
};
|
||||
|
||||
use crate::utils::write_json_atomic;
|
||||
|
||||
@@ -40,12 +42,15 @@ fn claude_settings_from_file() -> Option<ClaudeSettings> {
|
||||
|
||||
fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option<String>)> {
|
||||
let settings = claude_settings_from_file();
|
||||
|
||||
|
||||
let file_creds = settings.as_ref().and_then(|s| {
|
||||
let env = s.env.as_ref()?;
|
||||
Some((env.anthropic_base_url.clone()?, env.anthropic_api_key.clone()?))
|
||||
Some((
|
||||
env.anthropic_base_url.clone()?,
|
||||
env.anthropic_api_key.clone()?,
|
||||
))
|
||||
});
|
||||
|
||||
|
||||
let env_creds = || -> Option<(String, String)> {
|
||||
let base_url = std::env::var("ANTHROPIC_BASE_URL").ok()?;
|
||||
let key = std::env::var("ANTHROPIC_API_KEY").ok()?;
|
||||
@@ -55,7 +60,7 @@ fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option<String>)>
|
||||
let custom_model = settings.and_then(|s| s.custom_model);
|
||||
|
||||
let (base_url, key) = file_creds.or_else(env_creds)?;
|
||||
|
||||
|
||||
Some((
|
||||
ProviderConfig {
|
||||
api_base: base_url,
|
||||
@@ -63,7 +68,7 @@ fn detect_claude_settings_provider() -> Option<(ProviderConfig, Option<String>)>
|
||||
default_model: custom_model.clone(),
|
||||
default_api_key: Some(key),
|
||||
},
|
||||
custom_model
|
||||
custom_model,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -72,9 +77,7 @@ impl AppConfigRepository for JsonAppConfigRepository {
|
||||
let path = base_dir.join("app_config.json");
|
||||
let mut cfg: AppConfig = match std::fs::read_to_string(&path) {
|
||||
Ok(s) => serde_json::from_str(&s)?,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
AppConfig::default()
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => AppConfig::default(),
|
||||
Err(e) => return Err(RepositoryError::Io(e)),
|
||||
};
|
||||
|
||||
@@ -106,15 +109,13 @@ impl AppConfigRepository for JsonAppConfigRepository {
|
||||
}
|
||||
|
||||
if let Some(custom) = &custom_model {
|
||||
cfg.model_roles
|
||||
.entry(custom.clone())
|
||||
.or_insert(ModelRole {
|
||||
provider: "claude".to_string(),
|
||||
model: custom.clone(),
|
||||
max_tokens: Some(8192),
|
||||
context_window: Some(200_000),
|
||||
temperature: Some(0.7),
|
||||
});
|
||||
cfg.model_roles.entry(custom.clone()).or_insert(ModelRole {
|
||||
provider: "claude".to_string(),
|
||||
model: custom.clone(),
|
||||
max_tokens: Some(8192),
|
||||
context_window: Some(200_000),
|
||||
temperature: Some(0.7),
|
||||
});
|
||||
}
|
||||
|
||||
if cfg.default_provider == defaults.default_provider {
|
||||
|
||||
@@ -77,10 +77,7 @@ impl MarkdownMemoryRepository {
|
||||
.lines()
|
||||
.filter_map(|l| {
|
||||
let mut it = l.splitn(2, ':');
|
||||
Some((
|
||||
it.next()?.trim().to_string(),
|
||||
it.next()?.trim().to_string(),
|
||||
))
|
||||
Some((it.next()?.trim().to_string(), it.next()?.trim().to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -175,7 +172,9 @@ impl MemoryRepository for MarkdownMemoryRepository {
|
||||
|
||||
fn save(&self, memory_dir: &Path, memory: &Memory) -> Result<(), RepositoryError> {
|
||||
let path = Memory::path(memory_dir, &memory.name);
|
||||
let parent = path.parent().expect("memory path always has a parent directory");
|
||||
let parent = path
|
||||
.parent()
|
||||
.expect("memory path always has a parent directory");
|
||||
std::fs::create_dir_all(parent)?;
|
||||
|
||||
let frontmatter = Self::build_frontmatter(memory);
|
||||
|
||||
Loaded 50 of 137 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user