Skip to main content

spool/cli/
args.rs

1use crate::domain::{MemoryScope, OutputFormat, TargetTool, WakeupProfile};
2use clap::{Args, Parser, Subcommand, ValueEnum};
3use std::path::PathBuf;
4
5#[derive(Debug, Parser)]
6#[command(
7    name = "spool",
8    version,
9    about = "Obsidian knowledge-based context extractor"
10)]
11pub struct Cli {
12    #[command(subcommand)]
13    pub command: Command,
14}
15
16#[derive(Debug, Subcommand)]
17pub enum Command {
18    Get(GetArgs),
19    Explain(ExplainArgs),
20    Wakeup(WakeupArgs),
21    Memory(MemoryArgs),
22    Mcp(McpArgs),
23    Hook(HookArgs),
24    Init(InitArgs),
25    Status(StatusArgs),
26    #[cfg(feature = "embedding")]
27    Embedding(EmbeddingArgs),
28    Knowledge(KnowledgeArgs),
29}
30
31#[derive(Debug, Clone, Args)]
32pub struct InitArgs {
33    /// Vault 根目录路径
34    #[arg(long)]
35    pub vault: Option<PathBuf>,
36    /// 项目 ID
37    #[arg(long)]
38    pub project_id: Option<String>,
39    /// 项目仓库路径(默认当前目录)
40    #[arg(long)]
41    pub repo: Option<PathBuf>,
42}
43
44#[derive(Debug, Clone, Args)]
45pub struct StatusArgs {
46    /// 配置文件路径(默认 ~/.spool/config.toml)
47    #[arg(long)]
48    pub config: Option<PathBuf>,
49}
50
51#[derive(Debug, Clone, Args)]
52pub struct CommonRouteArgs {
53    #[arg(long)]
54    pub config: PathBuf,
55    #[arg(long)]
56    pub task: String,
57    #[arg(long)]
58    pub cwd: PathBuf,
59    #[arg(long, value_delimiter = ',')]
60    pub files: Vec<String>,
61    #[arg(long, value_enum, default_value_t = TargetValue::Claude)]
62    pub target: TargetValue,
63}
64
65#[derive(Debug, Clone, Args)]
66pub struct GetArgs {
67    #[command(flatten)]
68    pub common: CommonRouteArgs,
69    #[arg(long, value_enum)]
70    pub format: Option<FormatValue>,
71}
72
73#[derive(Debug, Clone, Args)]
74pub struct ExplainArgs {
75    #[command(flatten)]
76    pub common: CommonRouteArgs,
77}
78
79#[derive(Debug, Clone, Args)]
80pub struct WakeupArgs {
81    #[command(flatten)]
82    pub common: CommonRouteArgs,
83    #[arg(long, value_enum)]
84    pub profile: WakeupProfileValue,
85    #[arg(long, value_enum)]
86    pub format: Option<FormatValue>,
87}
88
89#[derive(Debug, Clone, Args)]
90pub struct MemoryArgs {
91    #[command(subcommand)]
92    pub command: MemoryCommand,
93}
94
95#[derive(Debug, Clone, Subcommand)]
96pub enum MemoryCommand {
97    List(MemoryListArgs),
98    Show(MemoryShowArgs),
99    History(MemoryShowArgs),
100    RecordManual(MemoryRecordArgs),
101    Propose(MemoryRecordArgs),
102    Accept(MemoryActionArgs),
103    Promote(MemoryActionArgs),
104    Archive(MemoryActionArgs),
105    Import(MemoryImportArgs),
106    ImportGit(MemoryImportGitArgs),
107    SyncVault(MemorySyncVaultArgs),
108    Dedup(MemoryDedupArgs),
109    Consolidate(MemoryConsolidateArgs),
110    Prune(MemoryPruneArgs),
111    Lint(MemoryLintArgs),
112    SyncIndex(MemorySyncIndexArgs),
113    Stats(MemoryStatsArgs),
114}
115
116#[derive(Debug, Clone, Args)]
117pub struct MemoryListArgs {
118    #[arg(long)]
119    pub config: PathBuf,
120    #[arg(long, value_enum)]
121    pub view: MemoryListViewValue,
122    #[arg(long, value_enum)]
123    pub format: Option<MemoryFormatValue>,
124    #[arg(long)]
125    pub daemon_bin: Option<PathBuf>,
126}
127
128#[derive(Debug, Clone, Args)]
129pub struct MemoryShowArgs {
130    #[arg(long)]
131    pub config: PathBuf,
132    #[arg(long)]
133    pub record_id: String,
134    #[arg(long, value_enum)]
135    pub format: Option<MemoryFormatValue>,
136    #[arg(long)]
137    pub daemon_bin: Option<PathBuf>,
138}
139
140#[derive(Debug, Clone, Args)]
141pub struct MemoryActionArgs {
142    #[arg(long)]
143    pub config: PathBuf,
144    #[arg(long)]
145    pub record_id: String,
146    #[arg(long)]
147    pub actor: Option<String>,
148    #[arg(long)]
149    pub reason: Option<String>,
150    #[arg(long, value_delimiter = ',')]
151    pub evidence_refs: Vec<String>,
152}
153
154#[derive(Debug, Clone, Args)]
155pub struct MemoryRecordArgs {
156    #[arg(long)]
157    pub config: PathBuf,
158    #[arg(long)]
159    pub title: String,
160    #[arg(long)]
161    pub summary: String,
162    #[arg(long)]
163    pub memory_type: String,
164    #[arg(long, value_enum)]
165    pub scope: MemoryScopeValue,
166    #[arg(long)]
167    pub source_ref: String,
168    #[arg(long)]
169    pub project_id: Option<String>,
170    #[arg(long)]
171    pub user_id: Option<String>,
172    #[arg(long)]
173    pub sensitivity: Option<String>,
174    #[arg(long)]
175    pub actor: Option<String>,
176    #[arg(long)]
177    pub reason: Option<String>,
178    #[arg(long, value_delimiter = ',')]
179    pub evidence_refs: Vec<String>,
180}
181
182#[derive(Debug, Clone, Args)]
183pub struct MemoryImportArgs {
184    #[arg(long)]
185    pub config: PathBuf,
186    /// 来源 provider(claude / codex),对应 session_sources 读取目录
187    #[arg(long, value_enum)]
188    pub provider: SessionProviderValue,
189    /// provider-local session id(raw,不带 "claude:" / "codex:" 前缀)
190    #[arg(long)]
191    pub session_id: String,
192    /// 默认只打印候选;加 --apply 才真正写入 ledger
193    #[arg(long, default_value_t = false)]
194    pub apply: bool,
195    #[arg(long, default_value = "spool-importer")]
196    pub actor: String,
197    #[arg(long, value_enum, default_value_t = MemoryFormatValue::Markdown)]
198    pub format: MemoryFormatValue,
199}
200
201#[derive(Debug, Clone, Args)]
202pub struct MemoryImportGitArgs {
203    #[arg(long)]
204    pub config: PathBuf,
205    /// Git 仓库路径(默认当前目录)
206    #[arg(long)]
207    pub repo: Option<PathBuf>,
208    /// 扫描最近 N 条 commit(默认 30)
209    #[arg(long, default_value_t = 30)]
210    pub limit: usize,
211    /// 只预览不写入
212    #[arg(long, default_value_t = false)]
213    pub dry_run: bool,
214}
215
216#[derive(Debug, Clone, Args)]
217pub struct MemorySyncVaultArgs {
218    #[arg(long)]
219    pub config: PathBuf,
220    /// 不写入文件,仅打印将会发生的动作
221    #[arg(long, default_value_t = false)]
222    pub dry_run: bool,
223    /// 对缺少 entities/tags/triggers 的记录进行启发式补充
224    #[arg(long, default_value_t = false)]
225    pub enrich: bool,
226}
227
228#[derive(Debug, Clone, Args)]
229pub struct MemoryDedupArgs {
230    #[arg(long)]
231    pub config: PathBuf,
232}
233
234#[derive(Debug, Clone, Args)]
235pub struct MemoryConsolidateArgs {
236    #[arg(long)]
237    pub config: PathBuf,
238    /// 只预览不执行(默认行为)
239    #[arg(long, default_value_t = false)]
240    pub dry_run: bool,
241    /// 执行合并
242    #[arg(long, default_value_t = false)]
243    pub apply: bool,
244}
245
246#[derive(Debug, Clone, Args)]
247pub struct MemoryPruneArgs {
248    #[arg(long)]
249    pub config: PathBuf,
250    /// 只预览不执行(默认行为)
251    #[arg(long, default_value_t = false)]
252    pub dry_run: bool,
253    /// 执行归档
254    #[arg(long, default_value_t = false)]
255    pub apply: bool,
256}
257
258#[derive(Debug, Clone, Args)]
259pub struct MemoryLintArgs {
260    #[arg(long)]
261    pub config: PathBuf,
262    /// 以 JSON 输出 (默认 markdown)
263    #[arg(long, default_value_t = false)]
264    pub json: bool,
265}
266
267#[derive(Debug, Clone, Args)]
268pub struct MemorySyncIndexArgs {
269    #[arg(long)]
270    pub config: PathBuf,
271    /// Write the regenerated INDEX.md (default is dry-run preview).
272    #[arg(long, default_value_t = false)]
273    pub apply: bool,
274}
275
276#[derive(Debug, Clone, Args)]
277pub struct MemoryStatsArgs {
278    #[arg(long)]
279    pub config: PathBuf,
280}
281
282// ─────────────────────────────────────────────────────────────────────
283// spool mcp <subcommand> — installer surface for AI client integration.
284// R1 wires Claude Code only; the trait infrastructure (see
285// `src/installers/`) reserves room for Codex/Cursor in P1.
286// ─────────────────────────────────────────────────────────────────────
287
288#[derive(Debug, Clone, Args)]
289pub struct McpArgs {
290    #[command(subcommand)]
291    pub command: McpCommand,
292}
293
294#[derive(Debug, Clone, Subcommand)]
295pub enum McpCommand {
296    Install(McpInstallArgs),
297    Update(McpUpdateArgs),
298    Uninstall(McpUninstallArgs),
299    Doctor(McpDoctorArgs),
300}
301
302#[derive(Debug, Clone, Args)]
303pub struct McpInstallArgs {
304    #[arg(long, value_enum, default_value_t = ClientValue::Claude)]
305    pub client: ClientValue,
306    /// 指向 spool.toml 配置文件 (mcpServers 条目里写绝对路径)
307    #[arg(long)]
308    pub config: PathBuf,
309    /// 自定义 spool-mcp 二进制路径 (默认走 ~/.cargo/bin/spool-mcp)
310    #[arg(long)]
311    pub binary_path: Option<PathBuf>,
312    /// 仅打印将要发生的写入,不修改任何文件
313    #[arg(long, default_value_t = false)]
314    pub dry_run: bool,
315    /// 已存在 spool 条目时强制覆盖 (默认拒绝并要求 uninstall 后重装)
316    #[arg(long, default_value_t = false)]
317    pub force: bool,
318    /// 输出格式
319    #[arg(long, value_enum, default_value_t = McpReportFormat::Text)]
320    pub format: McpReportFormat,
321}
322
323#[derive(Debug, Clone, Args)]
324pub struct McpUpdateArgs {
325    #[arg(long, value_enum, default_value_t = ClientValue::Claude)]
326    pub client: ClientValue,
327    /// 指向 spool.toml 配置文件 (mcpServers 条目里写绝对路径)
328    #[arg(long)]
329    pub config: PathBuf,
330    /// 自定义 spool-mcp 二进制路径 (默认走 ~/.cargo/bin/spool-mcp)
331    #[arg(long)]
332    pub binary_path: Option<PathBuf>,
333    /// 仅打印将要发生的写入,不修改任何文件
334    #[arg(long, default_value_t = false)]
335    pub dry_run: bool,
336    /// 输出格式
337    #[arg(long, value_enum, default_value_t = McpReportFormat::Text)]
338    pub format: McpReportFormat,
339}
340
341#[derive(Debug, Clone, Args)]
342pub struct McpUninstallArgs {
343    #[arg(long, value_enum, default_value_t = ClientValue::Claude)]
344    pub client: ClientValue,
345    #[arg(long, default_value_t = false)]
346    pub dry_run: bool,
347    #[arg(long, value_enum, default_value_t = McpReportFormat::Text)]
348    pub format: McpReportFormat,
349}
350
351#[derive(Debug, Clone, Args)]
352pub struct McpDoctorArgs {
353    #[arg(long, value_enum, default_value_t = ClientValue::Claude)]
354    pub client: ClientValue,
355    /// 指向 spool.toml 配置文件 (诊断会校验该路径存在)
356    #[arg(long)]
357    pub config: PathBuf,
358    /// 自定义 spool-mcp 二进制路径
359    #[arg(long)]
360    pub binary_path: Option<PathBuf>,
361    #[arg(long, value_enum, default_value_t = McpReportFormat::Text)]
362    pub format: McpReportFormat,
363}
364
365#[derive(Debug, Clone, Copy, ValueEnum)]
366pub enum ClientValue {
367    Claude,
368    Codex,
369    Cursor,
370    Opencode,
371}
372
373#[derive(Debug, Clone, Copy, ValueEnum)]
374pub enum McpReportFormat {
375    Text,
376    Json,
377}
378
379// ─────────────────────────────────────────────────────────────────────
380// spool hook <subcommand> — invoked by AI client hook scripts.
381// All hooks MUST exit 0 even on internal failure (D7 in PRD); the
382// `run_silent` wrapper in `src/hook_runtime/mod.rs` enforces this.
383// ─────────────────────────────────────────────────────────────────────
384
385#[derive(Debug, Clone, Args)]
386pub struct HookArgs {
387    #[command(subcommand)]
388    pub command: HookCommand,
389}
390
391#[derive(Debug, Clone, Subcommand)]
392pub enum HookCommand {
393    SessionStart(HookSessionStartArgs),
394    UserPrompt(HookSimpleArgs),
395    PostToolUse(HookPostToolUseArgs),
396    Stop(HookStopArgs),
397    PreCompact(HookSimpleArgs),
398}
399
400#[derive(Debug, Clone, Args)]
401pub struct HookSessionStartArgs {
402    #[arg(long)]
403    pub config: PathBuf,
404    /// 用户当前工作目录 (默认 current_dir)
405    #[arg(long)]
406    pub cwd: Option<PathBuf>,
407    /// 可选 task hint (默认 "session start")
408    #[arg(long)]
409    pub task: Option<String>,
410    /// wakeup profile (默认 project)
411    #[arg(long, value_enum, default_value_t = WakeupProfileValue::Project)]
412    pub profile: WakeupProfileValue,
413}
414
415#[derive(Debug, Clone, Args)]
416pub struct HookSimpleArgs {
417    #[arg(long)]
418    pub config: PathBuf,
419    #[arg(long)]
420    pub cwd: Option<PathBuf>,
421}
422
423#[derive(Debug, Clone, Args)]
424pub struct HookPostToolUseArgs {
425    #[arg(long)]
426    pub config: PathBuf,
427    #[arg(long)]
428    pub cwd: Option<PathBuf>,
429    /// 触发的工具名 (Bash / Edit / ...)
430    #[arg(long)]
431    pub tool_name: Option<String>,
432    /// 工具调用 payload (会被截断到 4 KiB)
433    #[arg(long)]
434    pub payload: Option<String>,
435}
436
437/// Stop hook input. Supports three ways to locate the session
438/// transcript (in priority order):
439///   1. `--transcript-path` — explicit absolute path
440///   2. `--hook-input` — raw JSON from Claude Code's stdin payload
441///      containing `{"transcript_path": "..."}`
442///   3. fallback: scan `~/.claude/projects/<sanitized-cwd>/` and
443///      pick the most recently modified `.jsonl`
444#[derive(Debug, Clone, Args)]
445pub struct HookStopArgs {
446    #[arg(long)]
447    pub config: PathBuf,
448    #[arg(long)]
449    pub cwd: Option<PathBuf>,
450    #[arg(long)]
451    pub transcript_path: Option<PathBuf>,
452    /// Raw stdin payload Claude Code sends to Stop hook. Hook script
453    /// passes this in via `--hook-input "$(cat)"`.
454    #[arg(long)]
455    pub hook_input: Option<String>,
456    /// Test/admin override for `$HOME` (used by smoke tests + dry
457    /// runs). Production callers leave this empty and we read the
458    /// real `$HOME`.
459    #[arg(long, hide = true)]
460    pub home: Option<PathBuf>,
461}
462
463#[derive(Debug, Clone, Copy, ValueEnum)]
464pub enum SessionProviderValue {
465    Claude,
466    Codex,
467}
468
469impl SessionProviderValue {
470    pub fn as_str(self) -> &'static str {
471        match self {
472            Self::Claude => "claude",
473            Self::Codex => "codex",
474        }
475    }
476}
477
478#[derive(Debug, Clone, Copy, ValueEnum)]
479pub enum TargetValue {
480    Claude,
481    Codex,
482    Opencode,
483}
484
485impl From<TargetValue> for TargetTool {
486    fn from(value: TargetValue) -> Self {
487        match value {
488            TargetValue::Claude => TargetTool::Claude,
489            TargetValue::Codex => TargetTool::Codex,
490            TargetValue::Opencode => TargetTool::Opencode,
491        }
492    }
493}
494
495#[derive(Debug, Clone, Copy, ValueEnum)]
496pub enum FormatValue {
497    Prompt,
498    Markdown,
499    Json,
500}
501
502#[derive(Debug, Clone, Copy, ValueEnum)]
503pub enum WakeupProfileValue {
504    Developer,
505    Project,
506}
507
508#[derive(Debug, Clone, Copy, ValueEnum)]
509pub enum MemoryListViewValue {
510    PendingReview,
511    WakeupReady,
512}
513
514#[derive(Debug, Clone, Copy, ValueEnum)]
515pub enum MemoryFormatValue {
516    Markdown,
517    Json,
518}
519
520#[derive(Debug, Clone, Copy, ValueEnum)]
521pub enum MemoryScopeValue {
522    User,
523    Project,
524    Workspace,
525    Team,
526    Agent,
527}
528
529impl From<FormatValue> for OutputFormat {
530    fn from(value: FormatValue) -> Self {
531        match value {
532            FormatValue::Prompt => OutputFormat::Prompt,
533            FormatValue::Markdown => OutputFormat::Markdown,
534            FormatValue::Json => OutputFormat::Json,
535        }
536    }
537}
538
539impl From<WakeupProfileValue> for WakeupProfile {
540    fn from(value: WakeupProfileValue) -> Self {
541        match value {
542            WakeupProfileValue::Developer => WakeupProfile::Developer,
543            WakeupProfileValue::Project => WakeupProfile::Project,
544        }
545    }
546}
547
548impl From<MemoryScopeValue> for MemoryScope {
549    fn from(value: MemoryScopeValue) -> Self {
550        match value {
551            MemoryScopeValue::User => MemoryScope::User,
552            MemoryScopeValue::Project => MemoryScope::Project,
553            MemoryScopeValue::Workspace => MemoryScope::Workspace,
554            MemoryScopeValue::Team => MemoryScope::Team,
555            MemoryScopeValue::Agent => MemoryScope::Agent,
556        }
557    }
558}
559
560// ─────────────────────────────────────────────────────────────────────
561// spool embedding <subcommand> — local embedding model management.
562// Feature-gated behind `embedding`.
563// ─────────────────────────────────────────────────────────────────────
564
565#[cfg(feature = "embedding")]
566#[derive(Debug, Clone, Args)]
567pub struct EmbeddingArgs {
568    #[command(subcommand)]
569    pub command: EmbeddingCommand,
570}
571
572#[cfg(feature = "embedding")]
573#[derive(Debug, Clone, Subcommand)]
574pub enum EmbeddingCommand {
575    /// 构建/重建嵌入索引
576    Build(EmbeddingBuildArgs),
577    /// 显示索引状态(记录数、模型、大小)
578    Status(EmbeddingStatusArgs),
579}
580
581#[cfg(feature = "embedding")]
582#[derive(Debug, Clone, Args)]
583pub struct EmbeddingBuildArgs {
584    #[arg(long)]
585    pub config: PathBuf,
586}
587
588#[cfg(feature = "embedding")]
589#[derive(Debug, Clone, Args)]
590pub struct EmbeddingStatusArgs {
591    #[arg(long)]
592    pub config: PathBuf,
593}
594
595// ─────────────────────────────────────────────────────────────────────
596// spool knowledge <subcommand> — knowledge distillation pipeline.
597// ─────────────────────────────────────────────────────────────────────
598
599#[derive(Debug, Clone, Args)]
600pub struct KnowledgeArgs {
601    #[command(subcommand)]
602    pub command: KnowledgeCommand,
603}
604
605#[derive(Debug, Clone, Subcommand)]
606pub enum KnowledgeCommand {
607    /// 检测可聚合的碎片集群并生成知识页草稿
608    Distill(KnowledgeDistillArgs),
609}
610
611#[derive(Debug, Clone, Args)]
612pub struct KnowledgeDistillArgs {
613    #[arg(long)]
614    pub config: PathBuf,
615    /// 只预览不写入
616    #[arg(long, default_value_t = false)]
617    pub dry_run: bool,
618    /// 执行写入(生成 candidate 知识页)
619    #[arg(long, default_value_t = false)]
620    pub apply: bool,
621    /// 操作者
622    #[arg(long, default_value = "spool-knowledge")]
623    pub actor: String,
624}