Skip to main content

subcog/mcp/
tool_types.rs

1//! Argument types and helper functions for MCP tools.
2//!
3//! Extracted from `tools.rs` to reduce file size.
4//!
5//! # Security
6//!
7//! All argument types use `#[serde(deny_unknown_fields)]` to prevent
8//! parameter pollution attacks where attackers inject unexpected fields
9//! that could bypass validation or trigger unintended behavior.
10
11use crate::models::{DetailLevel, MemoryStatus, Namespace, SearchFilter, SearchMode};
12use crate::storage::index::DomainScope;
13use serde::Deserialize;
14use std::collections::HashMap;
15
16/// Arguments for the capture tool.
17#[derive(Debug, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct CaptureArgs {
20    /// The memory content to capture.
21    pub content: String,
22    /// Memory category (e.g., "decisions", "patterns", "learnings").
23    pub namespace: String,
24    /// Optional tags for categorization and filtering.
25    pub tags: Option<Vec<String>>,
26    /// Optional source reference (file path, URL, etc.).
27    pub source: Option<String>,
28    /// Optional TTL (time-to-live) for automatic expiration.
29    /// Supports duration strings like "7d", "30d", "24h", "60m", or seconds.
30    /// Use "0" for no expiration (default behavior).
31    pub ttl: Option<String>,
32    /// Storage scope: "project" (default), "user", or "org".
33    /// - "project": Stored with project context (requires git repo)
34    /// - "user": Stored globally for user across all projects
35    /// - "org": Stored in organization-shared index
36    pub domain: Option<String>,
37}
38
39/// Arguments for the recall tool.
40///
41/// When `query` is omitted or empty, behaves like `subcog_list` and returns
42/// all memories matching the filter criteria (with pagination support).
43#[derive(Debug, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct RecallArgs {
46    /// Search query text. If omitted or empty, lists all memories (like `subcog_list`).
47    pub query: Option<String>,
48    /// GitHub-style filter query (e.g., "ns:decisions tag:rust -tag:test since:7d").
49    pub filter: Option<String>,
50    /// Filter by namespace (deprecated: use `filter` instead).
51    pub namespace: Option<String>,
52    /// Search mode: "hybrid" (default), "vector", or "text".
53    pub mode: Option<String>,
54    /// Detail level: "light", "medium" (default), or "everything".
55    pub detail: Option<String>,
56    /// Maximum number of results to return (default: 10 for search, 50 for list).
57    pub limit: Option<usize>,
58    /// Entity filter: filter to memories mentioning these entities (comma-separated for OR logic).
59    pub entity: Option<String>,
60    /// Offset for pagination (default: 0). Used when listing without query.
61    /// Note: Reserved for future pagination support in `list_all()`.
62    #[allow(dead_code)]
63    pub offset: Option<usize>,
64    /// Filter by user ID (for multi-tenant scoping).
65    pub user_id: Option<String>,
66    /// Filter by agent ID (for multi-agent scoping).
67    pub agent_id: Option<String>,
68}
69
70/// Arguments for the consolidate tool.
71#[derive(Debug, Deserialize)]
72#[serde(deny_unknown_fields)]
73pub struct ConsolidateArgs {
74    /// Namespaces to consolidate (optional, defaults to all).
75    pub namespaces: Option<Vec<String>>,
76    /// Time window in days (optional).
77    pub days: Option<u32>,
78    /// If true, show what would be consolidated without making changes.
79    pub dry_run: Option<bool>,
80    /// Minimum number of memories required to form a group (optional).
81    pub min_memories: Option<usize>,
82    /// Similarity threshold 0.0-1.0 for grouping memories (optional).
83    pub similarity: Option<f32>,
84}
85
86/// Arguments for the get summary tool.
87#[derive(Debug, Deserialize)]
88#[serde(deny_unknown_fields)]
89pub struct GetSummaryArgs {
90    /// ID of the summary memory to retrieve.
91    pub memory_id: String,
92}
93
94/// Arguments for the enrich tool.
95#[derive(Debug, Deserialize)]
96#[serde(deny_unknown_fields)]
97pub struct EnrichArgs {
98    /// ID of the memory to enrich.
99    pub memory_id: String,
100    /// Generate or improve tags (default: true).
101    pub enrich_tags: Option<bool>,
102    /// Restructure content for clarity (default: true).
103    pub enrich_structure: Option<bool>,
104    /// Add inferred context and rationale (default: false).
105    pub add_context: Option<bool>,
106}
107
108/// Arguments for the reindex tool.
109#[derive(Debug, Deserialize)]
110#[serde(deny_unknown_fields)]
111pub struct ReindexArgs {
112    /// Path to git repository (default: current directory).
113    pub repo_path: Option<String>,
114}
115
116// ============================================================================
117// Core CRUD Tool Arguments (Industry Parity: Mem0, Zep, LangMem)
118// ============================================================================
119
120/// Arguments for the get tool.
121///
122/// Direct memory retrieval by ID - a fundamental CRUD operation
123/// present in all major memory systems (Mem0 `get`, Zep `get_memory`).
124#[derive(Debug, Deserialize)]
125#[serde(deny_unknown_fields)]
126pub struct GetArgs {
127    /// Memory ID to retrieve.
128    pub memory_id: String,
129}
130
131/// Arguments for the delete tool.
132///
133/// Soft/hard delete capability matching industry patterns.
134/// Defaults to soft delete (tombstone) for safety, with explicit
135/// hard delete option for permanent removal.
136#[derive(Debug, Deserialize)]
137#[serde(deny_unknown_fields)]
138pub struct DeleteArgs {
139    /// Memory ID to delete.
140    pub memory_id: String,
141    /// If true, permanently delete. If false (default), soft delete (tombstone).
142    #[serde(default)]
143    pub hard: bool,
144}
145
146/// Arguments for the update tool.
147///
148/// Allows updating content and/or tags of an existing memory.
149/// Follows the industry pattern of partial updates (Mem0 `update`,
150/// `LangMem` `update`).
151#[derive(Debug, Deserialize)]
152#[serde(deny_unknown_fields)]
153pub struct UpdateArgs {
154    /// Memory ID to update.
155    pub memory_id: String,
156    /// New content (optional - if not provided, content unchanged).
157    pub content: Option<String>,
158    /// New tags (optional - if not provided, tags unchanged).
159    /// Replaces existing tags entirely when provided.
160    pub tags: Option<Vec<String>>,
161}
162
163/// Arguments for the list tool.
164///
165/// Lists all memories with optional filtering and pagination.
166/// Matches Mem0's `get_all()` and Zep's `list_memories()` patterns.
167#[derive(Debug, Deserialize)]
168#[serde(deny_unknown_fields)]
169pub struct ListArgs {
170    /// GitHub-style filter query (e.g., "ns:decisions tag:rust -tag:test").
171    pub filter: Option<String>,
172    /// Maximum number of results to return (default: 50).
173    pub limit: Option<usize>,
174    /// Offset for pagination (default: 0).
175    pub offset: Option<usize>,
176    /// Filter by user ID (flexible scoping via metadata).
177    pub user_id: Option<String>,
178    /// Filter by agent ID (flexible scoping via metadata).
179    pub agent_id: Option<String>,
180}
181
182/// Arguments for the `delete_all` tool.
183///
184/// Bulk delete memories matching filter criteria.
185/// Implements Mem0's `delete_all()` pattern with dry-run safety.
186#[derive(Debug, Deserialize)]
187#[serde(deny_unknown_fields)]
188pub struct DeleteAllArgs {
189    /// GitHub-style filter query (e.g., "ns:decisions tag:deprecated").
190    /// At least one filter criterion is required for safety.
191    pub filter: Option<String>,
192    /// If true, show what would be deleted without making changes (default: true).
193    #[serde(default = "default_true")]
194    pub dry_run: bool,
195    /// If true, permanently delete. If false (default), soft delete (tombstone).
196    #[serde(default)]
197    pub hard: bool,
198    /// Filter by user ID for scoped deletion.
199    pub user_id: Option<String>,
200}
201
202/// Arguments for the restore tool.
203///
204/// Restores a tombstoned (soft-deleted) memory.
205/// Implements the inverse of soft delete for data recovery.
206#[derive(Debug, Deserialize)]
207#[serde(deny_unknown_fields)]
208pub struct RestoreArgs {
209    /// Memory ID to restore.
210    pub memory_id: String,
211}
212
213/// Arguments for the history tool.
214///
215/// Retrieves change history for a memory by querying the event log.
216/// Provides audit trail visibility without storing full version snapshots.
217#[derive(Debug, Deserialize)]
218#[serde(deny_unknown_fields)]
219pub struct HistoryArgs {
220    /// Memory ID to get history for.
221    pub memory_id: String,
222    /// Maximum number of events to return (default: 20).
223    pub limit: Option<usize>,
224}
225
226/// Default value helper for `dry_run` (defaults to true for safety).
227const fn default_true() -> bool {
228    true
229}
230
231// ============================================================================
232// Graph / Knowledge Graph Tool Arguments
233// ============================================================================
234
235/// Arguments for the entities tool.
236///
237/// Provides CRUD and advanced operations for entities in the knowledge graph.
238/// Actions: create, get, list, delete, extract, merge.
239#[derive(Debug, Deserialize)]
240#[serde(deny_unknown_fields)]
241pub struct EntitiesArgs {
242    /// Operation to perform: create, get, list, delete, extract, merge.
243    pub action: String,
244    /// Entity ID (required for get/delete, `find_duplicates` sub-action of merge).
245    pub entity_id: Option<String>,
246    /// Entity name (required for create).
247    pub name: Option<String>,
248    /// Type of entity: Person, Organization, Technology, Concept, File.
249    pub entity_type: Option<String>,
250    /// Alternative names for the entity.
251    pub aliases: Option<Vec<String>>,
252    /// Maximum results for list operation (default: 20).
253    pub limit: Option<usize>,
254    // --- Fields for `extract` action ---
255    /// Text content to extract entities from (for extract action).
256    pub content: Option<String>,
257    /// Whether to store extracted entities in the graph (for extract/merge, default: false).
258    #[serde(default)]
259    pub store: bool,
260    /// Optional memory ID to link extracted entities to (for extract action).
261    pub memory_id: Option<String>,
262    /// Minimum confidence threshold 0.0-1.0 (for extract action, default: 0.5).
263    pub min_confidence: Option<f32>,
264    // --- Fields for `merge` action ---
265    /// Sub-action for merge: `find_duplicates`, `merge` (for merge action).
266    pub merge_action: Option<String>,
267    /// Entity IDs to merge (for merge action, minimum 2).
268    pub entity_ids: Option<Vec<String>>,
269    /// Name for the merged entity (for merge action).
270    pub canonical_name: Option<String>,
271    /// Similarity threshold for finding duplicates 0.0-1.0 (for merge action, default: 0.7).
272    pub threshold: Option<f32>,
273}
274
275/// Arguments for the relationships tool.
276///
277/// Provides CRUD and inference operations for relationships between entities.
278/// Actions: create, get, list, delete, infer.
279#[derive(Debug, Deserialize)]
280#[serde(deny_unknown_fields)]
281pub struct RelationshipsArgs {
282    /// Operation to perform: create, get, list, delete, infer.
283    pub action: String,
284    /// Source entity ID (required for create).
285    pub from_entity: Option<String>,
286    /// Target entity ID (required for create).
287    pub to_entity: Option<String>,
288    /// Type of relationship.
289    pub relationship_type: Option<String>,
290    /// Entity ID to get relationships for (for get/list).
291    pub entity_id: Option<String>,
292    /// Relationship direction: outgoing, incoming, both.
293    pub direction: Option<String>,
294    /// Maximum results (default: 20).
295    pub limit: Option<usize>,
296    // --- Fields for `infer` action ---
297    /// Entity IDs to analyze for relationships (for infer action).
298    pub entity_ids: Option<Vec<String>>,
299    /// Whether to store inferred relationships (for infer action, default: false).
300    #[serde(default)]
301    pub store: bool,
302    /// Minimum confidence threshold 0.0-1.0 (for infer action, default: 0.6).
303    pub min_confidence: Option<f32>,
304}
305
306/// Arguments for the graph query tool.
307///
308/// Enables graph traversal operations.
309#[derive(Debug, Deserialize)]
310#[serde(deny_unknown_fields)]
311pub struct GraphQueryArgs {
312    /// Query operation: neighbors, path, stats.
313    pub operation: String,
314    /// Starting entity ID (required for neighbors).
315    pub entity_id: Option<String>,
316    /// Source entity ID (required for path).
317    pub from_entity: Option<String>,
318    /// Target entity ID (required for path).
319    pub to_entity: Option<String>,
320    /// Traversal depth (default: 2, max: 5).
321    pub depth: Option<usize>,
322}
323
324/// Arguments for the extract entities tool.
325///
326/// Extracts entities and relationships from text using LLM.
327#[derive(Debug, Deserialize)]
328#[serde(deny_unknown_fields)]
329pub struct ExtractEntitiesArgs {
330    /// Text content to extract entities from.
331    pub content: String,
332    /// Whether to store extracted entities in the graph (default: false).
333    #[serde(default)]
334    pub store: bool,
335    /// Optional memory ID to link extracted entities to.
336    pub memory_id: Option<String>,
337    /// Minimum confidence threshold (0.0-1.0, default: 0.5).
338    pub min_confidence: Option<f32>,
339}
340
341/// Arguments for the entity merge tool.
342///
343/// Merges duplicate entities into a single canonical entity.
344#[derive(Debug, Deserialize)]
345#[serde(deny_unknown_fields)]
346pub struct EntityMergeArgs {
347    /// Operation: `find_duplicates`, `merge`.
348    pub action: String,
349    /// Entity ID to find duplicates for (for `find_duplicates`).
350    pub entity_id: Option<String>,
351    /// Entity IDs to merge (for merge, minimum 2).
352    pub entity_ids: Option<Vec<String>>,
353    /// Name for the merged entity (required for merge).
354    pub canonical_name: Option<String>,
355    /// Similarity threshold for finding duplicates (0.0-1.0, default: 0.7).
356    pub threshold: Option<f32>,
357}
358
359/// Arguments for the relationship inference tool.
360///
361/// Infers implicit relationships between entities using LLM.
362#[derive(Debug, Deserialize)]
363#[serde(deny_unknown_fields)]
364pub struct RelationshipInferArgs {
365    /// Entity IDs to analyze for relationships.
366    pub entity_ids: Option<Vec<String>>,
367    /// Whether to store inferred relationships (default: false).
368    #[serde(default)]
369    pub store: bool,
370    /// Minimum confidence threshold (0.0-1.0, default: 0.6).
371    pub min_confidence: Option<f32>,
372    /// Maximum entities to analyze if `entity_ids` not provided (default: 50).
373    pub limit: Option<usize>,
374}
375
376/// Arguments for the graph visualize tool.
377///
378/// Generates visual representations of the knowledge graph.
379#[derive(Debug, Deserialize)]
380#[serde(deny_unknown_fields)]
381pub struct GraphVisualizeArgs {
382    /// Output format: mermaid, dot, ascii.
383    pub format: Option<String>,
384    /// Center visualization on this entity.
385    pub entity_id: Option<String>,
386    /// Depth of relationships to include (default: 2).
387    pub depth: Option<usize>,
388    /// Filter to specific entity types.
389    pub entity_types: Option<Vec<String>>,
390    /// Filter to specific relationship types.
391    pub relationship_types: Option<Vec<String>>,
392    /// Maximum entities to include (default: 50).
393    pub limit: Option<usize>,
394}
395
396/// Arguments for the consolidated `subcog_graph` tool.
397///
398/// Combines graph query and visualization operations.
399/// Operations: neighbors, path, stats, visualize.
400#[derive(Debug, Deserialize)]
401#[serde(deny_unknown_fields)]
402pub struct GraphArgs {
403    /// Operation to perform: neighbors, path, stats, visualize.
404    pub operation: String,
405    /// Starting entity ID (required for neighbors, optional for visualize).
406    pub entity_id: Option<String>,
407    /// Source entity ID (required for path).
408    pub from_entity: Option<String>,
409    /// Target entity ID (required for path).
410    pub to_entity: Option<String>,
411    /// Traversal depth (default: 2, max: 5).
412    pub depth: Option<usize>,
413    // --- Fields for `visualize` operation ---
414    /// Output format for visualize: mermaid, dot, ascii (default: mermaid).
415    pub format: Option<String>,
416    /// Filter to specific entity types (for visualize).
417    pub entity_types: Option<Vec<String>>,
418    /// Filter to specific relationship types (for visualize).
419    pub relationship_types: Option<Vec<String>>,
420    /// Maximum entities to include (default: 50, for visualize).
421    pub limit: Option<usize>,
422}
423
424/// Parses an entity type string to `EntityType` enum.
425pub fn parse_entity_type(s: &str) -> Option<crate::models::graph::EntityType> {
426    use crate::models::graph::EntityType;
427    match s.to_lowercase().as_str() {
428        "person" => Some(EntityType::Person),
429        "organization" => Some(EntityType::Organization),
430        "technology" => Some(EntityType::Technology),
431        "concept" => Some(EntityType::Concept),
432        "file" => Some(EntityType::File),
433        _ => None,
434    }
435}
436
437/// Parses a relationship type string to `RelationshipType` enum.
438pub fn parse_relationship_type(s: &str) -> Option<crate::models::graph::RelationshipType> {
439    use crate::models::graph::RelationshipType;
440    match s {
441        "WorksAt" | "works_at" => Some(RelationshipType::WorksAt),
442        "Created" | "created" => Some(RelationshipType::Created),
443        "Uses" | "uses" => Some(RelationshipType::Uses),
444        "Implements" | "implements" => Some(RelationshipType::Implements),
445        "PartOf" | "part_of" => Some(RelationshipType::PartOf),
446        "RelatesTo" | "relates_to" => Some(RelationshipType::RelatesTo),
447        "MentionedIn" | "mentioned_in" => Some(RelationshipType::MentionedIn),
448        "Supersedes" | "supersedes" => Some(RelationshipType::Supersedes),
449        "ConflictsWith" | "conflicts_with" => Some(RelationshipType::ConflictsWith),
450        _ => None,
451    }
452}
453
454// ============================================================================
455// Prompt Tool Arguments (Consolidated)
456// ============================================================================
457
458/// Arguments for the consolidated `subcog_prompts` tool.
459///
460/// Supports all prompt operations via the `action` field:
461/// - `save`: Save or update a prompt template
462/// - `list`: List prompts with optional filtering
463/// - `get`: Get a prompt by name
464/// - `run`: Execute a prompt with variable substitution
465/// - `delete`: Delete a prompt
466#[derive(Debug, Deserialize)]
467#[serde(deny_unknown_fields)]
468pub struct PromptsArgs {
469    /// Operation to perform: save, list, get, run, delete.
470    pub action: String,
471    /// Prompt name (required for save/get/run/delete).
472    pub name: Option<String>,
473    /// Prompt content with `{{variable}}` placeholders (for save).
474    pub content: Option<String>,
475    /// Path to file containing prompt (alternative to content, for save).
476    pub file_path: Option<String>,
477    /// Human-readable description of the prompt (for save).
478    pub description: Option<String>,
479    /// Tags for categorization and search (for save/list).
480    pub tags: Option<Vec<String>>,
481    /// Storage scope: "project" (default), "user", or "org".
482    pub domain: Option<String>,
483    /// Explicit variable definitions with metadata (for save).
484    pub variables_def: Option<Vec<PromptVariableArg>>,
485    /// Variable values to substitute (for run).
486    pub variables: Option<HashMap<String, String>>,
487    /// Skip LLM-powered metadata enrichment (for save).
488    #[serde(default)]
489    pub skip_enrichment: bool,
490    /// Filter by name pattern (glob-style, for list).
491    pub name_pattern: Option<String>,
492    /// Maximum number of results (for list).
493    pub limit: Option<usize>,
494}
495
496/// Variable definition argument for prompt save.
497#[derive(Debug, Deserialize)]
498#[serde(deny_unknown_fields)]
499pub struct PromptVariableArg {
500    /// Variable name (without braces).
501    pub name: String,
502    /// Human-readable description for elicitation.
503    pub description: Option<String>,
504    /// Default value if not provided.
505    pub default: Option<String>,
506    /// Whether variable is required (default: true).
507    pub required: Option<bool>,
508}
509
510// Legacy prompt argument types (for backward compatibility during transition)
511
512/// Arguments for the prompt.save tool (legacy).
513#[derive(Debug, Deserialize)]
514#[serde(deny_unknown_fields)]
515pub struct PromptSaveArgs {
516    /// Unique prompt name (kebab-case, e.g., "code-review").
517    pub name: String,
518    /// Prompt content with `{{variable}}` placeholders.
519    pub content: Option<String>,
520    /// Path to file containing prompt (alternative to content).
521    pub file_path: Option<String>,
522    /// Human-readable description of the prompt.
523    pub description: Option<String>,
524    /// Tags for categorization and search.
525    pub tags: Option<Vec<String>>,
526    /// Storage scope: "project" (default), "user", or "org".
527    pub domain: Option<String>,
528    /// Explicit variable definitions with metadata.
529    pub variables: Option<Vec<PromptVariableArg>>,
530    /// Skip LLM-powered metadata enrichment.
531    #[serde(default)]
532    pub skip_enrichment: bool,
533}
534
535/// Arguments for the prompt.list tool (legacy).
536#[derive(Debug, Deserialize)]
537#[serde(deny_unknown_fields)]
538pub struct PromptListArgs {
539    /// Filter by domain scope: "project", "user", or "org".
540    pub domain: Option<String>,
541    /// Filter by tags (AND logic - must have all).
542    pub tags: Option<Vec<String>>,
543    /// Filter by name pattern (glob-style, e.g., "code-*").
544    pub name_pattern: Option<String>,
545    /// Maximum number of results (default: 20, max: 100).
546    pub limit: Option<usize>,
547}
548
549/// Arguments for the prompt.get tool (legacy).
550#[derive(Debug, Deserialize)]
551#[serde(deny_unknown_fields)]
552pub struct PromptGetArgs {
553    /// Prompt name to retrieve.
554    pub name: String,
555    /// Domain to search (if not specified, searches Project -> User -> Org).
556    pub domain: Option<String>,
557}
558
559/// Arguments for the prompt.run tool (legacy).
560#[derive(Debug, Deserialize)]
561#[serde(deny_unknown_fields)]
562pub struct PromptRunArgs {
563    /// Prompt name to execute.
564    pub name: String,
565    /// Variable values to substitute (key: value pairs).
566    pub variables: Option<HashMap<String, String>>,
567    /// Domain to search for the prompt.
568    pub domain: Option<String>,
569}
570
571/// Arguments for the prompt.delete tool (legacy).
572#[derive(Debug, Deserialize)]
573#[serde(deny_unknown_fields)]
574pub struct PromptDeleteArgs {
575    /// Prompt name to delete.
576    pub name: String,
577    /// Domain scope to delete from (required for safety).
578    pub domain: String,
579}
580
581// =============================================================================
582// Context Template Arguments (Consolidated)
583// =============================================================================
584
585/// Arguments for the consolidated `subcog_templates` tool.
586///
587/// Supports all context template operations via the `action` field:
588/// - `save`: Save or update a context template
589/// - `list`: List templates with optional filtering
590/// - `get`: Get a template by name
591/// - `render`: Render a template with memories
592/// - `delete`: Delete a template
593#[derive(Debug, Deserialize)]
594#[serde(deny_unknown_fields)]
595pub struct TemplatesArgs {
596    /// Operation to perform: save, list, get, render, delete.
597    pub action: String,
598    /// Template name (required for save/get/render/delete).
599    pub name: Option<String>,
600    /// Template content with `{{variable}}` placeholders (for save).
601    pub content: Option<String>,
602    /// Human-readable description of the template (for save).
603    pub description: Option<String>,
604    /// Tags for categorization and search (for save/list).
605    pub tags: Option<Vec<String>>,
606    /// Storage scope: "project" (default), "user", or "org".
607    pub domain: Option<String>,
608    /// Default output format: "markdown" (default), "json", or "xml" (for save).
609    pub output_format: Option<String>,
610    /// Explicit variable definitions with metadata (for save).
611    pub variables_def: Option<Vec<ContextTemplateVariableArg>>,
612    /// Custom variable values (for render).
613    pub variables: Option<std::collections::HashMap<String, String>>,
614    /// Filter by name pattern (for list).
615    pub name_pattern: Option<String>,
616    /// Maximum results (for list) or memories (for render).
617    pub limit: Option<u32>,
618    /// Specific version (for get/render/delete).
619    pub version: Option<u32>,
620    /// Query string for memory search (for render).
621    pub query: Option<String>,
622    /// Namespaces to filter memories (for render).
623    pub namespaces: Option<Vec<String>>,
624    /// Output format override (for render).
625    pub format: Option<String>,
626}
627
628/// Variable definition argument for context template save.
629#[derive(Debug, Deserialize)]
630#[serde(deny_unknown_fields)]
631pub struct ContextTemplateVariableArg {
632    /// Variable name (without `{{}}`).
633    pub name: String,
634    /// Variable description for documentation.
635    pub description: Option<String>,
636    /// Default value if not provided.
637    pub default: Option<String>,
638    /// Whether the variable is required (default: true).
639    pub required: Option<bool>,
640}
641
642// Legacy context template argument types (for backward compatibility)
643
644/// Arguments for the `context_template_save` tool (legacy).
645#[derive(Debug, Deserialize)]
646#[serde(deny_unknown_fields)]
647pub struct ContextTemplateSaveArgs {
648    /// Unique template name (kebab-case, e.g., "search-results").
649    pub name: String,
650    /// Template content with `{{variable}}` placeholders and `{{#each}}` iteration.
651    pub content: String,
652    /// Human-readable description of the template.
653    pub description: Option<String>,
654    /// Tags for categorization and search.
655    pub tags: Option<Vec<String>>,
656    /// Storage scope: "project" (default), "user", or "org".
657    pub domain: Option<String>,
658    /// Default output format: "markdown" (default), "json", or "xml".
659    pub output_format: Option<String>,
660    /// Explicit variable definitions with metadata.
661    pub variables: Option<Vec<ContextTemplateVariableArg>>,
662}
663
664/// Arguments for the `context_template_list` tool (legacy).
665#[derive(Debug, Deserialize)]
666#[serde(deny_unknown_fields)]
667pub struct ContextTemplateListArgs {
668    /// Filter by domain scope: "project", "user", or "org".
669    pub domain: Option<String>,
670    /// Filter by tags (AND logic - must have all).
671    pub tags: Option<Vec<String>>,
672    /// Filter by name pattern (glob-style, e.g., "search-*").
673    pub name_pattern: Option<String>,
674    /// Maximum number of results (default: 20, max: 100).
675    pub limit: Option<usize>,
676}
677
678/// Arguments for the `context_template_get` tool (legacy).
679#[derive(Debug, Deserialize)]
680#[serde(deny_unknown_fields)]
681pub struct ContextTemplateGetArgs {
682    /// Template name to retrieve.
683    pub name: String,
684    /// Specific version to retrieve (None = latest).
685    pub version: Option<u32>,
686    /// Domain to search (if not specified, searches User -> Project).
687    pub domain: Option<String>,
688}
689
690/// Arguments for the `context_template_render` tool (legacy).
691#[derive(Debug, Deserialize)]
692#[serde(deny_unknown_fields)]
693pub struct ContextTemplateRenderArgs {
694    /// Template name to render.
695    pub name: String,
696    /// Specific version to use (None = latest).
697    pub version: Option<u32>,
698    /// Query string for memory search to populate the template.
699    pub query: Option<String>,
700    /// Maximum memories to include (default: 10).
701    pub limit: Option<u32>,
702    /// Namespaces to filter memories (default: all).
703    pub namespaces: Option<Vec<String>>,
704    /// Custom variable values (key: value pairs).
705    pub variables: Option<std::collections::HashMap<String, String>>,
706    /// Output format override: "markdown", "json", or "xml".
707    pub format: Option<String>,
708}
709
710/// Arguments for the `context_template_delete` tool (legacy).
711#[derive(Debug, Deserialize)]
712#[serde(deny_unknown_fields)]
713pub struct ContextTemplateDeleteArgs {
714    /// Template name to delete.
715    pub name: String,
716    /// Specific version to delete (None = delete all versions).
717    pub version: Option<u32>,
718    /// Domain scope to delete from (required for safety).
719    pub domain: String,
720}
721
722/// Arguments for the `subcog_init` tool.
723#[derive(Debug, Deserialize)]
724#[serde(deny_unknown_fields)]
725pub struct InitArgs {
726    /// Whether to recall project context (default: true).
727    #[serde(default = "default_true")]
728    pub include_recall: bool,
729    /// Custom recall query (default: "project setup OR architecture OR conventions").
730    pub recall_query: Option<String>,
731    /// Maximum memories to recall (default: 5).
732    pub recall_limit: Option<u32>,
733}
734
735// =============================================================================
736// Group Management Arguments (Consolidated, Feature-gated: group-scope)
737// =============================================================================
738
739/// Arguments for the consolidated `subcog_groups` tool.
740///
741/// Supports all group management operations via the `action` field:
742/// - `create`: Create a new group
743/// - `list`: List groups you belong to
744/// - `get`: Get group details including members
745/// - `add_member`: Add a member to a group
746/// - `remove_member`: Remove a member from a group
747/// - `update_role`: Update a member's role
748/// - `delete`: Delete a group
749#[cfg(feature = "group-scope")]
750#[derive(Debug, Deserialize)]
751#[serde(deny_unknown_fields)]
752pub struct GroupsArgs {
753    /// Operation to perform: `create`, `list`, `get`, `add_member`, `remove_member`, `update_role`, `delete`.
754    pub action: String,
755    /// Group ID (required for `get`/`add_member`/`remove_member`/`update_role`/`delete`).
756    pub group_id: Option<String>,
757    /// Group name (required for create).
758    pub name: Option<String>,
759    /// Group description (for create).
760    pub description: Option<String>,
761    /// User ID to add/remove/update (for `add_member/remove_member/update_role`).
762    pub user_id: Option<String>,
763    /// Role for the member: read, write, admin (for `add_member/update_role`).
764    pub role: Option<String>,
765}
766
767/// Parses a namespace string to Namespace enum.
768pub fn parse_namespace(s: &str) -> Namespace {
769    match s.to_lowercase().as_str() {
770        "decisions" => Namespace::Decisions,
771        "patterns" => Namespace::Patterns,
772        "learnings" => Namespace::Learnings,
773        "context" => Namespace::Context,
774        "tech-debt" | "techdebt" => Namespace::TechDebt,
775        "apis" => Namespace::Apis,
776        "config" => Namespace::Config,
777        "security" => Namespace::Security,
778        "performance" => Namespace::Performance,
779        "testing" => Namespace::Testing,
780        _ => Namespace::Decisions,
781    }
782}
783
784/// Parses a search mode string to `SearchMode` enum.
785pub fn parse_search_mode(s: &str) -> SearchMode {
786    match s.to_lowercase().as_str() {
787        "vector" => SearchMode::Vector,
788        "text" => SearchMode::Text,
789        _ => SearchMode::Hybrid,
790    }
791}
792
793/// Parses a domain scope string to `DomainScope` enum.
794pub fn parse_domain_scope(s: Option<&str>) -> DomainScope {
795    match s.map(str::to_lowercase).as_deref() {
796        Some("user") => DomainScope::User,
797        Some("org") => DomainScope::Org,
798        _ => DomainScope::Project,
799    }
800}
801
802/// Converts a `DomainScope` to a display string.
803pub const fn domain_scope_to_display(scope: DomainScope) -> &'static str {
804    match scope {
805        DomainScope::Project => "project",
806        DomainScope::User => "user",
807        DomainScope::Org => "org",
808    }
809}
810
811/// Formats a `PromptVariable` for display.
812pub fn format_variable_info(v: &crate::models::PromptVariable) -> String {
813    let mut info = format!("- **{{{{{}}}}}**", v.name);
814    if let Some(ref desc) = v.description {
815        info.push_str(&format!(": {desc}"));
816    }
817    if let Some(ref default) = v.default {
818        info.push_str(&format!(" (default: `{default}`)"));
819    }
820    if !v.required {
821        info.push_str(" [optional]");
822    }
823    info
824}
825
826/// Finds missing required variables.
827pub fn find_missing_required_variables<'a>(
828    variables: &'a [crate::models::PromptVariable],
829    values: &HashMap<String, String>,
830) -> Vec<&'a str> {
831    variables
832        .iter()
833        .filter(|v| v.required && v.default.is_none() && !values.contains_key(&v.name))
834        .map(|v| v.name.as_str())
835        .collect()
836}
837
838/// Finds the largest valid UTF-8 character boundary at or before `index`.
839///
840/// This is an MSRV-compatible implementation of `str::floor_char_boundary`
841/// (stable since Rust 1.80, but we target 1.86 MSRV).
842///
843/// # Arguments
844///
845/// * `s` - The string to find a boundary in.
846/// * `index` - The byte index to search from (will find boundary at or before).
847///
848/// # Returns
849///
850/// The largest valid character boundary at or before `index`, or 0 if none found.
851fn floor_char_boundary(s: &str, index: usize) -> usize {
852    if index >= s.len() {
853        return s.len();
854    }
855
856    // Find the last character boundary at or before index using char_indices.
857    // char_indices() yields (byte_offset, char) for each character.
858    // We want the largest byte_offset <= index.
859    let mut boundary = 0;
860    for (byte_offset, _) in s.char_indices() {
861        if byte_offset <= index {
862            boundary = byte_offset;
863        } else {
864            break;
865        }
866    }
867    boundary
868}
869
870/// Truncates a string to a maximum length, respecting UTF-8 character boundaries.
871///
872/// This function safely handles multi-byte UTF-8 characters (e.g., degree symbol,
873/// emoji, CJK characters) by finding the nearest valid character boundary.
874///
875/// # Arguments
876///
877/// * `s` - The string to truncate.
878/// * `max_len` - Maximum byte length for the result (including "..." suffix).
879///
880/// # Returns
881///
882/// The original string if it fits, otherwise a truncated version with "..." appended.
883///
884/// # Examples
885///
886/// ```ignore
887/// // ASCII text
888/// assert_eq!(truncate("Hello, world!", 10), "Hello, ...");
889///
890/// // Multi-byte UTF-8 characters (degree symbol is 2 bytes)
891/// assert_eq!(truncate("32 °C temperature", 10), "32 °C ...");
892///
893/// // String shorter than max_len
894/// assert_eq!(truncate("short", 100), "short");
895/// ```
896pub fn truncate(s: &str, max_len: usize) -> String {
897    if s.len() <= max_len {
898        return s.to_string();
899    }
900
901    // Reserve 3 bytes for "..."
902    let target_len = max_len.saturating_sub(3);
903
904    // Find the largest valid character boundary <= target_len
905    let boundary = floor_char_boundary(s, target_len);
906
907    format!("{}...", &s[..boundary])
908}
909
910/// Formats content based on detail level.
911pub fn format_content_for_detail(content: &str, detail: DetailLevel) -> String {
912    if content.is_empty() {
913        return String::new();
914    }
915    match detail {
916        DetailLevel::Light => String::new(),
917        DetailLevel::Medium => format!("\n   {}", truncate(content, 200)),
918        DetailLevel::Everything => format!("\n   {content}"),
919    }
920}
921
922/// Builds a human-readable description of the active filters.
923pub fn build_filter_description(filter: &SearchFilter) -> String {
924    let mut parts = Vec::new();
925
926    if !filter.namespaces.is_empty() {
927        let ns_list: Vec<_> = filter.namespaces.iter().map(Namespace::as_str).collect();
928        parts.push(format!("ns:{}", ns_list.join(",")));
929    }
930
931    if !filter.tags.is_empty() {
932        for tag in &filter.tags {
933            parts.push(format!("tag:{tag}"));
934        }
935    }
936
937    if !filter.tags_any.is_empty() {
938        parts.push(format!("tag:{}", filter.tags_any.join(",")));
939    }
940
941    if !filter.excluded_tags.is_empty() {
942        for tag in &filter.excluded_tags {
943            parts.push(format!("-tag:{tag}"));
944        }
945    }
946
947    if let Some(ref pattern) = filter.source_pattern {
948        parts.push(format!("source:{pattern}"));
949    }
950
951    if let Some(ref project_id) = filter.project_id {
952        parts.push(format!("project:{project_id}"));
953    }
954
955    if let Some(ref branch) = filter.branch {
956        parts.push(format!("branch:{branch}"));
957    }
958
959    if let Some(ref file_path) = filter.file_path {
960        parts.push(format!("path:{file_path}"));
961    }
962
963    if !filter.statuses.is_empty() {
964        let status_list: Vec<_> = filter.statuses.iter().map(MemoryStatus::as_str).collect();
965        parts.push(format!("status:{}", status_list.join(",")));
966    }
967
968    if filter.created_after.is_some() {
969        parts.push("since:active".to_string());
970    }
971
972    if parts.is_empty() {
973        String::new()
974    } else {
975        format!(", filter: {}", parts.join(" "))
976    }
977}
978
979#[cfg(test)]
980mod tests {
981    use super::*;
982
983    // ==========================================================================
984    // MED-SEC-001: Tests for deny_unknown_fields protection
985    // ==========================================================================
986
987    #[test]
988    fn test_capture_args_rejects_unknown_fields() {
989        let json = r#"{"content": "test", "namespace": "decisions", "unknown_field": "bad"}"#;
990        let result: Result<CaptureArgs, _> = serde_json::from_str(json);
991        assert!(result.is_err());
992        assert!(result.unwrap_err().to_string().contains("unknown field"));
993    }
994
995    #[test]
996    fn test_capture_args_accepts_valid_fields() {
997        let json = r#"{"content": "test", "namespace": "decisions", "tags": ["a", "b"]}"#;
998        let result: Result<CaptureArgs, _> = serde_json::from_str(json);
999        assert!(result.is_ok());
1000    }
1001
1002    #[test]
1003    fn test_recall_args_rejects_unknown_fields() {
1004        let json = r#"{"query": "test", "malicious_param": "attack"}"#;
1005        let result: Result<RecallArgs, _> = serde_json::from_str(json);
1006        assert!(result.is_err());
1007    }
1008
1009    #[test]
1010    fn test_consolidate_args_rejects_unknown_fields() {
1011        let json = r#"{"namespaces": ["decisions"], "extra": true}"#;
1012        let result: Result<ConsolidateArgs, _> = serde_json::from_str(json);
1013        assert!(result.is_err());
1014    }
1015
1016    #[test]
1017    fn test_get_summary_args_rejects_unknown_fields() {
1018        let json = r#"{"memory_id": "123", "include_private": true}"#;
1019        let result: Result<GetSummaryArgs, _> = serde_json::from_str(json);
1020        assert!(result.is_err());
1021    }
1022
1023    #[test]
1024    fn test_enrich_args_rejects_unknown_fields() {
1025        let json = r#"{"memory_id": "123", "inject": "payload"}"#;
1026        let result: Result<EnrichArgs, _> = serde_json::from_str(json);
1027        assert!(result.is_err());
1028    }
1029
1030    #[test]
1031    fn test_reindex_args_rejects_unknown_fields() {
1032        let json = r#"{"repo_path": "/path", "delete_all": true}"#;
1033        let result: Result<ReindexArgs, _> = serde_json::from_str(json);
1034        assert!(result.is_err());
1035    }
1036
1037    #[test]
1038    fn test_prompt_save_args_rejects_unknown_fields() {
1039        let json = r#"{"name": "test", "admin_override": true}"#;
1040        let result: Result<PromptSaveArgs, _> = serde_json::from_str(json);
1041        assert!(result.is_err());
1042    }
1043
1044    #[test]
1045    fn test_prompt_variable_arg_rejects_unknown_fields() {
1046        let json = r#"{"name": "var", "execute_code": "rm -rf /"}"#;
1047        let result: Result<PromptVariableArg, _> = serde_json::from_str(json);
1048        assert!(result.is_err());
1049    }
1050
1051    #[test]
1052    fn test_prompt_list_args_rejects_unknown_fields() {
1053        let json = r#"{"domain": "user", "bypass_auth": true}"#;
1054        let result: Result<PromptListArgs, _> = serde_json::from_str(json);
1055        assert!(result.is_err());
1056    }
1057
1058    #[test]
1059    fn test_prompt_get_args_rejects_unknown_fields() {
1060        let json = r#"{"name": "test", "include_secrets": true}"#;
1061        let result: Result<PromptGetArgs, _> = serde_json::from_str(json);
1062        assert!(result.is_err());
1063    }
1064
1065    #[test]
1066    fn test_prompt_run_args_rejects_unknown_fields() {
1067        let json = r#"{"name": "test", "shell_escape": true}"#;
1068        let result: Result<PromptRunArgs, _> = serde_json::from_str(json);
1069        assert!(result.is_err());
1070    }
1071
1072    #[test]
1073    fn test_prompt_delete_args_rejects_unknown_fields() {
1074        let json = r#"{"name": "test", "domain": "user", "recursive": true}"#;
1075        let result: Result<PromptDeleteArgs, _> = serde_json::from_str(json);
1076        assert!(result.is_err());
1077    }
1078
1079    // ==========================================================================
1080    // Core CRUD tools (industry parity: Mem0, Zep, LangMem)
1081    // ==========================================================================
1082
1083    #[test]
1084    fn test_get_args_rejects_unknown_fields() {
1085        let json = r#"{"memory_id": "123", "include_deleted": true}"#;
1086        let result: Result<GetArgs, _> = serde_json::from_str(json);
1087        assert!(result.is_err());
1088        assert!(result.unwrap_err().to_string().contains("unknown field"));
1089    }
1090
1091    #[test]
1092    fn test_get_args_accepts_valid_fields() {
1093        let json = r#"{"memory_id": "abc123"}"#;
1094        let result: Result<GetArgs, _> = serde_json::from_str(json);
1095        assert!(result.is_ok());
1096        assert_eq!(result.unwrap().memory_id, "abc123");
1097    }
1098
1099    #[test]
1100    fn test_delete_args_rejects_unknown_fields() {
1101        let json = r#"{"memory_id": "123", "force": true}"#;
1102        let result: Result<DeleteArgs, _> = serde_json::from_str(json);
1103        assert!(result.is_err());
1104        assert!(result.unwrap_err().to_string().contains("unknown field"));
1105    }
1106
1107    #[test]
1108    fn test_delete_args_accepts_valid_fields() {
1109        let json = r#"{"memory_id": "abc123", "hard": true}"#;
1110        let result: Result<DeleteArgs, _> = serde_json::from_str(json);
1111        assert!(result.is_ok());
1112        let args = result.unwrap();
1113        assert_eq!(args.memory_id, "abc123");
1114        assert!(args.hard);
1115    }
1116
1117    #[test]
1118    fn test_delete_args_defaults_hard_to_false() {
1119        let json = r#"{"memory_id": "abc123"}"#;
1120        let result: Result<DeleteArgs, _> = serde_json::from_str(json);
1121        assert!(result.is_ok());
1122        let args = result.unwrap();
1123        assert!(!args.hard);
1124    }
1125
1126    #[test]
1127    fn test_update_args_rejects_unknown_fields() {
1128        let json = r#"{"memory_id": "123", "namespace": "decisions"}"#;
1129        let result: Result<UpdateArgs, _> = serde_json::from_str(json);
1130        assert!(result.is_err());
1131        assert!(result.unwrap_err().to_string().contains("unknown field"));
1132    }
1133
1134    #[test]
1135    fn test_update_args_accepts_valid_fields() {
1136        let json = r#"{"memory_id": "abc123", "content": "new content", "tags": ["a", "b"]}"#;
1137        let result: Result<UpdateArgs, _> = serde_json::from_str(json);
1138        assert!(result.is_ok());
1139        let args = result.unwrap();
1140        assert_eq!(args.memory_id, "abc123");
1141        assert_eq!(args.content, Some("new content".to_string()));
1142        assert_eq!(args.tags, Some(vec!["a".to_string(), "b".to_string()]));
1143    }
1144
1145    #[test]
1146    fn test_update_args_allows_partial_updates() {
1147        // Only content
1148        let json = r#"{"memory_id": "abc123", "content": "updated"}"#;
1149        let result: Result<UpdateArgs, _> = serde_json::from_str(json);
1150        assert!(result.is_ok());
1151        let args = result.unwrap();
1152        assert!(args.content.is_some());
1153        assert!(args.tags.is_none());
1154
1155        // Only tags
1156        let json = r#"{"memory_id": "abc123", "tags": ["x"]}"#;
1157        let result: Result<UpdateArgs, _> = serde_json::from_str(json);
1158        assert!(result.is_ok());
1159        let args = result.unwrap();
1160        assert!(args.content.is_none());
1161        assert!(args.tags.is_some());
1162    }
1163
1164    // ==========================================================================
1165    // UTF-8 safe truncation tests
1166    // ==========================================================================
1167
1168    #[test]
1169    fn test_truncate_ascii_short() {
1170        assert_eq!(truncate("short", 100), "short");
1171    }
1172
1173    #[test]
1174    fn test_truncate_ascii_exact() {
1175        assert_eq!(truncate("hello", 5), "hello");
1176    }
1177
1178    #[test]
1179    fn test_truncate_ascii_long() {
1180        assert_eq!(truncate("hello world", 8), "hello...");
1181    }
1182
1183    #[test]
1184    fn test_truncate_degree_symbol() {
1185        // The degree symbol (°) is 2 bytes (U+00B0: 0xC2 0xB0)
1186        // "32 °C" = [51, 50, 32, 194, 176, 67] = 6 bytes
1187        let s = "32 °C temperature";
1188        // With max_len=10, target_len=7, boundary should be at 6 (after °)
1189        let result = truncate(s, 10);
1190        assert!(result.ends_with("..."));
1191        // Should not panic and should contain valid UTF-8
1192        assert!(result.is_ascii() || !result.is_empty());
1193    }
1194
1195    #[test]
1196    fn test_truncate_multi_byte_boundary() {
1197        // Test the exact case from the panic: degree symbol at byte 196-198
1198        let s = "Document 301:\nThe Mallee and upper Wimmera are Victoria's warmest regions with hot winds blowing from nearby semi-deserts. Average temperatures exceed 32 °C (90 °F) during summer and 15 °C (59 °F) in winter...";
1199
1200        // This was panicking at max_len=200 because byte 197 is inside °
1201        let result = truncate(s, 200);
1202        assert!(result.ends_with("..."));
1203        // Verify it's valid UTF-8 (won't compile if not, but good to be explicit)
1204        assert!(!result.is_empty());
1205    }
1206
1207    #[test]
1208    fn test_truncate_emoji() {
1209        // Emoji are 4 bytes each
1210        let s = "Hello 👋 World 🌍 Test";
1211        let result = truncate(s, 15);
1212        assert!(result.ends_with("..."));
1213    }
1214
1215    #[test]
1216    fn test_truncate_cjk() {
1217        // CJK characters are 3 bytes each
1218        let s = "Hello 你好 World";
1219        let result = truncate(s, 12);
1220        assert!(result.ends_with("..."));
1221    }
1222
1223    #[test]
1224    fn test_truncate_empty() {
1225        assert_eq!(truncate("", 10), "");
1226    }
1227
1228    #[test]
1229    fn test_truncate_very_small_max() {
1230        // With max_len=3, we have 0 bytes for content
1231        let result = truncate("hello", 3);
1232        assert_eq!(result, "...");
1233    }
1234
1235    #[test]
1236    fn test_truncate_max_len_zero() {
1237        // Edge case: max_len=0
1238        let result = truncate("hello", 0);
1239        assert_eq!(result, "...");
1240    }
1241
1242    // ==========================================================================
1243    // floor_char_boundary tests
1244    // ==========================================================================
1245
1246    #[test]
1247    fn test_floor_char_boundary_ascii() {
1248        let s = "hello";
1249        assert_eq!(floor_char_boundary(s, 0), 0);
1250        assert_eq!(floor_char_boundary(s, 2), 2);
1251        assert_eq!(floor_char_boundary(s, 5), 5);
1252        assert_eq!(floor_char_boundary(s, 10), 5); // beyond end
1253    }
1254
1255    #[test]
1256    fn test_floor_char_boundary_multi_byte() {
1257        // "°" is bytes 0..2 (2 bytes)
1258        let s = "°C";
1259        assert_eq!(floor_char_boundary(s, 0), 0);
1260        assert_eq!(floor_char_boundary(s, 1), 0); // inside °, floor to 0
1261        assert_eq!(floor_char_boundary(s, 2), 2); // at C
1262        assert_eq!(floor_char_boundary(s, 3), 3); // end
1263    }
1264
1265    #[test]
1266    fn test_floor_char_boundary_emoji() {
1267        // "👋" is 4 bytes
1268        let s = "a👋b";
1269        assert_eq!(floor_char_boundary(s, 0), 0); // at 'a'
1270        assert_eq!(floor_char_boundary(s, 1), 1); // at start of emoji
1271        assert_eq!(floor_char_boundary(s, 2), 1); // inside emoji
1272        assert_eq!(floor_char_boundary(s, 3), 1); // inside emoji
1273        assert_eq!(floor_char_boundary(s, 4), 1); // inside emoji
1274        assert_eq!(floor_char_boundary(s, 5), 5); // at 'b'
1275    }
1276
1277    #[test]
1278    fn test_floor_char_boundary_empty() {
1279        assert_eq!(floor_char_boundary("", 0), 0);
1280        assert_eq!(floor_char_boundary("", 5), 0);
1281    }
1282}