Skip to main content

call_coding_clis/
help.rs

1use crate::config::load_config;
2use crate::parser::AliasDef;
3use crate::RUNNER_REGISTRY;
4use serde_json::Value;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio};
8
9struct RunnerStatus {
10    name: String,
11    #[allow(dead_code)]
12    alias: String,
13    binary: String,
14    found: bool,
15    version: String,
16}
17
18const CANONICAL_RUNNERS: &[(&str, &str)] = &[
19    ("opencode", "oc"),
20    ("claude", "cc"),
21    ("kimi", "k"),
22    ("codex", "c/cx"),
23    ("roocode", "rc"),
24    ("crush", "cr"),
25    ("cursor", "cu"),
26    ("gemini", "g"),
27];
28
29const HELP_TEXT: &str = r#"ccc — call coding CLIs
30
31Usage:
32  ccc [controls...] "<Prompt>"
33  ccc [controls...] -- "<Prompt starting with control-like tokens>"
34  ccc config
35  ccc config --edit [--user|--local]
36  ccc add [-g] <alias>
37  ccc --print-config
38  ccc help
39  ccc --help
40  ccc -h
41  ccc @reviewer --help
42
43Controls (free order before the prompt):
44  runner        Select which coding CLI to use (default: oc)
45                opencode (oc), claude (cc), kimi (k), codex (c/cx), roocode (rc), crush (cr), cursor (cu), gemini (g)
46  +thinking     Set thinking level: +0..+4 or +none/+low/+med/+mid/+medium/+high/+max/+xhigh
47                Claude maps +0 to --thinking disabled and +1..+4 to --thinking enabled with matching --effort
48                Kimi maps +0 to --no-thinking and +1..+4 to --thinking
49  :provider:model  Override provider and model
50  @name         Use a named preset from config; if no preset exists, runner names select runners before agent fallback
51                Presets can also define a default prompt when the user leaves prompt text blank
52                prompt_mode lets alias prompts prepend or append text; prepend/append require an explicit prompt argument
53  .mode / ..mode
54                Output-mode sugar with a shared dot identity:
55                  .text / ..text, .json / ..json, .fmt / ..fmt, .pt / ..pt, .pj / ..pj
56  --permission-mode <safe|auto|yolo|plan>
57                Request a higher-level permission profile when the selected runner supports it
58  --yolo / -y   Request the runner's lowest-friction auto-approval mode when supported
59  --save-session
60                Allow the selected runner to save this run in its normal session history
61  --cleanup-session
62                Try to clean up the created session after the run when no no-persist flag exists
63
64Flags:
65  --print-config                         Print the canonical example config.toml and exit
66  help / --help / -h                    Print help and exit, even when mixed with other args
67  --version / -v                        Print the ccc version and resolved client versions
68  --show-thinking / --no-show-thinking  Request visible thinking output when the selected runner supports it
69                                        (default: on; config key: show_thinking)
70  --sanitize-osc / --no-sanitize-osc    Strip disruptive OSC control output in human-facing modes
71                                        while preserving OSC 8 hyperlinks
72                                        (config key: defaults.sanitize_osc)
73  --output-log-path / --no-output-log-path
74                                        Print the parseable run-artifact footer line on stderr
75  --output-mode / -o <text|stream-text|json|stream-json|formatted|stream-formatted|pass-text|pt|stream-pass-text|stream-pt|pass-json|pj|stream-pass-json|stream-pj>
76                                        Select raw, streamed, or formatted output handling
77                                        (config key: defaults.output_mode)
78  --forward-unknown-json                In formatted modes, forward unhandled JSON objects to stderr
79  --timeout-secs <N>                    Kill the runner after N seconds and exit 124
80  Environment:
81    CCC_FWD_UNKNOWN_JSON                Also controls unknown-JSON forwarding; defaults on for now
82    FORCE_COLOR / NO_COLOR              Override TTY detection for formatted human output
83                                        (FORCE_COLOR wins if both are set)
84  --            Treat all remaining args as prompt text, even if they look like controls
85
86Examples:
87  ccc "Fix the failing tests"
88  ccc oc "Refactor auth module"
89  ccc cc +2 :anthropic:claude-sonnet-4-20250514 @reviewer "Add tests"
90  ccc c +4 :openai:gpt-5.4-mini @agent "Debug the parser"
91  ccc --permission-mode auto c "Add tests"
92  ccc --yolo cc +2 :anthropic:claude-sonnet-4-20250514 "Add tests"
93  ccc --permission-mode plan k "Think before editing"
94  ccc ..fmt cc +3 "Investigate the failing test"
95  ccc -o stream-json k "Reply with exactly pong"
96  ccc @reviewer k +4 "Debug the parser"
97  ccc @reviewer "Audit the API boundary"
98  ccc codex "Write a unit test"
99  ccc -y -- +1 @agent :model
100  ccc --print-config
101
102Config:
103  ccc config                            — print every resolved config file path and contents
104  ccc config --edit                     — open the selected config in $EDITOR
105  ccc config --edit --user              — open XDG_CONFIG_HOME/ccc/config.toml or ~/.config/ccc/config.toml
106  ccc config --edit --local             — open the nearest .ccc.toml, or create one in CWD
107  ccc add [-g] <alias>                  — prompt for alias settings and write them to config
108  ccc add <alias> --runner cc --prompt "Review" --yes
109                                        — write an alias non-interactively
110  ccc --print-config                    — print the canonical example config.toml
111  .ccc.toml (searched upward from CWD)  — project-local presets and defaults
112  XDG_CONFIG_HOME/ccc/config.toml       — global defaults when XDG is set
113  ~/.config/ccc/config.toml             — legacy global fallback
114
115Agent tips:
116  Run `ccc config` before relying on aliases or defaults; `ccc --help` lists the aliases visible from the current directory.
117  Use `ccc @alias "task"` when an alias matches the job, then add explicit runner/model/thinking controls only when they matter.
118  Use `--` before prompt text that starts with control-like tokens such as `+1`, `@agent`, or `:model`.
119"#;
120
121fn get_version(binary: &str) -> String {
122    match Command::new(binary)
123        .arg("--version")
124        .stdout(Stdio::piped())
125        .stderr(Stdio::null())
126        .output()
127    {
128        Ok(output) if output.status.success() => String::from_utf8_lossy(&output.stdout)
129            .lines()
130            .next()
131            .unwrap_or("")
132            .to_string(),
133        _ => String::new(),
134    }
135}
136
137fn ccc_version() -> String {
138    option_env!("CCC_VERSION")
139        .unwrap_or(env!("CARGO_PKG_VERSION"))
140        .to_string()
141}
142
143fn read_json_version(package_json_path: &Path, expected_name: &str) -> String {
144    let payload = match fs::read_to_string(package_json_path) {
145        Ok(text) => text,
146        Err(_) => return String::new(),
147    };
148    let parsed: Value = match serde_json::from_str(&payload) {
149        Ok(value) => value,
150        Err(_) => return String::new(),
151    };
152    if parsed.get("name").and_then(Value::as_str) != Some(expected_name) {
153        return String::new();
154    }
155    parsed
156        .get("version")
157        .and_then(Value::as_str)
158        .unwrap_or("")
159        .to_string()
160}
161
162fn discover_opencode_version(binary_path: &Path) -> String {
163    read_json_version(
164        &binary_path
165            .parent()
166            .unwrap_or(binary_path)
167            .parent()
168            .unwrap_or(binary_path)
169            .join("package.json"),
170        "opencode-ai",
171    )
172}
173
174fn discover_codex_version(binary_path: &Path) -> String {
175    let version = read_json_version(
176        &binary_path
177            .parent()
178            .unwrap_or(binary_path)
179            .parent()
180            .unwrap_or(binary_path)
181            .join("package.json"),
182        "@openai/codex",
183    );
184    if version.is_empty() {
185        String::new()
186    } else {
187        format!("codex-cli {version}")
188    }
189}
190
191fn discover_claude_version(binary_path: &Path) -> String {
192    let parts: Vec<_> = binary_path
193        .components()
194        .map(|component| component.as_os_str().to_string_lossy().into_owned())
195        .collect();
196    if parts.len() < 3 || parts[parts.len() - 3] != "claude" || parts[parts.len() - 2] != "versions"
197    {
198        return String::new();
199    }
200    let version = &parts[parts.len() - 1];
201    if version.is_empty() {
202        String::new()
203    } else {
204        format!("{version} (Claude Code)")
205    }
206}
207
208fn discover_kimi_version(binary_path: &Path) -> String {
209    if binary_path
210        .parent()
211        .and_then(Path::file_name)
212        .and_then(|value| value.to_str())
213        != Some("bin")
214    {
215        return String::new();
216    }
217    let lib_dir = match binary_path.parent().and_then(Path::parent) {
218        Some(parent) => parent.join("lib"),
219        None => return String::new(),
220    };
221    let lib_entries = match fs::read_dir(&lib_dir) {
222        Ok(entries) => entries,
223        Err(_) => return String::new(),
224    };
225    for lib_entry in lib_entries.flatten() {
226        let python_dir = lib_entry.path();
227        let site_packages = python_dir.join("site-packages");
228        let dist_entries = match fs::read_dir(&site_packages) {
229            Ok(entries) => entries,
230            Err(_) => continue,
231        };
232        for dist_entry in dist_entries.flatten() {
233            let dist_path = dist_entry.path();
234            let Some(name) = dist_path.file_name().and_then(|value| value.to_str()) else {
235                continue;
236            };
237            if !name.starts_with("kimi_cli-") || !name.ends_with(".dist-info") {
238                continue;
239            }
240            let metadata_path = dist_path.join("METADATA");
241            let Ok(metadata) = fs::read_to_string(metadata_path) else {
242                continue;
243            };
244            for line in metadata.lines() {
245                if let Some(version) = line.strip_prefix("Version: ") {
246                    if !version.trim().is_empty() {
247                        return format!("kimi, version {}", version.trim());
248                    }
249                    return String::new();
250                }
251            }
252        }
253    }
254    String::new()
255}
256
257fn json_name_matches(package_json_path: &Path, expected_name: &str) -> bool {
258    let payload = match fs::read_to_string(package_json_path) {
259        Ok(text) => text,
260        Err(_) => return false,
261    };
262    let parsed: Value = match serde_json::from_str(&payload) {
263        Ok(value) => value,
264        Err(_) => return false,
265    };
266    parsed.get("name").and_then(Value::as_str) == Some(expected_name)
267}
268
269fn read_cursor_release_version(index_path: &Path) -> String {
270    let text = match fs::read_to_string(index_path) {
271        Ok(text) => text,
272        Err(_) => return String::new(),
273    };
274    let marker = "agent-cli@";
275    let Some(start) = text.find(marker) else {
276        return String::new();
277    };
278    text[start + marker.len()..]
279        .chars()
280        .take_while(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-'))
281        .collect()
282}
283
284fn discover_cursor_version(binary_path: &Path) -> String {
285    let package_root = binary_path.parent().unwrap_or(binary_path);
286    if !json_name_matches(
287        &package_root.join("package.json"),
288        "@anysphere/agent-cli-runtime",
289    ) {
290        return String::new();
291    }
292    read_cursor_release_version(&package_root.join("index.js"))
293}
294
295fn discover_gemini_version(binary_path: &Path) -> String {
296    let home = std::env::var_os("HOME").map(PathBuf::from);
297    discover_gemini_version_with_home(binary_path, home.as_deref())
298}
299
300fn discover_gemini_version_with_home(binary_path: &Path, home: Option<&Path>) -> String {
301    let mut candidates = vec![
302        binary_path
303            .parent()
304            .unwrap_or(binary_path)
305            .join("package.json"),
306        binary_path
307            .parent()
308            .unwrap_or(binary_path)
309            .parent()
310            .unwrap_or(binary_path)
311            .join("package.json"),
312    ];
313    let mut is_npx_launcher = false;
314    if let Ok(launcher) = fs::read_to_string(binary_path) {
315        if launcher.contains("@google/gemini-cli") {
316            is_npx_launcher = true;
317            if let Some(home) = home {
318                let npx_root = home.join(".npm").join("_npx");
319                if let Ok(entries) = fs::read_dir(npx_root) {
320                    for entry in entries.flatten() {
321                        candidates.push(
322                            entry
323                                .path()
324                                .join("node_modules")
325                                .join("@google")
326                                .join("gemini-cli")
327                                .join("package.json"),
328                        );
329                    }
330                }
331            }
332        }
333    }
334    for candidate in candidates {
335        let version = read_json_version(&candidate, "@google/gemini-cli");
336        if !version.is_empty() {
337            return version;
338        }
339    }
340    if is_npx_launcher {
341        return "npx @google/gemini-cli".to_string();
342    }
343    String::new()
344}
345
346fn get_runner_version(runner_name: &str, binary: &str, binary_path: &Path) -> String {
347    let real_path = match fs::canonicalize(binary_path) {
348        Ok(path) => path,
349        Err(_) => binary_path.to_path_buf(),
350    };
351    let version = match runner_name {
352        "opencode" => discover_opencode_version(&real_path),
353        "codex" => discover_codex_version(&real_path),
354        "claude" => discover_claude_version(&real_path),
355        "kimi" => discover_kimi_version(&real_path),
356        "cursor" => discover_cursor_version(&real_path),
357        "gemini" => discover_gemini_version(&real_path),
358        _ => String::new(),
359    };
360    if version.is_empty() {
361        get_version(binary)
362    } else {
363        version
364    }
365}
366
367fn is_on_path(binary: &str) -> bool {
368    resolve_binary_path(binary).is_some()
369}
370
371fn resolve_binary_path(binary: &str) -> Option<String> {
372    Command::new("which")
373        .arg(binary)
374        .stdout(Stdio::piped())
375        .stderr(Stdio::null())
376        .output()
377        .ok()
378        .and_then(|output| {
379            if output.status.success() {
380                String::from_utf8(output.stdout).ok()
381            } else {
382                None
383            }
384        })
385        .map(|text| text.trim().to_string())
386        .filter(|text| !text.is_empty())
387}
388
389fn runner_checklist() -> Vec<RunnerStatus> {
390    let mut statuses = Vec::new();
391    for &(name, alias) in CANONICAL_RUNNERS {
392        let registry = RUNNER_REGISTRY.read().unwrap();
393        let binary = registry
394            .get(name)
395            .map(|info| info.binary.clone())
396            .unwrap_or_else(|| name.to_string());
397        drop(registry);
398
399        let found = is_on_path(&binary);
400        let version = if found {
401            let binary_path = resolve_binary_path(&binary);
402            match binary_path {
403                Some(path) => get_runner_version(name, &binary, Path::new(&path)),
404                None => get_version(&binary),
405            }
406        } else {
407            String::new()
408        };
409        statuses.push(RunnerStatus {
410            name: name.to_string(),
411            alias: alias.to_string(),
412            binary,
413            found,
414            version,
415        });
416    }
417    statuses
418}
419
420fn format_runner_checklist() -> String {
421    let mut out = String::from("Runners:\n");
422    for s in runner_checklist() {
423        if s.found {
424            let tag = if s.version.is_empty() {
425                "found"
426            } else {
427                &s.version
428            };
429            out.push_str(&format!("  [+] {:10} ({})  {}\n", s.name, s.binary, tag));
430        } else {
431            out.push_str(&format!("  [-] {:10} ({})  not found\n", s.name, s.binary));
432        }
433    }
434    out
435}
436
437fn format_alias_value(value: &str) -> String {
438    let text = value.replace(['\r', '\n'], " ");
439    if text
440        .chars()
441        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | ':' | '/' | '+' | '-'))
442    {
443        return text;
444    }
445    format!("\"{}\"", text.replace('\\', "\\\\").replace('"', "\\\""))
446}
447
448fn format_alias_summary(alias: &AliasDef) -> String {
449    let mut fields = Vec::new();
450    if let Some(value) = &alias.runner {
451        fields.push(format!("runner={}", format_alias_value(value)));
452    }
453    if let Some(value) = &alias.provider {
454        fields.push(format!("provider={}", format_alias_value(value)));
455    }
456    if let Some(value) = &alias.model {
457        fields.push(format!("model={}", format_alias_value(value)));
458    }
459    if let Some(value) = alias.thinking {
460        fields.push(format!("thinking={value}"));
461    }
462    if let Some(value) = alias.show_thinking {
463        fields.push(format!(
464            "show_thinking={}",
465            if value { "true" } else { "false" }
466        ));
467    }
468    if let Some(value) = alias.sanitize_osc {
469        fields.push(format!(
470            "sanitize_osc={}",
471            if value { "true" } else { "false" }
472        ));
473    }
474    if let Some(value) = &alias.output_mode {
475        fields.push(format!("output_mode={}", format_alias_value(value)));
476    }
477    if let Some(value) = &alias.agent {
478        fields.push(format!("agent={}", format_alias_value(value)));
479    }
480    if let Some(value) = &alias.prompt {
481        fields.push(format!("prompt={}", format_alias_value(value)));
482    }
483    if let Some(value) = &alias.prompt_mode {
484        fields.push(format!("prompt_mode={}", format_alias_value(value)));
485    }
486    if fields.is_empty() {
487        "(empty)".to_string()
488    } else {
489        fields.join(" ")
490    }
491}
492
493fn format_alias_checklist() -> String {
494    let config = load_config(None);
495    let mut out = String::from("Configured aliases:\n");
496    if config.aliases.is_empty() {
497        out.push_str("  (none)\n");
498        return out;
499    }
500    for (name, alias) in config.aliases {
501        out.push_str(&format!("  @{name:<11} {}\n", format_alias_summary(&alias)));
502    }
503    out
504}
505
506fn format_version_report(version: &str, statuses: &[RunnerStatus]) -> String {
507    let mut out = format!("ccc version {version}\nResolved clients:\n");
508    let mut resolved = 0usize;
509    for s in statuses {
510        if s.version.is_empty() {
511            continue;
512        }
513        resolved += 1;
514        out.push_str(&format!(
515            "  [+] {:10} ({})  {}\n",
516            s.name, s.binary, s.version
517        ));
518    }
519    let unresolved = statuses.len().saturating_sub(resolved);
520    if unresolved > 0 {
521        out.push_str(&format!("  (and {unresolved} unresolved)\n"));
522    }
523    out.trim_end_matches('\n').to_string()
524}
525
526pub fn print_help() {
527    print!("{}", HELP_TEXT);
528    println!();
529    println!("{}", format_alias_checklist().trim_end());
530    println!();
531    print!("{}", format_runner_checklist());
532}
533
534pub fn print_version() {
535    println!(
536        "{}",
537        format_version_report(&ccc_version(), &runner_checklist())
538    );
539}
540
541pub fn print_usage() {
542    eprintln!("usage: ccc [controls...] \"<Prompt>\"");
543    eprint!("{}", format_runner_checklist());
544}
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549    use std::time::{SystemTime, UNIX_EPOCH};
550
551    fn unique_temp_dir(label: &str) -> PathBuf {
552        let unique = SystemTime::now()
553            .duration_since(UNIX_EPOCH)
554            .unwrap()
555            .as_nanos();
556        let path = std::env::temp_dir().join(format!("ccc-help-{label}-{unique}"));
557        fs::create_dir_all(&path).unwrap();
558        path
559    }
560
561    #[test]
562    fn test_get_runner_version_reads_opencode_package_json_before_command() {
563        let root = unique_temp_dir("opencode");
564        let package_root = root.join("node_modules").join("opencode-ai");
565        let binary_path = package_root.join("bin").join("opencode");
566        fs::create_dir_all(binary_path.parent().unwrap()).unwrap();
567        fs::write(
568            package_root.join("package.json"),
569            r#"{"name":"opencode-ai","version":"1.2.3"}"#,
570        )
571        .unwrap();
572        fs::write(&binary_path, "#!/bin/sh\nexit 99\n").unwrap();
573
574        assert_eq!(
575            get_runner_version("opencode", "definitely-missing-binary", &binary_path),
576            "1.2.3"
577        );
578    }
579
580    #[test]
581    fn test_get_runner_version_reads_codex_package_json_before_command() {
582        let root = unique_temp_dir("codex");
583        let package_root = root.join("node_modules").join("@openai").join("codex");
584        let binary_path = package_root.join("bin").join("codex.js");
585        fs::create_dir_all(binary_path.parent().unwrap()).unwrap();
586        fs::write(
587            package_root.join("package.json"),
588            r#"{"name":"@openai/codex","version":"0.118.0"}"#,
589        )
590        .unwrap();
591        fs::write(&binary_path, "#!/usr/bin/env node\n").unwrap();
592
593        assert_eq!(
594            get_runner_version("codex", "definitely-missing-binary", &binary_path),
595            "codex-cli 0.118.0"
596        );
597    }
598
599    #[test]
600    fn test_get_runner_version_reads_claude_version_from_install_path() {
601        let root = unique_temp_dir("claude");
602        let versions_dir = root.join("claude").join("versions");
603        fs::create_dir_all(&versions_dir).unwrap();
604        let binary_path = versions_dir.join("2.1.98");
605        fs::write(&binary_path, "").unwrap();
606
607        assert_eq!(
608            get_runner_version("claude", "definitely-missing-binary", &binary_path),
609            "2.1.98 (Claude Code)"
610        );
611    }
612
613    #[test]
614    fn test_get_runner_version_reads_kimi_metadata_before_command() {
615        let root = unique_temp_dir("kimi");
616        let binary_path = root.join("bin").join("kimi");
617        let metadata_dir = root
618            .join("lib")
619            .join("python3.13")
620            .join("site-packages")
621            .join("kimi_cli-1.30.0.dist-info");
622        fs::create_dir_all(binary_path.parent().unwrap()).unwrap();
623        fs::create_dir_all(&metadata_dir).unwrap();
624        fs::write(&binary_path, "#!/usr/bin/env python3\n").unwrap();
625        fs::write(
626            metadata_dir.join("METADATA"),
627            "Metadata-Version: 2.3\nName: kimi-cli\nVersion: 1.30.0\n",
628        )
629        .unwrap();
630
631        assert_eq!(
632            get_runner_version("kimi", "definitely-missing-binary", &binary_path),
633            "kimi, version 1.30.0"
634        );
635    }
636
637    #[test]
638    fn test_get_runner_version_reads_cursor_release_marker_before_command() {
639        let root = unique_temp_dir("cursor");
640        let package_root = root.join("cursor-agent");
641        let binary_path = package_root.join("cursor-agent");
642        fs::create_dir_all(&package_root).unwrap();
643        fs::write(
644            package_root.join("package.json"),
645            r#"{"name":"@anysphere/agent-cli-runtime","private":true}"#,
646        )
647        .unwrap();
648        fs::write(
649            package_root.join("index.js"),
650            r#"globalThis.SENTRY_RELEASE={id:"agent-cli@2026.03.30-a5d3e17"};"#,
651        )
652        .unwrap();
653        fs::write(&binary_path, "#!/bin/sh\nexit 99\n").unwrap();
654
655        assert_eq!(
656            get_runner_version("cursor", "definitely-missing-binary", &binary_path),
657            "2026.03.30-a5d3e17"
658        );
659    }
660
661    #[test]
662    fn test_get_runner_version_reads_gemini_package_json_before_command() {
663        let root = unique_temp_dir("gemini");
664        let package_root = root.join("node_modules").join("@google").join("gemini-cli");
665        let binary_path = package_root.join("dist").join("index.js");
666        fs::create_dir_all(binary_path.parent().unwrap()).unwrap();
667        fs::write(
668            package_root.join("package.json"),
669            r#"{"name":"@google/gemini-cli","version":"0.37.2"}"#,
670        )
671        .unwrap();
672        fs::write(&binary_path, "#!/usr/bin/env node\n").unwrap();
673
674        assert_eq!(
675            get_runner_version("gemini", "definitely-missing-binary", &binary_path),
676            "0.37.2"
677        );
678    }
679
680    #[test]
681    fn test_get_runner_version_identifies_gemini_npx_launcher_without_command() {
682        let root = unique_temp_dir("gemini-npx");
683        let binary_path = root.join("gemini");
684        fs::write(
685            &binary_path,
686            "#!/bin/bash\nexec npx --yes @google/gemini-cli \"$@\"\n",
687        )
688        .unwrap();
689
690        assert_eq!(
691            discover_gemini_version_with_home(&binary_path, Some(&root)),
692            "npx @google/gemini-cli"
693        );
694    }
695
696    #[test]
697    fn test_get_runner_version_falls_back_when_metadata_is_missing() {
698        assert_eq!(
699            get_runner_version(
700                "opencode",
701                "definitely-missing-binary",
702                Path::new("/tmp/missing/opencode")
703            ),
704            ""
705        );
706    }
707}