handoff-mcp-server 0.26.0 → 0.28.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.
Files changed (72) hide show
  1. package/README.md +31 -4
  2. package/bin/handoff-mcp.js +56 -13
  3. package/bin/resolve-binary.js +122 -0
  4. package/package.json +14 -9
  5. package/Cargo.lock +0 -686
  6. package/Cargo.toml +0 -30
  7. package/scripts/cargo-env.sh +0 -29
  8. package/scripts/handoff-memory-hook.py +0 -208
  9. package/scripts/install-local.sh +0 -109
  10. package/scripts/postinstall.js +0 -50
  11. package/scripts/sync-plugin-skills.sh +0 -35
  12. package/scripts/sync-plugin-version.sh +0 -85
  13. package/scripts/sync-workflow-inline.sh +0 -138
  14. package/src/cli.rs +0 -551
  15. package/src/context/injection.rs +0 -276
  16. package/src/context/mod.rs +0 -129
  17. package/src/lib.rs +0 -5
  18. package/src/main.rs +0 -157
  19. package/src/mcp/handlers/assignees.rs +0 -254
  20. package/src/mcp/handlers/auto_schedule.rs +0 -489
  21. package/src/mcp/handlers/bulk_update.rs +0 -155
  22. package/src/mcp/handlers/calendar.rs +0 -196
  23. package/src/mcp/handlers/capacity.rs +0 -318
  24. package/src/mcp/handlers/check_criterion.rs +0 -70
  25. package/src/mcp/handlers/config.rs +0 -402
  26. package/src/mcp/handlers/config_crud.rs +0 -183
  27. package/src/mcp/handlers/dashboard.rs +0 -214
  28. package/src/mcp/handlers/docs.rs +0 -2288
  29. package/src/mcp/handlers/docs_query.rs +0 -1335
  30. package/src/mcp/handlers/fork_session.rs +0 -91
  31. package/src/mcp/handlers/get_session.rs +0 -48
  32. package/src/mcp/handlers/get_task.rs +0 -53
  33. package/src/mcp/handlers/import_context.rs +0 -470
  34. package/src/mcp/handlers/init.rs +0 -28
  35. package/src/mcp/handlers/list_sessions.rs +0 -187
  36. package/src/mcp/handlers/list_tasks.rs +0 -308
  37. package/src/mcp/handlers/load_context.rs +0 -361
  38. package/src/mcp/handlers/log_time.rs +0 -67
  39. package/src/mcp/handlers/memory.rs +0 -961
  40. package/src/mcp/handlers/merge_sessions.rs +0 -103
  41. package/src/mcp/handlers/metrics.rs +0 -196
  42. package/src/mcp/handlers/milestones.rs +0 -102
  43. package/src/mcp/handlers/mod.rs +0 -140
  44. package/src/mcp/handlers/refer.rs +0 -307
  45. package/src/mcp/handlers/referrals.rs +0 -74
  46. package/src/mcp/handlers/save_context.rs +0 -354
  47. package/src/mcp/handlers/task_checklist.rs +0 -507
  48. package/src/mcp/handlers/timer.rs +0 -529
  49. package/src/mcp/handlers/update_session.rs +0 -197
  50. package/src/mcp/handlers/update_task.rs +0 -452
  51. package/src/mcp/mod.rs +0 -6
  52. package/src/mcp/protocol.rs +0 -41
  53. package/src/mcp/resources.rs +0 -57
  54. package/src/mcp/router.rs +0 -154
  55. package/src/mcp/tools.rs +0 -1522
  56. package/src/mcp/types.rs +0 -108
  57. package/src/setup.rs +0 -1212
  58. package/src/storage/config.rs +0 -578
  59. package/src/storage/docs/frontmatter.rs +0 -509
  60. package/src/storage/docs/mod.rs +0 -835
  61. package/src/storage/docs/model.rs +0 -708
  62. package/src/storage/docs/reassemble.rs +0 -167
  63. package/src/storage/docs/split.rs +0 -377
  64. package/src/storage/git.rs +0 -47
  65. package/src/storage/memory/injected.rs +0 -340
  66. package/src/storage/memory/mod.rs +0 -236
  67. package/src/storage/memory/model.rs +0 -127
  68. package/src/storage/mod.rs +0 -96
  69. package/src/storage/referrals.rs +0 -248
  70. package/src/storage/sessions.rs +0 -859
  71. package/src/storage/tasks.rs +0 -957
  72. package/templates/claude-md-section.md +0 -12
@@ -1,214 +0,0 @@
1
- use std::path::{Path, PathBuf};
2
-
3
- use anyhow::{Context, Result};
4
- use serde_json::Value;
5
-
6
- use crate::storage::config::{read_config, DashboardConfig};
7
- use crate::storage::expand_tilde;
8
- use crate::storage::referrals::read_referral_summaries;
9
- use crate::storage::sessions::{read_active_sessions, read_open_sessions, read_paused_sessions};
10
- use crate::storage::tasks::build_task_index;
11
-
12
- pub fn handle(arguments: &Value) -> Result<String> {
13
- let scan_dirs: Vec<String> = arguments
14
- .get("scan_dirs")
15
- .and_then(|v| v.as_array())
16
- .map(|arr| {
17
- arr.iter()
18
- .filter_map(|v| v.as_str().map(String::from))
19
- .collect()
20
- })
21
- .unwrap_or_else(|| vec!["~/pro/".to_string()]);
22
-
23
- let mut projects = Vec::new();
24
- let mut total_active = 0u32;
25
- let mut total_blocked = 0u32;
26
-
27
- for scan_dir in &scan_dirs {
28
- let expanded = expand_tilde(scan_dir);
29
- let expanded_path = Path::new(&expanded);
30
-
31
- if !expanded_path.exists() {
32
- continue;
33
- }
34
-
35
- let (max_depth, exclude_patterns) = resolve_scan_config(expanded_path, arguments);
36
-
37
- let mut discovered = Vec::new();
38
- scan_recursive(
39
- expanded_path,
40
- 1,
41
- max_depth,
42
- &exclude_patterns,
43
- &mut discovered,
44
- );
45
-
46
- for project_path in discovered {
47
- if let Ok(info) = collect_project_info(&project_path) {
48
- total_active += info["active_tasks"].as_u64().unwrap_or(0) as u32;
49
- total_blocked += info["blocked_tasks"].as_u64().unwrap_or(0) as u32;
50
- projects.push(info);
51
- }
52
- }
53
- }
54
-
55
- let result = serde_json::json!({
56
- "projects": projects,
57
- "total_active_tasks": total_active,
58
- "total_blocked": total_blocked,
59
- });
60
-
61
- serde_json::to_string_pretty(&result).context("Failed to serialize dashboard")
62
- }
63
-
64
- /// Resolves effective `max_depth` / `exclude_patterns` for a single scan_dir.
65
- ///
66
- /// Precedence: explicit tool `arguments` override (applies uniformly across
67
- /// all scan_dirs, since it's an explicit user choice), then this scan_dir's
68
- /// own `.handoff/config.toml` (if present), then — in the common umbrella-
69
- /// workspace topology where the scan_dir itself is not a handoff project (e.g.
70
- /// `~/pro/`) — the first *discovered child* project's config within this same
71
- /// scan_dir's subtree, then built-in defaults.
72
- ///
73
- /// Scoped to a single `expanded_path` so config discovered under one scan_dir
74
- /// never leaks into sibling scan_dirs in a multi-root dashboard call.
75
- fn resolve_scan_config(expanded_path: &Path, arguments: &Value) -> (usize, Vec<String>) {
76
- let mut defaults = DashboardConfig::default();
77
-
78
- let own_config_path = expanded_path.join(".handoff").join("config.toml");
79
- if let Ok(config) = read_config(&own_config_path) {
80
- defaults = config.dashboard;
81
- } else {
82
- // scan_dir itself has no config of its own (typical umbrella-workspace
83
- // case) — do a discovery pass scoped to this scan_dir's own subtree and
84
- // look for a child project whose own dashboard config overrides the
85
- // built-in default, so per-project settings still take effect without
86
- // requiring an explicit tool argument. Discovery order is filesystem-
87
- // dependent, so sort child paths for deterministic selection.
88
- //
89
- // Probe depth is capped at the caller's explicit max_depth argument
90
- // (if given) so a shallow-depth request doesn't still pay for a full
91
- // default-depth (5) filesystem walk just to look for fallback config.
92
- let probe_depth = arguments
93
- .get("max_depth")
94
- .and_then(|v| v.as_u64())
95
- .map(|n| n as usize)
96
- .unwrap_or_else(|| DashboardConfig::default().max_depth)
97
- .min(DashboardConfig::default().max_depth);
98
- let mut discovered = Vec::new();
99
- scan_recursive(expanded_path, 1, probe_depth, &[], &mut discovered);
100
- discovered.sort();
101
- for child_path in discovered {
102
- let child_config_path = child_path.join(".handoff").join("config.toml");
103
- if let Ok(config) = read_config(&child_config_path) {
104
- if config.dashboard.max_depth != DashboardConfig::default().max_depth
105
- || !config.dashboard.exclude_patterns.is_empty()
106
- {
107
- defaults = config.dashboard;
108
- break;
109
- }
110
- }
111
- }
112
- }
113
-
114
- let max_depth = arguments
115
- .get("max_depth")
116
- .and_then(|v| v.as_u64())
117
- .map(|n| n as usize)
118
- .unwrap_or(defaults.max_depth);
119
-
120
- let exclude_patterns = arguments
121
- .get("exclude_patterns")
122
- .and_then(|v| v.as_array())
123
- .map(|arr| {
124
- arr.iter()
125
- .filter_map(|v| v.as_str().map(String::from))
126
- .collect()
127
- })
128
- .unwrap_or(defaults.exclude_patterns);
129
-
130
- (max_depth, exclude_patterns)
131
- }
132
-
133
- /// Recursively scans `dir` up to `max_depth` levels for `.handoff/config.toml`
134
- /// markers, skipping directories whose name exactly matches an entry in
135
- /// `exclude_patterns`. Never descends into a directory literally named
136
- /// `.handoff` — a project's own bookkeeping tree (tasks/sessions/memory/etc.)
137
- /// can never contain a nested project marker, so walking it would only waste
138
- /// I/O proportional to the project's task/session history.
139
- fn scan_recursive(
140
- dir: &Path,
141
- depth: usize,
142
- max_depth: usize,
143
- exclude_patterns: &[String],
144
- results: &mut Vec<PathBuf>,
145
- ) {
146
- if depth > max_depth {
147
- return;
148
- }
149
- let entries = match std::fs::read_dir(dir) {
150
- Ok(e) => e,
151
- Err(_) => return,
152
- };
153
- for entry in entries.flatten() {
154
- if !entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
155
- continue;
156
- }
157
- let name = entry.file_name();
158
- let name_str = name.to_string_lossy();
159
- if name_str == ".handoff" || exclude_patterns.iter().any(|p| p == name_str.as_ref()) {
160
- continue;
161
- }
162
- let path = entry.path();
163
- if path.join(".handoff").join("config.toml").exists() {
164
- results.push(path.clone());
165
- }
166
- scan_recursive(&path, depth + 1, max_depth, exclude_patterns, results);
167
- }
168
- }
169
-
170
- fn collect_project_info(project_path: &Path) -> Result<Value> {
171
- let handoff_dir = project_path.join(".handoff");
172
- let config = read_config(&handoff_dir.join("config.toml"))?;
173
-
174
- let sessions_dir = handoff_dir.join("sessions");
175
- let mut sessions = read_open_sessions(&sessions_dir)?;
176
- sessions.extend(read_active_sessions(&sessions_dir)?);
177
- let paused = read_paused_sessions(&sessions_dir)?;
178
- let paused_count = paused.len() as u32;
179
- sessions.extend(paused);
180
-
181
- let (_, summary) =
182
- build_task_index(&handoff_dir.join("tasks"), config.settings.done_task_limit)?;
183
-
184
- let last_session_ended = sessions.last().and_then(|s| s.ended_at.clone());
185
-
186
- let branch = sessions.last().and_then(|s| s.branch.clone());
187
-
188
- let active_tasks = *summary.by_status.get("in_progress").unwrap_or(&0)
189
- + *summary.by_status.get("todo").unwrap_or(&0)
190
- + *summary.by_status.get("review").unwrap_or(&0);
191
-
192
- let blocked_tasks = *summary.by_status.get("blocked").unwrap_or(&0);
193
-
194
- let blockers: Vec<String> = sessions
195
- .iter()
196
- .flat_map(|s| s.blockers.iter().cloned())
197
- .collect();
198
-
199
- let unread_referrals = read_referral_summaries(&handoff_dir.join("referrals"), Some("open"))
200
- .map(|r| r.len() as u32)
201
- .unwrap_or(0);
202
-
203
- Ok(serde_json::json!({
204
- "name": config.project.name,
205
- "path": project_path.to_string_lossy(),
206
- "last_session_ended": last_session_ended,
207
- "branch": branch,
208
- "active_tasks": active_tasks,
209
- "blocked_tasks": blocked_tasks,
210
- "blockers": blockers,
211
- "unread_referrals": unread_referrals,
212
- "paused_sessions": paused_count,
213
- }))
214
- }