Skip to main content

kaizen/shell/
init.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! `kaizen init` — idempotent workspace setup.
3
4use crate::ipc::{CaptureComponentStatus, CaptureStatus};
5use anyhow::Result;
6use std::fmt::Write;
7use std::path::{Path, PathBuf};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10const CONFIG_TOML: &str = r#"[kaizen]
11
12# Optional sync (usually override secrets in ~/.kaizen/config.toml):
13# [sync]
14# endpoint = "https://ingest.example.com"
15# team_token = "Bearer-token-from-server"
16# team_id = "your-team"
17# events_per_batch_max = 500
18# max_body_bytes = 1000000
19# flush_interval_ms = 10000
20# sample_rate = 1.0
21"#;
22const KAIZEN_RETRO_SKILL: &str = include_str!("../../assets/kaizen-retro-SKILL.md");
23const KAIZEN_EVAL_SKILL: &str = include_str!("../../assets/kaizen-eval-SKILL.md");
24
25const CURSOR_HOOK_EVENTS: &[&str] = &["SessionStart", "PreToolUse", "PostToolUse", "Stop"];
26const CLAUDE_HOOK_EVENTS: &[&str] = &["SessionStart", "PreToolUse", "PostToolUse", "Stop"];
27
28#[derive(Clone, Copy, Debug, Default)]
29pub struct InitOptions {
30    pub deep: bool,
31    pub start_capture: bool,
32}
33
34fn ts_ms() -> u64 {
35    SystemTime::now()
36        .duration_since(UNIX_EPOCH)
37        .unwrap_or_default()
38        .as_millis() as u64
39}
40
41fn backup_path(ws: &Path, filename: &str) -> Result<PathBuf> {
42    let relative = PathBuf::from("backup").join(format!("{}.{}.bak", filename, ts_ms()));
43    crate::core::paths::project_file_for_write(ws, &relative)
44}
45
46fn ensure_config(out: &mut String, ws: &Path) -> Result<()> {
47    let path = crate::core::paths::project_file_for_write(ws, Path::new("config.toml"))?;
48    if path.exists() {
49        writeln!(out, "  skipped  config.toml (project data dir)").unwrap();
50        return Ok(());
51    }
52    let mut file = crate::core::safe_fs::create_new(&path)?;
53    std::io::Write::write_all(&mut file, CONFIG_TOML.as_bytes())?;
54    writeln!(out, "  created  {}", path.display()).unwrap();
55    Ok(())
56}
57
58/// Hook command string written to `.cursor/hooks.json`.
59pub const KAIZEN_CURSOR_HOOK_CMD: &str = "kaizen ingest hook --source cursor";
60pub const KAIZEN_OPENCLAW_HOOK_CMD: &str = "kaizen ingest hook --source openclaw";
61const KAIZEN_OPENCLAW_SPAWN_ARGS: &str = r#""ingest", "hook", "--source", "openclaw""#;
62/// Hook command string written to `.claude/settings.json`.
63pub const KAIZEN_CLAUDE_HOOK_CMD: &str = "kaizen ingest hook --source claude";
64
65/// `true` if every Cursor hook event points at the kaizen ingest command.
66fn cursor_hooks_done(root: &serde_json::Value) -> bool {
67    CURSOR_HOOK_EVENTS
68        .iter()
69        .all(|event| cursor_hook_exists(root, event))
70}
71
72fn cursor_hook_exists(root: &serde_json::Value, event: &str) -> bool {
73    if let Some(arr) = root
74        .pointer(&format!("/hooks/{event}"))
75        .and_then(|v| v.as_array())
76    {
77        return arr
78            .iter()
79            .any(|v| v.get("command").and_then(|c| c.as_str()) == Some(KAIZEN_CURSOR_HOOK_CMD));
80    }
81    if let Some(arr) = root.as_array() {
82        return arr.iter().any(|v| {
83            v.get("matcher").and_then(|m| m.as_str()) == Some(event)
84                && v.get("command").and_then(|c| c.as_str()) == Some(KAIZEN_CURSOR_HOOK_CMD)
85        });
86    }
87    false
88}
89
90fn patch_cursor_hooks(out: &mut String, ws: &Path) -> Result<()> {
91    let Some(cursor_dir) = cursor_user_dir() else {
92        writeln!(out, "  skipped  ~/.cursor/hooks.json (HOME unset)").unwrap();
93        return Ok(());
94    };
95    let path = cursor_dir.join("hooks.json");
96    if !path.exists() {
97        std::fs::create_dir_all(path.parent().unwrap())?;
98        let mut obj = serde_json::Map::new();
99        let mut hooks = serde_json::Map::new();
100        for event in CURSOR_HOOK_EVENTS {
101            hooks.insert(
102                (*event).to_string(),
103                serde_json::json!([{"command": KAIZEN_CURSOR_HOOK_CMD}]),
104            );
105        }
106        obj.insert("hooks".to_string(), serde_json::Value::Object(hooks));
107        write_atomic(&path, &serde_json::to_string_pretty(&obj)?)?;
108        writeln!(out, "  created  ~/.cursor/hooks.json").unwrap();
109        return Ok(());
110    }
111    let raw = std::fs::read_to_string(&path)?;
112    let mut root: serde_json::Value = match serde_json::from_str(&raw) {
113        Ok(v) => v,
114        Err(e) => {
115            writeln!(out, "  error  ~/.cursor/hooks.json: {e}").unwrap();
116            anyhow::bail!("malformed ~/.cursor/hooks.json: {e}");
117        }
118    };
119    if cursor_hooks_done(&root) {
120        writeln!(out, "  skipped  ~/.cursor/hooks.json").unwrap();
121        return Ok(());
122    }
123    let bak = backup_path(ws, "cursor_hooks")?;
124    std::fs::copy(&path, &bak)?;
125    if let Some(obj) = root.pointer_mut("/hooks").and_then(|v| v.as_object_mut()) {
126        for event in CURSOR_HOOK_EVENTS {
127            let arr = obj
128                .entry((*event).to_string())
129                .or_insert_with(|| serde_json::json!([]));
130            if let Some(hooks) = arr.as_array_mut()
131                && !hooks.iter().any(|v| {
132                    v.get("command").and_then(|c| c.as_str()) == Some(KAIZEN_CURSOR_HOOK_CMD)
133                })
134            {
135                hooks.push(serde_json::json!({"command": KAIZEN_CURSOR_HOOK_CMD}));
136            }
137        }
138    } else if let Some(arr) = root.as_array_mut() {
139        for event in CURSOR_HOOK_EVENTS {
140            if !cursor_hook_exists(&serde_json::Value::Array(arr.clone()), event) {
141                arr.push(serde_json::json!({"matcher": event, "command": KAIZEN_CURSOR_HOOK_CMD}));
142            }
143        }
144    }
145    write_atomic(&path, &serde_json::to_string_pretty(&root)?)?;
146    writeln!(
147        out,
148        "  patched  ~/.cursor/hooks.json  (+session/tool hooks)"
149    )
150    .unwrap();
151    Ok(())
152}
153
154fn entry_has_kaizen_cmd(entry: &serde_json::Value) -> bool {
155    if entry.get("command").and_then(|c| c.as_str()) == Some(KAIZEN_CLAUDE_HOOK_CMD) {
156        return true;
157    }
158    entry
159        .get("hooks")
160        .and_then(|v| v.as_array())
161        .is_some_and(|inner| {
162            inner
163                .iter()
164                .any(|h| h.get("command").and_then(|c| c.as_str()) == Some(KAIZEN_CLAUDE_HOOK_CMD))
165        })
166}
167
168fn patch_claude_settings(out: &mut String, ws: &Path) -> Result<()> {
169    let Some(claude_dir) = claude_user_dir() else {
170        writeln!(out, "  skipped  ~/.claude/settings.json (HOME unset)").unwrap();
171        return Ok(());
172    };
173    let path = claude_dir.join("settings.json");
174    if !path.exists() {
175        std::fs::create_dir_all(path.parent().unwrap())?;
176        let mut obj = serde_json::Map::new();
177        let mut hooks = serde_json::Map::new();
178        for event in CLAUDE_HOOK_EVENTS {
179            hooks.insert(
180                (*event).to_string(),
181                serde_json::json!([
182                    {"hooks": [{"type": "command", "command": KAIZEN_CLAUDE_HOOK_CMD}]}
183                ]),
184            );
185        }
186        obj.insert("hooks".to_string(), serde_json::Value::Object(hooks));
187        write_atomic(&path, &serde_json::to_string_pretty(&obj)?)?;
188        writeln!(out, "  created  ~/.claude/settings.json").unwrap();
189        return Ok(());
190    }
191    let raw = std::fs::read_to_string(&path)?;
192    let mut obj: serde_json::Map<String, serde_json::Value> = match serde_json::from_str(&raw) {
193        Ok(v) => v,
194        Err(e) => {
195            writeln!(out, "  error  ~/.claude/settings.json: {e}").unwrap();
196            anyhow::bail!("malformed ~/.claude/settings.json: {e}");
197        }
198    };
199    let hooks = obj.entry("hooks").or_insert_with(|| serde_json::json!({}));
200    let hooks_obj = hooks.as_object_mut().unwrap();
201    let mut changed = false;
202    for event in CLAUDE_HOOK_EVENTS {
203        let arr = hooks_obj
204            .entry((*event).to_string())
205            .or_insert_with(|| serde_json::json!([]));
206        let Some(entries) = arr.as_array_mut() else {
207            continue;
208        };
209        // Migrate any bare {command,type} entries missing the `hooks` wrapper.
210        for entry in entries.iter_mut() {
211            if entry.get("hooks").is_some() {
212                continue;
213            }
214            if let Some(obj) = entry.as_object()
215                && obj.contains_key("command")
216            {
217                let inner = entry.clone();
218                *entry = serde_json::json!({ "hooks": [inner] });
219                changed = true;
220            }
221        }
222        if !entries.iter().any(entry_has_kaizen_cmd) {
223            entries.push(serde_json::json!({
224                "hooks": [
225                    {"type": "command", "command": KAIZEN_CLAUDE_HOOK_CMD}
226                ]
227            }));
228            changed = true;
229        }
230    }
231    if !changed {
232        writeln!(
233            out,
234            "  skipped  ~/.claude/settings.json  (already configured)"
235        )
236        .unwrap();
237        return Ok(());
238    }
239    let bak = backup_path(ws, "claude_settings")?;
240    std::fs::copy(&path, &bak)?;
241    write_atomic(&path, &serde_json::to_string_pretty(&obj)?)?;
242    writeln!(
243        out,
244        "  patched  ~/.claude/settings.json  (+session/tool hooks)"
245    )
246    .unwrap();
247    Ok(())
248}
249
250/// Read-only: `~/.cursor/hooks.json` missing, valid JSON with full kaizen wiring, or not.
251pub fn cursor_kaizen_hook_wiring(ws: &Path) -> Result<Option<bool>, String> {
252    let _ = ws;
253    let Some(cursor_dir) = cursor_user_dir() else {
254        return Ok(None);
255    };
256    let path = cursor_dir.join("hooks.json");
257    if !path.exists() {
258        return Ok(None);
259    }
260    let raw = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
261    let root: serde_json::Value = serde_json::from_str(&raw).map_err(|e| e.to_string())?;
262    Ok(Some(cursor_hooks_done(&root)))
263}
264
265/// Read-only: `~/.claude/settings.json` hooks all reference kaizen (same as post-patch `entry_has_kaizen_cmd`).
266pub fn claude_kaizen_hook_wiring(ws: &Path) -> Result<Option<bool>, String> {
267    let _ = ws;
268    let Some(claude_dir) = claude_user_dir() else {
269        return Ok(None);
270    };
271    let path = claude_dir.join("settings.json");
272    if !path.exists() {
273        return Ok(None);
274    }
275    let raw = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
276    let obj: serde_json::Map<String, serde_json::Value> =
277        serde_json::from_str(&raw).map_err(|e| e.to_string())?;
278    let Some(hooks) = obj.get("hooks").and_then(|v| v.as_object()) else {
279        return Ok(Some(false));
280    };
281    for event in CLAUDE_HOOK_EVENTS {
282        let Some(arr) = hooks.get(*event).and_then(|v| v.as_array()) else {
283            return Ok(Some(false));
284        };
285        if !arr.iter().any(entry_has_kaizen_cmd) {
286            return Ok(Some(false));
287        }
288    }
289    Ok(Some(true))
290}
291
292/// Returns any workspace-local files that still contain kaizen wiring (legacy from pre-global init).
293pub fn detect_legacy_wiring(ws: &Path) -> Vec<PathBuf> {
294    let mut found = Vec::new();
295    let cursor_local = ws.join(".cursor/hooks.json");
296    if cursor_local.exists()
297        && let Ok(raw) = std::fs::read_to_string(&cursor_local)
298        && raw.contains(KAIZEN_CURSOR_HOOK_CMD)
299    {
300        found.push(cursor_local);
301    }
302    let claude_local = ws.join(".claude/settings.json");
303    if claude_local.exists()
304        && let Ok(raw) = std::fs::read_to_string(&claude_local)
305        && raw.contains(KAIZEN_CLAUDE_HOOK_CMD)
306    {
307        found.push(claude_local);
308    }
309    found
310}
311
312fn write_eval_skill(out: &mut String, ws: &Path) -> Result<()> {
313    let Some(cursor_dir) = cursor_user_dir() else {
314        writeln!(
315            out,
316            "  skipped  ~/.cursor/skills/kaizen-eval/SKILL.md (HOME unset)"
317        )
318        .unwrap();
319        return Ok(());
320    };
321    let path = cursor_dir.join("skills/kaizen-eval/SKILL.md");
322    let _ = ws;
323    std::fs::create_dir_all(path.parent().unwrap())?;
324    if path.exists() {
325        let existing = std::fs::read_to_string(&path)?;
326        if !existing.contains("placeholder") && !existing.trim().is_empty() {
327            writeln!(out, "  skipped  ~/.cursor/skills/kaizen-eval/SKILL.md").unwrap();
328            return Ok(());
329        }
330    }
331    std::fs::write(&path, KAIZEN_EVAL_SKILL)?;
332    writeln!(out, "  wrote  ~/.cursor/skills/kaizen-eval/SKILL.md").unwrap();
333    Ok(())
334}
335
336fn write_skill(out: &mut String, ws: &Path) -> Result<()> {
337    let Some(cursor_dir) = cursor_user_dir() else {
338        writeln!(
339            out,
340            "  skipped  ~/.cursor/skills/kaizen-retro/SKILL.md (HOME unset)"
341        )
342        .unwrap();
343        return Ok(());
344    };
345    let path = cursor_dir.join("skills/kaizen-retro/SKILL.md");
346    let _ = ws;
347    std::fs::create_dir_all(path.parent().unwrap())?;
348    if path.exists() {
349        let existing = std::fs::read_to_string(&path)?;
350        if !existing.contains("placeholder") && !existing.trim().is_empty() {
351            writeln!(out, "  skipped  ~/.cursor/skills/kaizen-retro/SKILL.md").unwrap();
352            return Ok(());
353        }
354    }
355    std::fs::write(&path, KAIZEN_RETRO_SKILL)?;
356    writeln!(out, "  wrote  ~/.cursor/skills/kaizen-retro/SKILL.md").unwrap();
357    Ok(())
358}
359
360const OPENCLAW_HOOK_EVENTS: &[&str] = &[
361    "message:received",
362    "message:sent",
363    "command:new",
364    "command:reset",
365    "command:stop",
366    "session:compact:before",
367    "session:compact:after",
368    "session:patch",
369];
370
371const OPENCLAW_HANDLER_TS: &str = r#"import { spawn } from "child_process";
372
373export async function handler(event: Record<string, unknown>) {
374  const payload = JSON.stringify({
375    event: event["type"] ?? event["event"],
376    session_id: event["sessionId"] ?? event["session_id"] ?? "",
377    timestamp_ms: typeof event["timestamp"] === "number" ? event["timestamp"] : Date.now(),
378    ...event,
379  });
380  const child = spawn("kaizen", ["ingest", "hook", "--source", "openclaw"], {
381    stdio: ["pipe", "ignore", "ignore"],
382  });
383  child.stdin?.write(payload + "\n");
384  child.stdin?.end();
385}
386"#;
387
388const OPENCLAW_HOOK_MD: &str = "# kaizen-events\n\nCaptures OpenClaw sessions for kaizen.\n";
389
390fn cursor_user_dir() -> Option<PathBuf> {
391    std::env::var("HOME")
392        .ok()
393        .map(|h| PathBuf::from(h).join(".cursor"))
394}
395
396fn claude_user_dir() -> Option<PathBuf> {
397    std::env::var("HOME")
398        .ok()
399        .map(|h| PathBuf::from(h).join(".claude"))
400}
401
402fn write_atomic(path: &Path, content: &str) -> Result<()> {
403    let mut tmp = tempfile::NamedTempFile::new_in(path.parent().unwrap())?;
404    std::io::Write::write_all(&mut tmp, content.as_bytes())?;
405    tmp.persist(path)?;
406    Ok(())
407}
408
409fn openclaw_hooks_dir() -> Option<PathBuf> {
410    std::env::var("HOME")
411        .ok()
412        .map(|h| PathBuf::from(h).join(".openclaw/hooks/kaizen-events"))
413}
414
415/// Write (or idempotently skip) the OpenClaw TS hook handler.
416///
417/// Backs up any pre-existing `handler.ts` that does not already reference kaizen.
418pub fn patch_openclaw_handlers(out: &mut String, ws: &Path) -> Result<()> {
419    let Some(hook_dir) = openclaw_hooks_dir() else {
420        writeln!(
421            out,
422            "  skipped  ~/.openclaw/hooks/kaizen-events (HOME unset)"
423        )
424        .unwrap();
425        return Ok(());
426    };
427    let handler_path = hook_dir.join("handler.ts");
428    if handler_path.exists() {
429        let existing = std::fs::read_to_string(&handler_path)?;
430        if openclaw_handler_contains_kaizen(&existing) {
431            writeln!(out, "  skipped  ~/.openclaw/hooks/kaizen-events/handler.ts").unwrap();
432            return Ok(());
433        }
434        let bak = backup_path(ws, "openclaw_hook")?;
435        std::fs::copy(&handler_path, &bak)?;
436    }
437    std::fs::create_dir_all(&hook_dir)?;
438    std::fs::write(&handler_path, OPENCLAW_HANDLER_TS)?;
439    std::fs::write(hook_dir.join("HOOK.md"), OPENCLAW_HOOK_MD)?;
440    writeln!(out, "  created  ~/.openclaw/hooks/kaizen-events/handler.ts").unwrap();
441    let _ = std::process::Command::new("openclaw")
442        .args(["hooks", "enable", "kaizen-events"])
443        .status();
444    for event in OPENCLAW_HOOK_EVENTS {
445        let _ = std::process::Command::new("openclaw")
446            .args(["hooks", "subscribe", "kaizen-events", event])
447            .status();
448    }
449    Ok(())
450}
451
452/// Read-only: `~/.openclaw/hooks/kaizen-events` absent / wired / partial.
453pub fn openclaw_kaizen_hook_wiring(_ws: &Path) -> Result<Option<bool>, String> {
454    let Some(hook_dir) = openclaw_hooks_dir() else {
455        return Ok(None);
456    };
457    if !hook_dir.is_dir() {
458        return Ok(None);
459    }
460    let handler_path = hook_dir.join("handler.ts");
461    let hook_md = hook_dir.join("HOOK.md");
462    if !handler_path.exists() || !hook_md.exists() {
463        return Ok(Some(false));
464    }
465    let raw = std::fs::read_to_string(&handler_path).map_err(|e| e.to_string())?;
466    Ok(Some(openclaw_handler_contains_kaizen(&raw)))
467}
468
469fn openclaw_handler_contains_kaizen(raw: &str) -> bool {
470    raw.contains(KAIZEN_OPENCLAW_HOOK_CMD)
471        || (raw.contains(r#"spawn("kaizen""#) && raw.contains(KAIZEN_OPENCLAW_SPAWN_ARGS))
472}
473
474/// Text that `kaizen init` would print to stdout.
475pub fn init_text(workspace: Option<&std::path::Path>) -> Result<String> {
476    init_text_with_options(workspace, InitOptions::default())
477}
478
479pub fn init_text_with_options(
480    workspace: Option<&std::path::Path>,
481    options: InitOptions,
482) -> Result<String> {
483    let ws = match workspace {
484        Some(p) => p.to_path_buf(),
485        None => std::env::current_dir()?,
486    };
487    let mut out = String::new();
488    if let Ok(data_dir) = crate::core::paths::project_data_dir(&ws) {
489        match crate::core::legacy_import::import_legacy(&ws, &data_dir) {
490            Ok(crate::core::legacy_import::ImportOutcome::Imported) => {
491                writeln!(out, "  imported  .kaizen/ → {}", data_dir.display()).unwrap();
492            }
493            Ok(crate::core::legacy_import::ImportOutcome::Conflict) => {
494                writeln!(
495                    out,
496                    "  warning  .kaizen/ and {} both non-empty — skipping legacy import",
497                    data_dir.display()
498                )
499                .unwrap();
500            }
501            _ => {}
502        }
503    }
504    ensure_config(&mut out, &ws)?;
505    patch_cursor_hooks(&mut out, &ws)?;
506    patch_claude_settings(&mut out, &ws)?;
507    patch_openclaw_handlers(&mut out, &ws)?;
508    write_skill(&mut out, &ws)?;
509    write_eval_skill(&mut out, &ws)?;
510    let cws = crate::core::workspace::canonical(&ws);
511    if let Err(e) = crate::core::machine_registry::record_init(&cws) {
512        tracing::warn!("machine registry: {e:#}");
513    }
514    if options.start_capture {
515        append_capture_status(&mut out, &cws, options.deep);
516    }
517    writeln!(out).unwrap();
518    writeln!(
519        out,
520        "kaizen init complete — Cursor + Claude Code + OpenClaw hooks wired."
521    )
522    .unwrap();
523    writeln!(out).unwrap();
524    writeln!(out, "Run Cursor or Claude Code in this repo once, then:").unwrap();
525    writeln!(
526        out,
527        "  kaizen summary            # cost + rollups (agent / model)"
528    )
529    .unwrap();
530    writeln!(
531        out,
532        "  kaizen insights           # activity, top tools, guidance"
533    )
534    .unwrap();
535    writeln!(out, "  kaizen tui                # live session browser").unwrap();
536    writeln!(out, "  kaizen retro --days 7     # weekly heuristic bets").unwrap();
537    writeln!(out).unwrap();
538    writeln!(
539        out,
540        "Agents: `kaizen mcp` exposes every command as MCP tools — see docs/mcp.md."
541    )
542    .unwrap();
543    if let Ok(data_dir) = crate::core::paths::project_data_dir(&ws) {
544        writeln!(out).unwrap();
545        writeln!(out, "Project data: {}", data_dir.display()).unwrap();
546    }
547    Ok(out)
548}
549
550fn append_capture_status(out: &mut String, ws: &Path, deep: bool) {
551    if !crate::daemon::enabled() {
552        writeln!(out, "  skipped  daemon capture (KAIZEN_DAEMON=0)").unwrap();
553        return;
554    }
555    let workspace = ws.to_string_lossy().to_string();
556    match crate::daemon::ensure_capture_blocking(workspace, deep) {
557        Ok(status) => write_capture_status(out, &status),
558        Err(err) => writeln!(out, "  warning  daemon capture unavailable: {err:#}").unwrap(),
559    }
560}
561
562fn write_capture_status(out: &mut String, status: &CaptureStatus) {
563    writeln!(out, "  ready    daemon capture").unwrap();
564    writeln!(
565        out,
566        "  ready    {}",
567        status_line("watchers", &status.watchers)
568    )
569    .unwrap();
570    writeln!(out, "  ready    {}", status_line("hooks", &status.hooks)).unwrap();
571    if status.deep {
572        writeln!(out, "  partial  deep capture ({})", status.proxies.len()).unwrap();
573    }
574    for err in &status.errors {
575        writeln!(out, "  warning  {err}").unwrap();
576    }
577}
578
579fn status_line(label: &str, components: &[crate::ipc::CaptureComponent]) -> String {
580    let ready = components
581        .iter()
582        .filter(|c| c.status == CaptureComponentStatus::Ready)
583        .count();
584    format!("{label}: {ready}/{}", components.len())
585}
586
587/// Idempotent workspace setup.
588pub fn cmd_init(workspace: Option<&Path>, deep: bool) -> Result<()> {
589    print!(
590        "{}",
591        init_text_with_options(
592            workspace,
593            InitOptions {
594                deep,
595                start_capture: true,
596            },
597        )?
598    );
599    Ok(())
600}