1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Class {
9 Read,
10 Write,
11 Destructive,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct ToolMeta {
16 pub class: Class,
17 pub group: &'static str,
18}
19
20const KNOWN_GROUPS: &[(&str, &str)] = &[
23 ("task_type", "task-type"),
24 ("audit_log", "audit-log"),
25 ("auth", "auth"),
26 ("workspace", "workspace"),
27 ("space", "space"),
28 ("folder", "folder"),
29 ("list", "list"),
30 ("task", "task"),
31 ("checklist", "checklist"),
32 ("comment", "comment"),
33 ("tag", "tag"),
34 ("field", "field"),
35 ("attachment", "attachment"),
36 ("time", "time"),
37 ("goal", "goal"),
38 ("view", "view"),
39 ("member", "member"),
40 ("user", "user"),
41 ("chat", "chat"),
42 ("doc", "doc"),
43 ("webhook", "webhook"),
44 ("template", "template"),
45 ("guest", "guest"),
46 ("group", "group"),
47 ("role", "role"),
48 ("shared", "shared"),
49 ("acl", "acl"),
50];
51
52const READ_VERBS: &[&str] = &[
53 "list", "get", "search", "current", "pages", "followers", "members",
54 "history", "whoami", "check", "replies", "tagged", "query",
55];
56
57const WRITE_VERBS: &[&str] = &[
58 "create", "update", "set", "add", "start", "stop", "move", "apply",
59 "invite", "rename", "share", "attach", "link", "reply", "send", "dm",
60 "edit", "upload",
61];
62
63const DESTRUCTIVE_VERBS: &[&str] = &[
64 "delete", "remove", "unshare", "unlink", "unset",
65];
66
67const OVERRIDES: &[(&str, Class, &str)] = &[
70 ("clickup_search", Class::Read, "workspace"),
71 ("clickup_whoami", Class::Read, "auth"),
72 ("clickup_workspace_plan", Class::Read, "workspace"),
73 ("clickup_workspace_seats", Class::Read, "workspace"),
74 ("clickup_task_replace_estimates", Class::Write, "task"),
75 ("clickup_task_time_in_status", Class::Read, "task"),
76 ("clickup_time_tags", Class::Read, "time"),
77 ("clickup_template_apply_list", Class::Write, "template"),
78 ("clickup_doc_get_page", Class::Read, "doc"),
79 ("clickup_chat_tagged_users", Class::Read, "chat"),
80 ("clickup_view_tasks", Class::Read, "view"),
81 ("clickup_guest_share_list", Class::Write, "guest"),
82];
83
84pub fn classify(tool_name: &str) -> Option<ToolMeta> {
85 if let Some(&(_, class, group)) = OVERRIDES.iter().find(|(n, _, _)| *n == tool_name) {
87 return Some(ToolMeta { class, group });
88 }
89
90 let rest = tool_name.strip_prefix("clickup_")?;
92 let (raw_prefix, normalized_group) = KNOWN_GROUPS
93 .iter()
94 .find(|(prefix, _)| rest == *prefix || rest.starts_with(&format!("{}_", prefix)))
95 .copied()?;
96 let remainder = rest
97 .strip_prefix(raw_prefix)
98 .and_then(|r| r.strip_prefix('_'))
99 .unwrap_or("");
100
101 if remainder.is_empty() {
102 return None;
103 }
104
105 let segments: Vec<&str> = remainder.split('_').collect();
106 let last = *segments.last().unwrap();
107
108 if segments.iter().any(|s| DESTRUCTIVE_VERBS.contains(s)) {
110 return Some(ToolMeta { class: Class::Destructive, group: normalized_group });
111 }
112
113 if WRITE_VERBS.contains(&last) {
115 return Some(ToolMeta { class: Class::Write, group: normalized_group });
116 }
117 if READ_VERBS.contains(&last) {
118 return Some(ToolMeta { class: Class::Read, group: normalized_group });
119 }
120
121 if segments.iter().any(|s| WRITE_VERBS.contains(s)) {
123 return Some(ToolMeta { class: Class::Write, group: normalized_group });
124 }
125
126 None
127}
128
129pub const ALL_GROUPS: &[&str] = &[
130 "auth", "workspace", "space", "folder", "list", "task", "checklist",
131 "comment", "tag", "field", "task-type", "attachment", "time", "goal",
132 "view", "member", "user", "chat", "doc", "webhook", "template",
133 "guest", "group", "role", "shared", "audit-log", "acl",
134];
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn destructive_wins_over_write_in_same_name() {
142 assert_eq!(classify("clickup_task_remove_tag").unwrap().class, Class::Destructive);
143 }
144
145 #[test]
146 fn trailing_read_beats_earlier_write() {
147 assert_eq!(classify("clickup_chat_reply_list").unwrap().class, Class::Read);
149 }
150
151 #[test]
152 fn write_scan_catches_compound_verbs() {
153 assert_eq!(classify("clickup_goal_add_kr").unwrap().class, Class::Write);
154 assert_eq!(classify("clickup_task_add_dep").unwrap().class, Class::Write);
155 }
156
157 #[test]
158 fn two_word_group_prefix_wins() {
159 let m = classify("clickup_task_type_list").unwrap();
160 assert_eq!(m.group, "task-type");
161 assert_eq!(m.class, Class::Read);
162 }
163
164 #[test]
165 fn override_table_short_circuits() {
166 assert_eq!(classify("clickup_task_replace_estimates").unwrap().class, Class::Write);
167 assert_eq!(classify("clickup_search").unwrap().group, "workspace");
168 }
169
170 #[test]
171 fn unknown_tool_returns_none() {
172 assert!(classify("clickup_not_a_real_tool").is_none());
173 }
174}