Skip to content

Commit 4fbc5e3

Browse files
feat: add skill install CLI command
Install a SKILL.md file that teaches AI agents how to use the ascend-tools CLI. The skill template is embedded in the binary via include_str!() and written to <target>/ascend-tools/SKILL.md. Usage: ascend-tools skill install --target ./.claude/skills No authentication required. Runs before config/client initialization. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 453341a commit 4fbc5e3

File tree

3 files changed

+139
-3
lines changed

3 files changed

+139
-3
lines changed

CLAUDE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ src/ascend_tools/
3232
│ └── src/
3333
│ ├── lib.rs # pub fn run(args) — testable entry point
3434
│ ├── main.rs # binary entry point
35-
│ └── cli.rs # clap commands, table/json output, print_table helper
35+
│ ├── cli.rs # clap commands, table/json output, print_table helper
36+
│ └── skill.md # SKILL.md template (embedded via include_str!, installed by `skill install`)
3637
3738
└── ascend-tools-py/ # PyO3 binding crate (cdylib, built by maturin)
3839
└── src/
@@ -103,6 +104,8 @@ ascend-tools [-o text|json] [-V]
103104
flow list-runs -r/--runtime <UUID> [--status, -f/--flow-name]
104105
flow get-run <RUN_NAME> -r/--runtime <UUID>
105106
107+
skill install --target <PATH>
108+
106109
mcp [--http] [--bind <ADDR>]
107110
```
108111

@@ -208,6 +211,7 @@ The SDK/CLI calls the Instance API's `/api/v1/` endpoints, defined in `ascend-ba
208211
- MCP tool parameters use `schemars` `JsonSchema` derive for automatic JSON Schema generation; doc comments on fields become schema descriptions
209212
- MCP `FlowRunSpec` uses `#[serde(flatten)]` with a catch-all map for forward compatibility with new backend fields
210213
- PyO3 `run()` uses `py.detach()` to release the GIL during long-running Rust calls (MCP server)
214+
- When adding or changing CLI commands, update `src/ascend_tools/ascend-tools-cli/src/skill.md` to keep the skill in sync
211215

212216
## related repos
213217

src/ascend_tools/ascend-tools-cli/src/cli.rs

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
use anyhow::Result;
1+
use anyhow::{Context, Result};
22
use ascend_tools::client::AscendClient;
33
use ascend_tools::config::Config;
44
use ascend_tools::models::*;
55
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
66
use std::ffi::OsString;
7+
use std::path::PathBuf;
78

89
#[derive(Parser)]
910
#[command(name = "ascend-tools", version, about = "CLI for the Ascend REST API")]
@@ -54,6 +55,11 @@ enum Commands {
5455
#[arg(long, default_value = "127.0.0.1:8000")]
5556
bind: String,
5657
},
58+
/// Manage skills
59+
Skill {
60+
#[command(subcommand)]
61+
command: Option<SkillCommands>,
62+
},
5763
}
5864

5965
#[derive(Subcommand)]
@@ -132,6 +138,19 @@ enum FlowCommands {
132138
},
133139
}
134140

141+
#[derive(Subcommand)]
142+
enum SkillCommands {
143+
/// Install the ascend-tools skill
144+
#[command(arg_required_else_help = true)]
145+
Install {
146+
/// Target directory
147+
///
148+
/// Examples: ./.claude/skills, ~/.claude/skills, ./.agents/skills
149+
#[arg(long, required = true)]
150+
target: String,
151+
},
152+
}
153+
135154
pub fn run<I, T>(args: I) -> Result<()>
136155
where
137156
I: IntoIterator<Item = T>,
@@ -144,6 +163,11 @@ where
144163
return Ok(());
145164
};
146165

166+
// Commands that don't require authentication
167+
if let Commands::Skill { command } = command {
168+
return handle_skill(command);
169+
}
170+
147171
let config = Config::with_overrides(
148172
cli.service_account_id.as_deref(),
149173
cli.service_account_key.as_deref(),
@@ -164,7 +188,7 @@ where
164188
match command {
165189
Commands::Runtime { command } => handle_runtime(&client, command, &cli.output),
166190
Commands::Flow { command } => handle_flow(&client, command, &cli.output),
167-
Commands::Mcp { .. } => unreachable!(),
191+
Commands::Mcp { .. } | Commands::Skill { .. } => unreachable!(),
168192
}
169193
}
170194

@@ -350,6 +374,39 @@ fn handle_flow(
350374
Ok(())
351375
}
352376

377+
// -- skill --
378+
379+
const SKILL_CONTENT: &str = include_str!("skill.md");
380+
381+
fn handle_skill(cmd: Option<SkillCommands>) -> Result<()> {
382+
let Some(cmd) = cmd else {
383+
Cli::command()
384+
.find_subcommand_mut("skill")
385+
.expect("skill subcommand exists")
386+
.print_help()?;
387+
return Ok(());
388+
};
389+
match cmd {
390+
SkillCommands::Install { target } => {
391+
let target = if target.starts_with('~') {
392+
let home = std::env::var("HOME").context("HOME environment variable not set")?;
393+
PathBuf::from(target.replacen('~', &home, 1))
394+
} else {
395+
PathBuf::from(&target)
396+
};
397+
let dir = target.join("ascend-tools");
398+
std::fs::create_dir_all(&dir)
399+
.with_context(|| format!("failed to create directory {}", dir.display()))?;
400+
let path = dir.join("SKILL.md");
401+
std::fs::write(&path, SKILL_CONTENT)
402+
.with_context(|| format!("failed to write {}", path.display()))?;
403+
let abs = std::fs::canonicalize(&path).unwrap_or(path);
404+
println!("Installed ascend-tools skill to {}", abs.display());
405+
Ok(())
406+
}
407+
}
408+
}
409+
353410
// -- output helpers --
354411

355412
fn print_json<T: serde::Serialize>(value: &T) -> Result<()> {
@@ -543,6 +600,23 @@ mod tests {
543600
);
544601
}
545602

603+
#[test]
604+
fn test_cli_parses_skill_install() {
605+
let cli = Cli::parse_from([
606+
"ascend-tools",
607+
"skill",
608+
"install",
609+
"--target",
610+
"./.claude/skills",
611+
]);
612+
assert!(matches!(
613+
cli.command,
614+
Some(Commands::Skill {
615+
command: Some(SkillCommands::Install { .. })
616+
})
617+
));
618+
}
619+
546620
#[test]
547621
fn test_cli_parses_mcp_stdio() {
548622
let cli = Cli::parse_from(["ascend-tools", "mcp"]);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
name: ascend-tools
3+
description: Use the ascend-tools CLI to manage Ascend runtimes, flows, and flow runs.
4+
---
5+
6+
# ascend-tools
7+
8+
Manage Ascend runtimes, flows, and flow runs via the `ascend-tools` CLI.
9+
10+
## Authentication
11+
12+
Set three environment variables (from Ascend UI > Settings > Users > Create Service Account):
13+
14+
```bash
15+
export ASCEND_SERVICE_ACCOUNT_ID="asc-sa-..."
16+
export ASCEND_SERVICE_ACCOUNT_KEY="..."
17+
export ASCEND_INSTANCE_API_URL="https://api.instance.ascend.io"
18+
```
19+
20+
## Commands
21+
22+
### Runtimes
23+
24+
```bash
25+
ascend-tools runtime list [--id <ID>] [--kind <KIND>] [--project-uuid <UUID>] [--environment-uuid <UUID>]
26+
ascend-tools runtime get <UUID>
27+
ascend-tools runtime resume <UUID>
28+
ascend-tools runtime pause <UUID>
29+
```
30+
31+
### Flows
32+
33+
```bash
34+
ascend-tools flow list --runtime <UUID>
35+
ascend-tools flow run <FLOW_NAME> --runtime <UUID> [--spec '<JSON>'] [--resume]
36+
ascend-tools flow list-runs --runtime <UUID> [--status <STATUS>] [--flow-name <NAME>]
37+
ascend-tools flow get-run <RUN_NAME> --runtime <UUID>
38+
```
39+
40+
### Flow run spec
41+
42+
Pass `--spec` as JSON to control flow run behavior:
43+
44+
```bash
45+
ascend-tools flow run my-flow --runtime <UUID> --spec '{"full_refresh": true}'
46+
ascend-tools flow run my-flow --runtime <UUID> --spec '{"components": ["component_a", "component_b"]}'
47+
ascend-tools flow run my-flow --runtime <UUID> --spec '{"run_tests": false}'
48+
```
49+
50+
Available spec fields: `full_refresh`, `components`, `component_categories`, `parameters`, `run_tests`, `halt_flow_on_error`, `disable_optimizers`, `runner_overrides`.
51+
52+
## Output
53+
54+
Default output is a human-readable table. Use `-o json` for machine-readable output:
55+
56+
```bash
57+
ascend-tools -o json runtime list
58+
```

0 commit comments

Comments
 (0)