From babdb21c0adcbbbf5f1c60399ebcabd2f9eeb5a7 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Thu, 4 Jun 2026 11:09:53 -0700 Subject: [PATCH] feat: heph context reads/edits a task's context doc by id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Editing a task's canonical-context doc body previously meant looking up its `canonical_context_id` (e.g. via `heph list --json`) and then `heph node update --body`. Add a `heph context ` command that resolves the canonical-context doc from the task's outgoing links and: * prints the body with no flag, * `--body ` replaces it (`-` reads stdin, matching `node update`), * `--append ` adds a blank-line-separated paragraph. Errors clearly when the id has no canonical-context doc (e.g. a plain doc node rather than a task). Purely a client-side CLI convenience — no new RPC. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/heph/src/main.rs | 67 +++++++++++++++++++ .../heph-context-command.feature.md | 1 + 2 files changed, 68 insertions(+) create mode 100644 docs/changelog.d/heph-context-command.feature.md diff --git a/crates/heph/src/main.rs b/crates/heph/src/main.rs index d7c123b..c327f1d 100644 --- a/crates/heph/src/main.rs +++ b/crates/heph/src/main.rs @@ -160,6 +160,18 @@ enum Command { #[arg(short = 'n', long, default_value_t = 10)] n: usize, }, + /// Read or edit a task's canonical-context doc body **by task id** — no + /// manual `canonical_context_id` lookup. With neither flag, prints the body. + Context { + /// Task node id. + id: String, + /// Replace the body with this text (`-` reads from stdin). + #[arg(long)] + body: Option, + /// Append this text to the body (separated by a blank line). + #[arg(long)] + append: Option, + }, /// Working-set health — the §6.2 tensions (orange vs 6, active vs ~30, …). Health, /// Create a document node. @@ -577,6 +589,35 @@ fn main() -> Result<()> { println!("Logged to {id}"); } } + Command::Context { id, body, append } => { + let doc_id = canonical_context_id(&mut client, &id)?; + match (body, append) { + (Some(_), Some(_)) => bail!("pass only one of --body / --append"), + (Some(body), None) => { + let body = read_body_arg(Some(body))?; + client.call("node.update", json!({ "id": doc_id, "body": body }))?; + println!("Set context of {id}"); + } + (None, Some(text)) => { + let current = context_body(&mut client, &doc_id)?; + let combined = if current.trim().is_empty() { + text + } else { + format!("{}\n\n{text}", current.trim_end()) + }; + client.call("node.update", json!({ "id": doc_id, "body": combined }))?; + println!("Appended to context of {id}"); + } + (None, None) => { + let current = context_body(&mut client, &doc_id)?; + if current.trim().is_empty() { + println!("(empty context)"); + } else { + println!("{}", current.trim_end()); + } + } + } + } Command::Health => { let h = client.call("health", json!({}))?; println!( @@ -800,6 +841,32 @@ fn resolve_project(client: &mut Client, name: Option<&str>) -> Result Result { + let links = client.call("links.outgoing", json!({ "id": task_id }))?; + let dst = links + .as_array() + .into_iter() + .flatten() + .find(|l| l.get("link_type").and_then(Value::as_str) == Some("canonical-context")) + .and_then(|l| l.get("dst_id").and_then(Value::as_str)); + match dst { + Some(id) => Ok(id.to_string()), + None => bail!("{task_id} has no canonical-context doc (is it a task?)"), + } +} + +/// Fetch a context doc's current body (empty string when unset). +fn context_body(client: &mut Client, doc_id: &str) -> Result { + let node = client.call("node.get", json!({ "id": doc_id }))?; + Ok(node + .get("body") + .and_then(Value::as_str) + .unwrap_or("") + .to_string()) +} + /// `--body -` reads the body from stdin; otherwise pass it through. fn read_body_arg(body: Option) -> Result> { match body.as_deref() { diff --git a/docs/changelog.d/heph-context-command.feature.md b/docs/changelog.d/heph-context-command.feature.md new file mode 100644 index 0000000..146a99c --- /dev/null +++ b/docs/changelog.d/heph-context-command.feature.md @@ -0,0 +1 @@ +New `heph context ` command reads or edits a task's canonical-context doc body **by task id**, with no manual `canonical_context_id` lookup. With no flag it prints the body; `--body ` replaces it (`-` reads stdin, like `node update`); `--append ` adds a blank-line-separated paragraph. Errors clearly on a node that has no canonical-context doc (e.g. a plain doc, not a task).