Skip to main content

clickup_cli/commands/
agent_config.rs

1use crate::error::CliError;
2use crate::Cli;
3use clap::Subcommand;
4use std::path::PathBuf;
5
6/// Known AI agent instruction files, checked in order
7const AGENT_FILES: &[&str] = &[
8    "CLAUDE.md",
9    "agent.md",
10    "AGENT.md",
11    ".cursorrules",
12    ".github/copilot-instructions.md",
13    "AGENTS.md",
14    "AI.md",
15];
16
17#[derive(Subcommand)]
18pub enum AgentConfigCommands {
19    /// Print compressed CLI reference for AI agent instruction files
20    Show,
21    /// Inject CLI reference into a file (auto-detects or creates agent instruction file)
22    Inject {
23        /// Target file (omit to auto-detect: CLAUDE.md, agent.md, .cursorrules, etc.)
24        file: Option<PathBuf>,
25    },
26    /// Initialize project-level ClickUp config (.clickup.toml and/or .mcp.json)
27    Init {
28        /// API token
29        #[arg(long)]
30        token: Option<String>,
31        /// Workspace ID
32        #[arg(long)]
33        workspace: Option<String>,
34        /// Also create .mcp.json for MCP server integration
35        #[arg(long)]
36        mcp: bool,
37    },
38}
39
40const AGENT_REFERENCE: &str = "<!-- clickup-cli:begin -->To interface with ClickUp, use the `clickup` CLI (LLM-agnostic, works with any AI agent). Pattern: `clickup <resource> <action> [ID] [flags]`. Global flags: --output table|json|json-compact|csv, --fields LIST, -q (IDs only), --no-header, --all (paginate), --limit N, --page N, --token TOKEN, --workspace ID, --timeout SECS. Commands: setup [--token T]; auth whoami|check; workspace list|seats|plan; space list [--archived]|get ID|create --name N [--private]|update ID [--name N]|delete ID; folder list --space ID|get ID|create --space ID --name N|update ID --name N|delete ID; list list --folder ID|--space ID|get ID|create --folder ID|--space ID --name N [--content T] [--due-date DATE]|update ID|delete ID|add-task LIST TASK|remove-task LIST TASK; task list --list ID [--status S] [--assignee ID] [--tag T] [--include-closed]|search [--space ID] [--status S]|get [ID] [--subtasks] [--custom-task-id]|create --list ID --name N [--description T] [--status S] [--priority 1-4] [--assignee ID] [--tag T] [--due-date DATE] [--parent ID]|update [ID] [--name N] [--status S] [--priority N] [--add-assignee ID] [--rem-assignee ID]|delete ID|time-in-status [ID...]|add-tag [ID] TAG|remove-tag [ID] TAG|add-dep [ID] --depends-on ID|remove-dep [ID] --depends-on ID|link ID TARGET|unlink ID TARGET|move [ID] --list ID|set-estimate [ID] --assignee ID --time MS|replace-estimates [ID] --assignee ID --time MS; checklist create --name N [--task ID]|update ID [--name N]|delete ID|add-item ID --name N|update-item ID ITEM [--name N] [--resolved]|delete-item ID ITEM; comment list [--task ID]|--list ID|--view ID|create [--task ID]|--list ID|--view ID --text T [--notify-all]|update ID --text T [--resolved]|delete ID|replies ID|reply ID --text T; tag list --space ID|create --space ID --name N [--fg-color H] [--bg-color H]|update --space ID --tag N [--name NEW]|delete --space ID --tag N; field list --list ID|--folder ID|--space ID|--workspace-level|set FIELD --value V [TASK]|unset FIELD [TASK]; task-type list; attachment list [--task ID]|upload FILE [--task ID]; time list [--start-date D] [--end-date D] [--task ID]|get ID|current|create --start D --duration MS [--task ID]|update ID|delete ID|start [--task ID]|stop|tags|add-tags --entry-id ID --tag N|remove-tags --entry-id ID --tag N|rename-tag --name OLD --new-name NEW|history ID; goal list|get ID|create --name N --due-date D|update ID|delete ID|add-kr ID --name N --type T --steps-start N --steps-end N|update-kr ID --steps-current N|delete-kr ID; view list --workspace-level|--space ID|--folder ID|--list ID|get ID|create --name N --type T --space ID|--folder ID|--list ID|update ID|delete ID|tasks ID; member list [--task ID]|--list ID; user invite --email E|get ID|update ID|remove ID; chat channel-list|channel-create --name N|channel-get ID|channel-update ID|channel-delete ID|channel-followers ID|channel-members ID|dm USER...|message-list --channel ID|message-send --channel ID --text T|message-update ID --text T|message-delete ID|reaction-list MSG|reaction-add MSG --emoji E|reaction-remove MSG EMOJI|reply-list MSG|reply-send MSG --text T|tagged-users MSG; doc list|create --name N|get ID|pages ID [--content]|add-page DOC --name N [--content T]|page DOC PAGE|edit-page DOC PAGE --content T [--mode replace|append|prepend]; webhook list|create --endpoint URL --event E|update ID --endpoint URL --event E|delete ID; template list|apply-task TPL --list ID --name N|apply-list TPL --folder ID|--space ID --name N|apply-folder TPL --space ID --name N; guest invite --email E|get ID|update ID|remove ID|share-task TASK GUEST --permission P|unshare-task TASK GUEST|share-list LIST GUEST --permission P|unshare-list LIST GUEST|share-folder FOLDER GUEST --permission P|unshare-folder FOLDER GUEST; group list|create --name N --member ID|update ID [--add-member ID] [--rem-member ID]|delete ID; role list; shared list; audit-log query --type T [--user-id ID] [--start-date D] [--end-date D]; acl update TYPE ID [--private] [--body JSON]. Priority: 1=Urgent 2=High 3=Normal 4=Low. Dates: YYYY-MM-DD. All timestamps Unix ms. team_id=workspace_id in API. Exit codes: 0=ok 1=client-error 2=auth 3=not-found 4=rate-limited 5=server-error. Config: ~/.config/clickup-cli/config.toml or .clickup.toml (project-level). Setup: `clickup setup --token pk_XXX`. Branch-detect: when a task-scoped command runs without an explicit ID, the CLI resolves the ID from the current git branch (CU-abc123, PROJ-42 custom IDs; workflow prefixes like feat/, fix/ stripped; FEATURE-, BUGFIX-, WIP- etc. excluded). Priority: explicit arg > CLICKUP_TASK_ID env > branch. Explicit CU-abc123 is stripped to abc123. Destructive/ambiguous commands (task delete, task link/unlink, guest share-task/unshare-task) never auto-detect. Disable with CLICKUP_GIT_DETECT=0 or [git] enabled=false in config. MCP server: `clickup mcp serve [--profile all|read|safe] [--read-only] [--groups LIST] [--tools LIST]` (also via `CLICKUP_MCP_PROFILE`, `CLICKUP_MCP_GROUPS`, `CLICKUP_MCP_TOOLS`).<!-- clickup-cli:end -->";
41
42/// Find an existing agent instruction file in the current directory, or default to CLAUDE.md
43fn detect_agent_file() -> PathBuf {
44    for name in AGENT_FILES {
45        let path = PathBuf::from(name);
46        if path.exists() {
47            return path;
48        }
49    }
50    // No existing file found — default to CLAUDE.md
51    PathBuf::from("CLAUDE.md")
52}
53
54pub async fn execute(command: AgentConfigCommands, _cli: &Cli) -> Result<(), CliError> {
55    match command {
56        AgentConfigCommands::Show => {
57            println!("{}", AGENT_REFERENCE);
58            Ok(())
59        }
60        AgentConfigCommands::Inject { file } => {
61            let file = file.unwrap_or_else(detect_agent_file);
62            let begin_marker = "<!-- clickup-cli:begin -->";
63            let end_marker = "<!-- clickup-cli:end -->";
64
65            let existing = if file.exists() {
66                std::fs::read_to_string(&file)?
67            } else {
68                String::new()
69            };
70
71            let new_content = if existing.contains(begin_marker) && existing.contains(end_marker) {
72                let before = existing.split(begin_marker).next().unwrap_or("");
73                let after = existing.split(end_marker).nth(1).unwrap_or("");
74                format!("{}{}{}", before, AGENT_REFERENCE, after)
75            } else if existing.is_empty() {
76                format!("# Project\n\n{}\n", AGENT_REFERENCE)
77            } else {
78                format!("{}\n\n{}\n", existing.trim_end(), AGENT_REFERENCE)
79            };
80
81            if let Some(parent) = file.parent() {
82                if !parent.as_os_str().is_empty() {
83                    std::fs::create_dir_all(parent)?;
84                }
85            }
86            std::fs::write(&file, new_content)?;
87            eprintln!("CLI reference injected into {}", file.display());
88            Ok(())
89        }
90        AgentConfigCommands::Init {
91            token,
92            workspace,
93            mcp,
94        } => {
95            // Create .clickup.toml
96            let config_path = PathBuf::from(".clickup.toml");
97            if config_path.exists() {
98                eprintln!("Project config already exists: {}", config_path.display());
99            } else {
100                let mut content = String::from("[auth]\n");
101                if let Some(t) = &token {
102                    content.push_str(&format!("token = \"{}\"\n", t));
103                } else {
104                    content.push_str("# token = \"pk_...\"\n");
105                }
106                content.push_str("\n[defaults]\n");
107                if let Some(ws) = &workspace {
108                    content.push_str(&format!("workspace_id = \"{}\"\n", ws));
109                } else {
110                    content.push_str("# workspace_id = \"...\"\n");
111                }
112                std::fs::write(&config_path, &content)?;
113                eprintln!("Project config created: .clickup.toml");
114                eprintln!("Add .clickup.toml to .gitignore if it contains a token.");
115            }
116
117            // Create or update .mcp.json if --mcp flag is set
118            if mcp {
119                let mcp_path = PathBuf::from(".mcp.json");
120                let clickup_bin = std::env::current_exe()
121                    .ok()
122                    .and_then(|p| p.to_str().map(String::from))
123                    .unwrap_or_else(|| "clickup".to_string());
124
125                let server_entry = serde_json::json!({
126                    "command": clickup_bin,
127                    "args": ["mcp", "serve"]
128                });
129
130                let mut mcp_config: serde_json::Value = if mcp_path.exists() {
131                    let existing = std::fs::read_to_string(&mcp_path)?;
132                    serde_json::from_str(&existing).unwrap_or(serde_json::json!({"mcpServers": {}}))
133                } else {
134                    serde_json::json!({"mcpServers": {}})
135                };
136
137                mcp_config
138                    .as_object_mut()
139                    .unwrap()
140                    .entry("mcpServers")
141                    .or_insert(serde_json::json!({}))
142                    .as_object_mut()
143                    .unwrap()
144                    .insert("clickup-cli".to_string(), server_entry);
145
146                let formatted = serde_json::to_string_pretty(&mcp_config)
147                    .unwrap_or_else(|_| mcp_config.to_string());
148                std::fs::write(&mcp_path, format!("{}\n", formatted))?;
149
150                if mcp_path.exists() {
151                    eprintln!("MCP config updated: .mcp.json (clickup-cli server added)");
152                } else {
153                    eprintln!("MCP config created: .mcp.json");
154                }
155                eprintln!(
156                    "The MCP server provides 143 tools with token-efficient compact responses."
157                );
158            }
159
160            Ok(())
161        }
162    }
163}