handoff-mcp-server 0.24.5 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +29 -3
- package/Cargo.toml +4 -2
- package/README.md +23 -9
- package/package.json +1 -1
- package/skills/handoff-docs/SKILL.md +181 -9
- package/src/context/injection.rs +55 -18
- package/src/context/mod.rs +1 -1
- package/src/mcp/handlers/auto_schedule.rs +1 -13
- package/src/mcp/handlers/capacity.rs +1 -14
- package/src/mcp/handlers/config.rs +13 -0
- package/src/mcp/handlers/docs.rs +832 -76
- package/src/mcp/handlers/docs_query.rs +4 -4
- package/src/mcp/handlers/memory.rs +88 -13
- package/src/mcp/handlers/mod.rs +1 -0
- package/src/mcp/handlers/save_context.rs +8 -3
- package/src/mcp/handlers/task_checklist.rs +17 -5
- package/src/mcp/handlers/update_session.rs +6 -4
- package/src/mcp/tools.rs +28 -7
- package/src/storage/config.rs +217 -5
- package/src/storage/docs/frontmatter.rs +509 -0
- package/src/storage/docs/mod.rs +346 -108
- package/src/storage/docs/model.rs +186 -15
- package/src/storage/memory/mod.rs +1 -0
- package/src/storage/memory/model.rs +38 -7
- package/src/storage/sessions.rs +39 -11
package/src/storage/config.rs
CHANGED
|
@@ -2,7 +2,71 @@ use std::collections::HashMap;
|
|
|
2
2
|
use std::path::Path;
|
|
3
3
|
|
|
4
4
|
use anyhow::{Context, Result};
|
|
5
|
-
use serde::{
|
|
5
|
+
use serde::de::{self, SeqAccess};
|
|
6
|
+
use serde::{Deserialize, Deserializer, Serialize};
|
|
7
|
+
|
|
8
|
+
/// Convert a weekday name to its number (0=Sun..6=Sat).
|
|
9
|
+
pub fn weekday_to_num(s: &str) -> Option<u32> {
|
|
10
|
+
match s.to_lowercase().as_str() {
|
|
11
|
+
"sun" | "sunday" => Some(0),
|
|
12
|
+
"mon" | "monday" => Some(1),
|
|
13
|
+
"tue" | "tuesday" => Some(2),
|
|
14
|
+
"wed" | "wednesday" => Some(3),
|
|
15
|
+
"thu" | "thursday" => Some(4),
|
|
16
|
+
"fri" | "friday" => Some(5),
|
|
17
|
+
"sat" | "saturday" => Some(6),
|
|
18
|
+
_ => None,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fn deserialize_weekdays<'de, D>(deserializer: D) -> std::result::Result<Vec<u32>, D::Error>
|
|
23
|
+
where
|
|
24
|
+
D: Deserializer<'de>,
|
|
25
|
+
{
|
|
26
|
+
struct WeekdayVisitor;
|
|
27
|
+
|
|
28
|
+
impl<'de> de::Visitor<'de> for WeekdayVisitor {
|
|
29
|
+
type Value = Vec<u32>;
|
|
30
|
+
|
|
31
|
+
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
32
|
+
f.write_str("an array of weekday numbers (0-6) or names (\"sun\"..\"sat\")")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Vec<u32>, A::Error>
|
|
36
|
+
where
|
|
37
|
+
A: SeqAccess<'de>,
|
|
38
|
+
{
|
|
39
|
+
let mut vals = Vec::new();
|
|
40
|
+
while let Some(elem) = seq.next_element::<toml::Value>()? {
|
|
41
|
+
match &elem {
|
|
42
|
+
toml::Value::Integer(n) => {
|
|
43
|
+
let n = *n as u32;
|
|
44
|
+
if n > 6 {
|
|
45
|
+
return Err(de::Error::custom(format!(
|
|
46
|
+
"weekday number {n} out of range 0-6"
|
|
47
|
+
)));
|
|
48
|
+
}
|
|
49
|
+
vals.push(n);
|
|
50
|
+
}
|
|
51
|
+
toml::Value::String(s) => {
|
|
52
|
+
let n = weekday_to_num(s).ok_or_else(|| {
|
|
53
|
+
de::Error::custom(format!("unknown weekday name: \"{s}\""))
|
|
54
|
+
})?;
|
|
55
|
+
vals.push(n);
|
|
56
|
+
}
|
|
57
|
+
_ => {
|
|
58
|
+
return Err(de::Error::custom(
|
|
59
|
+
"closed_weekdays elements must be integers or strings",
|
|
60
|
+
));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
Ok(vals)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
deserializer.deserialize_seq(WeekdayVisitor)
|
|
69
|
+
}
|
|
6
70
|
|
|
7
71
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
8
72
|
pub struct Config {
|
|
@@ -66,9 +130,16 @@ pub struct SettingsConfig {
|
|
|
66
130
|
#[serde(default = "default_memory_dup_threshold")]
|
|
67
131
|
pub memory_dup_threshold: f64,
|
|
68
132
|
/// BM25 relevance floor for `memory_query`; scores below are not returned.
|
|
69
|
-
/// Default 0.
|
|
133
|
+
/// Default 2.0.
|
|
70
134
|
#[serde(default = "default_memory_query_min_score")]
|
|
71
135
|
pub memory_query_min_score: f64,
|
|
136
|
+
/// Relative threshold (0.0–1.0) for `memory_query`: after the absolute
|
|
137
|
+
/// `min_score` floor, a candidate is dropped unless its score is at least
|
|
138
|
+
/// `top_score × relative_threshold`. Prevents low-relevance "tail" matches
|
|
139
|
+
/// from riding a strong top hit. 0.0 disables (keep everything above
|
|
140
|
+
/// `min_score`). Default 0.3.
|
|
141
|
+
#[serde(default = "default_memory_query_relative_threshold")]
|
|
142
|
+
pub memory_query_relative_threshold: f64,
|
|
72
143
|
/// Maximum number of memories `memory_query` returns per call. Default 5.
|
|
73
144
|
#[serde(default = "default_memory_query_limit")]
|
|
74
145
|
pub memory_query_limit: u32,
|
|
@@ -114,7 +185,12 @@ pub struct CalendarConfig {
|
|
|
114
185
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
115
186
|
pub work_hours_per_day: Option<f64>,
|
|
116
187
|
/// Weekday numbers (0=Sun..6=Sat) that are non-working.
|
|
117
|
-
|
|
188
|
+
/// Accepts integers or weekday names ("sun", "sat", etc.) in TOML.
|
|
189
|
+
#[serde(
|
|
190
|
+
default,
|
|
191
|
+
skip_serializing_if = "Vec::is_empty",
|
|
192
|
+
deserialize_with = "deserialize_weekdays"
|
|
193
|
+
)]
|
|
118
194
|
pub closed_weekdays: Vec<u32>,
|
|
119
195
|
/// Specific YYYY-MM-DD dates that are non-working (override weekdays).
|
|
120
196
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
@@ -155,7 +231,11 @@ pub struct AssigneeConfig {
|
|
|
155
231
|
pub color: Option<String>,
|
|
156
232
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
157
233
|
pub work_hours_per_day: Option<f64>,
|
|
158
|
-
#[serde(
|
|
234
|
+
#[serde(
|
|
235
|
+
default,
|
|
236
|
+
skip_serializing_if = "Vec::is_empty",
|
|
237
|
+
deserialize_with = "deserialize_weekdays"
|
|
238
|
+
)]
|
|
159
239
|
pub closed_weekdays: Vec<u32>,
|
|
160
240
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
161
241
|
pub closed_dates: Vec<String>,
|
|
@@ -252,7 +332,11 @@ fn default_memory_dup_threshold() -> f64 {
|
|
|
252
332
|
}
|
|
253
333
|
|
|
254
334
|
fn default_memory_query_min_score() -> f64 {
|
|
255
|
-
0
|
|
335
|
+
2.0
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
fn default_memory_query_relative_threshold() -> f64 {
|
|
339
|
+
0.3
|
|
256
340
|
}
|
|
257
341
|
|
|
258
342
|
fn default_memory_query_limit() -> u32 {
|
|
@@ -299,6 +383,7 @@ impl Default for SettingsConfig {
|
|
|
299
383
|
memory_enabled: default_memory_enabled(),
|
|
300
384
|
memory_dup_threshold: default_memory_dup_threshold(),
|
|
301
385
|
memory_query_min_score: default_memory_query_min_score(),
|
|
386
|
+
memory_query_relative_threshold: default_memory_query_relative_threshold(),
|
|
302
387
|
memory_query_limit: default_memory_query_limit(),
|
|
303
388
|
memory_stale_days: default_memory_stale_days(),
|
|
304
389
|
memory_injected_gc_days: default_memory_injected_gc_days(),
|
|
@@ -364,3 +449,130 @@ pub fn write_config(path: &Path, config: &Config) -> Result<()> {
|
|
|
364
449
|
.with_context(|| format!("Failed to write config: {}", path.display()))?;
|
|
365
450
|
Ok(())
|
|
366
451
|
}
|
|
452
|
+
|
|
453
|
+
#[cfg(test)]
|
|
454
|
+
mod tests {
|
|
455
|
+
use super::*;
|
|
456
|
+
|
|
457
|
+
fn parse_config(toml_str: &str) -> Config {
|
|
458
|
+
toml::from_str(toml_str).unwrap()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
#[test]
|
|
462
|
+
fn closed_weekdays_string_names() {
|
|
463
|
+
let cfg = parse_config(
|
|
464
|
+
r#"
|
|
465
|
+
[project]
|
|
466
|
+
name = "test"
|
|
467
|
+
[calendar]
|
|
468
|
+
closed_weekdays = ["sun", "sat"]
|
|
469
|
+
"#,
|
|
470
|
+
);
|
|
471
|
+
assert_eq!(cfg.calendar.closed_weekdays, vec![0, 6]);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
#[test]
|
|
475
|
+
fn closed_weekdays_integer_values() {
|
|
476
|
+
let cfg = parse_config(
|
|
477
|
+
r#"
|
|
478
|
+
[project]
|
|
479
|
+
name = "test"
|
|
480
|
+
[calendar]
|
|
481
|
+
closed_weekdays = [0, 6]
|
|
482
|
+
"#,
|
|
483
|
+
);
|
|
484
|
+
assert_eq!(cfg.calendar.closed_weekdays, vec![0, 6]);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
#[test]
|
|
488
|
+
fn closed_weekdays_mixed() {
|
|
489
|
+
let cfg = parse_config(
|
|
490
|
+
r#"
|
|
491
|
+
[project]
|
|
492
|
+
name = "test"
|
|
493
|
+
[calendar]
|
|
494
|
+
closed_weekdays = ["sun", 6]
|
|
495
|
+
"#,
|
|
496
|
+
);
|
|
497
|
+
assert_eq!(cfg.calendar.closed_weekdays, vec![0, 6]);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
#[test]
|
|
501
|
+
fn closed_weekdays_empty() {
|
|
502
|
+
let cfg = parse_config(
|
|
503
|
+
r#"
|
|
504
|
+
[project]
|
|
505
|
+
name = "test"
|
|
506
|
+
"#,
|
|
507
|
+
);
|
|
508
|
+
assert!(cfg.calendar.closed_weekdays.is_empty());
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
#[test]
|
|
512
|
+
fn closed_weekdays_full_names() {
|
|
513
|
+
let cfg = parse_config(
|
|
514
|
+
r#"
|
|
515
|
+
[project]
|
|
516
|
+
name = "test"
|
|
517
|
+
[calendar]
|
|
518
|
+
closed_weekdays = ["sunday", "saturday"]
|
|
519
|
+
"#,
|
|
520
|
+
);
|
|
521
|
+
assert_eq!(cfg.calendar.closed_weekdays, vec![0, 6]);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#[test]
|
|
525
|
+
fn assignee_closed_weekdays_strings() {
|
|
526
|
+
let cfg = parse_config(
|
|
527
|
+
r#"
|
|
528
|
+
[project]
|
|
529
|
+
name = "test"
|
|
530
|
+
[assignees.alice]
|
|
531
|
+
closed_weekdays = ["mon", "fri"]
|
|
532
|
+
"#,
|
|
533
|
+
);
|
|
534
|
+
let alice = cfg.assignees.get("alice").unwrap();
|
|
535
|
+
assert_eq!(alice.closed_weekdays, vec![1, 5]);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
#[test]
|
|
539
|
+
fn closed_weekdays_invalid_name() {
|
|
540
|
+
let result = toml::from_str::<Config>(
|
|
541
|
+
r#"
|
|
542
|
+
[project]
|
|
543
|
+
name = "test"
|
|
544
|
+
[calendar]
|
|
545
|
+
closed_weekdays = ["funday"]
|
|
546
|
+
"#,
|
|
547
|
+
);
|
|
548
|
+
assert!(result.is_err());
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
#[test]
|
|
552
|
+
fn closed_weekdays_out_of_range() {
|
|
553
|
+
let result = toml::from_str::<Config>(
|
|
554
|
+
r#"
|
|
555
|
+
[project]
|
|
556
|
+
name = "test"
|
|
557
|
+
[calendar]
|
|
558
|
+
closed_weekdays = [7]
|
|
559
|
+
"#,
|
|
560
|
+
);
|
|
561
|
+
assert!(result.is_err());
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
#[test]
|
|
565
|
+
fn round_trip_preserves_integer_format() {
|
|
566
|
+
let cfg = parse_config(
|
|
567
|
+
r#"
|
|
568
|
+
[project]
|
|
569
|
+
name = "test"
|
|
570
|
+
[calendar]
|
|
571
|
+
closed_weekdays = ["sun", "sat"]
|
|
572
|
+
"#,
|
|
573
|
+
);
|
|
574
|
+
let serialized = toml::to_string_pretty(&cfg).unwrap();
|
|
575
|
+
let re_parsed = parse_config(&serialized);
|
|
576
|
+
assert_eq!(re_parsed.calendar.closed_weekdays, vec![0, 6]);
|
|
577
|
+
}
|
|
578
|
+
}
|