1use serde_json::{Value, json};
2
3use crate::error;
4
5fn field(name: &str, kind: &str) -> Value {
6 json!({"name": name, "type": kind})
7}
8fn array_field(name: &str, item_kind: &str) -> Value {
9 json!({"name": name, "type": "array", "items": {"type": item_kind}})
10}
11fn nullable_field(name: &str, kind: &str) -> Value {
12 json!({"name": name, "type": kind, "nullable": true})
13}
14fn nullable_array_field(name: &str, item_kind: &str) -> Value {
15 json!({"name": name, "type": "array", "items": {"type": item_kind}, "nullable": true})
16}
17fn object_field(name: &str, nullable: bool, description: &str, fields: Vec<Value>) -> Value {
18 json!({"name": name, "type": "object", "nullable": nullable, "description": description, "fields": fields})
19}
20fn described(mut field: Value, description: &str) -> Value {
21 field["description"] = json!(description);
22 field
23}
24fn debugging_setup() -> Value {
25 object_field(
26 "debugging_setup",
27 true,
28 "Managed connection commands, available when no endpoint listens. Restart commands require confirmation and preserve prior settings; recover restores interrupted setup settings",
29 vec![
30 described(
31 field("shell", "string"),
32 "zsh, wsl or powershell: where the commands run",
33 ),
34 field("open", "string"),
35 field("close", "string"),
36 ],
37 )
38}
39fn sender() -> Value {
40 object_field(
41 "from",
42 true,
43 "Sender; null for system events",
44 vec![object_field(
45 "user",
46 true,
47 "The sending user",
48 vec![
49 field("id", "string"),
50 nullable_field("displayName", "string"),
51 ],
52 )],
53 )
54}
55fn body() -> Value {
56 object_field(
57 "body",
58 false,
59 "Message content",
60 vec![
61 json!({"name": "contentType", "type": "string", "enum": ["text", "html"]}),
62 field("content", "string"),
63 ],
64 )
65}
66fn arg(name: &str, kind: &str, description: &str) -> Value {
67 json!({"name": name, "type": kind, "description": description})
68}
69fn required_arg(name: &str, kind: &str, description: &str) -> Value {
70 json!({"name": name, "type": kind, "description": description, "required": true})
71}
72
73fn single(name: &str, description: &str, effects: &str, fields: Vec<Value>) -> Value {
74 json!({"name": name, "description": description, "effects": effects, "cardinality": "single", "output_fields": fields})
75}
76
77fn paged(name: &str, description: &str, fields: Vec<Value>) -> Value {
78 json!({
79 "name": name, "description": description, "effects": "read_only", "cardinality": "unbounded",
80 "pagination": {"style": "cursor", "cursor_field": "next_cursor", "cursor_arg": "--cursor", "limit_arg": "--limit"},
81 "fields_arg": "--fields",
82 "args": [
83 {"name": "--limit", "type": "integer", "default": 50, "x-minimum": 1, "x-maximum": 50, "description": "Maximum records in this page"},
84 arg("--cursor", "string", "Opaque next_cursor value from the previous page; omit for the newest page"),
85 arg("--fields", "string", "Comma-separated top-level output fields to keep; unknown names are omitted")
86 ],
87 "output_fields": fields
88 })
89}
90
91fn offset_paged(name: &str, description: &str, fields: Vec<Value>) -> Value {
92 json!({
93 "name": name, "description": description, "effects": "read_only", "cardinality": "unbounded",
94 "pagination": {"style": "offset", "offset_arg": "--offset", "limit_arg": "--limit"},
95 "fields_arg": "--fields",
96 "args": [
97 {"name": "--limit", "type": "integer", "default": 50, "x-minimum": 1, "x-maximum": 100, "description": "Maximum records in this page"},
98 {"name": "--offset", "type": "integer", "default": 0, "x-minimum": 0, "description": "Number of records to skip; use next_offset from the previous page"},
99 arg("--fields", "string", "Comma-separated top-level output fields to keep; unknown names are omitted")
100 ],
101 "output_fields": fields
102 })
103}
104
105fn conversation_args() -> Vec<Value> {
106 vec![
107 json!({"name": "--chat", "type": "string", "x-conflicts": ["--team", "--channel"], "description": "Chat ID from chats list; give this, or --team with --channel"}),
108 json!({"name": "--team", "type": "string", "x-requires": ["--channel"], "description": "Team ID (Graph only)"}),
109 json!({"name": "--channel", "type": "string", "x-requires": ["--team"], "description": "Channel ID within --team (Graph only)"}),
110 ]
111}
112
113fn command_errors(name: &str) -> &'static [&'static str] {
116 const GRAPH_READ: &[&str] = &[
117 "auth",
118 "permission_denied",
119 "not_found",
120 "rate_limit",
121 "api_error",
122 "unsupported",
123 ];
124 const CONVERSATION_READ: &[&str] = &[
125 "auth",
126 "permission_denied",
127 "not_found",
128 "rate_limit",
129 "api_error",
130 "unsupported",
131 "desktop_error",
132 ];
133 match name {
134 "init" => &["auth", "tty_required", "desktop_error"],
135 "auth login" => &["auth", "tty_required", "unsupported"],
136 "auth logout" => &["unsupported"],
137 "auth status" => &["auth", "rate_limit", "api_error", "desktop_error"],
138 "profile remove" => &["confirmation_required"],
139 "whoami" | "list" | "channels list" => GRAPH_READ,
140 "chats list" | "messages list" => CONVERSATION_READ,
141 "messages send" => &[
142 "auth",
143 "read_only",
144 "permission_denied",
145 "not_found",
146 "rate_limit",
147 "api_error",
148 "confirmation_required",
149 "unconfirmed",
150 "unsupported",
151 "desktop_error",
152 ],
153 "messages status" => &["unsupported", "desktop_error"],
154 "desktop status" | "desktop recover" => &["desktop_error", "unsupported"],
155 "desktop connect" | "desktop disconnect" => {
156 &["desktop_error", "unsupported", "confirmation_required"]
157 }
158 "tui" => &["tty_required"],
159 "doctor" => &["unhealthy"],
160 "schema" => &["not_found"],
161 _ => &[],
162 }
163}
164
165fn example(name: &str) -> Value {
166 let (args, stdin): (&[&str], Option<&str>) = match name {
167 "init" => (&["init", "--no-login"], None),
168 "auth login" => (&["auth", "login", "--device-code"], None),
169 "auth logout" => (&["auth", "logout"], None),
170 "auth status" => (&["auth", "status", "--offline"], None),
171 "profile list" => (&["profile", "list"], None),
172 "profile use" => (&["profile", "use", "work"], None),
173 "profile remove" => (&["profile", "remove", "old", "--yes"], None),
174 "config show" => (&["config", "show"], None),
175 "config path" => (&["config", "path"], None),
176 "whoami" => (&["whoami"], None),
177 "list" => (&["list", "--limit", "20"], None),
178 "channels list" => (&["channels", "list", "TEAM_ID"], None),
179 "chats list" => (
180 &[
181 "chats",
182 "list",
183 "--limit",
184 "20",
185 "--fields",
186 "id,topic,lastUpdatedDateTime",
187 ],
188 None,
189 ),
190 "messages list" => (
191 &["messages", "list", "--chat", "CHAT_ID", "--limit", "20"],
192 None,
193 ),
194 "messages send" => (
195 &[
196 "messages", "send", "--chat", "CHAT_ID", "--body", "-", "--yes",
197 ],
198 Some("On it."),
199 ),
200 "messages status" => (
201 &[
202 "messages",
203 "status",
204 "--chat",
205 "CHAT_ID",
206 "--client-message-id",
207 "100000000000000001",
208 ],
209 None,
210 ),
211 "desktop status" => (&["desktop", "status", "--port", "9222"], None),
212 "desktop connect" => (&["desktop", "connect", "--port", "9222", "--yes"], None),
213 "desktop disconnect" => (&["desktop", "disconnect", "--port", "9222", "--yes"], None),
214 "desktop recover" => (&["desktop", "recover", "--port", "9222"], None),
215 "tui" => (&["tui", "--demo", "--snapshot"], None),
216 "doctor" => (&["doctor", "--offline"], None),
217 "capabilities" => (&["capabilities"], None),
218 "schema" => (&["schema", "--command", "messages send"], None),
219 "completions" => (&["completions", "bash"], None),
220 _ => return Value::Null,
221 };
222 let mut value = json!({"args": args});
223 if let Some(stdin) = stdin {
224 value["stdin"] = json!(stdin);
225 }
226 value
227}
228
229pub fn generate(command_filter: Option<&str>) -> Value {
230 let identity = vec![
231 field("id", "string"),
232 field("displayName", "string"),
233 field("userPrincipalName", "string"),
234 nullable_field("mail", "string"),
235 ];
236 let mut commands = vec![
237 {
238 let mut value = single(
239 "init",
240 "Configure a Graph or experimental desktop profile",
241 "idempotent",
242 vec![
243 field("profile", "string"),
244 field("backend", "string"),
245 field("config_path", "string"),
246 nullable_field("signed_in", "boolean"),
247 field("client_id", "string"),
248 field("tenant", "string"),
249 field("channel_history_requested", "boolean"),
250 field("read_only", "boolean"),
251 nullable_field("desktop_port", "integer"),
252 nullable_field("desktop_account", "string"),
253 nullable_field("endpoint_available", "boolean"),
254 nullable_field("connection", "object"),
255 field("session_verified", "boolean"),
256 debugging_setup(),
257 ],
258 );
259 value["args"] = json!([
260 {"name":"--backend","type":"string","enum":["graph","desktop"],"default":"graph","description":"Connection method; desktop supports diagnostics, chats and opt-in chat sending"},
261 {"name":"--desktop-port","type":"integer","description":"Existing loopback endpoint port (desktop only; default 9222)","x-minimum":1024,"x-maximum":65535},
262 {"name":"--desktop-account","type":"string","enum":["auto","personal","work-school"],"default":"auto","description":"Select the Teams account surface; multiple cached accounts within it remain unsupported"},
263 arg(
264 "--client-id",
265 "string",
266 "Override the maintained teams-cli public-client application ID"
267 ),
268 arg("--tenant", "string", "Tenant ID, domain, or organizations"),
269 arg(
270 "--no-login",
271 "boolean",
272 "Save configuration without sign-in or desktop endpoint access"
273 ),
274 arg("--device-code", "boolean", "Use device-code sign-in"),
275 arg(
276 "--channel-history",
277 "boolean",
278 "Request admin-consented channel-history access"
279 ),
280 {"name":"--read-only","type":"boolean","x-conflicts":["--allow-send"],"description":"Block remote write operations"},
281 {"name":"--allow-send","type":"boolean","x-requires":{"--backend":"desktop"},"x-conflicts":["--read-only"],"description":"Allow a desktop profile to send chat messages (desktop profiles are read-only otherwise)"}
282 ]);
283 value
284 },
285 {
286 let mut value = single(
287 "auth login",
288 "Sign in with delegated Microsoft OAuth and protect credentials in the OS store",
289 "idempotent",
290 vec![
291 field("profile", "string"),
292 field("expires_at", "integer"),
293 nullable_field("scope", "string"),
294 field("channel_history_requested", "boolean"),
295 ],
296 );
297 value["args"] = json!([
298 arg(
299 "--device-code",
300 "boolean",
301 "Use device-code sign-in for headless and remote environments"
302 ),
303 arg(
304 "--channel-history",
305 "boolean",
306 "Request admin-consented channel-history access"
307 )
308 ]);
309 value
310 },
311 single(
312 "auth logout",
313 "Remove locally stored delegated credentials",
314 "idempotent",
315 vec![field("profile", "string"), field("signed_in", "boolean")],
316 ),
317 {
318 let mut value = single(
319 "auth status",
320 "Verify authentication and inspect credential state",
321 "read_only",
322 vec![
323 field("profile", "string"),
324 field("configured", "boolean"),
325 field("backend", "string"),
326 nullable_field("signed_in", "boolean"),
327 field("verified", "boolean"),
328 nullable_field("identity", "object"),
329 field("config_path", "string"),
330 field("read_only", "boolean"),
331 nullable_array_field("granted_scopes", "string"),
332 nullable_field("channel_history", "boolean"),
333 nullable_field("endpoint_available", "boolean"),
334 nullable_field("connection", "object"),
335 field("session_verified", "boolean"),
336 ],
337 );
338 value["args"] = json!([arg(
339 "--offline",
340 "boolean",
341 "Inspect local state without contacting Graph or the desktop endpoint"
342 )]);
343 value
344 },
345 single(
346 "profile list",
347 "List configured profiles and identify the active one",
348 "read_only",
349 vec![
350 json!({"name": "items", "type": "array", "items": {"type": "object", "fields": [
351 field("name", "string"),
352 field("active", "boolean"),
353 json!({"name": "backend", "type": "string", "enum": ["graph", "desktop"]}),
354 field("client_id", "string"),
355 field("tenant", "string"),
356 field("read_only", "boolean")
357 ]}}),
358 field("total", "integer"),
359 ],
360 ),
361 {
362 let mut value = single(
363 "profile use",
364 "Select the default profile for future commands",
365 "idempotent",
366 vec![field("profile", "string"), field("active", "boolean")],
367 );
368 value["args"] = json!([required_arg("name", "string", "Profile name")]);
369 value
370 },
371 {
372 let mut value = single(
373 "profile remove",
374 "Remove a profile and its stored credential",
375 "idempotent",
376 vec![field("profile", "string"), field("removed", "boolean")],
377 );
378 value["args"] = json!([required_arg("name", "string", "Profile name")]);
379 value["confirmation_bypass_arg"] = json!("--yes");
380 value
381 },
382 single(
383 "config show",
384 "Print the resolved profile without credentials",
385 "read_only",
386 vec![
387 field("profile", "string"),
388 field("backend", "string"),
389 nullable_field("desktop_port", "integer"),
390 nullable_field("desktop_account", "string"),
391 field("client_id", "string"),
392 field("tenant", "string"),
393 field("read_only", "boolean"),
394 field("config_path", "string"),
395 ],
396 ),
397 single(
398 "config path",
399 "Print the absolute configuration file path",
400 "read_only",
401 vec![],
402 ),
403 single(
404 "whoami",
405 "Show the signed-in Microsoft 365 identity",
406 "read_only",
407 identity,
408 ),
409 offset_paged(
410 "list",
411 "List teams joined by the signed-in user",
412 vec![
413 field("id", "string"),
414 field("displayName", "string"),
415 nullable_field("description", "string"),
416 ],
417 ),
418 {
419 let mut value = offset_paged(
420 "channels list",
421 "List channels in a team",
422 vec![
423 field("id", "string"),
424 field("displayName", "string"),
425 nullable_field("description", "string"),
426 field("membershipType", "string"),
427 ],
428 );
429 value["args"]
430 .as_array_mut()
431 .unwrap()
432 .insert(0, required_arg("team", "string", "Team ID"));
433 value
434 },
435 paged(
436 "chats list",
437 "List one-to-one, group, and meeting chats",
438 vec![
439 described(
440 field("id", "string"),
441 "Chat ID for messages list/send; the chat with yourself is 48:notes on desktop",
442 ),
443 json!({"name": "chatType", "type": "string", "enum": ["oneOnOne", "group", "meeting"]}),
444 nullable_field("topic", "string"),
445 described(field("lastUpdatedDateTime", "string"), "RFC 3339"),
446 json!({"name": "members", "type": "array", "items": {"type": "object", "fields": [
447 field("userId", "string"),
448 nullable_field("displayName", "string")
449 ]}}),
450 object_field(
451 "viewpoint",
452 true,
453 "The signed-in user's read state",
454 vec![nullable_field("lastMessageReadDateTime", "string")],
455 ),
456 object_field(
457 "lastMessagePreview",
458 true,
459 "Latest message; desktop only",
460 vec![
461 field("id", "string"),
462 field("createdDateTime", "string"),
463 sender(),
464 body(),
465 ],
466 ),
467 ],
468 ),
469 {
470 let mut value = paged(
471 "messages list",
472 "List messages from one chat, or from a channel with optional admin-consented access",
473 vec![
474 field("id", "string"),
475 described(
476 field("createdDateTime", "string"),
477 "RFC 3339; pages are newest first",
478 ),
479 sender(),
480 body(),
481 described(nullable_field("deleted", "boolean"), "Desktop only"),
482 described(
483 nullable_field("clientMessageId", "string"),
484 "Desktop cache only; exact send correlation ID",
485 ),
486 described(
487 nullable_field("status", "string"),
488 "Desktop cache only; raw Teams status",
489 ),
490 ],
491 );
492 let args = value["args"].as_array_mut().unwrap();
493 args.splice(0..0, conversation_args());
494 value
495 },
496 {
497 let mut value = single(
498 "messages send",
499 "Send one plain-text message as the signed-in user",
500 "non_idempotent",
501 vec![
502 described(field("id", "string"), "Message ID"),
503 nullable_field("createdDateTime", "string"),
504 nullable_field("webUrl", "string"),
505 nullable_field("chatId", "string"),
506 described(
507 nullable_field("source", "string"),
508 "\"desktop\" for desktop sends",
509 ),
510 described(
511 nullable_field("clientMessageId", "string"),
512 "Desktop only; correlation ID, not an idempotency key",
513 ),
514 described(
515 nullable_field("delivery_state", "string"),
516 "Desktop only: submitted, queued, delivered, failed or unknown",
517 ),
518 described(
519 nullable_field("status", "string"),
520 "Desktop only; Teams' own delivery label, e.g. Delivered",
521 ),
522 ],
523 );
524 let mut args = conversation_args();
525 args.push(json!({"name": "--body", "type": "string", "required": true, "x-max-length": crate::desktop::MAX_BODY,
526 "description": "Plain-text message body, or - to read it from stdin; must not be blank"}));
527 value["args"] = json!(args);
528 value["confirmation_bypass_arg"] = json!("--yes");
529 value
530 },
531 json!({"name":"tui", "description":"Open the keyboard-first terminal interface; offers a confirmed Teams debugging restart on WSL when no endpoint listens; Enter sends a chat message on a profile that allows writes", "effects":"non_idempotent", "output_kind":"opaque", "media_type":"text/plain", "requires_tty":true, "args":[arg("--demo", "boolean", "Use deterministic sample data"), arg("--snapshot", "boolean", "Print one ANSI-free demo frame")]}),
532 {
533 let mut value = single(
534 "doctor",
535 "Check configuration and the selected backend's access (desktop checks endpoint metadata)",
536 "read_only",
537 vec![
538 field("backend", "string"),
539 array_field("checks", "object"),
540 field("healthy", "boolean"),
541 nullable_field("connection", "object"),
542 nullable_field("endpoint_available", "boolean"),
543 field("session_verified", "boolean"),
544 debugging_setup(),
545 ],
546 );
547 value["args"] = json!([arg(
548 "--offline",
549 "boolean",
550 "Check local state without contacting Graph or the desktop endpoint"
551 )]);
552 value
553 },
554 single(
555 "capabilities",
556 "Describe supported and deliberately unsupported capabilities",
557 "read_only",
558 vec![
559 array_field("supported", "string"),
560 array_field("unsupported", "string"),
561 array_field("account_types", "string"),
562 field("api", "string"),
563 array_field("notes", "string"),
564 field("backends", "object"),
565 ],
566 ),
567 json!({
568 "name":"schema",
569 "description":"Emit the offline CLI Spec v0.3 contract",
570 "effects":"read_only",
571 "cardinality":"single",
572 "stdout_schema":{},
573 "args":[arg("--command", "string", "Return only one complete command path")]
574 }),
575 json!({"name":"completions", "description":"Generate a shell completion script", "effects":"read_only", "output_kind":"opaque", "media_type":"text/plain", "args":[arg("shell", "string", "Shell name")]}),
576 ];
577 let mut status = single(
578 "messages status",
579 "Reconcile one client message ID against the newest 50 cached desktop messages; absence remains unknown",
580 "read_only",
581 vec![
582 field("chatId", "string"),
583 field("clientMessageId", "string"),
584 field("found", "boolean"),
585 nullable_field("id", "string"),
586 nullable_field("status", "string"),
587 field("delivery_state", "string"),
588 field("source", "string"),
589 field("scope", "string"),
590 field("retryable", "boolean"),
591 ],
592 );
593 status["args"] = json!([
594 required_arg("--chat", "string", "Chat ID"),
595 required_arg(
596 "--client-message-id",
597 "string",
598 "18-digit correlation ID returned by send or unconfirmed error"
599 )
600 ]);
601 commands.push(status);
602 for (name, description, effects) in [
603 (
604 "desktop status",
605 "Inspect endpoint metadata and managed-session state; does not verify sign-in",
606 "read_only",
607 ),
608 (
609 "desktop connect",
610 "Explicitly restart Teams with debugging; preserve settings and never adopt an existing endpoint",
611 "non_idempotent",
612 ),
613 (
614 "desktop disconnect",
615 "Explicitly restart this managed Teams process normally; preserve unrelated processes and settings",
616 "non_idempotent",
617 ),
618 (
619 "desktop recover",
620 "Restore interrupted setup settings without restarting Teams or accessing the endpoint",
621 "idempotent",
622 ),
623 ] {
624 let mut value = single(
625 name,
626 description,
627 effects,
628 vec![
629 field("port", "integer"),
630 nullable_field("endpoint_available", "boolean"),
631 field("session_verified", "boolean"),
632 field("management", "object"),
633 nullable_field("connection", "object"),
634 ],
635 );
636 value["args"] = json!([{"name":"--port","type":"integer","x-minimum":1024,"x-maximum":65535,"description":"Override selected profile port"}]);
637 if matches!(name, "desktop connect" | "desktop disconnect") {
638 value["confirmation_bypass_arg"] = json!("--yes");
639 }
640 commands.push(value);
641 }
642 for command in &mut commands {
643 let name = command["name"].as_str().unwrap_or_default().to_string();
644 let mut errors = vec!["invalid_input", "unexpected_error"];
645 errors.extend(command_errors(&name));
646 command["errors"] = json!(errors);
647 let example = example(&name);
648 if !example.is_null() {
649 command["example"] = example;
650 }
651 command["extensions"] = json!({"backends": match command["name"].as_str().unwrap_or_default() {
652 "auth login" | "auth logout" | "whoami" | "list" | "channels list" => vec!["graph"],
653 "desktop status" | "desktop connect" | "desktop disconnect" | "desktop recover" | "messages status" => vec!["desktop"],
654 _ => vec!["graph", "desktop"],
655 }});
656 match command["name"].as_str().unwrap_or_default() {
657 "tui" => {
658 command["extensions"]["desktop"] = json!(
659 "macOS/Windows/WSL; chats and chat messages from the Teams client's local cache; sending needs a profile created with --allow-send"
660 )
661 }
662 "chats list" | "messages list" => {
663 command["extensions"]["desktop"] = json!(
664 "macOS/Windows/WSL; chats, not channels; read from the Teams client's local cache; pages carry source \"desktop_cache\""
665 )
666 }
667 "messages send" => {
668 command["extensions"]["desktop"] = json!(
669 "macOS/Windows/WSL; plain-text chat messages through the running Teams window; needs a profile created with `init --allow-send`; prompts on a terminal and needs --yes otherwise; results carry source \"desktop\", clientMessageId and Teams' status"
670 )
671 }
672 _ => {}
673 }
674 }
675 if let Some(filter) = command_filter {
676 commands.retain(|value| value["name"] == filter);
677 }
678 json!({
679 "$schema": "https://clispec.dev/schema/v0.3.json",
680 "clispec": "0.3",
681 "name": "teams",
682 "version": env!("CARGO_PKG_VERSION"),
683 "description": "A Microsoft Teams CLI and TUI designed equally for humans and agents",
684 "output": {"tty": "text", "piped": "json"},
685 "global_args": [
686 {"name":"--output", "short":"-o", "type":"string", "enum":["auto","text","json"], "default":"auto", "description":"Output format"},
687 {"name":"--profile", "type":"string", "x-env":"TEAMS_PROFILE", "description":"Configuration profile; defaults to the active one"},
688 {"name":"--quiet", "type":"boolean", "description":"Suppress informational stderr output"},
689 {"name":"--yes", "short":"-y", "type":"boolean", "description":"Confirm without prompting (desktop sends, profile removal)"}
690 ],
691 "commands": commands,
692 "errors": error::ALL.iter().map(|e| json!({"kind":e.kind,"exit_code":e.exit_code,"retryable":e.retryable,"description":e.description})).collect::<Vec<_>>(),
693 "extensions": {
694 "backends":backend_capabilities(),
695 "authentication":"delegated_oauth",
696 "api":"Microsoft Graph v1.0",
697 "default_client_id": crate::config::DEFAULT_CLIENT_ID,
698 "login_modes":["browser_pkce","device_code"],
699 "read_only_env":"TEAMS_READ_ONLY",
700 "environment":{
701 "TEAMS_PROFILE":"Profile to use when --profile is not given",
702 "TEAMS_READ_ONLY":"true blocks every remote write; false cannot unlock a desktop profile",
703 "TEAMS_ACCESS_TOKEN":"Graph access token that overrides the stored credential for this run",
704 "TEAMS_CLIENT_ID":"Graph application ID override",
705 "TEAMS_TENANT":"Graph tenant override",
706 "TEAMS_DESKTOP_HELPER":"Path to a teams.exe speaking the same helper protocol to use as the WSL desktop helper"
707 },
708 "collection_envelope":{
709 "items":"array of records described by the command's output_fields",
710 "next_cursor":"cursor-paged commands: pass to --cursor for the next page; absent on the last page",
711 "next_offset":"offset-paged commands: pass to --offset for the next page; absent on the last page",
712 "truncated":"true when more records exist",
713 "source":"\"desktop_cache\" when records come from the Teams client's local cache; absent for Graph"
714 },
715 "error_envelope":"stderr carries {\"error\":{\"kind\",\"message\",\"retryable\"}} when stdout is not a terminal or --output json is given; rate_limit adds retry_after_seconds when Microsoft sends it; uncertain desktop sends add chatId, clientMessageId and delivery_state unknown",
716 "idempotency":"Sends cannot be made idempotent: neither Graph nor Teams accepts a caller-supplied key. Never resend after unconfirmed without reading the conversation first.",
717 "baseline_scopes": crate::auth::BASE_SCOPES.split_whitespace().collect::<Vec<_>>(),
718 "privileged_scopes":[crate::auth::CHANNEL_HISTORY_SCOPE]
719 }
720 })
721}
722
723pub fn backend_capabilities() -> Value {
724 json!({
725 "graph":{"default":true,"authentication":"delegated_oauth","api":"Microsoft Graph v1.0","account_types":["Microsoft 365 work or school"]},
726 "desktop":{"experimental":true,"read_only_by_default":true,"platforms":["macos","windows","wsl"],
727 "account_types":["Microsoft 365 work or school","personal Microsoft accounts"],"account_selection":"account_type_then_single_account_only",
728 "authentication":"teams_desktop_session_unverified","supported":["configuration","connection metadata","doctor","auth status","managed connect/status/disconnect/recover","delivery reconciliation by client message ID","chats (macos, windows, wsl)","chat messages (macos, windows, wsl)","send plain-text chat messages (macos, windows, wsl; opt-in)","TUI (macos, windows, wsl)"],
729 "unsupported":["teams and channels","identity","channel messages, replies, mentions, attachments and edits","subscriptions","automatic Teams launch or restart"],
730 "requires":"existing Teams-owned loopback debugging endpoint; WSL requires Windows executable interop",
731 "transport":{"macos":"native_loopback","windows":"windows_native","wsl":"windows_helper_stdio"},
732 "chat_source":"desktop_cache","session_verified":false,"default_port":crate::config::DEFAULT_DESKTOP_PORT}
733 })
734}