cubesys/cube-notes-mcp: project resume + context. 'cube project resume <name> <text>' sets a resume marker (note category=resume, associated to the project); 'cube project context <name>' emits a compact 'where we are' summary (latest notes + resume marker) for context injection into a new session. MCP tools project_resume/project_context added. 5 notes tests pass.
This commit is contained in:
@@ -67,6 +67,20 @@ def tool_schemas():
|
||||
"coord": {"type": "string"},
|
||||
}, "required": ["coord"]},
|
||||
},
|
||||
{
|
||||
"name": "project_resume",
|
||||
"description": "Set a resume marker for a project (where it left off).",
|
||||
"inputSchema": {"type": "object", "properties": {
|
||||
"name": {"type": "string"}, "text": {"type": "string"},
|
||||
}, "required": ["name", "text"]},
|
||||
},
|
||||
{
|
||||
"name": "project_context",
|
||||
"description": "Compact 'where we are' summary for a project (latest notes + resume marker) — for context injection.",
|
||||
"inputSchema": {"type": "object", "properties": {
|
||||
"name": {"type": "string"},
|
||||
}, "required": ["name"]},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -116,6 +130,15 @@ def dispatch(name, args):
|
||||
if name == "project_show":
|
||||
c = (args.get("coord") or "").strip()
|
||||
return run_cube(["project", "show", c]) if c else "error: missing 'coord'"
|
||||
if name == "project_resume":
|
||||
nm = (args.get("name") or "").strip()
|
||||
tx = (args.get("text") or "").strip()
|
||||
if not nm or not tx:
|
||||
return "error: project_resume needs name + text"
|
||||
return run_cube(["project", "resume", nm, tx])
|
||||
if name == "project_context":
|
||||
nm = (args.get("name") or "").strip()
|
||||
return run_cube(["project", "context", nm]) if nm else "error: missing 'name'"
|
||||
return f"error: unknown tool {name}"
|
||||
|
||||
|
||||
|
||||
+78
-2
@@ -546,7 +546,53 @@ fn filter_from_flags(flags: &[(String, String)]) -> NoteFilter {
|
||||
f
|
||||
}
|
||||
|
||||
/// Process a `project` command line: `project list` or `project show <C.Z.Y.X>`.
|
||||
/// Set a "resume" marker for a project. Stored as a note tagged
|
||||
/// `category = "resume"` and associated to the project, so a new session can
|
||||
/// ask "where did this project leave off?" and get it back.
|
||||
pub fn project_resume(
|
||||
store: &ConcurrentStore,
|
||||
name: &str,
|
||||
text: &str,
|
||||
session: &str,
|
||||
) -> Result<Czyx, String> {
|
||||
let subject = format!("resume: {name}");
|
||||
store_note(store, session, &subject, text, Some(name), Some("resume"))
|
||||
}
|
||||
|
||||
/// Build a compact "project state" summary for context injection: the project
|
||||
/// name, its note count, the resume marker (if any), and the latest notes with
|
||||
/// date/category. A new session reads this to know where a project left off.
|
||||
pub fn project_context(store: &ConcurrentStore, name: &str) -> String {
|
||||
let Some(coord) = find_project(store, name) else {
|
||||
return format!("(no project '{name}')");
|
||||
};
|
||||
let notes = project_walk(store, coord);
|
||||
let mut out = String::new();
|
||||
out.push_str(&format!("Project: {name}\n"));
|
||||
out.push_str(&format!(" {} note(s); thread {}.\n", notes.len(), coord_str(coord)));
|
||||
if let Some(r) = notes
|
||||
.iter()
|
||||
.find(|n| category_of(n.header.doc_type.as_deref().unwrap_or("")) == Some("resume"))
|
||||
{
|
||||
out.push_str(&format!(
|
||||
" RESUME: {}\n",
|
||||
String::from_utf8_lossy(&r.body).trim()
|
||||
));
|
||||
}
|
||||
out.push_str(" Latest:\n");
|
||||
for n in notes.iter().take(10) {
|
||||
let subj = n.header.title.as_deref().unwrap_or("");
|
||||
let when = n.header.created_at.map(|t| date_string(t)).unwrap_or_else(|| "?".into());
|
||||
let cat = category_of(n.header.doc_type.as_deref().unwrap_or(""))
|
||||
.map(|c| format!("[{c}] "))
|
||||
.unwrap_or_default();
|
||||
out.push_str(&format!(" {when} {cat}{subj}\n"));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Process a `project` command line: `project list`, `project show <C.Z.Y.X>`,
|
||||
/// `project resume <name> <text>`, `project context <name>`.
|
||||
pub fn project_command(store: &ConcurrentStore, line: &str) -> Result<String, String> {
|
||||
let body = line
|
||||
.splitn(2, char::is_whitespace)
|
||||
@@ -576,7 +622,26 @@ pub fn project_command(store: &ConcurrentStore, line: &str) -> Result<String, St
|
||||
None => Ok(format!("project {cs} -> (no project record)")),
|
||||
}
|
||||
}
|
||||
_ => Err(format!("project: unknown subcommand '{head}' (want list|show)")),
|
||||
"resume" => {
|
||||
let (_, pos) = split_flags(rest);
|
||||
let mut words = pos.splitn(2, char::is_whitespace);
|
||||
let name = words.next().unwrap_or("").to_string();
|
||||
let text = words.next().unwrap_or("").trim().to_string();
|
||||
if name.is_empty() || text.is_empty() {
|
||||
return Err("project resume needs <name> <text>".to_string());
|
||||
}
|
||||
let session = default_session();
|
||||
let coord = project_resume(store, &name, &text, &session)?;
|
||||
Ok(format!("resume set for '{name}' at {} (session {})", coord_str(coord), session))
|
||||
}
|
||||
"context" => {
|
||||
let name = rest.trim();
|
||||
if name.is_empty() {
|
||||
return Err("project context needs <name>".to_string());
|
||||
}
|
||||
Ok(project_context(store, name))
|
||||
}
|
||||
_ => Err(format!("project: unknown subcommand '{head}' (want list|show|resume|context)")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,4 +740,15 @@ mod tests {
|
||||
assert_eq!(date_to_day("2026-09-08"), Some(20_704));
|
||||
assert_eq!(date_string(20_704 * 86_400), "2026-09-08");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_resume_and_context() {
|
||||
let store = ConcurrentStore::memory();
|
||||
store_note(&store, "s", "design", "widget design", Some("p"), Some("design")).unwrap();
|
||||
project_resume(&store, "p", "at the design review", "s").unwrap();
|
||||
let ctx = project_context(&store, "p");
|
||||
assert!(ctx.contains("Project: p"));
|
||||
assert!(ctx.contains("RESUME: at the design review"));
|
||||
assert!(ctx.contains("[design]"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user