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,307 +0,0 @@
1
- use std::path::{Path, PathBuf};
2
-
3
- use anyhow::{Context, Result};
4
- use chrono::Utc;
5
- use serde_json::Value;
6
-
7
- use super::resolve_project_dir;
8
- use crate::storage::config::read_config;
9
- use crate::storage::expand_tilde;
10
- use crate::storage::referrals::{is_valid_referral_type, write_referral, ReferralData};
11
- use crate::storage::tasks::validate_priority;
12
-
13
- pub fn handle(arguments: &Value) -> Result<String> {
14
- let source_project_dir = resolve_project_dir(arguments)?;
15
-
16
- let source_handoff = source_project_dir.join(".handoff");
17
- if !source_handoff.join("config.toml").exists() {
18
- anyhow::bail!(
19
- "Source project is not initialized: {}",
20
- source_project_dir.display()
21
- );
22
- }
23
-
24
- let source_config = read_config(&source_handoff.join("config.toml"))?;
25
-
26
- let target_dir = resolve_target(arguments, &source_config.dashboard.scan_dirs)?;
27
-
28
- let target_handoff = target_dir.join(".handoff");
29
- if !target_handoff.exists() {
30
- anyhow::bail!(
31
- "Target project is not initialized (no .handoff/): {}",
32
- target_dir.display()
33
- );
34
- }
35
-
36
- let summary = arguments
37
- .get("summary")
38
- .and_then(|v| v.as_str())
39
- .ok_or_else(|| anyhow::anyhow!("'summary' is required"))?;
40
-
41
- let referral_type = arguments
42
- .get("referral_type")
43
- .and_then(|v| v.as_str())
44
- .unwrap_or("request");
45
-
46
- if !is_valid_referral_type(referral_type) {
47
- anyhow::bail!(
48
- "Invalid referral_type: '{referral_type}'. Must be one of: improvement, bug, request, info"
49
- );
50
- }
51
-
52
- let priority = arguments.get("priority").and_then(|v| v.as_str());
53
- validate_priority(priority)?;
54
-
55
- let details = arguments
56
- .get("details")
57
- .and_then(|v| v.as_str())
58
- .map(String::from);
59
-
60
- let tasks: Vec<Value> = arguments
61
- .get("tasks")
62
- .and_then(|v| v.as_array())
63
- .cloned()
64
- .unwrap_or_default();
65
-
66
- let context = arguments.get("context").cloned();
67
-
68
- let now_dt = Utc::now();
69
- let now = now_dt.to_rfc3339();
70
- let id = format!("ref-{}", now_dt.format("%Y%m%d-%H%M%S-%f"));
71
-
72
- let data = ReferralData {
73
- id: id.clone(),
74
- source_project: source_config.project.name.clone(),
75
- source_project_dir: source_project_dir.to_string_lossy().to_string(),
76
- created_at: now,
77
- referral_type: referral_type.to_string(),
78
- summary: summary.to_string(),
79
- details,
80
- priority: priority.map(String::from),
81
- tasks,
82
- context,
83
- };
84
-
85
- let referrals_dir = target_handoff.join("referrals");
86
- write_referral(&referrals_dir, &data)?;
87
-
88
- let target_name = if target_handoff.join("config.toml").exists() {
89
- read_config(&target_handoff.join("config.toml"))
90
- .map(|c| c.project.name)
91
- .unwrap_or_else(|_| target_dir.to_string_lossy().to_string())
92
- } else {
93
- target_dir.to_string_lossy().to_string()
94
- };
95
-
96
- let mut msg = format!(
97
- "Referral sent: {id}\n From: {}\n To: {target_name}\n Type: {referral_type}\n Summary: {summary}",
98
- source_config.project.name
99
- );
100
-
101
- for w in collect_refer_warnings(&data, &source_project_dir, &target_dir) {
102
- msg.push_str(&format!("\n{w}"));
103
- }
104
-
105
- Ok(msg)
106
- }
107
-
108
- fn collect_refer_warnings(
109
- data: &ReferralData,
110
- source_dir: &Path,
111
- target_dir: &Path,
112
- ) -> Vec<String> {
113
- let mut warnings = Vec::new();
114
-
115
- if data.details.is_none() {
116
- warnings.push(
117
- "Warning: No details. The target project won't know what specifically to do. \
118
- Add a 'details' field describing the change, its impact, and what needs updating."
119
- .to_string(),
120
- );
121
- }
122
-
123
- if data.tasks.is_empty() {
124
- warnings.push(
125
- "Warning: No tasks. Consider adding suggested tasks with done_criteria \
126
- so the target project has actionable items to work from."
127
- .to_string(),
128
- );
129
- }
130
-
131
- if let Some(ref ctx) = data.context {
132
- let refs = collect_refs_from_value(ctx);
133
- if refs.is_empty() {
134
- warnings.push(
135
- "Warning: context has no spec/doc references. Add 'spec_docs' with wiki paths, \
136
- MR URLs, or file paths so the target can find the authoritative specification."
137
- .to_string(),
138
- );
139
- } else {
140
- for r in &refs {
141
- if r.starts_with("http://") || r.starts_with("https://") {
142
- continue;
143
- }
144
- let clean = r.split(" — ").next().unwrap_or(r).trim();
145
- let clean = clean.split('#').next().unwrap_or(clean).trim();
146
- let p = Path::new(clean);
147
- if p.is_absolute() {
148
- if !p.exists() {
149
- warnings.push(format!(
150
- "Warning: spec reference path does not exist: {clean}"
151
- ));
152
- }
153
- } else {
154
- let in_source = source_dir.join(clean);
155
- let in_target = target_dir.join(clean);
156
- if !in_source.exists() && !in_target.exists() {
157
- warnings.push(format!(
158
- "Warning: spec reference path does not exist \
159
- in source or target project: {clean}"
160
- ));
161
- } else if !in_target.exists() {
162
- warnings.push(format!(
163
- "Warning: spec reference '{clean}' exists in source project \
164
- but not in target project. Use an absolute path or ensure \
165
- the target has this file."
166
- ));
167
- }
168
- }
169
- }
170
- }
171
- } else {
172
- warnings.push(
173
- "Warning: No context. Add a 'context' field with 'spec_docs' referencing the \
174
- authoritative specification (wiki paths, MR URLs, source file paths)."
175
- .to_string(),
176
- );
177
- }
178
-
179
- if data.priority.is_none() {
180
- warnings.push(
181
- "Warning: No priority. Set 'priority' (low/medium/high) so the target project \
182
- can triage this referral appropriately."
183
- .to_string(),
184
- );
185
- }
186
-
187
- for (i, task) in data.tasks.iter().enumerate() {
188
- let has_criteria = task
189
- .get("done_criteria")
190
- .and_then(|v| v.as_array())
191
- .is_some_and(|a| !a.is_empty());
192
- if !has_criteria {
193
- let title = task
194
- .get("title")
195
- .and_then(|v| v.as_str())
196
- .unwrap_or("(untitled)");
197
- warnings.push(format!(
198
- "Warning: Task #{} '{}' has no done_criteria. \
199
- Add criteria so the target knows when the task is complete.",
200
- i + 1,
201
- title
202
- ));
203
- }
204
- }
205
-
206
- warnings
207
- }
208
-
209
- fn is_ref_string(s: &str) -> bool {
210
- s.starts_with("http://")
211
- || s.starts_with("https://")
212
- || s.starts_with('/')
213
- || s.starts_with("wiki/")
214
- || s.starts_with("docs/")
215
- || s.starts_with("src/")
216
- || s.ends_with(".md")
217
- || s.ends_with(".rs")
218
- || s.ends_with(".ts")
219
- || s.ends_with(".toml")
220
- }
221
-
222
- fn collect_refs_from_value(val: &Value) -> Vec<String> {
223
- let mut refs = Vec::new();
224
- match val {
225
- Value::String(s) => {
226
- if is_ref_string(s) {
227
- refs.push(s.clone());
228
- }
229
- }
230
- Value::Array(arr) => {
231
- for item in arr {
232
- refs.extend(collect_refs_from_value(item));
233
- }
234
- }
235
- Value::Object(map) => {
236
- for (key, v) in map {
237
- if key == "spec_docs"
238
- || key == "source_wiki"
239
- || key == "source_data_model"
240
- || key.contains("spec")
241
- || key.contains("doc")
242
- || key.contains("wiki")
243
- {
244
- refs.extend(collect_refs_from_value(v));
245
- } else if let Value::String(s) = v {
246
- if is_ref_string(s) {
247
- refs.push(s.clone());
248
- }
249
- }
250
- }
251
- }
252
- _ => {}
253
- }
254
- refs
255
- }
256
-
257
- fn resolve_target(arguments: &Value, scan_dirs: &[String]) -> Result<PathBuf> {
258
- if let Some(dir) = arguments.get("target_project_dir").and_then(|v| v.as_str()) {
259
- let path = PathBuf::from(dir);
260
- return std::fs::canonicalize(&path)
261
- .with_context(|| format!("Invalid target project path: {}", path.display()));
262
- }
263
-
264
- if let Some(name) = arguments.get("target_project").and_then(|v| v.as_str()) {
265
- return resolve_by_name(name, scan_dirs);
266
- }
267
-
268
- anyhow::bail!("Either 'target_project' or 'target_project_dir' is required")
269
- }
270
-
271
- fn resolve_by_name(name: &str, scan_dirs: &[String]) -> Result<PathBuf> {
272
- for scan_dir in scan_dirs {
273
- let expanded = expand_tilde(scan_dir);
274
- let expanded_path = Path::new(&expanded);
275
-
276
- if !expanded_path.exists() {
277
- continue;
278
- }
279
-
280
- let entries = match std::fs::read_dir(expanded_path) {
281
- Ok(e) => e,
282
- Err(_) => continue,
283
- };
284
-
285
- for entry in entries.flatten() {
286
- if !entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
287
- continue;
288
- }
289
-
290
- let config_path = entry.path().join(".handoff/config.toml");
291
- if !config_path.exists() {
292
- continue;
293
- }
294
-
295
- if let Ok(config) = read_config(&config_path) {
296
- if config.project.name == name {
297
- return Ok(entry.path());
298
- }
299
- }
300
- }
301
- }
302
-
303
- anyhow::bail!(
304
- "Target project '{name}' not found in scan_dirs. \
305
- Use 'target_project_dir' with an absolute path instead."
306
- )
307
- }
@@ -1,74 +0,0 @@
1
- use anyhow::{Context, Result};
2
- use serde_json::Value;
3
-
4
- use super::resolve_project_dir;
5
- use crate::storage::ensure_handoff_exists;
6
- use crate::storage::referrals::{
7
- change_referral_status, is_valid_referral_status, read_referral_by_id, read_referral_summaries,
8
- };
9
-
10
- pub fn handle_list(arguments: &Value) -> Result<String> {
11
- let project_dir = resolve_project_dir(arguments)?;
12
- let handoff = ensure_handoff_exists(&project_dir)?;
13
- let referrals_dir = handoff.join("referrals");
14
-
15
- let status_filter = arguments.get("status_filter").and_then(|v| v.as_str());
16
-
17
- if let Some(filter) = status_filter {
18
- if !is_valid_referral_status(filter) {
19
- anyhow::bail!(
20
- "Invalid status_filter: '{filter}'. Must be one of: open, acknowledged, resolved"
21
- );
22
- }
23
- }
24
-
25
- let summaries = read_referral_summaries(&referrals_dir, status_filter)?;
26
-
27
- let result = serde_json::json!({
28
- "referrals": summaries,
29
- "total": summaries.len(),
30
- });
31
-
32
- serde_json::to_string_pretty(&result).context("Failed to serialize referrals")
33
- }
34
-
35
- pub fn handle_get(arguments: &Value) -> Result<String> {
36
- let project_dir = resolve_project_dir(arguments)?;
37
- let handoff = ensure_handoff_exists(&project_dir)?;
38
- let referrals_dir = handoff.join("referrals");
39
-
40
- let referral_id = arguments
41
- .get("referral_id")
42
- .and_then(|v| v.as_str())
43
- .ok_or_else(|| anyhow::anyhow!("'referral_id' is required"))?;
44
-
45
- let (data, status) = read_referral_by_id(&referrals_dir, referral_id)?
46
- .ok_or_else(|| anyhow::anyhow!("Referral not found: {referral_id}"))?;
47
-
48
- let mut result = serde_json::to_value(&data).context("Failed to serialize referral")?;
49
- result["status"] = Value::String(status);
50
-
51
- serde_json::to_string_pretty(&result).context("Failed to serialize referral")
52
- }
53
-
54
- pub fn handle_update(arguments: &Value) -> Result<String> {
55
- let project_dir = resolve_project_dir(arguments)?;
56
- let handoff = ensure_handoff_exists(&project_dir)?;
57
- let referrals_dir = handoff.join("referrals");
58
-
59
- let referral_id = arguments
60
- .get("referral_id")
61
- .and_then(|v| v.as_str())
62
- .ok_or_else(|| anyhow::anyhow!("'referral_id' is required"))?;
63
-
64
- let status = arguments
65
- .get("status")
66
- .and_then(|v| v.as_str())
67
- .ok_or_else(|| anyhow::anyhow!("'status' is required"))?;
68
-
69
- change_referral_status(&referrals_dir, referral_id, status)?;
70
-
71
- Ok(format!(
72
- "Updated referral {referral_id}: status -> {status}"
73
- ))
74
- }