forked from OpenCoven/coven-code
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlib.rs
More file actions
5468 lines (4955 loc) · 207 KB
/
Copy pathlib.rs
File metadata and controls
5468 lines (4955 loc) · 207 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// cc-core: Core types, error handling, configuration, settings, and constants
// for Coven Code.
//
// All sub-modules are defined inline below.
// Branded provider / model identifier newtypes.
pub mod provider_id;
pub use provider_id::{ModelId, ProviderId};
// Session transcript persistence (JSONL, matches TS sessionStorage.ts schema).
pub mod session_storage;
// SQLite-backed session storage (faster alternative to JSONL).
pub mod sqlite_storage;
pub use sqlite_storage::{SessionSummary, SqliteSessionStore};
// Attachment pipeline — assembles per-turn context attachments (T1-6).
pub mod attachments;
// Git utilities (T4-3).
pub mod git_utils;
// Credential storage for provider API keys and OAuth tokens.
pub mod auth_store;
pub use auth_store::{AuthStore, StoredCredential};
// GitHub Device Code Flow (RFC 8628) for OAuth device authorization.
pub mod device_code;
// Utility modules ported from src/utils/
pub mod auto_mode;
pub mod crypto_utils;
pub mod format_utils;
pub mod secret_file;
pub mod spinner;
pub mod status_notices;
pub mod token_budget;
pub mod truncate;
pub use spinner::{
sample_completion_verb, sample_spinner_verb, SPINNER_VERBS, TURN_COMPLETION_VERBS,
};
// Remote session sync and cloud session API (T3-1, T3-2).
pub mod cloud_session;
pub mod remote_session;
// AGENTS.md hierarchical memory loading (T4-1).
pub mod claudemd;
// Hosted review runtime isolation model.
pub mod hosted_review;
// OpenClaw installation/agent detection used by Coven integrations.
pub mod openclaw_agent;
// Message manipulation utilities (T4-2).
pub mod message_utils;
// Per-session file modification history (T4-6).
pub mod file_history;
// Snapshot/undo system — tracks file changes per session for /undo support.
pub mod snapshot;
// Per-session durable objectives (/goal feature).
pub mod goal;
pub use goal::{
goal_continuation_message, goal_kickoff_message, goal_system_prompt_addendum, goals_enabled,
Goal, GoalError, GoalStatus, GoalStore, MAX_GOAL_TURNS, MAX_OBJECTIVE_CHARS,
};
// Feature flag management via GrowthBook.
pub mod feature_flags;
// MCP resource prompt template rendering with variable substitution.
pub mod mcp_templates;
// IDE environment detection (VS Code, Cursor, JetBrains, …).
// Background update checker — compares running version against GitHub releases.
pub mod update_check;
pub use update_check::{check_for_updates, UpdateInfo};
// Self-contained HTML export of a session, used by the `/share` slash command.
pub mod share_export;
// Re-export commonly used types at the crate root
pub use config::{
builtin_managed_agent_presets, default_agents, strip_jsonc_comments, substitute_env_vars,
AgentDefinition, BudgetSplitPolicy, CommandTemplate, Config, FormatterConfig,
ManagedAgentConfig, ManagedAgentPreset, McpServerConfig, OutputFormat, PermissionMode,
ProviderConfig, Settings, SkillsConfig, Theme,
};
pub use error::{ClaudeError, Result};
pub use import_config::{
build_import_preview, execute_import, summarize_import_result, ClaudeMdPreview,
ImportExecutionResult, ImportPaths, ImportPreview, ImportSelection, PreviewAction,
PreviewField, SettingsPreview,
};
pub use types::{
CitationsConfig, ContentBlock, DocumentSource, ImageSource, Message, MessageContent,
MessageCost, Role, ToolDefinition, ToolResultContent, UsageInfo,
};
// Skill discovery: filesystem and git URL skill loading.
pub mod skill_discovery;
pub use skill_discovery::{
discover_skills, estimate_tokens, parse_skill_file, DiscoveredSkill, SkillScope,
};
// Coven daemon shared state — read-only bridge to ~/.coven/.
pub mod coven_shared;
// Tier B IPC — blocking HTTP-over-Unix-socket client for the live daemon.
pub mod coven_daemon;
pub mod roster_reset;
pub use cost::CostTracker;
pub use feature_flags::FeatureFlagManager;
pub use history::ConversationSession;
pub use permissions::{
format_permission_reason, AutoPermissionHandler, InteractivePermissionHandler,
ManagedAutoPermissionHandler, ManagedInteractivePermissionHandler, PermissionAction,
PermissionDecision, PermissionHandler, PermissionLevel, PermissionManager, PermissionRequest,
PermissionRule, PermissionScope, SerializedPermissionRule,
};
pub use roster_reset::{reset_familiars_and_agents, ResetRosterSummary};
// ---------------------------------------------------------------------------
// error module
// ---------------------------------------------------------------------------
pub mod error {
use thiserror::Error;
/// The unified error type for Coven Code.
#[derive(Error, Debug)]
pub enum ClaudeError {
#[error("API error: {0}")]
Api(String),
#[error("API error {status}: {message}")]
ApiStatus { status: u16, message: String },
#[error("Authentication error: {0}")]
Auth(String),
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("Tool error: {0}")]
Tool(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("Rate limit exceeded")]
RateLimit,
#[error("Context window exceeded")]
ContextWindowExceeded,
#[error("Max tokens reached")]
MaxTokensReached,
#[error("Cancelled")]
Cancelled,
#[error("Configuration error: {0}")]
Config(String),
#[error("MCP error: {0}")]
Mcp(String),
#[error("{0}")]
Other(String),
}
/// Convenience alias used throughout the project.
pub type Result<T> = std::result::Result<T, ClaudeError>;
impl ClaudeError {
/// Return `true` when the caller should retry the request.
pub fn is_retryable(&self) -> bool {
matches!(
self,
ClaudeError::RateLimit
| ClaudeError::ApiStatus { status: 429, .. }
| ClaudeError::ApiStatus { status: 529, .. }
)
}
/// Return `true` for errors that mean the conversation cannot continue
/// without intervention (e.g. compaction or context-window reset).
pub fn is_context_limit(&self) -> bool {
matches!(
self,
ClaudeError::ContextWindowExceeded | ClaudeError::MaxTokensReached
)
}
}
}
// ---------------------------------------------------------------------------
// types module
// ---------------------------------------------------------------------------
pub mod types {
use serde::{Deserialize, Serialize};
use serde_json::Value;
// ---- Roles -----------------------------------------------------------
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
}
// ---- Content blocks --------------------------------------------------
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text {
text: String,
},
Image {
source: ImageSource,
},
ToolUse {
id: String,
name: String,
input: Value,
},
ToolResult {
tool_use_id: String,
content: ToolResultContent,
#[serde(skip_serializing_if = "Option::is_none")]
is_error: Option<bool>,
},
Thinking {
thinking: String,
signature: String,
},
RedactedThinking {
data: String,
},
Document {
source: DocumentSource,
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
citations: Option<CitationsConfig>,
},
/// A `!`-prefixed shell command invoked by the user, with its captured output.
/// Rendered as a faint gray block with a `!command` header.
UserLocalCommandOutput {
command: String,
output: String,
},
/// A skill/slash-command invocation entered by the user.
/// Rendered as `▸ name args` with cyan styling.
UserCommand {
name: String,
args: String,
},
/// A memory key/value written by the user (e.g. via `/memory`).
/// Rendered as `# key: value` in cyan with a `Got it.` footer.
UserMemoryInput {
key: String,
value: String,
},
/// A system-level API error, rendered as a red-bordered block.
/// Shows first 5 lines with `[expand]` hint when truncated, and an
/// optional `Retrying in Ns...` countdown line when `retry_secs` is set.
SystemAPIError {
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
retry_secs: Option<u32>,
},
/// A collapsed summary of multiple read/search tool calls.
/// Rendered as `▸ Read N files (+ M more)` on a single line.
CollapsedReadSearch {
tool_name: String,
paths: Vec<String>,
n_hidden: usize,
},
/// A sub-task assignment in an agentic workflow.
/// Rendered as a cyan-bordered box with Task ID, subject, and description.
TaskAssignment {
id: String,
subject: String,
description: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolResultContent {
Text(String),
Blocks(Vec<ContentBlock>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageSource {
#[serde(rename = "type")]
pub source_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentSource {
#[serde(rename = "type")]
pub source_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CitationsConfig {
pub enabled: bool,
}
// ---- Messages --------------------------------------------------------
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
pub content: MessageContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub uuid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cost: Option<MessageCost>,
/// Files changed during this assistant turn, captured by the shadow snapshot.
/// Populated by the query loop on `finish-step`; absent on user messages.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub snapshot_patch: Option<crate::snapshot::Patch>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Blocks(Vec<ContentBlock>),
}
impl Message {
/// Create a simple user text message.
pub fn user(content: impl Into<String>) -> Self {
Self {
role: Role::User,
content: MessageContent::Text(content.into()),
uuid: None,
cost: None,
snapshot_patch: None,
}
}
/// Create a user message composed of multiple content blocks.
pub fn user_blocks(blocks: Vec<ContentBlock>) -> Self {
Self {
role: Role::User,
content: MessageContent::Blocks(blocks),
uuid: None,
cost: None,
snapshot_patch: None,
}
}
/// Create a simple assistant text message.
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: MessageContent::Text(content.into()),
uuid: None,
cost: None,
snapshot_patch: None,
}
}
/// Create an assistant message composed of multiple content blocks.
pub fn assistant_blocks(blocks: Vec<ContentBlock>) -> Self {
Self {
role: Role::Assistant,
content: MessageContent::Blocks(blocks),
uuid: None,
cost: None,
snapshot_patch: None,
}
}
/// Extract the first text content from this message.
pub fn get_text(&self) -> Option<&str> {
match &self.content {
MessageContent::Text(t) => Some(t.as_str()),
MessageContent::Blocks(blocks) => blocks.iter().find_map(|b| {
if let ContentBlock::Text { text } = b {
Some(text.as_str())
} else {
None
}
}),
}
}
/// Collect all text content blocks into one concatenated string.
pub fn get_all_text(&self) -> String {
match &self.content {
MessageContent::Text(t) => t.clone(),
MessageContent::Blocks(blocks) => blocks
.iter()
.filter_map(|b| {
if let ContentBlock::Text { text } = b {
Some(text.as_str())
} else {
None
}
})
.collect::<Vec<_>>()
.join(""),
}
}
/// Return references to all `ToolUse` blocks in this message.
pub fn get_tool_use_blocks(&self) -> Vec<&ContentBlock> {
match &self.content {
MessageContent::Blocks(blocks) => blocks
.iter()
.filter(|b| matches!(b, ContentBlock::ToolUse { .. }))
.collect(),
_ => vec![],
}
}
/// Return references to all `ToolResult` blocks in this message.
pub fn get_tool_result_blocks(&self) -> Vec<&ContentBlock> {
match &self.content {
MessageContent::Blocks(blocks) => blocks
.iter()
.filter(|b| matches!(b, ContentBlock::ToolResult { .. }))
.collect(),
_ => vec![],
}
}
/// Return references to all `Thinking` blocks in this message.
pub fn get_thinking_blocks(&self) -> Vec<&ContentBlock> {
match &self.content {
MessageContent::Blocks(blocks) => blocks
.iter()
.filter(|b| matches!(b, ContentBlock::Thinking { .. }))
.collect(),
_ => vec![],
}
}
/// Returns all content blocks (wrapping a single text into a vec).
pub fn content_blocks(&self) -> Vec<ContentBlock> {
match &self.content {
MessageContent::Text(t) => vec![ContentBlock::Text { text: t.clone() }],
MessageContent::Blocks(b) => b.clone(),
}
}
/// Check whether this message has any tool use blocks.
pub fn has_tool_use(&self) -> bool {
!self.get_tool_use_blocks().is_empty()
}
/// Create a user message representing a `!`-prefixed local shell command with output.
pub fn user_local_command_output(
command: impl Into<String>,
output: impl Into<String>,
) -> Self {
Self {
role: Role::User,
content: MessageContent::Blocks(vec![ContentBlock::UserLocalCommandOutput {
command: command.into(),
output: output.into(),
}]),
uuid: None,
cost: None,
snapshot_patch: None,
}
}
/// Create a user message representing a skill/slash-command invocation.
pub fn user_command(name: impl Into<String>, args: impl Into<String>) -> Self {
Self {
role: Role::User,
content: MessageContent::Blocks(vec![ContentBlock::UserCommand {
name: name.into(),
args: args.into(),
}]),
uuid: None,
cost: None,
snapshot_patch: None,
}
}
/// Create a user message representing a memory key/value entry.
pub fn user_memory_input(key: impl Into<String>, value: impl Into<String>) -> Self {
Self {
role: Role::User,
content: MessageContent::Blocks(vec![ContentBlock::UserMemoryInput {
key: key.into(),
value: value.into(),
}]),
uuid: None,
cost: None,
snapshot_patch: None,
}
}
/// Create a system message representing an API error (red-bordered block).
pub fn system_api_error(message: impl Into<String>, retry_secs: Option<u32>) -> Self {
Self {
role: Role::User,
content: MessageContent::Blocks(vec![ContentBlock::SystemAPIError {
message: message.into(),
retry_secs,
}]),
uuid: None,
cost: None,
snapshot_patch: None,
}
}
/// Create a system message representing a collapsed read/search summary.
pub fn collapsed_read_search(
tool_name: impl Into<String>,
paths: Vec<String>,
n_hidden: usize,
) -> Self {
Self {
role: Role::User,
content: MessageContent::Blocks(vec![ContentBlock::CollapsedReadSearch {
tool_name: tool_name.into(),
paths,
n_hidden,
}]),
uuid: None,
cost: None,
snapshot_patch: None,
}
}
/// Create a system message representing a sub-task assignment.
pub fn task_assignment(
id: impl Into<String>,
subject: impl Into<String>,
description: impl Into<String>,
) -> Self {
Self {
role: Role::User,
content: MessageContent::Blocks(vec![ContentBlock::TaskAssignment {
id: id.into(),
subject: subject.into(),
description: description.into(),
}]),
uuid: None,
cost: None,
snapshot_patch: None,
}
}
}
// ---- Cost / usage ----------------------------------------------------
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MessageCost {
pub input_tokens: u64,
pub output_tokens: u64,
pub cache_creation_input_tokens: u64,
pub cache_read_input_tokens: u64,
pub cost_usd: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition {
pub name: String,
pub description: String,
pub input_schema: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UsageInfo {
pub input_tokens: u64,
pub output_tokens: u64,
#[serde(default)]
pub cache_creation_input_tokens: u64,
#[serde(default)]
pub cache_read_input_tokens: u64,
}
impl UsageInfo {
pub fn total_input(&self) -> u64 {
self.input_tokens + self.cache_creation_input_tokens + self.cache_read_input_tokens
}
pub fn total(&self) -> u64 {
self.total_input() + self.output_tokens
}
}
}
// ---------------------------------------------------------------------------
// config module
// ---------------------------------------------------------------------------
pub mod config {
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
// ---- Hook configuration ----------------------------------------------
/// Events that can trigger hooks.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "PascalCase")]
pub enum HookEvent {
/// Fires before a tool is executed.
PreToolUse,
/// Fires after a tool has returned its result.
PostToolUse,
/// Fires when the model finishes its turn (stop).
Stop,
/// Fires after the model samples a response, before tool execution.
/// Corresponds to `hooks.PostModelTurn` in settings.json.
PostModelTurn,
/// Fires when the user submits a prompt.
UserPromptSubmit,
/// General-purpose notification event.
Notification,
}
/// A single hook entry: a shell command to run on a specific event.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HookEntry {
/// Shell command to execute. Receives event JSON on stdin.
pub command: String,
/// Optional tool name filter — only run for this tool (PreToolUse/PostToolUse).
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_filter: Option<String>,
/// If true, a non-zero exit code blocks the operation.
#[serde(default)]
pub blocking: bool,
}
// ---- AgentDefinition -------------------------------------------------
fn default_agent_access() -> String {
"full".to_string()
}
fn default_true() -> bool {
true
}
fn default_file_autocomplete_limit() -> usize {
15
}
fn default_file_injection_max_size() -> usize {
100 // 100 KB
}
/// Definition of a named agent with per-agent model, permissions,
/// temperature, and system prompt.
pub fn api_key_env_vars_for_provider(provider_id: &str) -> &'static [&'static str] {
match provider_id {
"anthropic" => &["ANTHROPIC_API_KEY"],
"openai" | "codex" | "openai-codex" => &["OPENAI_API_KEY"],
_ => &[],
}
}
pub fn primary_api_key_env_var_for_provider(provider_id: &str) -> Option<&'static str> {
api_key_env_vars_for_provider(provider_id).first().copied()
}
pub fn api_base_env_var_for_provider(provider_id: &str) -> Option<&'static str> {
match provider_id {
"anthropic" => Some("ANTHROPIC_BASE_URL"),
"openai" | "codex" | "openai-codex" => Some("OPENAI_BASE_URL"),
_ => None,
}
}
pub fn default_api_base_for_provider(provider_id: &str) -> Option<&'static str> {
match provider_id {
"anthropic" => Some(crate::constants::ANTHROPIC_API_BASE),
"openai" | "codex" | "openai-codex" => Some("https://api.openai.com"),
_ => None,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentDefinition {
/// Display name / description
pub description: Option<String>,
/// Model override for this agent (e.g., "anthropic/claude-haiku-4-5")
pub model: Option<String>,
/// Temperature override
pub temperature: Option<f64>,
/// System prompt prefix (prepended before the main system prompt)
pub prompt: Option<String>,
/// Permission restriction: "full", "read-only", "search-only"
#[serde(default = "default_agent_access")]
pub access: String,
/// Whether to show in @agent autocomplete
#[serde(default = "default_true")]
pub visible: bool,
/// Max agentic turns for this agent (overrides global)
pub max_turns: Option<u32>,
/// ANSI color for display: "cyan", "magenta", "green", etc.
pub color: Option<String>,
}
impl Default for AgentDefinition {
fn default() -> Self {
Self {
description: None,
model: None,
temperature: None,
prompt: None,
access: default_agent_access(),
visible: true,
max_turns: None,
color: None,
}
}
}
// ---- ManagedAgentConfig ----------------------------------------------
/// Budget allocation strategy between manager and executor agents.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[derive(Default)]
pub enum BudgetSplitPolicy {
/// Shared pool — no split (default).
#[default]
SharedPool,
/// Manager gets manager_pct% of total budget.
Percentage { manager_pct: u8 },
/// Hard USD caps per role.
FixedCaps { manager_usd: f64, executor_usd: f64 },
}
/// Configuration for manager-executor agent architecture.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManagedAgentConfig {
pub enabled: bool,
/// "provider/model" string, e.g. "anthropic/claude-opus-4-6"
pub manager_model: String,
/// "provider/model" string, e.g. "anthropic/claude-sonnet-4-6"
pub executor_model: String,
#[serde(default = "default_executor_max_turns")]
pub executor_max_turns: u32,
#[serde(default = "default_max_concurrent_executors")]
pub max_concurrent_executors: u32,
#[serde(default)]
pub budget_split: BudgetSplitPolicy,
#[serde(default)]
pub total_budget_usd: Option<f64>,
#[serde(default)]
pub preset_name: Option<String>,
#[serde(default)]
pub executor_isolation: bool,
}
fn default_executor_max_turns() -> u32 {
10
}
fn default_max_concurrent_executors() -> u32 {
4
}
/// A named preset for common manager-executor configurations.
pub struct ManagedAgentPreset {
pub name: &'static str,
pub label: &'static str,
pub description: &'static str,
pub manager_model: &'static str,
pub executor_model: &'static str,
pub executor_max_turns: u32,
pub max_concurrent_executors: u32,
}
pub fn builtin_managed_agent_presets() -> Vec<ManagedAgentPreset> {
vec![
ManagedAgentPreset {
name: "anthropic-tiered",
label: "Anthropic Tiered",
description: "Opus 4.8 manages, Sonnet 4.6 executes (best quality)",
manager_model: "anthropic/claude-opus-4-8",
executor_model: "anthropic/claude-sonnet-4-6",
executor_max_turns: 10,
max_concurrent_executors: 4,
},
ManagedAgentPreset {
name: "anthropic-budget",
label: "Anthropic Budget",
description: "Sonnet 4.6 manages, Haiku 4.5 executes (cost-optimized)",
manager_model: "anthropic/claude-sonnet-4-6",
executor_model: "anthropic/claude-haiku-4-5-20251001",
executor_max_turns: 10,
max_concurrent_executors: 6,
},
ManagedAgentPreset {
name: "codex-tiered",
label: "Codex Tiered",
description: "gpt-5.2-codex manages, gpt-5.1-codex-mini executes",
manager_model: "codex/gpt-5.2-codex",
executor_model: "codex/gpt-5.1-codex-mini",
executor_max_turns: 10,
max_concurrent_executors: 4,
},
ManagedAgentPreset {
name: "cross-codex-anthropic",
label: "Cross: Codex + Claude",
description: "gpt-5.2-codex manages, Sonnet 4.6 executes",
manager_model: "codex/gpt-5.2-codex",
executor_model: "anthropic/claude-sonnet-4-6",
executor_max_turns: 10,
max_concurrent_executors: 4,
},
]
}
// ---- ProviderConfig --------------------------------------------------
/// Per-provider configuration: API keys, base URLs, and options.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderConfig {
/// API key (overrides environment variable)
pub api_key: Option<String>,
/// Override the default base URL for this provider
pub api_base: Option<String>,
/// Whether this provider is enabled (default: true)
#[serde(default = "default_true")]
pub enabled: bool,
/// Model ID whitelist (empty = allow all)
#[serde(default)]
pub models_whitelist: Vec<String>,
/// Model ID blacklist
#[serde(default)]
pub models_blacklist: Vec<String>,
/// Provider-specific options (passed through to provider implementation)
#[serde(default)]
pub options: HashMap<String, serde_json::Value>,
}
impl Default for ProviderConfig {
fn default() -> Self {
Self {
api_key: None,
api_base: None,
enabled: true,
models_whitelist: Vec::new(),
models_blacklist: Vec::new(),
options: HashMap::new(),
}
}
}
// ---- Config ----------------------------------------------------------
/// Top-level configuration values, merged from CLI args + settings file + env.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Config {
pub api_key: Option<String>,
pub model: Option<String>,
pub max_tokens: Option<u32>,
pub permission_mode: PermissionMode,
pub theme: Theme,
#[serde(default)]
pub output_style: Option<String>,
pub auto_compact: bool,
pub compact_threshold: f32,
pub verbose: bool,
pub output_format: OutputFormat,
pub mcp_servers: Vec<McpServerConfig>,
#[serde(default)]
pub lsp_servers: Vec<crate::lsp::LspServerConfig>,
pub allowed_tools: Vec<String>,
pub disallowed_tools: Vec<String>,
pub env: HashMap<String, String>,
pub enable_all_mcp_servers: bool,
pub custom_system_prompt: Option<String>,
pub append_system_prompt: Option<String>,
pub disable_claude_mds: bool,
/// Hosted review isolation mode. Enabled by settings, CLI, or
/// COVEN_CODE_HOSTED_REVIEW=1.
#[serde(
default,
rename = "hostedReview",
skip_serializing_if = "crate::hosted_review::HostedReviewConfig::is_default"
)]
pub hosted_review: crate::hosted_review::HostedReviewConfig,
pub project_dir: Option<PathBuf>,
#[serde(default)]
pub workspace_paths: Vec<PathBuf>,
/// Additional directories granted access via --add-dir.
#[serde(default)]
pub additional_dirs: Vec<PathBuf>,
/// Event hooks: map of event → list of hook commands.
#[serde(default)]
pub hooks: HashMap<HookEvent, Vec<HookEntry>>,
/// Active provider ID (default: "anthropic")
#[serde(default)]
pub provider: Option<String>,
/// Per-provider configurations
#[serde(default)]
pub provider_configs: HashMap<String, ProviderConfig>,
/// Formatter configurations (copied from Settings on load).
#[serde(default)]
pub formatter: HashMap<String, FormatterConfig>,
/// User-defined command templates (copied from Settings on load).
#[serde(default)]
pub commands: HashMap<String, CommandTemplate>,
/// Named agent definitions (copied from Settings on load).
#[serde(default)]
pub agents: HashMap<String, AgentDefinition>,
/// Active familiar — drives the mascot glyph in the welcome screen.
/// Values are user-defined familiar ids from `~/.coven/familiars.toml`.
/// Set via `"familiar": "<id>"` in settings.json.
#[serde(default)]
pub familiar: Option<String>,
/// Whether to show the empty-session welcome/splash panel. Defaults to true.
#[serde(
default,
rename = "showSplash",
skip_serializing_if = "Option::is_none"
)]
pub show_splash: Option<bool>,
/// Skill-discovery configuration (copied from Settings on load).
#[serde(default)]
pub skills: SkillsConfig,
/// Managed agent (manager-executor) configuration.
#[serde(default)]
pub managed_agents: Option<ManagedAgentConfig>,
/// Shadow-git auto-commit snapshot system. `Some(true)` = enabled. `None` or `Some(false)` = disabled (default).
/// Set via `--auto-commits` flag or `"autoCommits": true` in settings.json.
#[serde(
default,
rename = "autoCommits",
skip_serializing_if = "Option::is_none"
)]
pub auto_commits: Option<bool>,
/// Enable cursor blinking in the chat prompt. Defaults to false (disabled).
#[serde(
default,
rename = "cursorBlinkEnabled",
skip_serializing_if = "is_false"
)]
pub cursor_blink_enabled: bool,
/// Maximum number of file suggestions shown in autocomplete. Defaults to 15.
#[serde(
default = "default_file_autocomplete_limit",
rename = "fileAutocompleteLimit"
)]
pub file_autocomplete_limit: usize,
/// Whether to show hidden files in file autocomplete. Defaults to false.
#[serde(default, rename = "fileAutocompleteShowHiddenFiles")]
pub file_autocomplete_show_hidden_files: bool,
/// Whether @ file references are automatically injected into message context. Defaults to true.
/// When true: @file auto-injects file contents into your message before sending.
/// When false: @ is just autocomplete and reference (no auto-injection).
/// Note: This only affects user messages. @include in CLAUDE.md/AGENTS.md always injects with no size limits.
#[serde(default = "default_true", rename = "fileInjectionEnabled")]
pub file_injection_enabled: bool,
/// Maximum file size to auto-inject (in KB). Defaults to 100. Set to 0 for no limit.
/// When a file exceeds this limit, users get a warning and can choose to override or cancel.
/// Note: @include in CLAUDE.md/AGENTS.md always injects regardless of this limit.
#[serde(
default = "default_file_injection_max_size",
rename = "fileInjectionMaxSize"
)]
pub file_injection_max_size: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]