Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 117 additions & 21 deletions crates/icm-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1216,16 +1216,27 @@ fn cmd_init(mode: InitMode) -> Result<()> {
<!-- icm:start -->\n\
## Persistent memory (ICM)\n\
\n\
This project uses [ICM](https://github.com/rtk-ai/icm) for persistent memory.\n\
This project uses [ICM](https://github.com/rtk-ai/icm) for persistent memory across sessions.\n\
\n\
After completing a significant task, store a summary:\n\
### Recall (before starting work)\n\
```bash\n\
icm store -t \"project\" -c \"Short summary of what was done\"\n\
icm recall \"topic keywords\" # search memories\n\
icm recall-context \"query\" --limit 5 # formatted for prompt injection\n\
```\n\
\n\
Before starting work, recall relevant context:\n\
### Store (after completing significant work)\n\
```bash\n\
icm recall \"topic keywords\"\n\
icm store -t \"topic\" -c \"summary\" -i medium # importance: critical|high|medium|low\n\
icm store -t \"decisions\" -c \"chose X over Y because...\" -i high\n\
icm store -t \"errors-resolved\" -c \"fix: ...\" -k \"error,fix\"\n\
```\n\
\n\
### Other commands\n\
```bash\n\
icm update <id> -c \"updated content\" # edit memory in-place\n\
icm health # topic hygiene audit\n\
icm topics # list all topics\n\
icm feedback record -t \"topic\" -c \"context\" -p \"predicted\" --corrected \"actual\"\n\
```\n\
<!-- icm:end -->";

Expand All @@ -1252,15 +1263,15 @@ icm recall \"topic keywords\"\n\
let icm_recall_prompt = "\
Search ICM memory for: $ARGUMENTS

Use the icm_memory_recall MCP tool if available, otherwise run:
Run:
```bash
icm recall \"$ARGUMENTS\"
```
";
let icm_remember_prompt = "\
Store the following in ICM memory: $ARGUMENTS

Use the icm_memory_store MCP tool if available, otherwise run:
Run:
```bash
icm store -t \"note\" -c \"$ARGUMENTS\"
```
Expand Down Expand Up @@ -1293,10 +1304,14 @@ alwaysApply: true
This project uses ICM (Infinite Context Memory) for persistent memory.

At the start of each task, recall relevant context:
- Use `icm_memory_recall` MCP tool or run `icm recall \"topic\"`
```bash
icm recall \"topic keywords\"
```

After completing significant work, store a summary:
- Use `icm_memory_store` MCP tool or run `icm store -t \"topic\" -c \"summary\"`
```bash
icm store -t \"topic\" -c \"summary\"
```
";
install_skill(&cursor_rules_dir, "icm.mdc", cursor_icm_rule, "Cursor rule")?;

Expand All @@ -1320,30 +1335,47 @@ After completing significant work, store a summary:
)?;
}

// --- Hook mode: install Claude Code PostToolUse hook ---
// --- Hook mode: install Claude Code PreToolUse + PostToolUse hooks ---
if do_hook {
let claude_settings_path = PathBuf::from(&home).join(".claude/settings.json");
let hook_dir = PathBuf::from(&home).join(".claude/hooks");
std::fs::create_dir_all(&hook_dir).ok();

// Write the hook script
let hook_script_path = hook_dir.join("icm-post-tool.sh");
let hook_script = include_str!("../../../scripts/hooks/icm-post-tool.sh");
if hook_script_path.exists() {
// Write the PreToolUse hook (auto-allow icm commands)
let pretool_path = hook_dir.join("icm-pretool.sh");
let pretool_script = include_str!("../../../scripts/hooks/icm-pretool.sh");
if pretool_path.exists() {
println!("[hook] icm-pretool.sh already exists, updating.");
}
std::fs::write(&pretool_path, pretool_script).context("cannot write pretool hook")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&pretool_path, std::fs::Permissions::from_mode(0o755)).ok();
}

// Write the PostToolUse hook (auto-extract context)
let posttool_path = hook_dir.join("icm-post-tool.sh");
let posttool_script = include_str!("../../../scripts/hooks/icm-post-tool.sh");
if posttool_path.exists() {
println!("[hook] icm-post-tool.sh already exists, updating.");
}
std::fs::write(&hook_script_path, hook_script).context("cannot write hook script")?;
std::fs::write(&posttool_path, posttool_script).context("cannot write posttool hook")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&hook_script_path, std::fs::Permissions::from_mode(0o755))
.ok();
std::fs::set_permissions(&posttool_path, std::fs::Permissions::from_mode(0o755)).ok();
}

// Inject into settings.json
let hook_cmd = hook_script_path.to_string_lossy().to_string();
let status = inject_claude_hook(&claude_settings_path, &hook_cmd)?;
println!("[hook] Claude Code PostToolUse: {status}");
// Inject PreToolUse hook (Bash matcher — auto-allow icm commands)
let pretool_cmd = pretool_path.to_string_lossy().to_string();
let pre_status = inject_claude_pretool_hook(&claude_settings_path, &pretool_cmd)?;
println!("[hook] Claude Code PreToolUse (auto-allow): {pre_status}");

// Inject PostToolUse hook (auto-extract context)
let posttool_cmd = posttool_path.to_string_lossy().to_string();
let post_status = inject_claude_hook(&claude_settings_path, &posttool_cmd)?;
println!("[hook] Claude Code PostToolUse (auto-extract): {post_status}");
}

println!();
Expand Down Expand Up @@ -1417,6 +1449,70 @@ fn inject_claude_hook(settings_path: &PathBuf, hook_command: &str) -> Result<Str
Ok("configured".into())
}

/// Inject ICM PreToolUse hook into Claude Code settings.json
/// This hook auto-allows `icm` CLI commands (no permission prompt).
fn inject_claude_pretool_hook(settings_path: &PathBuf, hook_command: &str) -> Result<String> {
let mut config: Value = if settings_path.exists() {
let content = std::fs::read_to_string(settings_path)
.with_context(|| format!("cannot read {}", settings_path.display()))?;
serde_json::from_str(&content)
.with_context(|| format!("cannot parse {}", settings_path.display()))?
} else {
serde_json::json!({})
};

let hooks = config
.as_object_mut()
.context("settings is not a JSON object")?
.entry("hooks")
.or_insert_with(|| serde_json::json!({}));

let pre_tool = hooks
.as_object_mut()
.context("hooks is not a JSON object")?
.entry("PreToolUse")
.or_insert_with(|| serde_json::json!([]));

let pre_tool_arr = pre_tool
.as_array_mut()
.context("PreToolUse is not an array")?;

// Check if ICM pretool hook already exists
let already = pre_tool_arr.iter().any(|entry| {
entry
.get("hooks")
.and_then(|h| h.as_array())
.map(|hooks| {
hooks.iter().any(|h| {
h.get("command")
.and_then(|c| c.as_str())
.map(|c| c.contains("icm-pretool"))
.unwrap_or(false)
})
})
.unwrap_or(false)
});

if already {
return Ok("already configured".into());
}

// Add ICM PreToolUse hook entry (matcher: Bash — auto-allow icm commands)
pre_tool_arr.push(serde_json::json!({
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": hook_command
}]
}));

let output = serde_json::to_string_pretty(&config)?;
std::fs::write(settings_path, output)
.with_context(|| format!("cannot write {}", settings_path.display()))?;

Ok("configured".into())
}

/// Install a skill/rule file if it doesn't exist yet.
fn install_skill(dir: &PathBuf, filename: &str, content: &str, label: &str) -> Result<()> {
std::fs::create_dir_all(dir).ok();
Expand Down
50 changes: 50 additions & 0 deletions scripts/hooks/icm-pretool.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# icm-hook-version: 1
# ICM PreToolUse hook for Claude Code
# Auto-allows `icm` CLI commands without permission prompts.
# Install: icm init --mode hook
#
# Input (stdin): JSON with tool_name, tool_input (command, etc.)
# Output: JSON with permissionDecision=allow if it's an icm command

set -euo pipefail

if ! command -v jq &>/dev/null; then
exit 0
fi

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)

# Only intercept Bash commands
if [ "$TOOL_NAME" != "Bash" ]; then
exit 0
fi

CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)

if [ -z "$CMD" ]; then
exit 0
fi

# Check if this is an icm command (starts with "icm " or is just "icm")
# Also handle piped commands: "echo ... | icm extract"
case "$CMD" in
icm|icm\ *|*\ icm\ *|*\|\ icm\ *|*\|icm\ *)
;;
*)
exit 0
;;
esac

# Auto-allow icm commands — no permission prompt needed
jq -n \
--argjson input "$(echo "$INPUT" | jq -c '.tool_input')" \
'{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "ICM auto-allow",
"updatedInput": $input
}
}'
Loading