clickup_cli/commands/
agent_config.rs1use crate::error::CliError;
2use crate::Cli;
3use clap::Subcommand;
4use std::path::PathBuf;
5
6const 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 Show,
21 Inject {
23 file: Option<PathBuf>,
25 },
26 Init {
28 #[arg(long)]
30 token: Option<String>,
31 #[arg(long)]
33 workspace: Option<String>,
34 #[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
42fn 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 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 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 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}