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