use async_trait::async_trait;
use std::path::PathBuf;
use crate::base::{AgentHook, BaseHook, LifecycleCapabilities, SessionEndCallback};
use crate::error::{HookError, Result};
use crate::monitor::ProcessMonitor;
use crate::session::SessionContext;
use crate::types::{AgentType, SessionActivity, SupportTier};
pub struct ClaudeCodeHook {
base: BaseHook,
skill_path: PathBuf,
skill_installed: bool,
settings_hook_installed: bool,
process_monitor: ProcessMonitor,
}
impl ClaudeCodeHook {
pub const SKILL_NAME: &'static str = "nexus-memory-extraction";
pub const CONFIG_DIR: &'static str = ".claude";
pub const SKILLS_DIR: &'static str = "skills";
pub fn new() -> Self {
Self::new_with_install(true)
}
pub fn new_readonly() -> Self {
Self::new_with_install(false)
}
fn new_with_install(should_install: bool) -> Self {
let skill_path = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(Self::CONFIG_DIR)
.join(Self::SKILLS_DIR)
.join(Self::SKILL_NAME);
let mut hook = Self {
base: BaseHook::new("claude-code"),
skill_path,
skill_installed: false,
settings_hook_installed: Self::has_settings_hook(),
process_monitor: ProcessMonitor::new(),
};
if should_install {
if let Err(e) = hook.install_skill() {
tracing::warn!("Failed to install Claude Code skill: {}", e);
}
}
hook
}
fn install_skill(&mut self) -> Result<()> {
std::fs::create_dir_all(&self.skill_path).map_err(|e| {
HookError::InstallationFailed(format!("Failed to create skill dir: {}", e))
})?;
let skill_md = self.skill_path.join("SKILL.md");
let skill_content = r#"---
name: nexus-memory-extraction
description: Automatically extract session context to Nexus Memory System
version: 1.0.0
author: Nexus Memory System
trigger:
- on_session_end
- on_checkpoint
- on_completion
- on_error
priority: high
---
# Nexus Memory Extraction Skill
## Overview
This skill automatically triggers when your Claude Code session ends, ensuring no context is lost.
## What It Does
1. **Captures Context**: Extracts current conversation, decisions, and context
2. **Summarizes**: Creates structured summary of key points
3. **Stores**: Automatically stores to Nexus Memory System
4. **Confirms**: Shows what was stored
## Triggers
- **on_session_end**: When you close Claude Code
- **on_checkpoint**: At periodic checkpoints during long sessions
- **on_completion**: When a task is completed
- **on_error**: If an error occurs (stores context for debugging)
## No Manual Action Required
This skill runs automatically. You don't need to remember to trigger it.
You do not need to start a Nexus server manually for normal CLI memory capture.
## Configuration
The skill reads from:
- `NEXUS_AUTO_INGEST=true` environment variable
- the local Nexus CLI runtime for default operation
Optional:
- an external Nexus endpoint only when explicitly configured for advanced remote workflows
## Output
After storing, you'll see:
```
[Nexus] Stored 3 memories from Claude Code session:
- 2 decisions
- 1 context item
- Memory IDs: nexus_123, nexus_124, nexus_125
```
"#;
std::fs::write(&skill_md, skill_content).map_err(|e| {
HookError::InstallationFailed(format!("Failed to write skill file: {}", e))
})?;
self.skill_installed = true;
tracing::info!("Claude Code Skill installed at: {:?}", self.skill_path);
Ok(())
}
fn settings_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(Self::CONFIG_DIR)
.join("settings.json")
}
fn install_settings_hook(&mut self) -> Result<()> {
let settings_path = Self::settings_path();
let command = Self::desired_session_start_command();
let mut settings = if settings_path.exists() {
let content = std::fs::read_to_string(&settings_path).map_err(|e| {
HookError::InstallationFailed(format!("Failed to read settings.json: {}", e))
})?;
serde_json::from_str::<serde_json::Value>(&content).map_err(|e| {
HookError::InstallationFailed(format!("Failed to parse settings.json: {}", e))
})?
} else {
serde_json::json!({})
};
Self::upsert_session_start_hook(&mut settings, &command)?;
let subconscious_mode = std::env::var("NEXUS_SUBCONSCIOUS_MODE")
.unwrap_or_default()
.to_lowercase();
if subconscious_mode != "off" {
for event_type in ["UserPromptSubmit", "PreToolUse", "Stop"] {
let cmd = Self::desired_subconscious_command(event_type);
if cmd.is_empty() {
continue;
}
Self::upsert_hook_entry(&mut settings, event_type, &cmd, &|command: &str| {
Self::command_is_subconscious_hook(command, event_type)
})?;
}
}
let serialized = serde_json::to_string_pretty(&settings).map_err(|e| {
HookError::InstallationFailed(format!("Failed to serialize settings: {}", e))
})?;
if let Some(parent) = settings_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
HookError::InstallationFailed(format!("Failed to create settings dir: {}", e))
})?;
}
std::fs::write(&settings_path, serialized).map_err(|e| {
HookError::InstallationFailed(format!("Failed to write settings.json: {}", e))
})?;
self.settings_hook_installed = true;
tracing::info!(
"Claude Code SessionStart hook written to: {:?}",
settings_path
);
Ok(())
}
fn find_nexus_binary() -> String {
if let Ok(bin) = std::env::var("NEXUS_HOOK_BINARY") {
if !bin.trim().is_empty() {
return bin;
}
}
if let Ok(current_exe) = std::env::current_exe() {
if current_exe
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name == "nexus")
{
return current_exe.to_string_lossy().to_string();
}
}
let candidates: Vec<PathBuf> = [
dirs::home_dir().map(|h| h.join(".local").join("bin").join("nexus")),
Some(PathBuf::from("/usr/local/bin/nexus")),
]
.into_iter()
.flatten()
.collect();
for candidate in candidates {
if candidate.exists() {
return candidate.to_string_lossy().to_string();
}
}
"nexus".to_string()
}
fn desired_session_start_command() -> String {
let nexus_bin = Self::find_nexus_binary();
format!(
"'{}' session start --agent claude-code --mode session",
nexus_bin.replace('\'', "'\\''")
)
}
fn desired_subconscious_command(event_type: &str) -> String {
let nexus_bin = Self::find_nexus_binary();
let escaped = nexus_bin.replace('\'', "'\\''");
match event_type {
"UserPromptSubmit" => format!("'{}' subconscious recall --agent claude-code", escaped),
"PreToolUse" => format!("'{}' subconscious sync-check --agent claude-code", escaped),
"Stop" => format!(
"'{}' subconscious ingest-transcript --agent claude-code",
escaped
),
_ => String::new(),
}
}
fn command_is_subconscious_hook(command: &str, event_type: &str) -> bool {
command.contains("nexus")
&& command.contains("subconscious")
&& match event_type {
"UserPromptSubmit" => command.contains("recall"),
"PreToolUse" => command.contains("sync-check"),
"Stop" => command.contains("ingest-transcript"),
_ => false,
}
}
fn upsert_hook_entry(
settings: &mut serde_json::Value,
event_type: &str,
desired_command: &str,
is_match: &dyn Fn(&str) -> bool,
) -> Result<()> {
let settings_obj = settings.as_object_mut().ok_or_else(|| {
HookError::InstallationFailed(
"settings.json must contain a top-level JSON object".to_string(),
)
})?;
let hooks = settings_obj
.entry("hooks")
.or_insert_with(|| serde_json::json!({}));
let hooks_obj = hooks.as_object_mut().ok_or_else(|| {
HookError::InstallationFailed("'hooks' must be a JSON object".to_string())
})?;
let event_arr = hooks_obj
.entry(event_type)
.or_insert_with(|| serde_json::json!([]));
let entries = event_arr.as_array_mut().ok_or_else(|| {
HookError::InstallationFailed(format!("'hooks.{}' must be an array", event_type))
})?;
for entry in entries.iter_mut() {
if entry
.get("command")
.and_then(|v| v.as_str())
.map(is_match)
.unwrap_or(false)
{
*entry = serde_json::json!({
"matcher": "",
"hooks": [{
"type": "command",
"command": desired_command,
}]
});
return Ok(());
}
if let Some(hooks) = entry.get_mut("hooks").and_then(|v| v.as_array_mut()) {
for hook in hooks.iter_mut() {
if hook
.get("command")
.and_then(|v| v.as_str())
.map(is_match)
.unwrap_or(false)
{
*hook = serde_json::json!({
"type": "command",
"command": desired_command,
});
return Ok(());
}
}
}
}
entries.push(serde_json::json!({
"matcher": "",
"hooks": [{
"type": "command",
"command": desired_command,
}]
}));
Ok(())
}
fn has_settings_hook() -> bool {
let settings_path = Self::settings_path();
let Ok(content) = std::fs::read_to_string(settings_path) else {
return false;
};
let Ok(settings) = serde_json::from_str::<serde_json::Value>(&content) else {
return false;
};
let desired_command = Self::desired_session_start_command();
settings
.get("hooks")
.and_then(|hooks| hooks.get("SessionStart"))
.and_then(|value| value.as_array())
.is_some_and(|entries| {
entries.iter().any(|entry| {
Self::entry_contains_exact_session_start_hook(entry, &desired_command)
})
})
}
#[cfg(test)]
fn entry_has_session_start_hook(entry: &serde_json::Value) -> bool {
entry
.get("command")
.and_then(|command| command.as_str())
.map(Self::command_is_session_start_hook)
.unwrap_or(false)
|| entry
.get("hooks")
.and_then(|hooks| hooks.as_array())
.is_some_and(|hooks| {
hooks.iter().any(|hook| {
hook.get("command")
.and_then(|command| command.as_str())
.map(Self::command_is_session_start_hook)
.unwrap_or(false)
})
})
}
fn command_is_session_start_hook(command: &str) -> bool {
command.contains("nexus")
&& command.contains("session start")
&& command.contains("claude-code")
}
fn entry_contains_exact_session_start_hook(
entry: &serde_json::Value,
desired_command: &str,
) -> bool {
entry
.get("command")
.and_then(|command| command.as_str())
.map(|command| command == desired_command)
.unwrap_or(false)
|| entry
.get("hooks")
.and_then(|hooks| hooks.as_array())
.is_some_and(|hooks| {
hooks.iter().any(|hook| {
hook.get("command")
.and_then(|command| command.as_str())
.map(|command| command == desired_command)
.unwrap_or(false)
})
})
}
fn upsert_session_start_hook(
settings: &mut serde_json::Value,
desired_command: &str,
) -> Result<()> {
let settings_obj = settings.as_object_mut().ok_or_else(|| {
HookError::InstallationFailed(
"settings.json must contain a top-level JSON object".to_string(),
)
})?;
let hooks = settings_obj
.entry("hooks")
.or_insert_with(|| serde_json::json!({}));
let hooks_obj = hooks.as_object_mut().ok_or_else(|| {
HookError::InstallationFailed("'hooks' must be a JSON object".to_string())
})?;
let session_start = hooks_obj
.entry("SessionStart")
.or_insert_with(|| serde_json::json!([]));
let entries = session_start.as_array_mut().ok_or_else(|| {
HookError::InstallationFailed("'hooks.SessionStart' must be an array".to_string())
})?;
if Self::replace_existing_session_start_hook(entries, desired_command) {
return Ok(());
}
entries.push(serde_json::json!({
"matcher": "",
"hooks": [{
"type": "command",
"command": desired_command,
}]
}));
Ok(())
}
fn replace_existing_session_start_hook(
entries: &mut [serde_json::Value],
desired_command: &str,
) -> bool {
for entry in entries {
if entry
.get("command")
.and_then(|value| value.as_str())
.is_some_and(Self::command_is_session_start_hook)
{
*entry = serde_json::json!({
"matcher": "",
"hooks": [{
"type": "command",
"command": desired_command,
}]
});
return true;
}
if let Some(hooks) = entry
.get_mut("hooks")
.and_then(|value| value.as_array_mut())
{
for hook in hooks {
if hook
.get("command")
.and_then(|value| value.as_str())
.is_some_and(Self::command_is_session_start_hook)
{
*hook = serde_json::json!({
"type": "command",
"command": desired_command,
});
return true;
}
}
}
}
false
}
fn read_session_file(&self) -> Option<serde_json::Value> {
let session_file = dirs::home_dir()?
.join(Self::CONFIG_DIR)
.join("session.json");
if session_file.exists() {
let content = std::fs::read_to_string(&session_file).ok()?;
serde_json::from_str(&content).ok()
} else {
None
}
}
fn read_checkpoint_data(&self) -> Option<Vec<serde_json::Value>> {
let checkpoint_dir = dirs::home_dir()?.join(Self::CONFIG_DIR).join("checkpoints");
if !checkpoint_dir.exists() {
return None;
}
let mut checkpoints = Vec::new();
if let Ok(entries) = std::fs::read_dir(&checkpoint_dir) {
for entry in entries.flatten() {
if entry
.path()
.extension()
.map(|e| e == "json")
.unwrap_or(false)
{
if let Ok(content) = std::fs::read_to_string(entry.path()) {
if let Ok(data) = serde_json::from_str(&content) {
checkpoints.push(data);
}
}
}
}
}
Some(checkpoints)
}
}
impl Default for ClaudeCodeHook {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl AgentHook for ClaudeCodeHook {
fn agent_type(&self) -> &str {
&self.base.agent_type
}
async fn install_session_end_hook(&mut self, callback: SessionEndCallback) -> Result<()> {
self.base.add_callback(callback);
self.base.installed = true;
if !self.skill_installed {
tracing::warn!("Claude Code Skill not installed, using fallback detection");
}
Ok(())
}
async fn install_session_start_hook(&mut self, callback: SessionEndCallback) -> Result<()> {
self.base.add_session_start_callback(callback);
self.install_settings_hook()?;
Ok(())
}
async fn install_checkpoint_hook(&mut self, callback: SessionEndCallback) -> Result<()> {
self.base.add_checkpoint_callback(callback);
Ok(())
}
async fn install_compact_hook(&mut self, callback: SessionEndCallback) -> Result<()> {
self.base.add_callback(callback);
Ok(())
}
async fn install_error_hook(&mut self, callback: SessionEndCallback) -> Result<()> {
self.base.add_error_callback(callback);
Ok(())
}
async fn detect_session_activity(&self) -> Result<SessionActivity> {
let mut recent_content = "claude session active".to_string();
if let Some(session) = self.read_session_file() {
if let Some(messages) = session.get("messages").and_then(|m| m.as_array()) {
if let Some(last_msg) = messages.last() {
if let Some(content) = last_msg.get("content").and_then(|c| c.as_str()) {
recent_content = content.to_string();
}
}
}
}
let mut monitor = self.process_monitor.clone();
let processes = monitor.find_agent_processes(AgentType::ClaudeCode);
let mut activity = SessionActivity::new(AgentType::ClaudeCode);
if !processes.is_empty() {
activity.is_active = true;
activity.processes = processes;
self.base.record_activity_with_content(&recent_content);
}
if let Some(session) = self.read_session_file() {
if let Some(id) = session.get("session_id").and_then(|s| s.as_str()) {
activity.session_id = Some(id.to_string());
}
}
Ok(activity)
}
async fn extract_session_context(&self) -> Result<SessionContext> {
let mut context = SessionContext::new("claude-code")
.with_source("native")
.with_reliability(1.0);
if let Some(session) = self.read_session_file() {
if let Some(messages) = session.get("messages").and_then(|m| m.as_array()) {
for msg in messages {
let role = msg
.get("role")
.and_then(|r| r.as_str())
.unwrap_or("unknown");
let content = msg.get("content").and_then(|c| c.as_str()).unwrap_or("");
context.add_message(role, content);
}
}
if let Some(project_ctx) = session.get("project_context") {
context.add_custom("project_context", project_ctx.clone());
}
}
if let Some(checkpoints) = self.read_checkpoint_data() {
for checkpoint in checkpoints {
if let Some(decisions) = checkpoint.get("decisions").and_then(|d| d.as_array()) {
for decision in decisions {
if let Some(summary) = decision.get("summary").and_then(|s| s.as_str()) {
let mut dec = crate::session::Decision::new(summary);
if let Some(rationale) =
decision.get("rationale").and_then(|r| r.as_str())
{
dec.rationale = Some(rationale.to_string());
}
context.add_decision(dec);
}
}
}
if let Some(files) = checkpoint.get("files").and_then(|f| f.as_array()) {
for file in files {
if let Some(path) = file.get("path").and_then(|p| p.as_str()) {
let action = file
.get("action")
.and_then(|a| a.as_str())
.unwrap_or("modified");
let file_action = match action {
"created" => crate::session::FileAction::Created,
"deleted" => crate::session::FileAction::Deleted,
"read" => crate::session::FileAction::Read,
_ => crate::session::FileAction::Modified,
};
context.add_file(crate::session::FileInfo::new(path, file_action));
}
}
}
}
}
context.complete();
Ok(context)
}
fn is_hook_installed(&self) -> bool {
self.skill_installed || self.settings_hook_installed
}
fn reliability_score(&self) -> f32 {
if self.skill_installed && self.settings_hook_installed {
1.0
} else if self.skill_installed || self.settings_hook_installed {
0.98
} else {
0.95 }
}
fn lifecycle_capabilities(&self) -> LifecycleCapabilities {
LifecycleCapabilities {
session_start: true,
session_end: true,
checkpoint: true,
error_hook: true,
compact: true,
}
}
fn support_tier(&self) -> SupportTier {
SupportTier::NativeLifecycle
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_claude_hook_new() {
let hook = ClaudeCodeHook::new();
assert_eq!(hook.agent_type(), "claude-code");
}
#[tokio::test]
async fn test_claude_hook_detect_activity() {
let hook = ClaudeCodeHook::new();
let activity = hook.detect_session_activity().await.unwrap();
assert_eq!(activity.agent_type, AgentType::ClaudeCode);
}
#[test]
fn test_claude_hook_lifecycle_capabilities() {
let hook = ClaudeCodeHook::new();
let caps = hook.lifecycle_capabilities();
assert!(
caps.session_start,
"Claude Code should support session_start"
);
assert!(caps.session_end, "Claude Code should support session_end");
assert!(caps.checkpoint, "Claude Code should support checkpoint");
assert!(caps.error_hook, "Claude Code should support error_hook");
assert!(caps.compact, "Claude Code should support compact");
}
#[tokio::test]
async fn test_claude_hook_install_session_start() {
let mut hook = ClaudeCodeHook::new();
let callback = std::sync::Arc::new(|_ctx| {});
let result = hook.install_session_start_hook(callback).await;
match result {
Ok(()) => {
assert!(hook.settings_hook_installed);
}
Err(HookError::InstallationFailed(_)) => {
}
Err(HookError::NotSupported(msg)) => {
panic!(
"Session start should be supported for Claude Code, got: {}",
msg
);
}
Err(e) => {
panic!("Unexpected error: {}", e);
}
}
}
#[tokio::test]
async fn test_claude_hook_install_checkpoint_supported() {
let mut hook = ClaudeCodeHook::new();
let callback = std::sync::Arc::new(|_ctx| {});
let result = hook.install_checkpoint_hook(callback).await;
assert!(
result.is_ok(),
"Checkpoint should be supported for Claude Code"
);
}
#[tokio::test]
async fn test_claude_hook_install_error_supported() {
let mut hook = ClaudeCodeHook::new();
let callback = std::sync::Arc::new(|_ctx| {});
let result = hook.install_error_hook(callback).await;
assert!(
result.is_ok(),
"Error hook should be supported for Claude Code"
);
}
#[test]
fn test_find_nexus_binary() {
let bin = ClaudeCodeHook::find_nexus_binary();
assert!(!bin.is_empty());
assert!(bin.contains("nexus"));
}
#[test]
fn test_entry_has_session_start_hook_detects_nested_command() {
let entry = serde_json::json!({
"matcher": "",
"hooks": [
{
"type": "command",
"command": "/tmp/nexus session start --agent claude-code --mode session"
}
]
});
assert!(ClaudeCodeHook::entry_has_session_start_hook(&entry));
}
#[test]
fn test_upsert_session_start_hook_repairs_stale_command() {
let desired = "'/new/nexus' session start --agent claude-code --mode session";
let mut settings = serde_json::json!({
"hooks": {
"SessionStart": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "'/old/nexus' session start --agent claude-code --mode session"
}]
}]
}
});
ClaudeCodeHook::upsert_session_start_hook(&mut settings, desired).unwrap();
let hooks = settings["hooks"]["SessionStart"].as_array().unwrap();
assert_eq!(hooks.len(), 1);
assert_eq!(hooks[0]["hooks"][0]["command"], desired);
}
#[test]
fn test_upsert_session_start_hook_rejects_invalid_shapes() {
let mut settings = serde_json::json!({
"hooks": {
"SessionStart": {}
}
});
let error = ClaudeCodeHook::upsert_session_start_hook(
&mut settings,
"'/nexus' session start --agent claude-code --mode session",
)
.unwrap_err();
assert!(error.to_string().contains("SessionStart"));
}
#[test]
fn test_desired_subconscious_command_returns_commands() {
let recall = ClaudeCodeHook::desired_subconscious_command("UserPromptSubmit");
assert!(recall.contains("subconscious recall"));
let sync = ClaudeCodeHook::desired_subconscious_command("PreToolUse");
assert!(sync.contains("subconscious sync-check"));
let stop = ClaudeCodeHook::desired_subconscious_command("Stop");
assert!(stop.contains("subconscious ingest-transcript"));
}
#[test]
fn test_desired_subconscious_command_unknown_returns_empty() {
let cmd = ClaudeCodeHook::desired_subconscious_command("Unknown");
assert!(cmd.is_empty());
}
#[test]
fn test_command_is_subconscious_hook_matches() {
assert!(ClaudeCodeHook::command_is_subconscious_hook(
"/nexus subconscious recall --agent claude-code",
"UserPromptSubmit"
));
assert!(!ClaudeCodeHook::command_is_subconscious_hook(
"/nexus subconscious recall --agent claude-code",
"PreToolUse"
));
}
#[test]
fn test_upsert_hook_entry_adds_new_event() {
let mut settings = serde_json::json!({"hooks": {}});
ClaudeCodeHook::upsert_hook_entry(
&mut settings,
"UserPromptSubmit",
"nexus subconscious recall",
&|cmd: &str| cmd.contains("subconscious recall"),
)
.unwrap();
let entries = settings["hooks"]["UserPromptSubmit"].as_array().unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0]["hooks"][0]["command"],
"nexus subconscious recall"
);
}
#[test]
fn test_upsert_hook_entry_replaces_existing() {
let mut settings = serde_json::json!({
"hooks": {
"PreToolUse": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "/old/nexus subconscious sync-check"
}]
}]
}
});
ClaudeCodeHook::upsert_hook_entry(
&mut settings,
"PreToolUse",
"/new/nexus subconscious sync-check",
&|cmd: &str| cmd.contains("subconscious sync-check"),
)
.unwrap();
let entries = settings["hooks"]["PreToolUse"].as_array().unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0]["hooks"][0]["command"],
"/new/nexus subconscious sync-check"
);
}
#[test]
fn test_upsert_hook_entry_replaces_flat_command() {
let mut settings = serde_json::json!({
"hooks": {
"UserPromptSubmit": [{
"type": "command",
"command": "/old/nexus subconscious recall"
}]
}
});
ClaudeCodeHook::upsert_hook_entry(
&mut settings,
"UserPromptSubmit",
"/new/nexus subconscious recall",
&|cmd: &str| cmd.contains("subconscious recall"),
)
.unwrap();
let entries = settings["hooks"]["UserPromptSubmit"].as_array().unwrap();
assert_eq!(entries.len(), 1);
assert!(entries[0].get("hooks").is_some(), "Should use nested shape");
assert_eq!(
entries[0]["hooks"][0]["command"],
"/new/nexus subconscious recall"
);
}
#[test]
fn test_command_is_subconscious_hook_stop_event() {
assert!(ClaudeCodeHook::command_is_subconscious_hook(
"/nexus subconscious ingest-transcript --agent claude-code",
"Stop"
));
assert!(!ClaudeCodeHook::command_is_subconscious_hook(
"/nexus subconscious recall",
"Stop"
));
}
}