Skip to content

Commit f1cee60

Browse files
committed
feat(cli): rewrite init with onboarding UX, add identity command
1 parent 5d8ee1f commit f1cee60

1 file changed

Lines changed: 87 additions & 139 deletions

File tree

src/main.rs

Lines changed: 87 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use engraph::config;
22
use engraph::indexer;
3-
use engraph::profile;
43
use engraph::search;
54
use engraph::store;
65

@@ -66,10 +65,44 @@ enum Command {
6665
all: bool,
6766
},
6867

69-
/// Initialize vault profile with auto-detection.
68+
/// Initialize vault profile, identity, and search index.
7069
Init {
71-
/// Path to the vault (defaults to current directory).
70+
/// Path to vault directory.
7271
path: Option<PathBuf>,
72+
/// Only run identity setup (skip indexing).
73+
#[arg(long)]
74+
identity: bool,
75+
/// Only re-index (skip identity prompts).
76+
#[arg(long)]
77+
reindex: bool,
78+
/// Detect vault without writing anything (agent mode).
79+
#[arg(long)]
80+
detect: bool,
81+
/// Output as JSON (agent mode).
82+
#[arg(long)]
83+
json: bool,
84+
/// Suppress interactive prompts.
85+
#[arg(long)]
86+
quiet: bool,
87+
/// User name (non-interactive mode).
88+
#[arg(long)]
89+
name: Option<String>,
90+
/// User role (non-interactive mode).
91+
#[arg(long)]
92+
role: Option<String>,
93+
/// Vault purpose (non-interactive mode).
94+
#[arg(long)]
95+
purpose: Option<String>,
96+
},
97+
98+
/// Print identity block (L0 + L1 context for AI agents).
99+
Identity {
100+
/// Output as JSON.
101+
#[arg(long)]
102+
json: bool,
103+
/// Force L1 re-extraction without full reindex.
104+
#[arg(long)]
105+
refresh: bool,
73106
},
74107

75108
/// Configure engraph settings.
@@ -515,158 +548,73 @@ async fn main() -> Result<()> {
515548
}
516549
}
517550

518-
Command::Init { path } => {
519-
// Resolve vault path: CLI arg > config > cwd.
551+
Command::Init { path, identity, reindex, detect, json, quiet, name, role, purpose } => {
520552
cfg.merge_vault_path(path);
521553
let vault_path = match &cfg.vault_path {
522554
Some(p) => p.clone(),
523555
None => std::env::current_dir()?,
524556
};
525557
let vault_path = vault_path.canonicalize().unwrap_or(vault_path);
526558

527-
println!("Detecting vault profile for: {}", vault_path.display());
528-
529-
let vault_type = profile::detect_vault_type(&vault_path);
530-
let structure = profile::detect_structure(&vault_path)?;
531-
let stats = profile::scan_vault_stats(&vault_path)?;
532-
533-
// Print detection results.
534-
println!();
535-
println!(" Vault type: {:?}", vault_type);
536-
println!(" Structure: {:?}", structure.method);
537-
if let Some(ref inbox) = structure.folders.inbox {
538-
println!(" inbox: {}", inbox);
539-
}
540-
if let Some(ref projects) = structure.folders.projects {
541-
println!(" projects: {}", projects);
542-
}
543-
if let Some(ref areas) = structure.folders.areas {
544-
println!(" areas: {}", areas);
545-
}
546-
if let Some(ref resources) = structure.folders.resources {
547-
println!(" resources: {}", resources);
548-
}
549-
if let Some(ref archive) = structure.folders.archive {
550-
println!(" archive: {}", archive);
551-
}
552-
if let Some(ref templates) = structure.folders.templates {
553-
println!(" templates: {}", templates);
554-
}
555-
if let Some(ref daily) = structure.folders.daily {
556-
println!(" daily: {}", daily);
557-
}
558-
if let Some(ref people) = structure.folders.people {
559-
println!(" people: {}", people);
560-
}
561-
println!();
562-
println!(" Total .md files: {}", stats.total_files);
563-
println!(" With frontmatter: {}", stats.files_with_frontmatter);
564-
println!(" Wikilinks: {}", stats.wikilink_count);
565-
println!(" Unique tags: {}", stats.unique_tags);
566-
println!(" Folders: {}", stats.folder_count);
567-
println!(" Max folder depth: {}", stats.folder_depth);
568-
569-
let vault_profile = profile::VaultProfile {
570-
vault_path: vault_path.clone(),
571-
vault_type,
572-
structure,
573-
stats,
574-
};
575-
576-
// Ensure data dir exists and write vault.toml.
577-
std::fs::create_dir_all(&data_dir)?;
578-
profile::write_vault_toml(&vault_profile, &data_dir)?;
579-
580-
println!();
581-
println!("Wrote {}", data_dir.join("vault.toml").display());
582-
583-
// Intelligence onboarding (only if not yet configured)
584-
if cfg.intelligence.is_none() {
585-
let enable = prompt_intelligence(&data_dir)?;
586-
cfg.intelligence = Some(enable);
587-
cfg.save()?;
588-
}
589-
590-
// Obsidian CLI detection
591-
let obsidian_running = std::process::Command::new("pgrep")
592-
.args(["-x", "Obsidian"])
593-
.stdout(std::process::Stdio::null())
594-
.stderr(std::process::Stdio::null())
595-
.status()
596-
.map(|s| s.success())
597-
.unwrap_or(false);
598-
599-
let obsidian_in_path = std::process::Command::new("which")
600-
.arg("obsidian")
601-
.stdout(std::process::Stdio::null())
602-
.stderr(std::process::Stdio::null())
603-
.status()
604-
.map(|s| s.success())
605-
.unwrap_or(false);
606-
607-
if obsidian_running && obsidian_in_path {
608-
eprint!("\nObsidian CLI detected. Enable integration? [Y/n] ");
609-
io::stderr().flush()?;
610-
let mut answer = String::new();
611-
io::stdin().lock().read_line(&mut answer)?;
612-
let answer = answer.trim();
613-
let enable = answer.is_empty() || answer.eq_ignore_ascii_case("y");
614-
if enable {
615-
let vault_name = vault_path
616-
.file_name()
617-
.and_then(|n| n.to_str())
618-
.unwrap_or("Personal")
619-
.to_string();
620-
cfg.obsidian.enabled = true;
621-
cfg.obsidian.vault_name = Some(vault_name.clone());
622-
cfg.save()?;
623-
println!("Obsidian CLI enabled (vault: {vault_name}).");
559+
if detect {
560+
let result = engraph::onboarding::run_detect_json(&vault_path)?;
561+
if json {
562+
println!("{}", serde_json::to_string_pretty(&result)?);
624563
} else {
625-
println!(
626-
"Obsidian CLI disabled. Enable later with: engraph configure --enable-obsidian-cli"
627-
);
564+
println!("{}", serde_json::to_string_pretty(&result)?);
628565
}
566+
return Ok(());
629567
}
630568

631-
// AI agent detection
632-
let home = dirs::home_dir().unwrap_or_default();
633-
let agent_configs: &[(&str, &str, &str)] = &[
634-
("Claude Code", "claude-code", ".claude/settings.json"),
635-
("Cursor", "cursor", ".cursor/mcp.json"),
636-
("Windsurf", "windsurf", ".codeium/windsurf/mcp_config.json"),
637-
];
638-
639-
let mut detected: Vec<(&str, &str, String)> = Vec::new();
640-
for (name, key, rel_path) in agent_configs {
641-
let full = home.join(rel_path);
642-
if full.exists() {
643-
detected.push((name, key, format!("~/{rel_path}")));
644-
}
569+
if json {
570+
let flags = engraph::onboarding::ApplyFlags {
571+
name, role, purpose,
572+
identity_only: identity,
573+
reindex_only: reindex,
574+
};
575+
let result = engraph::onboarding::run_apply_json(&vault_path, &mut cfg, &data_dir, flags)?;
576+
println!("{}", serde_json::to_string_pretty(&result)?);
577+
return Ok(());
645578
}
646579

647-
if !detected.is_empty() {
648-
println!("\nAI agents detected:");
649-
for (name, _key, path) in &detected {
650-
println!(" \u{2713} {name} ({path})");
651-
}
652-
println!(
653-
"\nTo register engraph as MCP server, add to your agent's config:\n \
654-
\"engraph\": {{\n \
655-
\"command\": \"engraph\",\n \
656-
\"args\": [\"serve\"]\n \
657-
}}"
658-
);
580+
let flags = engraph::onboarding::InteractiveFlags {
581+
name, role, purpose,
582+
identity_only: identity,
583+
reindex_only: reindex,
584+
quiet,
585+
};
586+
engraph::onboarding::run_interactive(&vault_path, &mut cfg, &data_dir, flags)?;
587+
}
659588

660-
// Record detected agents in config
661-
for (_name, key, _path) in &detected {
662-
match *key {
663-
"claude-code" => cfg.agents.claude_code = true,
664-
"cursor" => cfg.agents.cursor = true,
665-
"windsurf" => cfg.agents.windsurf = true,
666-
_ => {}
589+
Command::Identity { json, refresh } => {
590+
let db_path = data_dir.join("engraph.db");
591+
if !db_path.exists() {
592+
anyhow::bail!("No index found. Run `engraph init` first.");
593+
}
594+
let store = engraph::store::Store::open(&db_path)?;
595+
if refresh {
596+
let profile = engraph::config::Config::load_vault_profile()?;
597+
match profile {
598+
Some(ref p) => {
599+
engraph::identity::extract_l1_facts(&store, p)?;
600+
eprintln!("L1 facts refreshed.");
601+
}
602+
None => {
603+
anyhow::bail!("No vault profile found. Run `engraph init` first.");
667604
}
668605
}
669-
cfg.save()?;
606+
}
607+
if json {
608+
let l0 = store.get_identity_facts(0)?;
609+
let l1 = store.get_identity_facts(1)?;
610+
let result = serde_json::json!({
611+
"l0": l0.iter().map(|f| serde_json::json!({"key": &f.key, "value": &f.value})).collect::<Vec<_>>(),
612+
"l1": l1.iter().map(|f| serde_json::json!({"key": &f.key, "value": &f.value, "source": &f.source, "updated_at": &f.updated_at})).collect::<Vec<_>>(),
613+
});
614+
println!("{}", serde_json::to_string_pretty(&result)?);
615+
} else {
616+
let block = engraph::identity::format_identity_block(&cfg, &store)?;
617+
println!("{}", block);
670618
}
671619
}
672620

0 commit comments

Comments
 (0)