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,254 +0,0 @@
1
- use std::collections::HashMap;
2
- use std::path::{Path, PathBuf};
3
-
4
- use anyhow::{Context, Result};
5
- use serde_json::{json, Value};
6
- use toml_edit::{DocumentMut, Item, Table};
7
-
8
- use super::config_crud::{
9
- load_doc, require_str, save_doc, set_opt_f64, set_opt_str, set_string_array,
10
- };
11
- use super::resolve_project_dir;
12
- use crate::storage::ensure_handoff_exists;
13
- use crate::storage::tasks::{build_task_index, TaskIndex};
14
-
15
- pub fn handle(arguments: &Value) -> Result<String> {
16
- let project_dir = resolve_project_dir(arguments)?;
17
- let handoff = ensure_handoff_exists(&project_dir)?;
18
- let config_path = handoff.join("config.toml");
19
- let tasks_dir = handoff.join("tasks");
20
-
21
- let raw = std::fs::read_to_string(&config_path)
22
- .with_context(|| format!("Failed to read config: {}", config_path.display()))?;
23
- let doc: DocumentMut = raw.parse().with_context(|| "Failed to parse config.toml")?;
24
-
25
- let assignees_table = doc.get("assignees").and_then(|v| v.as_table());
26
-
27
- let mut result: HashMap<String, Value> = HashMap::new();
28
-
29
- if let Some(table) = assignees_table {
30
- for (key, item) in table.iter() {
31
- let sub = match item.as_table() {
32
- Some(t) => t,
33
- None => continue,
34
- };
35
-
36
- let display_name = sub
37
- .get("display_name")
38
- .and_then(|v| v.as_str())
39
- .unwrap_or(key);
40
- let color = sub.get("color").and_then(|v| v.as_str()).unwrap_or("");
41
- let work_hours = sub
42
- .get("work_hours_per_day")
43
- .and_then(|v| v.as_integer().or_else(|| v.as_float().map(|f| f as i64)))
44
- .unwrap_or(8);
45
- let closed_weekdays: Vec<Value> = sub
46
- .get("closed_weekdays")
47
- .and_then(|v| v.as_array())
48
- .map(|arr| {
49
- arr.iter()
50
- .filter_map(|v| {
51
- v.as_str()
52
- .map(|s| json!(s))
53
- .or_else(|| v.as_integer().map(|n| json!(n)))
54
- })
55
- .collect()
56
- })
57
- .unwrap_or_default();
58
-
59
- result.insert(
60
- key.to_string(),
61
- json!({
62
- "display_name": display_name,
63
- "color": color,
64
- "work_hours_per_day": work_hours,
65
- "closed_weekdays": closed_weekdays,
66
- "task_count": 0,
67
- "active_task_count": 0,
68
- "total_estimate_hours": 0.0,
69
- "total_actual_hours": 0.0,
70
- }),
71
- );
72
- }
73
- }
74
-
75
- // Count tasks per assignee
76
- if tasks_dir.exists() {
77
- let (tree, _) = build_task_index(&tasks_dir, u32::MAX)?;
78
- count_assignee_tasks(&tree, &mut result);
79
- }
80
-
81
- let output = json!({ "assignees": result });
82
- serde_json::to_string_pretty(&output).map_err(Into::into)
83
- }
84
-
85
- fn count_assignee_tasks(tree: &[TaskIndex], result: &mut HashMap<String, Value>) {
86
- for node in tree {
87
- if let Some(ref assignee) = node.assignee {
88
- if let Some(entry) = result.get_mut(assignee) {
89
- if let Some(obj) = entry.as_object_mut() {
90
- let tc = obj.get("task_count").and_then(|v| v.as_u64()).unwrap_or(0);
91
- obj.insert("task_count".to_string(), json!(tc + 1));
92
-
93
- if node.status == "in_progress" || node.status == "review" {
94
- let ac = obj
95
- .get("active_task_count")
96
- .and_then(|v| v.as_u64())
97
- .unwrap_or(0);
98
- obj.insert("active_task_count".to_string(), json!(ac + 1));
99
- }
100
-
101
- if let Some(ref sched) = node.schedule {
102
- if let Some(est) = sched.estimate_hours {
103
- let cur = obj
104
- .get("total_estimate_hours")
105
- .and_then(|v| v.as_f64())
106
- .unwrap_or(0.0);
107
- obj.insert("total_estimate_hours".to_string(), json!(cur + est));
108
- }
109
- if let Some(act) = sched.actual_hours {
110
- let cur = obj
111
- .get("total_actual_hours")
112
- .and_then(|v| v.as_f64())
113
- .unwrap_or(0.0);
114
- obj.insert("total_actual_hours".to_string(), json!(cur + act));
115
- }
116
- }
117
- }
118
- }
119
- }
120
-
121
- count_assignee_tasks(&node.children, result);
122
- }
123
- }
124
-
125
- /// handoff_add_assignee — create a new `[assignees.<key>]` entry. Fails if the
126
- /// key already exists.
127
- pub fn handle_add(arguments: &Value) -> Result<String> {
128
- let path = super::config_crud::config_path(arguments)?;
129
- let key = require_str(arguments, "key")?;
130
- let mut doc = load_doc(&path)?;
131
-
132
- let assignees = super::config_crud::ensure_table(&mut doc, "assignees")?;
133
- assignees.set_implicit(true);
134
- if assignees.contains_key(key) {
135
- anyhow::bail!("Assignee '{key}' already exists. Use handoff_update_assignee to modify it.");
136
- }
137
- let mut sub = Table::new();
138
- apply_assignee_fields(&mut sub, arguments);
139
- doc["assignees"][key] = Item::Table(sub);
140
-
141
- save_doc(&path, &doc)?;
142
- Ok(format!("Added assignee '{key}'"))
143
- }
144
-
145
- /// handoff_update_assignee — patch an existing `[assignees.<key>]` entry.
146
- pub fn handle_update(arguments: &Value) -> Result<String> {
147
- let path = super::config_crud::config_path(arguments)?;
148
- let key = require_str(arguments, "key")?;
149
- let mut doc = load_doc(&path)?;
150
-
151
- let exists = doc
152
- .get("assignees")
153
- .and_then(|v| v.as_table())
154
- .map(|t| t.contains_key(key))
155
- .unwrap_or(false);
156
- if !exists {
157
- anyhow::bail!("Assignee '{key}' not found. Use handoff_add_assignee to create it.");
158
- }
159
- let sub = super::config_crud::ensure_subtable(&mut doc, "assignees", key)?;
160
- apply_assignee_fields(sub, arguments);
161
-
162
- save_doc(&path, &doc)?;
163
- Ok(format!("Updated assignee '{key}'"))
164
- }
165
-
166
- /// handoff_remove_assignee — delete a `[assignees.<key>]` entry and unassign it
167
- /// from every task (matches the VSCode extension's removeAssignee behaviour).
168
- pub fn handle_remove(arguments: &Value) -> Result<String> {
169
- let path = super::config_crud::config_path(arguments)?;
170
- let key = require_str(arguments, "key")?;
171
- let mut doc = load_doc(&path)?;
172
-
173
- let removed = doc
174
- .get_mut("assignees")
175
- .and_then(|v| v.as_table_mut())
176
- .map(|t| t.remove(key).is_some())
177
- .unwrap_or(false);
178
- if !removed {
179
- anyhow::bail!("Assignee '{key}' not found.");
180
- }
181
- save_doc(&path, &doc)?;
182
-
183
- // Unassign from tasks.
184
- let tasks_dir = path
185
- .parent()
186
- .map(|p| p.join("tasks"))
187
- .ok_or_else(|| anyhow::anyhow!("Cannot locate tasks dir"))?;
188
- let unassigned = unassign_all(&tasks_dir, key)?;
189
-
190
- Ok(format!(
191
- "Removed assignee '{key}' and unassigned it from {unassigned} task(s)"
192
- ))
193
- }
194
-
195
- /// Apply the optional assignee fields from `arguments` onto a TOML table.
196
- fn apply_assignee_fields(table: &mut Table, arguments: &Value) {
197
- table.set_implicit(false);
198
- set_opt_str(table, "display_name", arguments.get("display_name"));
199
- set_opt_str(table, "color", arguments.get("color"));
200
- set_opt_f64(
201
- table,
202
- "work_hours_per_day",
203
- arguments.get("work_hours_per_day"),
204
- );
205
- super::config_crud::set_mixed_array(table, "closed_weekdays", arguments.get("closed_weekdays"));
206
- set_string_array(table, "closed_dates", arguments.get("closed_dates"));
207
- set_string_array(table, "open_dates", arguments.get("open_dates"));
208
- super::config_crud::set_f64_map(table, "day_hours", arguments.get("day_hours"));
209
- }
210
-
211
- /// Clear `assignee` on every task currently assigned to `key`. Returns the count.
212
- fn unassign_all(tasks_dir: &Path, key: &str) -> Result<usize> {
213
- use crate::storage::tasks::{read_task, write_task};
214
- use chrono::Utc;
215
-
216
- let mut count = 0;
217
- let dirs = collect_task_dirs(tasks_dir)?;
218
- for dir in dirs {
219
- if let Some((mut data, status)) = read_task(&dir)? {
220
- if data.assignee.as_deref() == Some(key) {
221
- data.assignee = None;
222
- data.updated_at = Some(Utc::now().to_rfc3339());
223
- write_task(&dir, &status, &data)?;
224
- count += 1;
225
- }
226
- }
227
- }
228
- Ok(count)
229
- }
230
-
231
- /// Recursively collect every task directory under `tasks_dir`.
232
- fn collect_task_dirs(dir: &Path) -> Result<Vec<PathBuf>> {
233
- let mut out = Vec::new();
234
- collect_task_dirs_into(dir, &mut out)?;
235
- Ok(out)
236
- }
237
-
238
- fn collect_task_dirs_into(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
239
- if !dir.exists() {
240
- return Ok(());
241
- }
242
- for entry in std::fs::read_dir(dir)? {
243
- let entry = entry?;
244
- let path = entry.path();
245
- if path.is_dir() {
246
- // A task directory contains a `_task.<status>.json` file.
247
- if crate::storage::tasks::find_task_file(&path)?.is_some() {
248
- out.push(path.clone());
249
- }
250
- collect_task_dirs_into(&path, out)?;
251
- }
252
- }
253
- Ok(())
254
- }