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,354 +0,0 @@
|
|
|
1
|
-
use std::path::Path;
|
|
2
|
-
|
|
3
|
-
use anyhow::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::ensure_handoff_exists;
|
|
10
|
-
use crate::storage::git::capture_git_state;
|
|
11
|
-
use crate::storage::sessions::{
|
|
12
|
-
close_session_by_id, enforce_history_limit, generate_session_id, pause_active_sessions,
|
|
13
|
-
pause_session_by_id, read_active_sessions, update_active_session,
|
|
14
|
-
update_and_close_active_session, write_session_with_status, SessionData,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
pub fn handle(arguments: &Value) -> Result<String> {
|
|
18
|
-
let project_dir = resolve_project_dir(arguments)?;
|
|
19
|
-
|
|
20
|
-
let handoff = ensure_handoff_exists(&project_dir)?;
|
|
21
|
-
let sessions_dir = handoff.join("sessions");
|
|
22
|
-
let config_path = handoff.join("config.toml");
|
|
23
|
-
|
|
24
|
-
let close_id = arguments.get("close_session_id").and_then(|v| v.as_str());
|
|
25
|
-
let pause_id = arguments.get("pause_session_id").and_then(|v| v.as_str());
|
|
26
|
-
let pause_all = arguments
|
|
27
|
-
.get("pause_active")
|
|
28
|
-
.and_then(|v| v.as_bool())
|
|
29
|
-
.unwrap_or(false);
|
|
30
|
-
let is_pause_only = arguments
|
|
31
|
-
.get("pause_only")
|
|
32
|
-
.and_then(|v| v.as_bool())
|
|
33
|
-
.unwrap_or(false);
|
|
34
|
-
|
|
35
|
-
let summary_opt = arguments.get("summary").and_then(|v| v.as_str());
|
|
36
|
-
|
|
37
|
-
if !is_pause_only && summary_opt.is_none() {
|
|
38
|
-
anyhow::bail!("'summary' is required (unless pause_only=true)");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let summary = summary_opt.unwrap_or("");
|
|
42
|
-
|
|
43
|
-
let mut total_paused = 0usize;
|
|
44
|
-
if let Some(id) = pause_id {
|
|
45
|
-
if pause_session_by_id(&sessions_dir, id)?.is_some() {
|
|
46
|
-
total_paused = 1;
|
|
47
|
-
}
|
|
48
|
-
} else if pause_all {
|
|
49
|
-
total_paused = pause_active_sessions(&sessions_dir)?.len();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if is_pause_only {
|
|
53
|
-
let mut msg = String::new();
|
|
54
|
-
if total_paused > 0 {
|
|
55
|
-
msg.push_str(&format!("Paused {} session(s)", total_paused));
|
|
56
|
-
}
|
|
57
|
-
if let Some(id) = pause_id {
|
|
58
|
-
if total_paused == 0 {
|
|
59
|
-
msg.push_str(&format!(
|
|
60
|
-
"Warning: pause_session_id '{id}' not found among active/open sessions"
|
|
61
|
-
));
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if !pause_all && pause_id.is_none() {
|
|
65
|
-
msg.push_str("Warning: pause_only=true requires pause_session_id or pause_active=true");
|
|
66
|
-
}
|
|
67
|
-
if msg.is_empty() {
|
|
68
|
-
msg.push_str("No sessions were paused (none active)");
|
|
69
|
-
}
|
|
70
|
-
return Ok(msg);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let session_status = arguments
|
|
74
|
-
.get("session_status")
|
|
75
|
-
.and_then(|v| v.as_str())
|
|
76
|
-
.unwrap_or("closed");
|
|
77
|
-
let keep_active = session_status == "active";
|
|
78
|
-
|
|
79
|
-
let git_state = capture_git_state(&project_dir)?;
|
|
80
|
-
let now = Utc::now().to_rfc3339();
|
|
81
|
-
|
|
82
|
-
let target_session_id = arguments.get("session_id").and_then(|v| v.as_str());
|
|
83
|
-
|
|
84
|
-
let handoff_updates = SessionData {
|
|
85
|
-
version: 2,
|
|
86
|
-
id: None,
|
|
87
|
-
ended_at: Some(now),
|
|
88
|
-
summary: summary.to_string(),
|
|
89
|
-
branch: Some(git_state.branch),
|
|
90
|
-
commit: Some(git_state.commit),
|
|
91
|
-
dirty_files: git_state.dirty_files,
|
|
92
|
-
decisions: extract_array(arguments, "decisions"),
|
|
93
|
-
blockers: extract_string_array(arguments, "blockers"),
|
|
94
|
-
checklist: extract_array(arguments, "checklist"),
|
|
95
|
-
handoff_notes: extract_array(arguments, "handoff_notes"),
|
|
96
|
-
references: extract_array(arguments, "references"),
|
|
97
|
-
context_pointers: extract_array(arguments, "context_pointers"),
|
|
98
|
-
environment: arguments.get("environment").cloned(),
|
|
99
|
-
timeline: arguments
|
|
100
|
-
.get("timeline")
|
|
101
|
-
.and_then(|v| v.as_str())
|
|
102
|
-
.map(String::from),
|
|
103
|
-
label: arguments
|
|
104
|
-
.get("label")
|
|
105
|
-
.and_then(|v| v.as_str())
|
|
106
|
-
.map(String::from),
|
|
107
|
-
parent_session_id: None,
|
|
108
|
-
related_task_ids: extract_string_array(arguments, "related_task_ids"),
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
let (total_closed, path, session_id) = if let Some(id) = close_id {
|
|
112
|
-
let closed = close_session_by_id(&sessions_dir, id)?;
|
|
113
|
-
(if closed.is_some() { 1 } else { 0 }, None, None)
|
|
114
|
-
} else if pause_id.is_some() || pause_all {
|
|
115
|
-
(0, None, None)
|
|
116
|
-
} else {
|
|
117
|
-
let active = read_active_sessions(&sessions_dir)?;
|
|
118
|
-
let target = if let Some(tid) = target_session_id {
|
|
119
|
-
active.iter().find(|s| {
|
|
120
|
-
s.id.as_deref()
|
|
121
|
-
.is_some_and(|id| id == tid || id.starts_with(tid) || tid.starts_with(id))
|
|
122
|
-
})
|
|
123
|
-
} else if active.len() == 1 {
|
|
124
|
-
active.first()
|
|
125
|
-
} else if active.len() > 1 {
|
|
126
|
-
active.iter().last()
|
|
127
|
-
} else {
|
|
128
|
-
None
|
|
129
|
-
};
|
|
130
|
-
if let Some(active_session) = target {
|
|
131
|
-
let sid = active_session.id.clone().unwrap_or_default();
|
|
132
|
-
if keep_active {
|
|
133
|
-
let updated_path =
|
|
134
|
-
update_active_session(&sessions_dir, &sid, &handoff_updates, Some(arguments))?;
|
|
135
|
-
(0, updated_path, Some(sid))
|
|
136
|
-
} else {
|
|
137
|
-
let closed_path = update_and_close_active_session(
|
|
138
|
-
&sessions_dir,
|
|
139
|
-
&sid,
|
|
140
|
-
&handoff_updates,
|
|
141
|
-
Some(arguments),
|
|
142
|
-
)?;
|
|
143
|
-
(
|
|
144
|
-
if closed_path.is_some() { 1 } else { 0 },
|
|
145
|
-
closed_path,
|
|
146
|
-
Some(sid),
|
|
147
|
-
)
|
|
148
|
-
}
|
|
149
|
-
} else if target_session_id.is_some() {
|
|
150
|
-
anyhow::bail!(
|
|
151
|
-
"session_id '{}' not found among active sessions",
|
|
152
|
-
target_session_id.unwrap_or("")
|
|
153
|
-
);
|
|
154
|
-
} else {
|
|
155
|
-
let new_id = generate_session_id();
|
|
156
|
-
let mut data = handoff_updates.clone();
|
|
157
|
-
data.id = Some(new_id.clone());
|
|
158
|
-
let target_status = if keep_active { "active" } else { "closed" };
|
|
159
|
-
let p = write_session_with_status(&sessions_dir, &data, target_status)?;
|
|
160
|
-
(0, Some(p), Some(new_id))
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
let history_limit = if config_path.exists() {
|
|
165
|
-
read_config(&config_path)
|
|
166
|
-
.map(|c| c.settings.history_limit)
|
|
167
|
-
.unwrap_or(20)
|
|
168
|
-
} else {
|
|
169
|
-
20
|
|
170
|
-
};
|
|
171
|
-
let removed = enforce_history_limit(&sessions_dir, history_limit)?;
|
|
172
|
-
|
|
173
|
-
let file_display = path
|
|
174
|
-
.as_ref()
|
|
175
|
-
.and_then(|p| p.file_name())
|
|
176
|
-
.map(|n| n.to_string_lossy().to_string())
|
|
177
|
-
.unwrap_or_else(|| "(none)".to_string());
|
|
178
|
-
let sid_display = session_id.as_deref().unwrap_or("(no active session)");
|
|
179
|
-
let mut msg = format!(
|
|
180
|
-
"Session saved: {}\nSession ID: {}\nFile: {}",
|
|
181
|
-
summary, sid_display, file_display
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
if keep_active {
|
|
185
|
-
msg.push_str("\nSession kept active (session_status: active)");
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if total_paused > 0 {
|
|
189
|
-
msg.push_str(&format!("\nPaused {} session(s)", total_paused));
|
|
190
|
-
}
|
|
191
|
-
if let Some(id) = pause_id {
|
|
192
|
-
if total_paused == 0 {
|
|
193
|
-
msg.push_str(&format!(
|
|
194
|
-
"\nWarning: pause_session_id '{id}' not found among active/open sessions"
|
|
195
|
-
));
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
if total_closed > 0 {
|
|
199
|
-
msg.push_str(&format!("\nClosed {} previous session(s)", total_closed));
|
|
200
|
-
}
|
|
201
|
-
if let Some(id) = close_id {
|
|
202
|
-
if total_closed == 0 {
|
|
203
|
-
msg.push_str(&format!(
|
|
204
|
-
"\nWarning: close_session_id '{id}' not found among active/open/paused sessions"
|
|
205
|
-
));
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
if removed > 0 {
|
|
209
|
-
msg.push_str(&format!(
|
|
210
|
-
"\nRemoved {removed} old session(s) (history_limit: {history_limit})"
|
|
211
|
-
));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
for w in collect_save_warnings(&handoff_updates, &project_dir) {
|
|
215
|
-
msg.push_str(&format!("\n{w}"));
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
Ok(msg)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
fn collect_save_warnings(data: &SessionData, project_dir: &Path) -> Vec<String> {
|
|
222
|
-
let mut warnings = Vec::new();
|
|
223
|
-
|
|
224
|
-
if data.checklist.is_empty() {
|
|
225
|
-
warnings.push(
|
|
226
|
-
"Warning: No checklist items. Consider adding verification items for the next session."
|
|
227
|
-
.to_string(),
|
|
228
|
-
);
|
|
229
|
-
} else {
|
|
230
|
-
let unchecked: Vec<&str> = data
|
|
231
|
-
.checklist
|
|
232
|
-
.iter()
|
|
233
|
-
.filter_map(|item| {
|
|
234
|
-
let checked = item
|
|
235
|
-
.get("checked")
|
|
236
|
-
.and_then(|v| v.as_bool())
|
|
237
|
-
.unwrap_or(false);
|
|
238
|
-
if !checked {
|
|
239
|
-
item.get("item").and_then(|v| v.as_str())
|
|
240
|
-
} else {
|
|
241
|
-
None
|
|
242
|
-
}
|
|
243
|
-
})
|
|
244
|
-
.collect();
|
|
245
|
-
|
|
246
|
-
if !unchecked.is_empty() {
|
|
247
|
-
warnings.push(format!(
|
|
248
|
-
"Warning: {} unchecked checklist item(s) \u{2014} {}",
|
|
249
|
-
unchecked.len(),
|
|
250
|
-
unchecked.join(", ")
|
|
251
|
-
));
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
let has_suggestion = data.handoff_notes.iter().any(|note| {
|
|
256
|
-
note.get("category")
|
|
257
|
-
.and_then(|v| v.as_str())
|
|
258
|
-
.is_some_and(|c| c == "suggestion")
|
|
259
|
-
});
|
|
260
|
-
if !has_suggestion {
|
|
261
|
-
warnings.push(
|
|
262
|
-
"Warning: No 'suggestion' handoff_notes \u{2014} the next session won't know what to \
|
|
263
|
-
do first. Add at least one note with category 'suggestion' describing the recommended \
|
|
264
|
-
next action."
|
|
265
|
-
.to_string(),
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if data.context_pointers.is_empty() {
|
|
270
|
-
warnings.push(
|
|
271
|
-
"Warning: No context_pointers. The next session won't know which files to read first."
|
|
272
|
-
.to_string(),
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if data.decisions.is_empty() {
|
|
277
|
-
warnings.push(
|
|
278
|
-
"Warning: No decisions recorded. Consider documenting key decisions made during this session."
|
|
279
|
-
.to_string(),
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if data.references.is_empty() {
|
|
284
|
-
warnings.push(
|
|
285
|
-
"Warning: No references. Consider adding links to relevant docs, issues, or MRs."
|
|
286
|
-
.to_string(),
|
|
287
|
-
);
|
|
288
|
-
} else {
|
|
289
|
-
for r in &data.references {
|
|
290
|
-
let uri = r.get("uri").and_then(|v| v.as_str()).unwrap_or("");
|
|
291
|
-
if uri.is_empty() {
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
if uri.starts_with("http://") || uri.starts_with("https://") {
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
if uri.starts_with("ref-") {
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
let resolved = if Path::new(uri).is_absolute() {
|
|
301
|
-
std::path::PathBuf::from(uri)
|
|
302
|
-
} else {
|
|
303
|
-
project_dir.join(uri)
|
|
304
|
-
};
|
|
305
|
-
let check_path = resolved
|
|
306
|
-
.to_string_lossy()
|
|
307
|
-
.split('#')
|
|
308
|
-
.next()
|
|
309
|
-
.unwrap_or("")
|
|
310
|
-
.to_string();
|
|
311
|
-
if !Path::new(&check_path).exists() {
|
|
312
|
-
let label = r.get("label").and_then(|v| v.as_str()).unwrap_or("");
|
|
313
|
-
warnings.push(format!(
|
|
314
|
-
"Warning: reference '{label}' path does not exist: {uri}"
|
|
315
|
-
));
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
for cp in &data.context_pointers {
|
|
321
|
-
let p = cp.get("path").and_then(|v| v.as_str()).unwrap_or("");
|
|
322
|
-
if p.is_empty() {
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
let resolved = if Path::new(p).is_absolute() {
|
|
326
|
-
std::path::PathBuf::from(p)
|
|
327
|
-
} else {
|
|
328
|
-
project_dir.join(p)
|
|
329
|
-
};
|
|
330
|
-
if !resolved.exists() {
|
|
331
|
-
warnings.push(format!("Warning: context_pointer path does not exist: {p}"));
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
warnings
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
fn extract_array(val: &Value, key: &str) -> Vec<Value> {
|
|
339
|
-
val.get(key)
|
|
340
|
-
.and_then(|v| v.as_array())
|
|
341
|
-
.cloned()
|
|
342
|
-
.unwrap_or_default()
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
fn extract_string_array(val: &Value, key: &str) -> Vec<String> {
|
|
346
|
-
val.get(key)
|
|
347
|
-
.and_then(|v| v.as_array())
|
|
348
|
-
.map(|arr| {
|
|
349
|
-
arr.iter()
|
|
350
|
-
.filter_map(|v| v.as_str().map(String::from))
|
|
351
|
-
.collect()
|
|
352
|
-
})
|
|
353
|
-
.unwrap_or_default()
|
|
354
|
-
}
|