Skip to main content

clickup_cli/commands/
agent_config.rs

1use clap::Subcommand;
2use crate::error::CliError;
3use crate::Cli;
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 --task ID --name N|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 TASK FIELD --value V|unset TASK FIELD; task-type list; attachment list --task ID|upload --task ID FILE; 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`.<!-- 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 { token, workspace, mcp } => {
91            // Create .clickup.toml
92            let config_path = PathBuf::from(".clickup.toml");
93            if config_path.exists() {
94                eprintln!("Project config already exists: {}", config_path.display());
95            } else {
96                let mut content = String::from("[auth]\n");
97                if let Some(t) = &token {
98                    content.push_str(&format!("token = \"{}\"\n", t));
99                } else {
100                    content.push_str("# token = \"pk_...\"\n");
101                }
102                content.push_str("\n[defaults]\n");
103                if let Some(ws) = &workspace {
104                    content.push_str(&format!("workspace_id = \"{}\"\n", ws));
105                } else {
106                    content.push_str("# workspace_id = \"...\"\n");
107                }
108                std::fs::write(&config_path, &content)?;
109                eprintln!("Project config created: .clickup.toml");
110                eprintln!("Add .clickup.toml to .gitignore if it contains a token.");
111            }
112
113            // Create .mcp.json if --mcp flag is set
114            if mcp {
115                let mcp_path = PathBuf::from(".mcp.json");
116                if mcp_path.exists() {
117                    eprintln!("MCP config already exists: {}", mcp_path.display());
118                } else {
119                    let clickup_bin = std::env::current_exe()
120                        .ok()
121                        .and_then(|p| p.to_str().map(String::from))
122                        .unwrap_or_else(|| "clickup".to_string());
123                    let mcp_content = format!(
124                        "{{\n  \"mcpServers\": {{\n    \"clickup-cli\": {{\n      \"command\": \"{}\",\n      \"args\": [\"mcp\", \"serve\"]\n    }}\n  }}\n}}\n",
125                        clickup_bin
126                    );
127                    std::fs::write(&mcp_path, &mcp_content)?;
128                    eprintln!("MCP config created: .mcp.json");
129                    eprintln!("The MCP server provides 143 tools with token-efficient compact responses.");
130                }
131            }
132
133            Ok(())
134        }
135    }
136}