Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ pip install "skillware[cli]"
skillware list
```

This prints a table of all locally available skills and confirms the install and path resolution are working.
This prints a table of all locally available skills and confirms the install and path resolution are working. Running `skillware` with no arguments opens the interactive menu.

### 3. Configuration

Expand Down
1 change: 1 addition & 0 deletions docs/contributing/ai_native_workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ Complete the checklist that matches your issue during Stage 5.

- [ ] `skills/<category>/<skill_name>/` exists with full bundle
- [ ] `manifest.yaml`: `name`, `version`, `description`, `parameters`, `constitution`, real `issuer`
- [ ] Optional: `short_description` field (~80 chars) for a concise one-line summary in `skillware list`
- [ ] `skill.py`: deterministic, JSON-serializable returns, safe error handling
- [ ] `instructions.md`: when to use, how to interpret output, limitations
- [ ] `card.json`: `issuer` matches manifest
Expand Down
15 changes: 13 additions & 2 deletions docs/usage/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,19 @@ interactive numbered menu:

skillware

The menu accepts both number input (`1`) and command name (`list`). Press `q`
or Enter to exit cleanly.
The splash displays the current version and links to the project site and
repository. The menu accepts both number input (`1`) and command name (`list`).
After each command completes, the menu re-prints automatically. Press `q` or
`Ctrl+C` to exit.

Available commands:

| Input | Command | Status |
| :--- | :--- | :--- |
| `1` / `list` | List all locally installed skills | Available |
| `2` / `paths` | Show and repair skill directory resolution paths | Coming in #81 |
| `3` / `test` | Run test_skill.py for one or all skills | Coming in #83 |
| `4` / `help` | Print usage information | Available |

## Commands

Expand Down
55 changes: 35 additions & 20 deletions skillware/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
ID_STYLE = "#B5EAD7" # mint - skill ID column
BORDER_STYLE = "#C7CEEA" # lavender - table border

SPLASH_STYLE = "#C7CEEA" # lavender - swillware splash color
SPLASH_STYLE = "#C7CEEA" # lavender - skillware splash color
MENU_STYLE = "#FFDAC1" # peach - menu category


Expand Down Expand Up @@ -41,7 +41,7 @@ def _short_description(data: Dict[str, Any], max_len: int = 80) -> str:
"""Return short_description if present, else first sentence of description truncated."""
short = data.get("short_description", "").strip()
if short:
return short[:max_len] + ("..." if len(short) > max_len else "")
return short[:max_len] + ("" if len(short) > max_len else "")

desc = data.get("description", "").strip()

Expand All @@ -53,7 +53,7 @@ def _short_description(data: Dict[str, Any], max_len: int = 80) -> str:
desc = desc[: idx + 1]
break

return desc[:max_len] + ("..." if len(desc) > max_len else "")
return desc[:max_len] + ("" if len(desc) > max_len else "")


def _discover_skills(
Expand Down Expand Up @@ -130,15 +130,18 @@ def cmd_list(
return

table = Table(
box=box.SIMPLE_HEAVY, border_style=BORDER_STYLE, header_style=TABLE_STYLE
box=box.SIMPLE_HEAVY,
border_style=BORDER_STYLE,
header_style=TABLE_STYLE,
expand=True,
)

table.add_column("ID", style=ID_STYLE, no_wrap=True)
table.add_column("VERSION", style="dim")
table.add_column("CATEGORY", style=CATEGORY_STYLE)
table.add_column("ISSUER", style="dim")
table.add_column("DESCRIPTION")
table.add_column("REQUIREMENTS", style="dim")
table.add_column("ID", style=ID_STYLE, no_wrap=True, ratio=2)
table.add_column("VERSION", style="dim", no_wrap=True, ratio=1)
table.add_column("CATEGORY", style=CATEGORY_STYLE, no_wrap=True, ratio=1)
table.add_column("ISSUER", style="dim", no_wrap=True, ratio=1)
table.add_column("DESCRIPTION", ratio=3)
table.add_column("REQUIREMENTS", style="dim", ratio=2)

for skill in skills:
table.add_row(
Expand All @@ -153,6 +156,13 @@ def cmd_list(
console.print(table)


def _print_menu(console, menu) -> None:
for num, name, desc in menu:
console.print(f" [{num}] {name:<20}— {desc}", style=MENU_STYLE)

console.print()


def cmd_interactive(console=None, parser=None) -> None:
"""Launch ASCII splash screen and interactive menu."""
try:
Expand Down Expand Up @@ -184,23 +194,25 @@ def cmd_interactive(console=None, parser=None) -> None:
console.print(Text(splash, style=SPLASH_STYLE))
console.print(
Text(
f" Skill Management Framework - v{version}\n",
f" Skill Management Framework — v{version}",
style=f"dim {SPLASH_STYLE}",
)
)

console.print(
Text(
" https://skillware.site · https://github.com/arpahls/skillware\n",
style=f"dim {SPLASH_STYLE}",
)
)

menu = [
("1", "list", "discover and display all locally installed skills"),
("2", "paths", "show and repair skill directory resolution paths"),
("3", "test", "run test_skill.py for one or all skills"),
("2", "paths (soon, #81)", "show and repair skill directory resolution paths"),
("3", "test (soon, #83)", "run test_skill.py for one or all skills"),
("4", "help", "usage guide for any command"),
]

for num, name, desc in menu:
console.print(f" [{num}] {name:<10}— {desc}", style=MENU_STYLE)

console.print()

commands = {
"1": "list",
"list": "list",
Expand All @@ -212,21 +224,23 @@ def cmd_interactive(console=None, parser=None) -> None:
"help": "help",
}

_print_menu(console, menu)

while True:
try:
choice = input(" > ").strip().lower()
except (KeyboardInterrupt, EOFError):
console.print("\n Bye.", style="dim")
return

if choice in ("q", ""):
if choice == "q":
console.print(" Bye.", style="dim")
return

command = commands.get(choice)

if command == "list":
cmd_list()
cmd_list(console=console)
elif command in ("paths", "test"):
console.print(
f" '{command}' is not yet implemented. Coming in a future release.",
Expand All @@ -243,6 +257,7 @@ def cmd_interactive(console=None, parser=None) -> None:
console.print(f" Unknown command: '{choice}'", style="dim #FF9AA2")

console.print()
_print_menu(console, menu)


def main() -> None:
Expand Down
2 changes: 1 addition & 1 deletion templates/python_skill/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Starter bundle under `skills/<category>/<skill_name>/`. Copy this template from

1. **Rename** the folder to match your skill ID (e.g. `skills/finance/my_skill`).
2. **Packaging**: Add empty `__init__.py` files in `skills/<category>/` (new categories only) and in your skill folder so PyPI wheels include the full bundle. No `pyproject.toml` changes per skill.
3. **`manifest.yaml`**: Set real `name`, `version`, `description`, `parameters`, `constitution`, and `issuer` (`name` + `email` required; `github` / `org` optional).
3. **`manifest.yaml`**: Set real `name`, `version`, `description`, `short_description`, `parameters`, `constitution`, and `issuer` (`name` + `email` required; `github` / `org` optional).
4. **`skill.py`**: Implement deterministic logic; no LLM-generated code in the skill body.
5. **`instructions.md`**: Tell the agent when and how to use the tool.
6. **`card.json`**: Mirror `issuer` from the manifest; customize UI fields.
Expand Down
1 change: 1 addition & 0 deletions templates/python_skill/manifest.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: "my-awesome-skill"
version: "0.1.0"
description: "A short description of what this skill does."
short_description: "One-line summary for skillware list (~80 chars)."
issuer:
name: Your Name
email: you@example.com
Expand Down
38 changes: 26 additions & 12 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,11 @@ def test_short_description_uses_short_description_field():


def test_short_description_truncates_at_80_chars():
"""short_description longer than 80 chars should be truncated with ..."""
"""short_description longer than 80 chars should be truncated with """
data = {"short_description": "A" * 90}
result = _short_description(data)
assert len(result) == 83 # 80 + "..."
assert result.endswith("...")
assert len(result) == 81 # 80 + ""
assert result.endswith("")


def test_short_description_falls_back_to_first_sentence():
Expand All @@ -184,24 +184,38 @@ def test_cmd_interactive_exits_on_q(monkeypatch):
assert "Bye" in buf.getvalue()


def test_cmd_interactive_exits_on_empty(monkeypatch):
"""Pressing enter without input should exit cleanly."""
def test_cmd_interactive_unknown_command(monkeypatch):
"""Unknown command should print error then exit on q."""
import io
from rich.console import Console

monkeypatch.setattr("builtins.input", lambda _: "")
responses = iter(["unknown_cmd", "q"])
monkeypatch.setattr("builtins.input", lambda _: next(responses))
buf = io.StringIO()
cmd_interactive(console=Console(file=buf, force_terminal=False))
assert "Bye" in buf.getvalue()
assert "Unknown command" in buf.getvalue()


def test_cmd_interactive_unknown_command(monkeypatch):
"""Unknown command should print error then exit on q."""
def test_cmd_interactive_list_dispatch(tmp_path, monkeypatch):
"""Entering 1 or list should dispatch to cmd_list."""
import io
from rich.console import Console

responses = iter(["unknown_cmd", "q"])
skill_dir = tmp_path / "office" / "test_skill"
skill_dir.mkdir(parents=True)
(skill_dir / "skill.py").touch()
(skill_dir / "manifest.yaml").write_text(
"name: test_skill\nversion: 0.1.0\ndescription: Test.\n"
"short_description: Test skill.\n"
)

responses = iter(["1", "q"])
monkeypatch.setattr("builtins.input", lambda _: next(responses))
monkeypatch.chdir(tmp_path)

buf = io.StringIO()
cmd_interactive(console=Console(file=buf, force_terminal=False))
assert "Unknown command" in buf.getvalue()
console = Console(file=buf, force_terminal=False)
cmd_interactive(console=console)

output = buf.getvalue()
assert "test_skill" in output
Loading