mod domain;
mod markdown;
mod service;
mod store;
#[cfg(feature = "mcp")]
pub use domain::WorkspaceState;
pub use domain::{AttentionClass, BlockKind, RiskLevel};
pub use service::{resolve_block_id, WorkspaceOperation, WorkspaceService};
pub use store::FileEventStore;
use clap::Subcommand;
use markdown::{MarkdownExporter, WorkspaceExporter};
use std::path::PathBuf;
#[derive(Subcommand)]
pub enum WorkspaceAction {
Create {
#[arg(value_enum)]
kind: BlockKind,
title: String,
#[arg(long, default_value = "")]
body: String,
#[arg(long, value_enum, default_value = "review")]
attention: AttentionClass,
#[arg(long, default_value = "human:local")]
actor: String,
},
Update {
id: String,
#[arg(long)]
title: Option<String>,
#[arg(long)]
body: Option<String>,
#[arg(long, value_enum)]
attention: Option<AttentionClass>,
#[arg(long, default_value = "human:local")]
actor: String,
},
Link {
source: String,
target: String,
relation: String,
#[arg(long, default_value = "human:local")]
actor: String,
},
Show { id: String },
Search { query: String },
Inbox {
#[arg(long)]
include_noise: bool,
#[arg(long)]
json: bool,
},
Remember {
title: String,
source_ids: Vec<String>,
#[arg(long, default_value = "yana-rt")]
actor: String,
},
Action {
#[command(subcommand)]
action: WorkspaceGovernanceAction,
},
Export {
#[arg(long, default_value = ".yana-ai/workspace/export")]
output: PathBuf,
},
Status {
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand)]
pub enum WorkspaceGovernanceAction {
Request {
block_id: String,
description: String,
#[arg(long, value_enum)]
risk: RiskLevel,
#[arg(long, default_value = "yana-rt")]
actor: String,
},
Approve {
action_id: String,
#[arg(long)]
approver: String,
},
}
pub fn dispatch(action: WorkspaceAction) {
if let Err(error) = run(action) {
eprintln!("yana-rt workspace: {error}");
std::process::exit(1);
}
}
fn run(action: WorkspaceAction) -> Result<(), String> {
let root = std::env::current_dir().map_err(|error| error.to_string())?;
let service = WorkspaceService::new(FileEventStore::new(&root));
match action {
WorkspaceAction::Create {
kind,
title,
body,
attention,
actor,
} => print_event(service.execute(WorkspaceOperation::CreateBlock {
kind,
title,
body,
attention,
actor,
})?),
WorkspaceAction::Update {
id,
title,
body,
attention,
actor,
} => print_event(service.execute(WorkspaceOperation::UpdateBlock {
block_id: id,
title,
body,
attention,
actor,
})?),
WorkspaceAction::Link {
source,
target,
relation,
actor,
} => print_event(service.execute(WorkspaceOperation::LinkBlocks {
source,
target,
relation,
actor,
})?),
WorkspaceAction::Show { id } => {
let state = service.state()?;
let id = resolve_block_id(&state, &id)?;
let block = state.blocks.get(&id).expect("resolved block");
println!(
"{}",
serde_json::to_string_pretty(block).map_err(|error| error.to_string())?
);
let related = state.related(&id);
if !related.is_empty() {
println!("\nRelated:");
for (other, link) in related {
println!(" {} {} {}", short(&other.id), link.relation, other.title);
}
}
}
WorkspaceAction::Search { query } => {
let state = service.state()?;
for block in state.search(&query) {
println!(
"{} {:?} {:?} {}",
short(&block.id),
block.kind,
block.attention,
block.title
);
}
}
WorkspaceAction::Inbox {
include_noise,
json,
} => {
let state = service.state()?;
let inbox = state.inbox(include_noise);
if json {
println!(
"{}",
serde_json::to_string_pretty(&inbox).map_err(|error| error.to_string())?
);
} else if inbox.is_empty() {
println!("Workspace inbox empty.");
} else {
for block in inbox {
println!(
"{} {:?} {:?} {}",
short(&block.id),
block.attention,
block.kind,
block.title
);
}
}
}
WorkspaceAction::Remember {
title,
source_ids,
actor,
} => print_event(service.execute(WorkspaceOperation::SynthesizeMemory {
title,
source_ids,
actor,
})?),
WorkspaceAction::Action { action } => match action {
WorkspaceGovernanceAction::Request {
block_id,
description,
risk,
actor,
} => print_event(service.execute(WorkspaceOperation::RequestAction {
block_id,
description,
risk,
actor,
})?),
WorkspaceGovernanceAction::Approve {
action_id,
approver,
} => print_event(service.execute(WorkspaceOperation::ApproveAction {
action_id,
approver,
})?),
},
WorkspaceAction::Export { output } => {
let state = service.state()?;
let paths = MarkdownExporter::new(output).export(&state)?;
println!("exported {} Markdown files", paths.len());
}
WorkspaceAction::Status { json } => {
let state = service.state()?;
let pending_human = state
.actions
.values()
.filter(|action| action.status == domain::ActionStatus::PendingHuman)
.count();
let summary = serde_json::json!({
"blocks": state.blocks.len(),
"links": state.links.len(),
"actions": state.actions.len(),
"pending_human": pending_human,
"signal": state.blocks.values().filter(|block| block.attention == AttentionClass::Signal).count(),
"review": state.blocks.values().filter(|block| block.attention == AttentionClass::Review).count(),
"noise": state.blocks.values().filter(|block| block.attention == AttentionClass::Noise).count(),
});
if json {
println!(
"{}",
serde_json::to_string_pretty(&summary).map_err(|error| error.to_string())?
);
} else {
println!(
"blocks: {} · links: {} · actions: {} · pending human: {}",
summary["blocks"],
summary["links"],
summary["actions"],
summary["pending_human"]
);
println!(
"signal: {} · review: {} · noise: {}",
summary["signal"], summary["review"], summary["noise"]
);
}
}
}
Ok(())
}
fn print_event(event: domain::WorkspaceEvent) {
println!(
"{}",
serde_json::to_string_pretty(&event).expect("serialize workspace event")
);
}
fn short(id: &str) -> &str {
&id[..id.len().min(8)]
}
#[cfg(test)]
mod tests;