1pub mod watcher;
2
3use std::collections::{HashMap, HashSet};
4use std::path::PathBuf;
5use std::str::FromStr;
6use std::sync::Arc;
7
8use liwe::find::{DocumentFinder, FindOptions, FindOutput};
9use liwe::query::cli::parse_projection;
10use liwe::query::{self, Filter, InclusionAnchor, ProjectionMode};
11use liwe::retrieve::{DocumentReader, RetrieveOptions, RetrieveOutput};
12use liwe::stats::{GraphStatistics, KeyStatistics};
13use liwe::fs::{new_for_path, new_from_hashmap};
14use liwe::graph::{Graph, GraphContext};
15use chrono::Local;
16use liwe::model::config::{ActionDefinition, CompletionOptions, Configuration, MarkdownOptions, NoteTemplate, DEFAULT_KEY_DATE_FORMAT};
17use liwe::model::node::{Node, NodeIter, NodePointer, Reference, ReferenceType};
18use liwe::model::tree::{Tree, TreeIter};
19use liwe::model::Key;
20use liwe::operations::{
21 delete as op_delete, extract as op_extract, inline as op_inline, rename as op_rename, Changes,
22 ExtractConfig, InlineConfig, OperationError,
23};
24use minijinja::{context, Environment};
25use rmcp::handler::server::router::prompt::PromptRouter;
26use rmcp::handler::server::router::tool::ToolRouter;
27use rmcp::handler::server::wrapper::Parameters;
28use rmcp::model::*;
29use rmcp::schemars::JsonSchema;
30use rmcp::service::RequestContext;
31use rmcp::{prompt, prompt_handler, prompt_router, tool, tool_router, RoleServer};
32use rmcp::{ErrorData as McpError, ServerHandler, tool_handler};
33use serde::{Deserialize, Serialize};
34use tokio::sync::Mutex;
35
36fn to_json_result<T: Serialize>(output: &T) -> Result<CallToolResult, McpError> {
37 let json =
38 serde_json::to_string(output).map_err(|e| McpError::internal_error(e.to_string(), None))?;
39 Ok(CallToolResult::success(vec![Content::text(json)]))
40}
41
42fn to_text_result(text: String) -> Result<CallToolResult, McpError> {
43 Ok(CallToolResult::success(vec![Content::text(text)]))
44}
45
46#[derive(Debug, Deserialize, JsonSchema)]
47#[serde(untagged)]
48pub enum KeyDepthParam {
49 Bare(String),
50 Qualified {
51 key: String,
52 depth: Option<u8>,
53 },
54}
55
56impl KeyDepthParam {
57 fn anchor(&self, default_depth: Option<u8>) -> InclusionAnchor {
58 let (key, depth) = match self {
59 KeyDepthParam::Bare(s) => (s.clone(), None),
60 KeyDepthParam::Qualified { key, depth } => (key.clone(), *depth),
61 };
62 let raw = depth.or(default_depth);
63 let max = match raw {
64 None => u32::MAX,
65 Some(0) => u32::MAX,
66 Some(n) => u32::from(n),
67 };
68 InclusionAnchor::with_max(key, max)
69 }
70}
71
72#[derive(Debug, Default, Deserialize, JsonSchema)]
73pub struct SelectorParams {
74 #[schemars(
75 description = "Restrict to candidates that are sub-documents of EVERY listed key (AND). Each entry is either a bare KEY or {key, depth}."
76 )]
77 #[serde(rename = "in", default)]
78 pub in_: Vec<KeyDepthParam>,
79 #[schemars(
80 description = "Restrict to candidates that are sub-documents of AT LEAST ONE listed key (OR)."
81 )]
82 #[serde(default)]
83 pub in_any: Vec<KeyDepthParam>,
84 #[schemars(
85 description = "Exclude candidates that are sub-documents of ANY listed key (NOT)."
86 )]
87 #[serde(default)]
88 pub not_in: Vec<KeyDepthParam>,
89 #[schemars(
90 description = "Default depth for in / in_any / not_in entries that don't specify their own depth. Omit for unbounded."
91 )]
92 #[serde(default)]
93 pub max_depth: Option<u8>,
94}
95
96impl SelectorParams {
97 pub fn is_empty(&self) -> bool {
98 self.in_.is_empty()
99 && self.in_any.is_empty()
100 && self.not_in.is_empty()
101 && self.max_depth.is_none()
102 }
103
104 pub fn to_filter(&self) -> Option<Filter> {
105 if self.is_empty() {
106 return None;
107 }
108 let mut conjuncts: Vec<Filter> = Vec::new();
109 for kd in &self.in_ {
110 conjuncts.push(Filter::IncludedBy(Box::new(kd.anchor(self.max_depth))));
111 }
112 if !self.in_any.is_empty() {
113 conjuncts.push(Filter::Or(
114 self.in_any
115 .iter()
116 .map(|kd| Filter::IncludedBy(Box::new(kd.anchor(self.max_depth))))
117 .collect(),
118 ));
119 }
120 for kd in &self.not_in {
121 conjuncts.push(Filter::Nor(vec![Filter::IncludedBy(Box::new(
122 kd.anchor(self.max_depth),
123 ))]));
124 }
125 Some(Filter::And(conjuncts))
126 }
127}
128
129#[derive(Debug, Deserialize, JsonSchema)]
130pub struct FindParams {
131 #[schemars(description = "Fuzzy search query matching against document title and key")]
132 pub query: Option<String>,
133 #[schemars(description = "Only return documents that reference this key")]
134 pub refs_to: Option<String>,
135 #[schemars(description = "Only return documents referenced by this key")]
136 pub refs_from: Option<String>,
137 #[schemars(description = "Maximum number of results to return")]
138 pub limit: Option<usize>,
139 #[schemars(description = "Replacement projection (e.g. 'title,priority' or 'body=$content,parents=$includedBy'). Mutually exclusive with add_fields.")]
140 pub project: Option<String>,
141 #[schemars(description = "Additive projection: same grammar as project, extends defaults rather than replacing. Mutually exclusive with project.")]
142 pub add_fields: Option<String>,
143 #[serde(flatten)]
144 pub selector: SelectorParams,
145}
146
147impl TryFrom<FindParams> for FindOptions {
148 type Error = McpError;
149
150 fn try_from(p: FindParams) -> Result<Self, Self::Error> {
151 let project = match (p.project.as_deref(), p.add_fields.as_deref()) {
152 (Some(_), Some(_)) => {
153 return Err(McpError::invalid_params(
154 "project and add_fields are mutually exclusive".to_string(),
155 None,
156 ))
157 }
158 (Some(s), None) => Some(
159 parse_projection(s, ProjectionMode::Replace)
160 .map_err(|e| McpError::invalid_params(e, None))?,
161 ),
162 (None, Some(s)) => Some(
163 parse_projection(s, ProjectionMode::Extend)
164 .map_err(|e| McpError::invalid_params(e, None))?,
165 ),
166 (None, None) => None,
167 };
168 Ok(FindOptions {
169 query: p.query,
170 refs_to: p.refs_to.map(|k| Key::name(&k)),
171 refs_from: p.refs_from.map(|k| Key::name(&k)),
172 filter: p.selector.to_filter(),
173 limit: p.limit,
174 sort: None,
175 project,
176 })
177 }
178}
179
180#[derive(Debug, Deserialize, JsonSchema)]
181pub struct RetrieveParams {
182 #[schemars(description = "Document keys to retrieve. Can be empty when a structural selector is provided.")]
183 #[serde(default)]
184 pub keys: Vec<String>,
185 #[schemars(description = "Levels of block references to expand (0 = document only, 1 = include direct sub-documents). Default: 1")]
186 pub depth: Option<u8>,
187 #[schemars(description = "Levels of parent documents to include. Default: 1")]
188 pub context: Option<u8>,
189 #[schemars(description = "Include inline-linked documents. Default: false")]
190 pub links: Option<bool>,
191 #[schemars(description = "Include incoming inline references. Default: true")]
192 pub backlinks: Option<bool>,
193 #[schemars(description = "Document keys to exclude from results")]
194 pub exclude: Option<Vec<String>>,
195 #[schemars(description = "Return metadata only without document content. Default: false")]
196 pub no_content: Option<bool>,
197 #[schemars(description = "Populate the `includes` array with child document edges. Default: false")]
198 pub children: Option<bool>,
199 #[serde(flatten)]
200 pub selector: SelectorParams,
201}
202
203impl From<RetrieveParams> for RetrieveOptions {
204 fn from(p: RetrieveParams) -> Self {
205 RetrieveOptions {
206 depth: p.depth.unwrap_or(1),
207 context: p.context.unwrap_or(1),
208 links: p.links.unwrap_or(false),
209 backlinks: p.backlinks.unwrap_or(true),
210 exclude: p
211 .exclude
212 .unwrap_or_default()
213 .into_iter()
214 .map(|k| Key::name(&k))
215 .collect::<HashSet<_>>(),
216 no_content: p.no_content.unwrap_or(false),
217 children: p.children.unwrap_or(false),
218 filter: p.selector.to_filter(),
219 }
220 }
221}
222
223#[derive(Debug, Deserialize, JsonSchema)]
224pub struct TreeParams {
225 #[schemars(description = "Starting document keys. If empty and no selector, shows all root documents.")]
226 pub keys: Option<Vec<String>>,
227 #[schemars(description = "Maximum traversal depth. Default: 4")]
228 pub depth: Option<u8>,
229 #[serde(flatten)]
230 pub selector: SelectorParams,
231}
232
233#[derive(Debug, Serialize)]
234struct TreeNode {
235 key: String,
236 title: String,
237 children: Vec<TreeNode>,
238}
239
240#[derive(Debug, Deserialize, JsonSchema)]
241pub struct StatsParams {
242 #[schemars(description = "Document key for per-document stats. Omit for aggregate graph statistics")]
243 pub key: Option<String>,
244}
245
246#[derive(Debug, Deserialize, JsonSchema)]
247pub struct SquashParams {
248 #[schemars(description = "Root document key to expand")]
249 pub key: String,
250 #[schemars(description = "Levels of references to expand. Default: 2")]
251 pub depth: Option<u8>,
252}
253
254#[derive(Debug, Deserialize, JsonSchema)]
255pub struct CreateParams {
256 #[schemars(description = "Document title")]
257 pub title: String,
258 #[schemars(description = "Markdown content body (without the title heading)")]
259 pub content: Option<String>,
260}
261
262#[derive(Debug, Deserialize, JsonSchema)]
263pub struct UpdateParams {
264 #[schemars(description = "Document key to update")]
265 pub key: String,
266 #[schemars(description = "New full markdown content")]
267 pub content: String,
268}
269
270#[derive(Debug, Deserialize, JsonSchema)]
271pub struct DeleteParams {
272 #[schemars(description = "Document key to delete")]
273 pub key: String,
274 #[schemars(description = "Preview changes without applying. Default: false")]
275 pub dry_run: Option<bool>,
276}
277
278#[derive(Debug, Deserialize, JsonSchema)]
279pub struct RenameParams {
280 #[schemars(description = "Current document key")]
281 pub old_key: String,
282 #[schemars(description = "New document key")]
283 pub new_key: String,
284 #[schemars(description = "Preview changes without applying. Default: false")]
285 pub dry_run: Option<bool>,
286}
287
288#[derive(Debug, Serialize)]
289struct ChangesOutput {
290 creates: Vec<ChangeEntry>,
291 updates: Vec<ChangeEntry>,
292 removes: Vec<String>,
293}
294
295#[derive(Debug, Serialize)]
296struct ChangeEntry {
297 key: String,
298 content: String,
299}
300
301impl From<&Changes> for ChangesOutput {
302 fn from(c: &Changes) -> Self {
303 ChangesOutput {
304 creates: c.creates.iter().map(|(k, v)| ChangeEntry { key: k.to_string(), content: v.clone() }).collect(),
305 updates: c.updates.iter().map(|(k, v)| ChangeEntry { key: k.to_string(), content: v.clone() }).collect(),
306 removes: c.removes.iter().map(|k| k.to_string()).collect(),
307 }
308 }
309}
310
311#[derive(Debug, Deserialize, JsonSchema)]
312pub struct ExtractParams {
313 #[schemars(description = "Source document key")]
314 pub key: String,
315 #[schemars(description = "Section title to extract (case-insensitive partial match)")]
316 pub section: Option<String>,
317 #[schemars(description = "Block number to extract (1-indexed, use list mode to discover)")]
318 pub block: Option<usize>,
319 #[schemars(description = "List all sections with block numbers instead of extracting. Default: false")]
320 pub list: Option<bool>,
321 #[schemars(description = "Preview changes without applying. Default: false")]
322 pub dry_run: Option<bool>,
323}
324
325#[derive(Debug, Deserialize, JsonSchema)]
326pub struct InlineParams {
327 #[schemars(description = "Document key containing the block reference")]
328 pub key: String,
329 #[schemars(description = "Reference key or title to inline (partial match)")]
330 pub reference: Option<String>,
331 #[schemars(description = "Block number to inline (1-indexed, use list mode to discover)")]
332 pub block: Option<usize>,
333 #[schemars(description = "List all block references instead of inlining. Default: false")]
334 pub list: Option<bool>,
335 #[schemars(description = "Inline as blockquote instead of section. Default: false")]
336 pub as_quote: Option<bool>,
337 #[schemars(description = "Keep the target document after inlining. Default: false")]
338 pub keep_target: Option<bool>,
339 #[schemars(description = "Preview changes without applying. Default: false")]
340 pub dry_run: Option<bool>,
341}
342
343#[derive(Debug, Serialize)]
344struct SectionEntry {
345 block_number: usize,
346 title: String,
347}
348
349#[derive(Debug, Serialize)]
350struct ReferenceEntry {
351 block_number: usize,
352 key: String,
353 title: String,
354}
355
356#[derive(Debug, Deserialize, JsonSchema)]
357pub struct AttachParams {
358 #[schemars(description = "Configured attach action(s) to attach to (e.g. 'today'). Pass one or more action names; the source is attached under each resolved target.")]
359 #[serde(default)]
360 pub to: Vec<String>,
361 #[schemars(description = "Document key to attach as a block reference in the target(s)")]
362 pub key: Option<String>,
363 #[schemars(description = "List available attach actions instead of executing. Default: false")]
364 pub list: Option<bool>,
365 #[schemars(description = "Preview changes without applying. Default: false")]
366 pub dry_run: Option<bool>,
367}
368
369#[derive(Debug, Serialize)]
370struct AttachActionEntry {
371 name: String,
372 title: String,
373 target_key: String,
374}
375
376#[derive(Debug, Serialize)]
377struct ConfigResource {
378 markdown: MarkdownOptions,
379 library: LibraryResourceView,
380 completion: CompletionOptions,
381 templates: HashMap<String, NoteTemplate>,
382 actions: Vec<ActionResourceView>,
383}
384
385#[derive(Debug, Serialize)]
386struct LibraryResourceView {
387 date_format: Option<String>,
388 default_template: Option<String>,
389 frontmatter_document_title: Option<String>,
390 locale: Option<String>,
391}
392
393#[derive(Debug, Serialize)]
394struct ActionResourceView {
395 name: String,
396 action_type: String,
397 title: String,
398 #[serde(skip_serializing_if = "Option::is_none")]
399 target_key: Option<String>,
400}
401
402impl ConfigResource {
403 fn from_config(config: &Configuration, server: &IweServer) -> Self {
404 let actions = config
405 .actions
406 .iter()
407 .map(|(name, action)| {
408 let (action_type, title, target_key) = match action {
409 ActionDefinition::Transform(a) => ("transform", a.title.clone(), None),
410 ActionDefinition::Attach(a) => (
411 "attach",
412 a.title.clone(),
413 Some(server.render_key_template(&a.key_template)),
414 ),
415 ActionDefinition::Sort(a) => ("sort", a.title.clone(), None),
416 ActionDefinition::Inline(a) => ("inline", a.title.clone(), None),
417 ActionDefinition::Extract(a) => ("extract", a.title.clone(), None),
418 ActionDefinition::ExtractAll(a) => ("extract_all", a.title.clone(), None),
419 ActionDefinition::Link(a) => ("link", a.title.clone(), None),
420 };
421 ActionResourceView {
422 name: name.clone(),
423 action_type: action_type.to_string(),
424 title,
425 target_key,
426 }
427 })
428 .collect();
429
430 Self {
431 markdown: config.markdown.clone(),
432 library: LibraryResourceView {
433 date_format: config.library.date_format.clone(),
434 default_template: config.library.default_template.clone(),
435 frontmatter_document_title: config.library.frontmatter_document_title.clone(),
436 locale: config.library.locale.clone(),
437 },
438 completion: config.completion.clone(),
439 templates: config.templates.clone(),
440 actions,
441 }
442 }
443}
444
445fn op_error_to_mcp(e: OperationError) -> McpError {
446 McpError::invalid_params(e.to_string(), None)
447}
448
449#[derive(Debug, Deserialize, JsonSchema)]
450pub struct ReviewPromptArgs {
451 #[schemars(description = "Document key to review")]
452 pub key: String,
453}
454
455#[derive(Debug, Deserialize, JsonSchema)]
456pub struct RefactorPromptArgs {
457 #[schemars(description = "Root document key to analyze for restructuring")]
458 pub key: String,
459}
460
461#[derive(Clone)]
462pub struct IweServer {
463 graph: Arc<Mutex<Graph>>,
464 base_path: Option<PathBuf>,
465 config: Configuration,
466 tool_router: ToolRouter<IweServer>,
467 prompt_router: PromptRouter<IweServer>,
468}
469
470#[tool_router]
471impl IweServer {
472 #[tool(description = "Search and discover documents in the knowledge graph. Supports fuzzy text query (`query`), root filter (`roots`), direct-reference filters (`refs_to`, `refs_from`), and the structural set selector (`in` / `in_any` / `not_in` / `max_depth`) for transitive sub-document AND/OR/NOT queries with configurable depth.")]
473 async fn iwe_find(
474 &self,
475 Parameters(params): Parameters<FindParams>,
476 ) -> Result<CallToolResult, McpError> {
477 let options: FindOptions = params.try_into()?;
478 let graph = self.graph.lock().await;
479 let finder = DocumentFinder::new(&graph);
480 let output: FindOutput = finder.find(&options);
481 to_json_result(&output.results)
482 }
483
484 #[tool(description = "Retrieve documents from the knowledge graph with configurable depth expansion, parent context, backlinks, and linked documents")]
485 async fn iwe_retrieve(
486 &self,
487 Parameters(params): Parameters<RetrieveParams>,
488 ) -> Result<CallToolResult, McpError> {
489 let graph = self.graph.lock().await;
490 let reader = DocumentReader::new(&graph);
491 let keys: Vec<Key> = params.keys.iter().map(|k| Key::name(k)).collect();
492 let options: RetrieveOptions = params.into();
493 let output: RetrieveOutput = reader.retrieve_many(&keys, &options);
494 to_json_result(&output.documents)
495 }
496
497 #[tool(description = "View the hierarchical tree structure of the knowledge graph showing how documents are connected via block references. Supports the structural set selector (in / in_any / not_in / max_depth) — when provided, the tree roots are restricted to (or selected from) that set.")]
498 async fn iwe_tree(
499 &self,
500 Parameters(params): Parameters<TreeParams>,
501 ) -> Result<CallToolResult, McpError> {
502 let graph = self.graph.lock().await;
503
504 let filter = params.selector.to_filter();
505 let explicit_keys: Vec<Key> = params
506 .keys
507 .filter(|k| !k.is_empty())
508 .map(|ks| ks.iter().map(|k| Key::name(k)).collect())
509 .unwrap_or_default();
510
511 let root_keys: Vec<Key> = if let Some(f) = filter {
512 let selector_set: HashSet<Key> =
513 query::evaluate(&f, &graph).into_iter().collect();
514 if explicit_keys.is_empty() {
515 let mut v: Vec<Key> = selector_set.into_iter().collect();
516 v.sort();
517 v
518 } else {
519 explicit_keys
520 .into_iter()
521 .filter(|k| selector_set.contains(k))
522 .collect()
523 }
524 } else if !explicit_keys.is_empty() {
525 explicit_keys
526 } else {
527 let paths = graph.paths();
528 let mut keys: Vec<Key> = paths
529 .iter()
530 .filter(|n| n.ids().len() == 1)
531 .filter_map(|n| n.first_id())
532 .map(|id| (&*graph).node(id).node_key())
533 .collect();
534 keys.sort();
535 keys.dedup();
536 keys
537 };
538
539 let max_depth = params.depth.unwrap_or(4);
540 let mut trees: Vec<TreeNode> = Vec::new();
541 for root_key in &root_keys {
542 let mut visited: HashSet<Key> = HashSet::new();
543 if let Some(node) = build_tree_node(&graph, root_key, max_depth, &mut visited) {
544 trees.push(node);
545 }
546 }
547 to_json_result(&trees)
548 }
549
550 #[tool(description = "Get comprehensive statistics about the knowledge graph including document counts, reference patterns, broken links, and most connected documents")]
551 async fn iwe_stats(
552 &self,
553 Parameters(params): Parameters<StatsParams>,
554 ) -> Result<CallToolResult, McpError> {
555 let graph = self.graph.lock().await;
556 if let Some(key) = params.key {
557 let all_stats = KeyStatistics::from_graph(&graph);
558 let stat = all_stats
559 .into_iter()
560 .find(|s| s.key == key)
561 .ok_or_else(|| {
562 McpError::invalid_params(format!("Document '{}' not found", key), None)
563 })?;
564 to_json_result(&stat)
565 } else {
566 let stats = GraphStatistics::from_graph(&graph);
567 to_json_result(&stats)
568 }
569 }
570
571 #[tool(description = "Expand all block references into a single flat markdown document. Useful for export or generating a complete view of a document tree")]
572 async fn iwe_squash(
573 &self,
574 Parameters(params): Parameters<SquashParams>,
575 ) -> Result<CallToolResult, McpError> {
576 let graph = self.graph.lock().await;
577 let key = Key::name(¶ms.key);
578 let depth = params.depth.unwrap_or(2);
579
580 if (&*graph).get_node_id(&key).is_none() {
581 return Err(McpError::invalid_params(
582 format!("Document '{}' not found", params.key),
583 None,
584 ));
585 }
586
587 let squashed: Tree = (&*graph).squash(&key, depth);
588 let mut patch = Graph::new();
589 patch.build_key_from_iter(&key, TreeIter::new(&squashed));
590 let content = patch.export_key(&key).unwrap_or_default();
591 to_text_result(content)
592 }
593
594 #[tool(description = "Create a new document in the knowledge graph from a title and optional content")]
595 async fn iwe_create(
596 &self,
597 Parameters(params): Parameters<CreateParams>,
598 ) -> Result<CallToolResult, McpError> {
599 let slug = params
600 .title
601 .to_lowercase()
602 .chars()
603 .map(|c| if c.is_alphanumeric() { c } else { '-' })
604 .collect::<String>()
605 .split('-')
606 .filter(|s| !s.is_empty())
607 .collect::<Vec<_>>()
608 .join("-");
609
610 let content_body = params.content.unwrap_or_default();
611 let markdown = if content_body.is_empty() {
612 format!("# {}\n", params.title)
613 } else {
614 format!("# {}\n\n{}\n", params.title, content_body)
615 };
616
617 let key = Key::name(&slug);
618 let mut graph = self.graph.lock().await;
619
620 if (&*graph).get_node_id(&key).is_some() {
621 return Err(McpError::invalid_params(
622 format!("Document '{}' already exists", slug),
623 None,
624 ));
625 }
626
627 graph.insert_document(key.clone(), markdown.clone());
628 self.write_file(&key, &markdown);
629
630 #[derive(Serialize)]
631 struct CreateResult {
632 key: String,
633 }
634 to_json_result(&CreateResult {
635 key: slug,
636 })
637 }
638
639 #[tool(description = "Update the full markdown content of an existing document")]
640 async fn iwe_update(
641 &self,
642 Parameters(params): Parameters<UpdateParams>,
643 ) -> Result<CallToolResult, McpError> {
644 let key = Key::name(¶ms.key);
645 let mut graph = self.graph.lock().await;
646
647 if (&*graph).get_node_id(&key).is_none() {
648 return Err(McpError::invalid_params(
649 format!("Document '{}' not found", params.key),
650 None,
651 ));
652 }
653
654 let previous_title = (&*graph)
655 .get_key_title(&key)
656 .unwrap_or_else(|| params.key.clone());
657
658 graph.update_document(key.clone(), params.content.clone());
659 self.write_file(&key, ¶ms.content);
660
661 let new_title = (&*graph)
662 .get_key_title(&key)
663 .unwrap_or_else(|| params.key.clone());
664
665 #[derive(Serialize)]
666 struct UpdateResult {
667 key: String,
668 previous_title: String,
669 new_title: String,
670 }
671 to_json_result(&UpdateResult {
672 key: params.key,
673 previous_title,
674 new_title,
675 })
676 }
677
678 #[tool(description = "Delete a document from the knowledge graph. All block references and inline links to this document in other documents are cleaned up")]
679 async fn iwe_delete(
680 &self,
681 Parameters(params): Parameters<DeleteParams>,
682 ) -> Result<CallToolResult, McpError> {
683 let key = Key::name(¶ms.key);
684 let mut graph = self.graph.lock().await;
685 let changes = op_delete(&graph, &key).map_err(op_error_to_mcp)?;
686
687 if !params.dry_run.unwrap_or(false) {
688 Self::apply_changes(&mut graph, &changes);
689 self.write_changes(&changes);
690 }
691
692 to_json_result(&ChangesOutput::from(&changes))
693 }
694
695 #[tool(description = "Rename a document key. All block references and inline links across the entire graph are updated to point to the new key")]
696 async fn iwe_rename(
697 &self,
698 Parameters(params): Parameters<RenameParams>,
699 ) -> Result<CallToolResult, McpError> {
700 let old_key = Key::name(¶ms.old_key);
701 let new_key = Key::name(¶ms.new_key);
702 let mut graph = self.graph.lock().await;
703 let changes = op_rename(&graph, &old_key, &new_key).map_err(op_error_to_mcp)?;
704
705 if !params.dry_run.unwrap_or(false) {
706 Self::apply_changes(&mut graph, &changes);
707 self.write_changes(&changes);
708 }
709
710 to_json_result(&ChangesOutput::from(&changes))
711 }
712
713 #[tool(description = "Extract a section from a document into a new standalone document. The original section is replaced with a block reference. Use list mode to discover sections first")]
714 async fn iwe_extract(
715 &self,
716 Parameters(params): Parameters<ExtractParams>,
717 ) -> Result<CallToolResult, McpError> {
718 let source_key = Key::name(¶ms.key);
719 let mut graph = self.graph.lock().await;
720
721 if (&*graph).get_node_id(&source_key).is_none() {
722 return Err(McpError::invalid_params(
723 format!("Document '{}' not found", params.key),
724 None,
725 ));
726 }
727
728 let tree = (&*graph).collect(&source_key);
729 let sections = collect_sections(&tree);
730
731 if params.list.unwrap_or(false) {
732 return to_json_result(§ions);
733 }
734
735 let selected = if let Some(ref title) = params.section {
736 let matches: Vec<_> = sections
737 .iter()
738 .filter(|s| s.title.to_lowercase().contains(&title.to_lowercase()))
739 .collect();
740 if matches.is_empty() {
741 return Err(McpError::invalid_params(
742 format!("No section matches '{}'", title),
743 None,
744 ));
745 }
746 if matches.len() > 1 {
747 return Err(McpError::invalid_params(
748 format!(
749 "Multiple sections match '{}': {}",
750 title,
751 matches.iter().map(|s| s.title.as_str()).collect::<Vec<_>>().join(", ")
752 ),
753 None,
754 ));
755 }
756 matches[0].block_number
757 } else if let Some(block) = params.block {
758 if block == 0 || block > sections.len() {
759 return Err(McpError::invalid_params(
760 format!("Block number {} out of range (1-{})", block, sections.len()),
761 None,
762 ));
763 }
764 block
765 } else {
766 return Err(McpError::invalid_params(
767 "Must specify section, block, or list",
768 None,
769 ));
770 };
771
772 let section_id = tree
773 .children
774 .iter()
775 .flat_map(|c| collect_section_ids(c))
776 .nth(selected - 1)
777 .ok_or_else(|| McpError::invalid_params("Section not found", None))?;
778
779 let config = ExtractConfig::default();
780 let changes = op_extract(&graph, &source_key, section_id, &config, std::time::SystemTime::now()).map_err(op_error_to_mcp)?;
781
782 if !params.dry_run.unwrap_or(false) {
783 Self::apply_changes(&mut graph, &changes);
784 self.write_changes(&changes);
785 }
786
787 to_json_result(&ChangesOutput::from(&changes))
788 }
789
790 #[tool(description = "Replace a block reference with the actual content of the referenced document. Use list mode to discover block references first")]
791 async fn iwe_inline(
792 &self,
793 Parameters(params): Parameters<InlineParams>,
794 ) -> Result<CallToolResult, McpError> {
795 let source_key = Key::name(¶ms.key);
796 let mut graph = self.graph.lock().await;
797
798 if (&*graph).get_node_id(&source_key).is_none() {
799 return Err(McpError::invalid_params(
800 format!("Document '{}' not found", params.key),
801 None,
802 ));
803 }
804
805 let tree = (&*graph).collect(&source_key);
806 let refs = collect_block_refs(&tree);
807
808 if params.list.unwrap_or(false) {
809 return to_json_result(&refs);
810 }
811
812 let selected = if let Some(ref reference) = params.reference {
813 let matches: Vec<_> = refs
814 .iter()
815 .filter(|r| {
816 r.title.to_lowercase().contains(&reference.to_lowercase())
817 || r.key.to_lowercase().contains(&reference.to_lowercase())
818 })
819 .collect();
820 if matches.is_empty() {
821 return Err(McpError::invalid_params(
822 format!("No reference matches '{}'", reference),
823 None,
824 ));
825 }
826 if matches.len() > 1 {
827 return Err(McpError::invalid_params(
828 format!(
829 "Multiple references match '{}': {}",
830 reference,
831 matches.iter().map(|r| r.key.as_str()).collect::<Vec<_>>().join(", ")
832 ),
833 None,
834 ));
835 }
836 matches[0].block_number
837 } else if let Some(block) = params.block {
838 if block == 0 || block > refs.len() {
839 return Err(McpError::invalid_params(
840 format!("Block number {} out of range (1-{})", block, refs.len()),
841 None,
842 ));
843 }
844 block
845 } else {
846 return Err(McpError::invalid_params(
847 "Must specify reference, block, or list",
848 None,
849 ));
850 };
851
852 let ref_id = collect_ref_ids(&tree)
853 .into_iter()
854 .nth(selected - 1)
855 .ok_or_else(|| McpError::invalid_params("Reference not found", None))?;
856
857 let inline_type = if params.as_quote.unwrap_or(false) {
858 liwe::model::config::InlineType::Quote
859 } else {
860 liwe::model::config::InlineType::Section
861 };
862
863 let config = InlineConfig {
864 inline_type,
865 keep_target: params.keep_target.unwrap_or(false),
866 };
867
868 let changes = op_inline(&graph, &source_key, ref_id, &config).map_err(op_error_to_mcp)?;
869
870 if !params.dry_run.unwrap_or(false) {
871 Self::apply_changes(&mut graph, &changes);
872 self.write_changes(&changes);
873 }
874
875 to_json_result(&ChangesOutput::from(&changes))
876 }
877
878 #[tool(description = "Normalize all document formatting across the knowledge graph. Re-parses and re-writes all documents to ensure consistent formatting")]
879 async fn iwe_normalize(&self) -> Result<CallToolResult, McpError> {
880 let mut graph = self.graph.lock().await;
881 let state = graph.export();
882 let original_count = state.len();
883
884 let mut changed = 0usize;
885 for (key_str, original_content) in &state {
886 let key = Key::name(key_str);
887 let new_content = graph.to_markdown(&key);
888 if new_content != *original_content {
889 graph.update_document(key.clone(), new_content.clone());
890 self.write_file(&key, &new_content);
891 changed += 1;
892 }
893 }
894
895 #[derive(Serialize)]
896 struct NormalizeResult {
897 total: usize,
898 normalized: usize,
899 }
900 to_json_result(&NormalizeResult {
901 total: original_count,
902 normalized: changed,
903 })
904 }
905
906 #[tool(description = "Attach a document as a block reference in one or more target documents determined by configured attach actions. Each target key is derived from the action's key_template (e.g. daily/{{today}}). The `to` field accepts a list of action names; the source is attached under each resolved target. Targets that already contain the source are silently skipped. Use list mode to discover available attach actions.")]
907 async fn iwe_attach(
908 &self,
909 Parameters(params): Parameters<AttachParams>,
910 ) -> Result<CallToolResult, McpError> {
911 if params.list.unwrap_or(false) {
912 let entries: Vec<AttachActionEntry> = self
913 .config
914 .actions
915 .iter()
916 .filter_map(|(name, action)| {
917 if let ActionDefinition::Attach(attach) = action {
918 Some(AttachActionEntry {
919 name: name.clone(),
920 title: attach.title.clone(),
921 target_key: self.render_key_template(&attach.key_template),
922 })
923 } else {
924 None
925 }
926 })
927 .collect();
928 return to_json_result(&entries);
929 }
930
931 if params.to.is_empty() {
932 return Err(McpError::invalid_params(
933 "'to' is required when not in list mode (pass one or more action names)".to_string(),
934 None,
935 ));
936 }
937 let source_key_str = params.key.as_deref().ok_or_else(|| {
938 McpError::invalid_params("'key' is required when not in list mode".to_string(), None)
939 })?;
940
941 let mut graph = self.graph.lock().await;
942
943 let source_key = Key::name(source_key_str);
944 if (&*graph).get_node_id(&source_key).is_none() {
945 return Err(McpError::invalid_params(
946 format!("Document '{}' not found", source_key_str),
947 None,
948 ));
949 }
950
951 let reference_text = (&*graph)
952 .get_key_title(&source_key)
953 .unwrap_or_else(|| source_key_str.to_string());
954
955 let mut combined = Changes::new();
956 let markdown_options = graph.markdown_options();
957
958 for action_name in ¶ms.to {
959 let attach = match self.config.actions.get(action_name) {
960 Some(ActionDefinition::Attach(a)) => a,
961 Some(_) => {
962 return Err(McpError::invalid_params(
963 format!("Action '{}' is not an attach action", action_name),
964 None,
965 ));
966 }
967 None => {
968 return Err(McpError::invalid_params(
969 format!("Action '{}' not found", action_name),
970 None,
971 ));
972 }
973 };
974
975 let target_key = Key::name(&self.render_key_template(&attach.key_template));
976
977 if (&*graph).get_node_id(&target_key).is_some() {
978 let tree = (&*graph).collect(&target_key);
979 if tree
980 .get_all_inclusion_edge_keys()
981 .contains(&source_key)
982 {
983 continue;
984 }
985 }
986
987 let reference = Tree {
988 id: None,
989 node: Node::Reference(Reference {
990 key: source_key.clone(),
991 text: reference_text.clone(),
992 reference_type: ReferenceType::Regular,
993 }),
994 children: vec![],
995 };
996
997 if (&*graph).get_node_id(&target_key).is_some() {
998 let tree = (&*graph).collect(&target_key);
999 let updated = tree.attach(reference);
1000 combined.add_update(
1001 target_key.clone(),
1002 updated
1003 .iter()
1004 .to_markdown(&target_key.parent(), &markdown_options),
1005 );
1006 } else {
1007 let content = reference
1008 .iter()
1009 .to_markdown(&target_key.parent(), &markdown_options);
1010 let document = self.render_document_template(
1011 &attach.document_template,
1012 &content,
1013 );
1014 combined.add_create(target_key.clone(), document);
1015 }
1016 }
1017
1018 if !params.dry_run.unwrap_or(false) {
1019 Self::apply_changes(&mut graph, &combined);
1020 self.write_changes(&combined);
1021 }
1022
1023 to_json_result(&ChangesOutput::from(&combined))
1024 }
1025}
1026
1027fn build_tree_node(
1028 graph: &Graph,
1029 key: &Key,
1030 max_depth: u8,
1031 visited: &mut HashSet<Key>,
1032) -> Option<TreeNode> {
1033 graph.get_node_id(key)?;
1034
1035 let title = graph.get_ref_text(key).unwrap_or_default();
1036 let key_str = key.to_string();
1037
1038 if visited.contains(key) {
1039 return Some(TreeNode {
1040 key: key_str,
1041 title,
1042 children: vec![],
1043 });
1044 }
1045 visited.insert(key.clone());
1046
1047 let children = if max_depth > 1 {
1048 let ref_node_ids = graph.get_inclusion_edges_in(key);
1049 let mut refs: Vec<Key> = ref_node_ids
1050 .iter()
1051 .filter_map(|id| graph.graph_node(*id).ref_key())
1052 .collect();
1053 refs.sort();
1054 refs.into_iter()
1055 .filter_map(|ref_key| build_tree_node(graph, &ref_key, max_depth - 1, visited))
1056 .collect()
1057 } else {
1058 vec![]
1059 };
1060
1061 Some(TreeNode {
1062 key: key_str,
1063 title,
1064 children,
1065 })
1066}
1067
1068use liwe::model::tree::Tree as ModelTree;
1069use liwe::model::NodeId;
1070
1071fn collect_sections(tree: &ModelTree) -> Vec<SectionEntry> {
1072 let mut result = Vec::new();
1073 collect_sections_rec(tree, &mut result);
1074 result
1075}
1076
1077fn collect_sections_rec(tree: &ModelTree, sections: &mut Vec<SectionEntry>) {
1078 if let Node::Section(inlines) = &tree.node {
1079 let title = inlines.iter().map(|i| i.plain_text()).collect::<String>();
1080 sections.push(SectionEntry {
1081 block_number: sections.len() + 1,
1082 title,
1083 });
1084 }
1085 for child in &tree.children {
1086 collect_sections_rec(child, sections);
1087 }
1088}
1089
1090fn collect_section_ids(tree: &ModelTree) -> Vec<NodeId> {
1091 let mut ids = Vec::new();
1092 if tree.is_section() {
1093 if let Some(id) = tree.id {
1094 ids.push(id);
1095 }
1096 }
1097 for child in &tree.children {
1098 ids.extend(collect_section_ids(child));
1099 }
1100 ids
1101}
1102
1103fn collect_block_refs(tree: &ModelTree) -> Vec<ReferenceEntry> {
1104 let mut result = Vec::new();
1105 collect_block_refs_rec(tree, &mut result);
1106 result
1107}
1108
1109fn collect_block_refs_rec(tree: &ModelTree, refs: &mut Vec<ReferenceEntry>) {
1110 if let Node::Reference(reference) = &tree.node {
1111 refs.push(ReferenceEntry {
1112 block_number: refs.len() + 1,
1113 key: reference.key.to_string(),
1114 title: reference.text.clone(),
1115 });
1116 }
1117 for child in &tree.children {
1118 collect_block_refs_rec(child, refs);
1119 }
1120}
1121
1122fn collect_ref_ids(tree: &ModelTree) -> Vec<NodeId> {
1123 let mut ids = Vec::new();
1124 if let Node::Reference(_) = &tree.node {
1125 if let Some(id) = tree.id {
1126 ids.push(id);
1127 }
1128 }
1129 for child in &tree.children {
1130 ids.extend(collect_ref_ids(child));
1131 }
1132 ids
1133}
1134
1135#[prompt_router]
1136impl IweServer {
1137 #[prompt(
1138 name = "explore",
1139 description = "Start exploring the knowledge graph. Provides an overview of size, structure, root entry points, broken links, and orphaned documents"
1140 )]
1141 async fn explore(&self) -> Result<GetPromptResult, McpError> {
1142 let graph = self.graph.lock().await;
1143 let stats = GraphStatistics::from_graph(&graph);
1144 let stats_json =
1145 serde_json::to_string_pretty(&stats).unwrap_or_else(|_| "{}".to_string());
1146
1147 let messages = vec![PromptMessage::new_text(
1148 PromptMessageRole::User,
1149 format!(
1150 "Here is an overview of the IWE knowledge graph.\n\n## Statistics\n\n```json\n{}\n```\n\nExplore the graph using iwe_retrieve to read documents, iwe_find to search, and iwe_tree to navigate the structure.",
1151 stats_json
1152 ),
1153 )];
1154
1155 Ok(GetPromptResult::new(messages)
1156 .with_description("Overview of the IWE knowledge graph"))
1157 }
1158
1159 #[prompt(
1160 name = "review",
1161 description = "Review a specific document within its graph context — its content, parents, children, and backlinks"
1162 )]
1163 async fn review(
1164 &self,
1165 Parameters(args): Parameters<ReviewPromptArgs>,
1166 ) -> Result<GetPromptResult, McpError> {
1167 let graph = self.graph.lock().await;
1168 let key = Key::name(&args.key);
1169 let reader = DocumentReader::new(&graph);
1170 let output = reader.retrieve(
1171 &key,
1172 &RetrieveOptions {
1173 depth: 2,
1174 context: 2,
1175 backlinks: true,
1176 ..Default::default()
1177 },
1178 );
1179 let json = serde_json::to_string_pretty(&output.documents)
1180 .unwrap_or_else(|_| "[]".to_string());
1181
1182 let messages = vec![PromptMessage::new_text(
1183 PromptMessageRole::User,
1184 format!(
1185 "Review this document and its context in the knowledge graph:\n\n```json\n{}\n```\n\nConsider: Is it well-placed in the graph? Are there missing links? Is the content clear and well-structured? What sections might be extracted into separate documents?",
1186 json
1187 ),
1188 )];
1189
1190 Ok(GetPromptResult::new(messages)
1191 .with_description(format!("Review of document '{}'", args.key)))
1192 }
1193
1194 #[prompt(
1195 name = "refactor",
1196 description = "Analyze a section of the knowledge graph and suggest restructuring using extract, inline, and rename operations"
1197 )]
1198 async fn refactor(
1199 &self,
1200 Parameters(args): Parameters<RefactorPromptArgs>,
1201 ) -> Result<GetPromptResult, McpError> {
1202 let graph = self.graph.lock().await;
1203 let key = Key::name(&args.key);
1204 let reader = DocumentReader::new(&graph);
1205 let output = reader.retrieve(
1206 &key,
1207 &RetrieveOptions {
1208 depth: 3,
1209 context: 1,
1210 backlinks: true,
1211 ..Default::default()
1212 },
1213 );
1214 let json = serde_json::to_string_pretty(&output.documents)
1215 .unwrap_or_else(|_| "[]".to_string());
1216
1217 let messages = vec![PromptMessage::new_text(
1218 PromptMessageRole::User,
1219 format!(
1220 "Analyze this document tree and suggest restructuring:\n\n```json\n{}\n```\n\nIdentify documents that are too large (should be extracted with iwe_extract), too small (should be inlined with iwe_inline), poorly named (should be renamed with iwe_rename), or missing connections. Propose a sequence of operations to improve the structure.",
1221 json
1222 ),
1223 )];
1224
1225 Ok(GetPromptResult::new(messages)
1226 .with_description(format!("Refactoring analysis for '{}'", args.key)))
1227 }
1228}
1229
1230#[tool_handler]
1231#[prompt_handler]
1232impl ServerHandler for IweServer {
1233 fn get_info(&self) -> ServerInfo {
1234 ServerInfo::new(
1235 ServerCapabilities::builder()
1236 .enable_tools()
1237 .enable_prompts()
1238 .enable_resources()
1239 .build(),
1240 )
1241 .with_server_info(Implementation::new("iwe", env!("CARGO_PKG_VERSION")))
1242 .with_instructions(
1243 "IWE knowledge graph server. Tools: iwe_find, iwe_retrieve, iwe_tree, iwe_stats, iwe_squash, iwe_create, iwe_update, iwe_delete, iwe_rename, iwe_extract, iwe_inline, iwe_normalize, iwe_attach. Prompts: explore, review, refactor. Resources: iwe://documents/{key}, iwe://tree, iwe://stats, iwe://config."
1244 .to_string(),
1245 )
1246 }
1247
1248 async fn list_resources(
1249 &self,
1250 _request: Option<PaginatedRequestParams>,
1251 _: RequestContext<RoleServer>,
1252 ) -> Result<ListResourcesResult, McpError> {
1253 let graph = self.graph.lock().await;
1254 let mut resources = vec![
1255 RawResource::new("iwe://tree", "tree")
1256 .with_description("Full document tree structure")
1257 .with_mime_type("application/json")
1258 .no_annotation(),
1259 RawResource::new("iwe://stats", "stats")
1260 .with_description("Aggregate graph statistics")
1261 .with_mime_type("application/json")
1262 .no_annotation(),
1263 RawResource::new("iwe://config", "config")
1264 .with_description("Project configuration: markdown options, templates, actions")
1265 .with_mime_type("application/json")
1266 .no_annotation(),
1267 ];
1268
1269 for key in graph.keys().iter().take(100) {
1270 let title = (&*graph)
1271 .get_key_title(key)
1272 .unwrap_or_else(|| key.to_string());
1273 resources.push(
1274 RawResource::new(format!("iwe://documents/{}", key), title)
1275 .with_mime_type("text/markdown")
1276 .no_annotation(),
1277 );
1278 }
1279
1280 Ok(ListResourcesResult {
1281 resources,
1282 next_cursor: None,
1283 meta: None,
1284 })
1285 }
1286
1287 async fn read_resource(
1288 &self,
1289 request: ReadResourceRequestParams,
1290 _: RequestContext<RoleServer>,
1291 ) -> Result<ReadResourceResult, McpError> {
1292 let uri = &request.uri;
1293 let graph = self.graph.lock().await;
1294
1295 if uri == "iwe://tree" {
1296 let paths = graph.paths();
1297 let mut root_keys: Vec<Key> = paths
1298 .iter()
1299 .filter(|n| n.ids().len() == 1)
1300 .filter_map(|n| n.first_id())
1301 .map(|id| (&*graph).node(id).node_key())
1302 .collect();
1303 root_keys.sort();
1304 root_keys.dedup();
1305
1306 let mut trees: Vec<TreeNode> = Vec::new();
1307 for root_key in &root_keys {
1308 let mut visited: HashSet<Key> = HashSet::new();
1309 if let Some(node) = build_tree_node(&graph, root_key, 4, &mut visited) {
1310 trees.push(node);
1311 }
1312 }
1313 let json = serde_json::to_string_pretty(&trees)
1314 .map_err(|e| McpError::internal_error(e.to_string(), None))?;
1315 return Ok(ReadResourceResult::new(vec![ResourceContents::text(
1316 json,
1317 uri.clone(),
1318 )]));
1319 }
1320
1321 if uri == "iwe://stats" {
1322 let stats = GraphStatistics::from_graph(&graph);
1323 let json = serde_json::to_string_pretty(&stats)
1324 .map_err(|e| McpError::internal_error(e.to_string(), None))?;
1325 return Ok(ReadResourceResult::new(vec![ResourceContents::text(
1326 json,
1327 uri.clone(),
1328 )]));
1329 }
1330
1331 if uri == "iwe://config" {
1332 let config_view = ConfigResource::from_config(&self.config, self);
1333 let json = serde_json::to_string_pretty(&config_view)
1334 .map_err(|e| McpError::internal_error(e.to_string(), None))?;
1335 return Ok(ReadResourceResult::new(vec![ResourceContents::text(
1336 json,
1337 uri.clone(),
1338 )]));
1339 }
1340
1341 if let Some(key_str) = uri.strip_prefix("iwe://documents/") {
1342 let key = Key::name(key_str);
1343 let content = graph
1344 .get_document(&key)
1345 .ok_or_else(|| {
1346 McpError::resource_not_found(
1347 format!("Document '{}' not found", key_str),
1348 None,
1349 )
1350 })?
1351 .to_string();
1352 return Ok(ReadResourceResult::new(vec![ResourceContents::text(
1353 content,
1354 uri.clone(),
1355 )]));
1356 }
1357
1358 Err(McpError::resource_not_found(
1359 format!("Unknown resource: {}", uri),
1360 None,
1361 ))
1362 }
1363
1364 async fn list_resource_templates(
1365 &self,
1366 _request: Option<PaginatedRequestParams>,
1367 _: RequestContext<RoleServer>,
1368 ) -> Result<ListResourceTemplatesResult, McpError> {
1369 Ok(ListResourceTemplatesResult {
1370 resource_templates: vec![
1371 RawResourceTemplate::new("iwe://documents/{key}", "document")
1372 .with_description("A document in the knowledge graph by key")
1373 .with_mime_type("text/markdown")
1374 .no_annotation(),
1375 ],
1376 next_cursor: None,
1377 meta: None,
1378 })
1379 }
1380}
1381
1382impl IweServer {
1383 pub fn new(base_path: &str, configuration: &Configuration) -> Self {
1384 let path = PathBuf::from_str(base_path).expect("valid path");
1385 let state = new_for_path(&path);
1386 let graph = Graph::from_state(
1387 &state,
1388 false,
1389 configuration.markdown.clone().into(),
1390 configuration.library.frontmatter_document_title.clone(),
1391 );
1392 Self {
1393 graph: Arc::new(Mutex::new(graph)),
1394 base_path: Some(path),
1395 config: configuration.clone(),
1396 tool_router: Self::tool_router(),
1397 prompt_router: Self::prompt_router(),
1398 }
1399 }
1400
1401 pub fn from_documents(documents: Vec<(&str, &str)>) -> Self {
1402 Self::from_documents_with_config(documents, Configuration::default())
1403 }
1404
1405 pub fn from_documents_with_config(documents: Vec<(&str, &str)>, config: Configuration) -> Self {
1406 let state = new_from_hashmap(
1407 documents
1408 .into_iter()
1409 .map(|(k, v)| (k.to_string(), v.to_string()))
1410 .collect::<HashMap<String, String>>(),
1411 );
1412 let graph = Graph::from_state(&state, true, MarkdownOptions::default(), None);
1413 Self {
1414 graph: Arc::new(Mutex::new(graph)),
1415 base_path: None,
1416 config,
1417 tool_router: Self::tool_router(),
1418 prompt_router: Self::prompt_router(),
1419 }
1420 }
1421
1422 fn apply_changes(graph: &mut Graph, changes: &Changes) {
1423 for key in &changes.removes {
1424 graph.remove_document(key.clone());
1425 }
1426 for (key, markdown) in &changes.creates {
1427 graph.insert_document(key.clone(), markdown.clone());
1428 }
1429 for (key, markdown) in &changes.updates {
1430 graph.update_document(key.clone(), markdown.clone());
1431 }
1432 }
1433
1434 fn write_file(&self, key: &Key, content: &str) {
1435 if let Some(base_path) = &self.base_path {
1436 let file_path = base_path.join(format!("{}.md", key));
1437 if let Some(parent) = file_path.parent() {
1438 std::fs::create_dir_all(parent).ok();
1439 }
1440 std::fs::write(&file_path, content).ok();
1441 }
1442 }
1443
1444 fn write_changes(&self, changes: &Changes) {
1445 if let Some(base_path) = &self.base_path {
1446 for key in &changes.removes {
1447 let file_path = base_path.join(format!("{}.md", key));
1448 if file_path.exists() {
1449 std::fs::remove_file(&file_path).ok();
1450 }
1451 }
1452 for (key, markdown) in &changes.creates {
1453 let file_path = base_path.join(format!("{}.md", key));
1454 if let Some(parent) = file_path.parent() {
1455 std::fs::create_dir_all(parent).ok();
1456 }
1457 std::fs::write(&file_path, markdown).ok();
1458 }
1459 for (key, markdown) in &changes.updates {
1460 let file_path = base_path.join(format!("{}.md", key));
1461 std::fs::write(&file_path, markdown).ok();
1462 }
1463 }
1464 }
1465
1466 pub fn start_watching(&self) {
1467 if let Some(base_path) = &self.base_path {
1468 watcher::start(self.graph.clone(), base_path.clone());
1469 }
1470 }
1471
1472 fn render_key_template(&self, template: &str) -> String {
1473 let now = Local::now();
1474 let date_format = self
1475 .config
1476 .library
1477 .date_format
1478 .as_deref()
1479 .unwrap_or(DEFAULT_KEY_DATE_FORMAT);
1480 let formatted = now.format(date_format).to_string();
1481 Environment::new()
1482 .template_from_str(template)
1483 .expect("valid key template")
1484 .render(context! {
1485 today => formatted,
1486 now => formatted,
1487 })
1488 .expect("key template to render")
1489 }
1490
1491 fn render_document_template(&self, template: &str, content: &str) -> String {
1492 let now = Local::now();
1493 let date_format = self
1494 .config
1495 .markdown
1496 .date_format
1497 .as_deref()
1498 .unwrap_or("%b %d, %Y");
1499 let formatted = now.format(date_format).to_string();
1500 Environment::new()
1501 .template_from_str(template)
1502 .expect("valid document template")
1503 .render(context! {
1504 today => formatted,
1505 now => formatted,
1506 content => content,
1507 })
1508 .expect("document template to render")
1509 }
1510}