Skip to content

Commit 738177a

Browse files
HangsuHangsu
authored andcommitted
merge feature/skill-cli
2 parents 2bef95f + bcf6838 commit 738177a

19 files changed

Lines changed: 2660 additions & 12 deletions

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,20 @@ pnpm test
216216
pnpm lint
217217
```
218218

219+
### Skill-oriented CLI
220+
221+
The `echo-profile` binary also exposes a JSON-only CLI for skills and automation:
222+
223+
```bash
224+
echo-profile version
225+
echo-profile list providers
226+
echo-profile list sessions --current-project
227+
echo-profile profile collect --scope project --current-project --budget-chars 30000
228+
```
229+
230+
`profile collect` only gathers local user-message text as structured JSON. It does not call an LLM or generate a profile; Codex skills or other agents can use the returned messages as profile-generation input.
231+
232+
219233
## Good first contribution areas
220234

221235
EchoProfile is especially open to contributions in these areas:

README.zh-CN.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,20 @@ pnpm test
186186
pnpm lint
187187
```
188188

189+
### 面向 Skill 的 CLI
190+
191+
`echo-profile` 二进制也提供 JSON-only CLI,方便 skill 和自动化流程调用:
192+
193+
```bash
194+
echo-profile version
195+
echo-profile list providers
196+
echo-profile list sessions --current-project
197+
echo-profile profile collect --scope project --current-project --budget-chars 30000
198+
```
199+
200+
`profile collect` 只会把本地用户消息文本收集为结构化 JSON。它不会调用 LLM,也不会直接生成画像;Codex skill 或其他 agent 可以使用返回的消息作为画像生成输入。
201+
202+
189203
## 适合贡献什么?
190204

191205
EchoProfile 很适合以下方向的贡献:

src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ tauri-build = { version = "2.5.3", features = [] }
2424
[dependencies]
2525
serde_json = "1.0"
2626
serde = { version = "1.0", features = ["derive"] }
27+
clap = { version = "4.5", features = ["derive"] }
2728
log = "0.4"
2829
tauri = { version = "2.10.1", features = [] }
2930
tauri-plugin-log = "2.7.1"

src-tauri/src/cli/args.rs

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
use clap::{Args, Parser, Subcommand, ValueEnum};
2+
3+
#[derive(Debug, Parser)]
4+
#[command(
5+
name = "echo-profile",
6+
disable_help_flag = true,
7+
disable_help_subcommand = true
8+
)]
9+
pub struct CliArgs {
10+
#[arg(long, global = true)]
11+
pub json: bool,
12+
#[command(subcommand)]
13+
pub command: CliCommand,
14+
}
15+
16+
#[derive(Debug, Subcommand)]
17+
pub enum CliCommand {
18+
Help(HelpArgs),
19+
Version,
20+
List(ListArgs),
21+
Profile(ProfileArgs),
22+
Serve(ServeArgs),
23+
}
24+
25+
#[derive(Debug, Args)]
26+
pub struct HelpArgs {
27+
pub topic: Vec<String>,
28+
}
29+
30+
#[derive(Debug, Args)]
31+
pub struct ServeArgs {
32+
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
33+
pub args: Vec<String>,
34+
}
35+
36+
#[derive(Debug, Args)]
37+
pub struct ListArgs {
38+
#[command(subcommand)]
39+
pub command: ListCommand,
40+
}
41+
42+
#[derive(Debug, Subcommand)]
43+
pub enum ListCommand {
44+
Providers(ProviderListArgs),
45+
Projects(ProjectListArgs),
46+
Sessions(SessionListArgs),
47+
}
48+
49+
#[derive(Debug, Args, Default)]
50+
pub struct ProviderListArgs {
51+
#[arg(long = "provider")]
52+
pub providers: Vec<String>,
53+
#[arg(long = "providers")]
54+
pub providers_csv: Option<String>,
55+
}
56+
57+
#[derive(Debug, Args, Default)]
58+
pub struct ProjectListArgs {
59+
#[arg(long = "provider")]
60+
pub providers: Vec<String>,
61+
#[arg(long = "providers")]
62+
pub providers_csv: Option<String>,
63+
#[arg(long, default_value_t = 50)]
64+
pub limit: usize,
65+
#[arg(long, default_value_t = 0)]
66+
pub offset: usize,
67+
#[arg(long)]
68+
pub since: Option<String>,
69+
#[arg(long)]
70+
pub until: Option<String>,
71+
#[arg(long)]
72+
pub no_cache: bool,
73+
}
74+
75+
#[derive(Debug, Args, Default)]
76+
pub struct SessionListArgs {
77+
#[arg(long = "provider")]
78+
pub providers: Vec<String>,
79+
#[arg(long = "providers")]
80+
pub providers_csv: Option<String>,
81+
#[arg(long, default_value_t = 50)]
82+
pub limit: usize,
83+
#[arg(long, default_value_t = 0)]
84+
pub offset: usize,
85+
#[arg(long)]
86+
pub current_project: bool,
87+
#[arg(long)]
88+
pub include_ancestor_projects: bool,
89+
#[arg(long)]
90+
pub actual_project_path: Option<String>,
91+
#[arg(long)]
92+
pub project_path: Option<String>,
93+
#[arg(long)]
94+
pub since: Option<String>,
95+
#[arg(long)]
96+
pub until: Option<String>,
97+
#[arg(long)]
98+
pub no_cache: bool,
99+
}
100+
101+
#[derive(Debug, Args)]
102+
pub struct ProfileArgs {
103+
#[command(subcommand)]
104+
pub command: ProfileCommand,
105+
}
106+
107+
#[derive(Debug, Subcommand)]
108+
pub enum ProfileCommand {
109+
Collect(CollectArgs),
110+
}
111+
112+
#[derive(Debug, Args)]
113+
#[allow(clippy::struct_excessive_bools)]
114+
pub struct CollectArgs {
115+
#[arg(long, value_enum)]
116+
pub scope: CollectScope,
117+
#[arg(long = "provider")]
118+
pub providers: Vec<String>,
119+
#[arg(long = "providers")]
120+
pub providers_csv: Option<String>,
121+
#[arg(long)]
122+
pub current_project: bool,
123+
#[arg(long)]
124+
pub include_ancestor_projects: bool,
125+
#[arg(long)]
126+
pub actual_project_path: Option<String>,
127+
#[arg(long)]
128+
pub project_path: Option<String>,
129+
#[arg(long)]
130+
pub session_path: Option<String>,
131+
#[arg(long, value_enum)]
132+
pub sample: Option<SampleStrategy>,
133+
#[arg(long, default_value_t = 30_000)]
134+
pub budget_chars: usize,
135+
#[arg(long, default_value_t = 50)]
136+
pub paste_detect_min_chars: usize,
137+
#[arg(long, default_value_t = 0.7)]
138+
pub paste_like_threshold: f64,
139+
#[arg(long)]
140+
pub include_paste_like: bool,
141+
#[arg(long)]
142+
pub since: Option<String>,
143+
#[arg(long)]
144+
pub until: Option<String>,
145+
#[arg(long, default_value_t = 1000)]
146+
pub max_sessions_scan: usize,
147+
#[arg(long, default_value_t = 50_000)]
148+
pub max_messages_scan: usize,
149+
#[arg(long)]
150+
pub no_cache: bool,
151+
}
152+
153+
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
154+
pub enum CollectScope {
155+
Global,
156+
Project,
157+
Session,
158+
}
159+
160+
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
161+
pub enum SampleStrategy {
162+
Recent,
163+
Representative,
164+
Mixed,
165+
Chronological,
166+
}
167+
168+
#[cfg(test)]
169+
mod tests {
170+
use super::*;
171+
172+
#[test]
173+
fn parses_version_command() {
174+
let cli = CliArgs::try_parse_from(["echo-profile", "version"]).unwrap();
175+
assert!(matches!(cli.command, CliCommand::Version));
176+
}
177+
178+
#[test]
179+
fn parses_profile_collect_project_current() {
180+
let cli = CliArgs::try_parse_from([
181+
"echo-profile",
182+
"profile",
183+
"collect",
184+
"--scope",
185+
"project",
186+
"--current-project",
187+
])
188+
.unwrap();
189+
let CliCommand::Profile(ProfileArgs {
190+
command: ProfileCommand::Collect(args),
191+
}) = cli.command
192+
else {
193+
panic!("expected profile collect");
194+
};
195+
assert_eq!(args.scope, CollectScope::Project);
196+
assert!(args.current_project);
197+
}
198+
}

0 commit comments

Comments
 (0)