use super::protocol::JsonRpcError;
use super::schemas::{
RESOURCE_CURRENT_PLAN_URI, RESOURCE_RESTART_GUIDE_URI, RESOURCE_SESSION_HANDOFF_URI,
};
use crate::domain::{MemoryScope, OutputFormat, RouteInput, TargetTool, WakeupProfile};
use crate::lifecycle_store::{ProposeMemoryRequest, RecordMemoryRequest, TransitionMetadata};
use serde_json::{Value, json};
use std::path::{Path, PathBuf};
pub(super) fn parse_record_request(arguments: &Value) -> Result<RecordMemoryRequest, JsonRpcError> {
Ok(RecordMemoryRequest {
title: required_string(arguments, "title")?,
summary: required_string(arguments, "summary")?,
memory_type: required_string(arguments, "memory_type")?,
scope: parse_scope(arguments)?,
source_ref: required_string(arguments, "source_ref")?,
project_id: optional_string(arguments, "project_id"),
user_id: optional_string(arguments, "user_id"),
sensitivity: optional_string(arguments, "sensitivity"),
metadata: parse_metadata(arguments)?,
entities: parse_string_array(arguments, "entities")?,
tags: parse_string_array(arguments, "tags")?,
triggers: parse_string_array(arguments, "triggers")?,
related_files: parse_string_array(arguments, "related_files")?,
related_records: parse_string_array(arguments, "related_records")?,
supersedes: optional_string(arguments, "supersedes"),
applies_to: parse_string_array(arguments, "applies_to")?,
valid_until: optional_string(arguments, "valid_until"),
})
}
pub(super) fn parse_propose_request(
arguments: &Value,
) -> Result<ProposeMemoryRequest, JsonRpcError> {
Ok(ProposeMemoryRequest {
title: required_string(arguments, "title")?,
summary: required_string(arguments, "summary")?,
memory_type: required_string(arguments, "memory_type")?,
scope: parse_scope(arguments)?,
source_ref: required_string(arguments, "source_ref")?,
project_id: optional_string(arguments, "project_id"),
user_id: optional_string(arguments, "user_id"),
sensitivity: optional_string(arguments, "sensitivity"),
metadata: parse_metadata(arguments)?,
entities: parse_string_array(arguments, "entities")?,
tags: parse_string_array(arguments, "tags")?,
triggers: parse_string_array(arguments, "triggers")?,
related_files: parse_string_array(arguments, "related_files")?,
related_records: parse_string_array(arguments, "related_records")?,
supersedes: optional_string(arguments, "supersedes"),
applies_to: parse_string_array(arguments, "applies_to")?,
valid_until: optional_string(arguments, "valid_until"),
})
}
pub(super) fn parse_metadata(arguments: &Value) -> Result<TransitionMetadata, JsonRpcError> {
Ok(TransitionMetadata {
actor: optional_string(arguments, "actor"),
reason: optional_string(arguments, "reason"),
evidence_refs: parse_string_array(arguments, "evidence_refs")?,
})
}
pub(super) fn parse_string_array(
arguments: &Value,
key: &str,
) -> Result<Vec<String>, JsonRpcError> {
let Some(value) = arguments.get(key) else {
return Ok(Vec::new());
};
let items = value.as_array().ok_or_else(|| {
JsonRpcError::new(-32602, format!("field must be an array of strings: {key}"))
})?;
items
.iter()
.map(|item| {
item.as_str().map(ToString::to_string).ok_or_else(|| {
JsonRpcError::new(-32602, format!("field must be an array of strings: {key}"))
})
})
.collect()
}
pub(super) fn parse_scope(arguments: &Value) -> Result<MemoryScope, JsonRpcError> {
match required_string(arguments, "scope")?.as_str() {
"user" => Ok(MemoryScope::User),
"project" => Ok(MemoryScope::Project),
"workspace" => Ok(MemoryScope::Workspace),
"team" => Ok(MemoryScope::Team),
"agent" => Ok(MemoryScope::Agent),
other => Err(JsonRpcError::new(-32602, format!("invalid scope: {other}"))),
}
}
#[derive(Debug)]
pub(super) struct ParsedRouteRequest {
pub input: RouteInput,
pub format: OutputFormat,
}
#[derive(Debug)]
pub(super) struct ParsedWakeupRequest {
pub input: RouteInput,
pub format: OutputFormat,
pub profile: WakeupProfile,
}
#[derive(Debug)]
pub(super) struct ParsedPromptOptimizeRequest {
pub task: String,
pub cwd: String,
pub files: Vec<String>,
pub target: TargetTool,
pub profile: WakeupProfile,
pub provider: Option<String>,
pub session_id: Option<String>,
}
pub(super) fn parse_route_request(
arguments: &Value,
default_format: OutputFormat,
) -> Result<ParsedRouteRequest, JsonRpcError> {
let format = parse_output_format(arguments, "format")?.unwrap_or(default_format);
Ok(ParsedRouteRequest {
input: RouteInput {
task: required_string(arguments, "task")?,
cwd: PathBuf::from(required_string(arguments, "cwd")?),
files: parse_files(arguments)?,
target: parse_target(arguments)?.unwrap_or(TargetTool::Codex),
format,
},
format,
})
}
pub(super) fn parse_wakeup_request(arguments: &Value) -> Result<ParsedWakeupRequest, JsonRpcError> {
let format = parse_output_format(arguments, "format")?.unwrap_or(OutputFormat::Json);
let route = parse_route_request(arguments, format)?;
Ok(ParsedWakeupRequest {
input: route.input,
format,
profile: parse_profile(arguments)?.unwrap_or(WakeupProfile::Developer),
})
}
pub(super) fn parse_prompt_optimize_request(
arguments: &Value,
) -> Result<ParsedPromptOptimizeRequest, JsonRpcError> {
let target = parse_target(arguments)?.unwrap_or(TargetTool::Codex);
Ok(ParsedPromptOptimizeRequest {
task: required_string(arguments, "task")?,
cwd: required_string(arguments, "cwd")?,
files: parse_files(arguments)?,
target,
profile: parse_profile(arguments)?.unwrap_or(WakeupProfile::Project),
provider: optional_string(arguments, "provider")
.or_else(|| Some(default_provider_for_target(target).to_string())),
session_id: optional_string(arguments, "session_id"),
})
}
pub(super) fn default_provider_for_target(target: TargetTool) -> &'static str {
match target {
TargetTool::Claude => "claude",
TargetTool::Codex => "codex",
TargetTool::Opencode => "opencode",
}
}
pub(super) fn parse_target(arguments: &Value) -> Result<Option<TargetTool>, JsonRpcError> {
match optional_string(arguments, "target").as_deref() {
None => Ok(None),
Some("claude") => Ok(Some(TargetTool::Claude)),
Some("codex") => Ok(Some(TargetTool::Codex)),
Some("opencode") => Ok(Some(TargetTool::Opencode)),
Some(other) => Err(JsonRpcError::new(
-32602,
format!("invalid target: {other}"),
)),
}
}
pub(super) fn parse_output_format(
arguments: &Value,
key: &str,
) -> Result<Option<OutputFormat>, JsonRpcError> {
match optional_string(arguments, key).as_deref() {
None => Ok(None),
Some("prompt") => Ok(Some(OutputFormat::Prompt)),
Some("markdown") => Ok(Some(OutputFormat::Markdown)),
Some("json") => Ok(Some(OutputFormat::Json)),
Some(other) => Err(JsonRpcError::new(
-32602,
format!("invalid format: {other}"),
)),
}
}
pub(super) fn parse_profile(arguments: &Value) -> Result<Option<WakeupProfile>, JsonRpcError> {
match optional_string(arguments, "profile").as_deref() {
None => Ok(None),
Some("developer") => Ok(Some(WakeupProfile::Developer)),
Some("project") => Ok(Some(WakeupProfile::Project)),
Some(other) => Err(JsonRpcError::new(
-32602,
format!("invalid profile: {other}"),
)),
}
}
pub(super) fn parse_files(arguments: &Value) -> Result<Vec<String>, JsonRpcError> {
parse_string_array(arguments, "files")
}
pub(super) fn handle_prompt_get(params: &Value) -> Result<Value, JsonRpcError> {
let params = required_object(params, "params")?;
let name = required_string_from_object(params, "name")?;
let arguments = optional_object_field(params, "arguments")?.unwrap_or_else(|| json!({}));
let arguments = required_object(&arguments, "arguments")?;
match name.as_str() {
"review_lifecycle_queue" => Ok(json!({
"description": "Guide an AI client to review pending lifecycle memories.",
"messages": [{
"role": "user",
"content": {
"type": "text",
"text": format!(
"Use `memory_review_queue` to inspect pending items, then use `memory_get` or `memory_history` for the records that need more context. Apply `memory_accept` / `memory_archive` only after you can justify the decision. Reviewer focus: {}",
optional_string_from_object(arguments, "focus").unwrap_or_else(|| "validate stable preferences, constraints, and decisions".to_string())
)
}
}]
})),
"generate_project_wakeup" => Ok(json!({
"description": "Guide an AI client to build a project wakeup packet.",
"messages": [{
"role": "user",
"content": {
"type": "text",
"text": format!(
"Call `memory_wakeup` with `profile=project`, `cwd={}`, and task `{}`. After reading the packet, summarize the active project context, constraints, and the most relevant notes.",
optional_string_from_object(arguments, "cwd").unwrap_or_else(|| "<repo cwd>".to_string()),
optional_string_from_object(arguments, "task").unwrap_or_else(|| "prepare project wakeup".to_string())
)
}
}]
})),
"retrieve_project_context" => Ok(json!({
"description": "Guide an AI client to retrieve routed project context.",
"messages": [{
"role": "user",
"content": {
"type": "text",
"text": format!(
"Call `memory_search` with task `{}` and cwd `{}`. Then inspect `memory_explain` if the matched notes or project routing look suspicious.",
optional_string_from_object(arguments, "task").unwrap_or_else(|| "understand the current project state".to_string()),
optional_string_from_object(arguments, "cwd").unwrap_or_else(|| "<repo cwd>".to_string())
)
}
}]
})),
_ => Err(JsonRpcError::new(
-32601,
format!("prompt not found: {name}"),
)),
}
}
pub(super) fn handle_resource_read(
config_path: &Path,
params: &Value,
) -> Result<Value, JsonRpcError> {
let params = required_object(params, "params")?;
let uri = required_string_from_object(params, "uri")?;
let (resource_uri, relative_path, description) = match uri.as_str() {
RESOURCE_SESSION_HANDOFF_URI => (
RESOURCE_SESSION_HANDOFF_URI,
Path::new("docs/SESSION_HANDOFF.md"),
"Current spool handoff and restart context.",
),
RESOURCE_CURRENT_PLAN_URI => (
RESOURCE_CURRENT_PLAN_URI,
Path::new("docs/MCP_PROMPTS_ROUND_8_PLAN.md"),
"Current MCP prompts/resources implementation plan.",
),
RESOURCE_RESTART_GUIDE_URI => (
RESOURCE_RESTART_GUIDE_URI,
Path::new("docs/SESSION_HANDOFF.md"),
"Restart guide and current next steps extracted from the handoff.",
),
_ => {
return Err(JsonRpcError::new(
-32601,
format!("resource not found: {uri}"),
));
}
};
let config_base_dir = config_path.parent().unwrap_or_else(|| Path::new("."));
let config_resource_path = config_base_dir.join(relative_path);
let fallback_resource_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(relative_path);
let resource_path = if config_resource_path.exists() {
config_resource_path
} else {
fallback_resource_path
};
let text = std::fs::read_to_string(&resource_path).map_err(|error| {
JsonRpcError::new(
-32603,
format!(
"failed to read resource {}: {error}",
resource_path.display()
),
)
})?;
Ok(json!({
"contents": [{
"uri": resource_uri,
"mimeType": "text/markdown",
"text": text
}],
"description": description
}))
}
pub(super) fn required_object<'a>(
value: &'a Value,
field: &str,
) -> Result<&'a serde_json::Map<String, Value>, JsonRpcError> {
value
.as_object()
.ok_or_else(|| JsonRpcError::new(-32602, format!("field must be an object: {field}")))
}
pub(super) fn optional_object_field(
value: &serde_json::Map<String, Value>,
key: &str,
) -> Result<Option<Value>, JsonRpcError> {
let Some(field) = value.get(key) else {
return Ok(None);
};
if !field.is_object() {
return Err(JsonRpcError::new(
-32602,
format!("field must be an object: {key}"),
));
}
Ok(Some(field.clone()))
}
pub(super) fn required_string(value: &Value, key: &str) -> Result<String, JsonRpcError> {
value
.get(key)
.and_then(Value::as_str)
.map(ToString::to_string)
.ok_or_else(|| JsonRpcError::new(-32602, format!("missing string field: {key}")))
}
pub(super) fn optional_string(value: &Value, key: &str) -> Option<String> {
value
.get(key)
.and_then(Value::as_str)
.map(ToString::to_string)
}
pub(super) fn required_string_from_object(
value: &serde_json::Map<String, Value>,
key: &str,
) -> Result<String, JsonRpcError> {
value
.get(key)
.and_then(Value::as_str)
.map(ToString::to_string)
.ok_or_else(|| JsonRpcError::new(-32602, format!("missing string field: {key}")))
}
pub(super) fn optional_string_from_object(
value: &serde_json::Map<String, Value>,
key: &str,
) -> Option<String> {
value
.get(key)
.and_then(Value::as_str)
.map(ToString::to_string)
}