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.
- package/README.md +31 -4
- package/bin/handoff-mcp.js +56 -13
- package/bin/resolve-binary.js +122 -0
- package/package.json +14 -9
- package/Cargo.lock +0 -686
- package/Cargo.toml +0 -30
- package/scripts/cargo-env.sh +0 -29
- package/scripts/handoff-memory-hook.py +0 -208
- package/scripts/install-local.sh +0 -109
- package/scripts/postinstall.js +0 -50
- package/scripts/sync-plugin-skills.sh +0 -35
- package/scripts/sync-plugin-version.sh +0 -85
- package/scripts/sync-workflow-inline.sh +0 -138
- package/src/cli.rs +0 -551
- package/src/context/injection.rs +0 -276
- package/src/context/mod.rs +0 -129
- package/src/lib.rs +0 -5
- package/src/main.rs +0 -157
- package/src/mcp/handlers/assignees.rs +0 -254
- package/src/mcp/handlers/auto_schedule.rs +0 -489
- package/src/mcp/handlers/bulk_update.rs +0 -155
- package/src/mcp/handlers/calendar.rs +0 -196
- package/src/mcp/handlers/capacity.rs +0 -318
- package/src/mcp/handlers/check_criterion.rs +0 -70
- package/src/mcp/handlers/config.rs +0 -402
- package/src/mcp/handlers/config_crud.rs +0 -183
- package/src/mcp/handlers/dashboard.rs +0 -214
- package/src/mcp/handlers/docs.rs +0 -2288
- package/src/mcp/handlers/docs_query.rs +0 -1335
- package/src/mcp/handlers/fork_session.rs +0 -91
- package/src/mcp/handlers/get_session.rs +0 -48
- package/src/mcp/handlers/get_task.rs +0 -53
- package/src/mcp/handlers/import_context.rs +0 -470
- package/src/mcp/handlers/init.rs +0 -28
- package/src/mcp/handlers/list_sessions.rs +0 -187
- package/src/mcp/handlers/list_tasks.rs +0 -308
- package/src/mcp/handlers/load_context.rs +0 -361
- package/src/mcp/handlers/log_time.rs +0 -67
- package/src/mcp/handlers/memory.rs +0 -961
- package/src/mcp/handlers/merge_sessions.rs +0 -103
- package/src/mcp/handlers/metrics.rs +0 -196
- package/src/mcp/handlers/milestones.rs +0 -102
- package/src/mcp/handlers/mod.rs +0 -140
- package/src/mcp/handlers/refer.rs +0 -307
- package/src/mcp/handlers/referrals.rs +0 -74
- package/src/mcp/handlers/save_context.rs +0 -354
- package/src/mcp/handlers/task_checklist.rs +0 -507
- package/src/mcp/handlers/timer.rs +0 -529
- package/src/mcp/handlers/update_session.rs +0 -197
- package/src/mcp/handlers/update_task.rs +0 -452
- package/src/mcp/mod.rs +0 -6
- package/src/mcp/protocol.rs +0 -41
- package/src/mcp/resources.rs +0 -57
- package/src/mcp/router.rs +0 -154
- package/src/mcp/tools.rs +0 -1522
- package/src/mcp/types.rs +0 -108
- package/src/setup.rs +0 -1212
- package/src/storage/config.rs +0 -578
- package/src/storage/docs/frontmatter.rs +0 -509
- package/src/storage/docs/mod.rs +0 -835
- package/src/storage/docs/model.rs +0 -708
- package/src/storage/docs/reassemble.rs +0 -167
- package/src/storage/docs/split.rs +0 -377
- package/src/storage/git.rs +0 -47
- package/src/storage/memory/injected.rs +0 -340
- package/src/storage/memory/mod.rs +0 -236
- package/src/storage/memory/model.rs +0 -127
- package/src/storage/mod.rs +0 -96
- package/src/storage/referrals.rs +0 -248
- package/src/storage/sessions.rs +0 -859
- package/src/storage/tasks.rs +0 -957
- 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
|
-
}
|