skillware 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. {skillware-0.3.1/skillware.egg-info → skillware-0.3.2}/PKG-INFO +4 -2
  2. {skillware-0.3.1 → skillware-0.3.2}/README.md +3 -1
  3. {skillware-0.3.1 → skillware-0.3.2}/pyproject.toml +1 -1
  4. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/mica_module/manifest.yaml +1 -0
  5. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/pii_masker/manifest.yaml +1 -0
  6. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/tos_evaluator/manifest.yaml +1 -0
  7. {skillware-0.3.1 → skillware-0.3.2}/skills/data_engineering/novelty_extractor/manifest.yaml +1 -0
  8. {skillware-0.3.1 → skillware-0.3.2}/skills/data_engineering/synthetic_generator/manifest.yaml +1 -0
  9. {skillware-0.3.1 → skillware-0.3.2}/skills/dev_tools/issue_resolver/manifest.yaml +1 -0
  10. {skillware-0.3.1 → skillware-0.3.2}/skills/finance/wallet_screening/manifest.yaml +1 -0
  11. {skillware-0.3.1 → skillware-0.3.2}/skills/finance/wallet_screening/skill.py +90 -44
  12. {skillware-0.3.1 → skillware-0.3.2}/skills/office/pdf_form_filler/manifest.yaml +1 -0
  13. {skillware-0.3.1 → skillware-0.3.2}/skills/optimization/prompt_rewriter/manifest.yaml +1 -0
  14. skillware-0.3.2/skillware/cli.py +299 -0
  15. {skillware-0.3.1 → skillware-0.3.2/skillware.egg-info}/PKG-INFO +4 -2
  16. {skillware-0.3.1 → skillware-0.3.2}/tests/test_cli.py +82 -1
  17. skillware-0.3.1/skillware/cli.py +0 -162
  18. {skillware-0.3.1 → skillware-0.3.2}/LICENSE +0 -0
  19. {skillware-0.3.1 → skillware-0.3.2}/MANIFEST.in +0 -0
  20. {skillware-0.3.1 → skillware-0.3.2}/setup.cfg +0 -0
  21. {skillware-0.3.1 → skillware-0.3.2}/skills/__init__.py +0 -0
  22. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/__init__.py +0 -0
  23. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/mica_module/__init__.py +0 -0
  24. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/mica_module/card.json +0 -0
  25. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/mica_module/instructions.md +0 -0
  26. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/mica_module/mica_corpus.json +0 -0
  27. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/mica_module/skill.py +0 -0
  28. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/pii_masker/__init__.py +0 -0
  29. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/pii_masker/card.json +0 -0
  30. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/pii_masker/instructions.md +0 -0
  31. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/pii_masker/skill.py +0 -0
  32. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/tos_evaluator/__init__.py +0 -0
  33. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/tos_evaluator/card.json +0 -0
  34. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/tos_evaluator/instructions.md +0 -0
  35. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/tos_evaluator/skill.py +0 -0
  36. {skillware-0.3.1 → skillware-0.3.2}/skills/compliance/tos_evaluator/test_skill.py +0 -0
  37. {skillware-0.3.1 → skillware-0.3.2}/skills/data_engineering/__init__.py +0 -0
  38. {skillware-0.3.1 → skillware-0.3.2}/skills/data_engineering/novelty_extractor/__init__.py +0 -0
  39. {skillware-0.3.1 → skillware-0.3.2}/skills/data_engineering/novelty_extractor/card.json +0 -0
  40. {skillware-0.3.1 → skillware-0.3.2}/skills/data_engineering/novelty_extractor/instructions.md +0 -0
  41. {skillware-0.3.1 → skillware-0.3.2}/skills/data_engineering/novelty_extractor/skill.py +0 -0
  42. {skillware-0.3.1 → skillware-0.3.2}/skills/data_engineering/novelty_extractor/test_skill.py +0 -0
  43. {skillware-0.3.1 → skillware-0.3.2}/skills/data_engineering/synthetic_generator/__init__.py +0 -0
  44. {skillware-0.3.1 → skillware-0.3.2}/skills/data_engineering/synthetic_generator/card.json +0 -0
  45. {skillware-0.3.1 → skillware-0.3.2}/skills/data_engineering/synthetic_generator/instructions.md +0 -0
  46. {skillware-0.3.1 → skillware-0.3.2}/skills/data_engineering/synthetic_generator/skill.py +0 -0
  47. {skillware-0.3.1 → skillware-0.3.2}/skills/dev_tools/__init__.py +0 -0
  48. {skillware-0.3.1 → skillware-0.3.2}/skills/dev_tools/issue_resolver/__init__.py +0 -0
  49. {skillware-0.3.1 → skillware-0.3.2}/skills/dev_tools/issue_resolver/card.json +0 -0
  50. {skillware-0.3.1 → skillware-0.3.2}/skills/dev_tools/issue_resolver/instructions.md +0 -0
  51. {skillware-0.3.1 → skillware-0.3.2}/skills/dev_tools/issue_resolver/skill.py +0 -0
  52. {skillware-0.3.1 → skillware-0.3.2}/skills/dev_tools/issue_resolver/test_skill.py +0 -0
  53. {skillware-0.3.1 → skillware-0.3.2}/skills/finance/__init__.py +0 -0
  54. {skillware-0.3.1 → skillware-0.3.2}/skills/finance/wallet_screening/__init__.py +0 -0
  55. {skillware-0.3.1 → skillware-0.3.2}/skills/finance/wallet_screening/card.json +0 -0
  56. {skillware-0.3.1 → skillware-0.3.2}/skills/finance/wallet_screening/data/entities.ftm.json +0 -0
  57. {skillware-0.3.1 → skillware-0.3.2}/skills/finance/wallet_screening/data/malicious_scs_2025.json +0 -0
  58. {skillware-0.3.1 → skillware-0.3.2}/skills/finance/wallet_screening/data/normalized_fbi_lazarus_20250722_180019.json +0 -0
  59. {skillware-0.3.1 → skillware-0.3.2}/skills/finance/wallet_screening/data/normalized_israel_nbctf_20250722_180019.json +0 -0
  60. {skillware-0.3.1 → skillware-0.3.2}/skills/finance/wallet_screening/data/normalized_uniswap_trm.json +0 -0
  61. {skillware-0.3.1 → skillware-0.3.2}/skills/finance/wallet_screening/instructions.md +0 -0
  62. {skillware-0.3.1 → skillware-0.3.2}/skills/finance/wallet_screening/maintenance/normalization_tool.py +0 -0
  63. {skillware-0.3.1 → skillware-0.3.2}/skills/finance/wallet_screening/maintenance/normalize_uniswap_trm.py +0 -0
  64. {skillware-0.3.1 → skillware-0.3.2}/skills/office/__init__.py +0 -0
  65. {skillware-0.3.1 → skillware-0.3.2}/skills/office/pdf_form_filler/__init__.py +0 -0
  66. {skillware-0.3.1 → skillware-0.3.2}/skills/office/pdf_form_filler/card.json +0 -0
  67. {skillware-0.3.1 → skillware-0.3.2}/skills/office/pdf_form_filler/instructions.md +0 -0
  68. {skillware-0.3.1 → skillware-0.3.2}/skills/office/pdf_form_filler/skill.py +0 -0
  69. {skillware-0.3.1 → skillware-0.3.2}/skills/office/pdf_form_filler/utils.py +0 -0
  70. {skillware-0.3.1 → skillware-0.3.2}/skills/optimization/__init__.py +0 -0
  71. {skillware-0.3.1 → skillware-0.3.2}/skills/optimization/prompt_rewriter/__init__.py +0 -0
  72. {skillware-0.3.1 → skillware-0.3.2}/skills/optimization/prompt_rewriter/card.json +0 -0
  73. {skillware-0.3.1 → skillware-0.3.2}/skills/optimization/prompt_rewriter/instructions.md +0 -0
  74. {skillware-0.3.1 → skillware-0.3.2}/skills/optimization/prompt_rewriter/skill.py +0 -0
  75. {skillware-0.3.1 → skillware-0.3.2}/skillware/__init__.py +0 -0
  76. {skillware-0.3.1 → skillware-0.3.2}/skillware/core/__init__.py +0 -0
  77. {skillware-0.3.1 → skillware-0.3.2}/skillware/core/base_skill.py +0 -0
  78. {skillware-0.3.1 → skillware-0.3.2}/skillware/core/env.py +0 -0
  79. {skillware-0.3.1 → skillware-0.3.2}/skillware/core/loader.py +0 -0
  80. {skillware-0.3.1 → skillware-0.3.2}/skillware.egg-info/SOURCES.txt +0 -0
  81. {skillware-0.3.1 → skillware-0.3.2}/skillware.egg-info/dependency_links.txt +0 -0
  82. {skillware-0.3.1 → skillware-0.3.2}/skillware.egg-info/entry_points.txt +0 -0
  83. {skillware-0.3.1 → skillware-0.3.2}/skillware.egg-info/requires.txt +0 -0
  84. {skillware-0.3.1 → skillware-0.3.2}/skillware.egg-info/top_level.txt +0 -0
  85. {skillware-0.3.1 → skillware-0.3.2}/tests/test_loader.py +0 -0
  86. {skillware-0.3.1 → skillware-0.3.2}/tests/test_mica_module.py +0 -0
  87. {skillware-0.3.1 → skillware-0.3.2}/tests/test_skill_issuer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skillware
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: A framework for modular, self-contained AI skills.
5
5
  Author-email: ARPA Hellenic Logical Systems <skillware-os@arpacorp.net>
6
6
  License: MIT License
@@ -81,6 +81,7 @@ Dynamic: license-file
81
81
  <a href="#architecture">Architecture</a> •
82
82
  <a href="#quick-start">Quick Start</a> •
83
83
  <a href="#documentation">Documentation</a> •
84
+ <a href="CHANGELOG.md">Changelog</a> •
84
85
  <a href="#contributing">Contributing</a> •
85
86
  <a href="#comparison">Comparison</a> •
86
87
  <a href="#contact">Contact</a>
@@ -163,7 +164,7 @@ pip install "skillware[cli]"
163
164
  skillware list
164
165
  ```
165
166
 
166
- This prints a table of all locally available skills and confirms the install and path resolution are working.
167
+ 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.
167
168
 
168
169
  ### 3. Configuration
169
170
 
@@ -240,6 +241,7 @@ For other providers and shared integration patterns, see the [usage guides index
240
241
  | Topic | Link |
241
242
  | :--- | :--- |
242
243
  | Introduction | [docs/introduction.md](docs/introduction.md) |
244
+ | Changelog | [CHANGELOG.md](CHANGELOG.md) |
243
245
  | Testing | [docs/TESTING.md](docs/TESTING.md) |
244
246
  | Skill library | [docs/skills/README.md](docs/skills/README.md) |
245
247
  | Usage guides | [index](docs/usage/README.md) — [Gemini](docs/usage/gemini.md) · [Claude](docs/usage/claude.md) · [OpenAI](docs/usage/openai.md) · [DeepSeek](docs/usage/deepseek.md) · [Ollama](docs/usage/ollama.md); [agent loops](docs/usage/agent_loops.md); [API keys](docs/usage/api_keys.md) |
@@ -19,6 +19,7 @@
19
19
  <a href="#architecture">Architecture</a> •
20
20
  <a href="#quick-start">Quick Start</a> •
21
21
  <a href="#documentation">Documentation</a> •
22
+ <a href="CHANGELOG.md">Changelog</a> •
22
23
  <a href="#contributing">Contributing</a> •
23
24
  <a href="#comparison">Comparison</a> •
24
25
  <a href="#contact">Contact</a>
@@ -101,7 +102,7 @@ pip install "skillware[cli]"
101
102
  skillware list
102
103
  ```
103
104
 
104
- This prints a table of all locally available skills and confirms the install and path resolution are working.
105
+ 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.
105
106
 
106
107
  ### 3. Configuration
107
108
 
@@ -178,6 +179,7 @@ For other providers and shared integration patterns, see the [usage guides index
178
179
  | Topic | Link |
179
180
  | :--- | :--- |
180
181
  | Introduction | [docs/introduction.md](docs/introduction.md) |
182
+ | Changelog | [CHANGELOG.md](CHANGELOG.md) |
181
183
  | Testing | [docs/TESTING.md](docs/TESTING.md) |
182
184
  | Skill library | [docs/skills/README.md](docs/skills/README.md) |
183
185
  | Usage guides | [index](docs/usage/README.md) — [Gemini](docs/usage/gemini.md) · [Claude](docs/usage/claude.md) · [OpenAI](docs/usage/openai.md) · [DeepSeek](docs/usage/deepseek.md) · [Ollama](docs/usage/ollama.md); [agent loops](docs/usage/agent_loops.md); [API keys](docs/usage/api_keys.md) |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skillware"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "A framework for modular, self-contained AI skills."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -1,6 +1,7 @@
1
1
  name: compliance/mica_module
2
2
  version: 0.1.0
3
3
  description: A highly specialized, localized RAG and policy enforcement engine for the Markets in Crypto-Assets (MiCA) regulation.
4
+ short_description: "Local RAG and policy enforcement engine for MiCA regulation."
4
5
  issuer:
5
6
  name: Ross Peili
6
7
  email: vpeilivanidis@gmail.com
@@ -1,6 +1,7 @@
1
1
  name: compliance/pii_masker
2
2
  version: 0.1.0
3
3
  description: High-precision, local PII detection and redaction using the micro-f1-mask model via Ollama.
4
+ short_description: "Detects and redacts PII locally using the micro-f1-mask model."
4
5
  issuer:
5
6
  name: Ross Peili
6
7
  email: vpeilivanidis@gmail.com
@@ -1,6 +1,7 @@
1
1
  name: "compliance/tos_evaluator"
2
2
  version: "0.1.0"
3
3
  description: "Evaluates whether an intended automated website action appears permissible by checking robots.txt, discovered legal pages, and optional low-cost LLM review for ambiguous policy language."
4
+ short_description: "Evaluates whether an automated web action is permitted by site policy."
4
5
  issuer:
5
6
  name: Ross Peili
6
7
  email: vpeilivanidis@gmail.com
@@ -1,6 +1,7 @@
1
1
  name: "data_engineering/novelty_extractor"
2
2
  version: "0.1.0"
3
3
  description: "Filters a text dataset by semantic novelty, retaining only chunks that carry new information above a configurable threshold."
4
+ short_description: "Filters text datasets by semantic novelty using local embeddings."
4
5
  issuer:
5
6
  name: Martin Rizzo Lozano
6
7
  email: rizzolozanomartin@gmail.com
@@ -1,6 +1,7 @@
1
1
  name: "data_engineering/synthetic_generator"
2
2
  version: "0.1.0"
3
3
  description: "Generates high-entropy structured synthetic data for model fine-tuning to avoid mode collapse."
4
+ short_description: "Generates high-entropy synthetic JSONL data for model fine-tuning."
4
5
  issuer:
5
6
  name: Ross Peili
6
7
  email: vpeilivanidis@gmail.com
@@ -1,6 +1,7 @@
1
1
  name: "dev_tools/issue_resolver"
2
2
  version: "0.1.0"
3
3
  description: "Analyzes any GitHub issue against its target repository and produces a structured resolution plan with ranked implementation options, affected files, and caveats."
4
+ short_description: "Analyzes a GitHub issue and produces a ranked implementation plan."
4
5
  issuer:
5
6
  name: Ross Peili
6
7
  email: vpeilivanidis@gmail.com
@@ -2,6 +2,7 @@ name: wallet_screening
2
2
  version: 1.0.0
3
3
  description: |
4
4
  A comprehensive crypto wallet screening tool. Checks Ethereum addresses against sanctions lists (OFAC, FBI, etc.) and known malicious contracts (Mixers, Scams). analyze transaction history for risk.
5
+ short_description: "Screens Ethereum wallets against OFAC sanctions and mixer lists."
5
6
  issuer:
6
7
  name: Ross Peili
7
8
  email: vpeilivanidis@gmail.com
@@ -1,11 +1,15 @@
1
1
  import json
2
2
  import os
3
+ import re
3
4
  import requests
4
5
  import glob
5
6
  from typing import Any, Dict, List, Optional
6
7
  from datetime import datetime
7
8
  from skillware.core.base_skill import BaseSkill
8
9
 
10
+ _ETH_ADDRESS_RE = re.compile(r"^0x[a-fA-F0-9]{40}$")
11
+ _ZERO_WIDTH_CHARS = "\u200b\u200c\u200d\ufeff"
12
+
9
13
 
10
14
  class WalletScreeningSkill(BaseSkill):
11
15
  """
@@ -31,13 +35,17 @@ class WalletScreeningSkill(BaseSkill):
31
35
  # Load Additional Datasets dynamically (normalized files, etc.)
32
36
  self.additional_datasets = self._load_additional_datasets()
33
37
 
38
+ # ETH address -> sanctions records (built once; O(1) lookup per screen)
39
+ self._sanctions_index: Dict[str, List[Dict]] = {}
40
+ self._build_sanctions_index()
41
+
34
42
  @property
35
43
  def manifest(self) -> Dict[str, Any]:
36
44
  return {}
37
45
 
38
46
  def execute(self, params: Dict[str, Any]) -> Dict[str, Any]:
39
- address = params.get("address")
40
- if not address or not self._validate_eth_address(address):
47
+ address = self.normalize_eth_address(params.get("address") or "")
48
+ if not address:
41
49
  return {"error": "Invalid Ethereum address provided."}
42
50
 
43
51
  if not self.etherscan_api_key:
@@ -49,9 +57,8 @@ class WalletScreeningSkill(BaseSkill):
49
57
  eth_usd = self._get_price(self.coingecko_url_usd, "usd")
50
58
  eth_eur = self._get_price(self.coingecko_url_eur, "eur")
51
59
 
52
- # 2. Sanctions Check (Core + Additional)
53
- sanctions_hits = self._check_against_sanctions(address)
54
- sanctions_hits.extend(self._check_against_additional_sanctions(address))
60
+ # 2. Sanctions Check (FTM + additional lists via index)
61
+ sanctions_hits = self._lookup_sanctions_hits(address)
55
62
 
56
63
  # 3. Analyze Transactions
57
64
  analysis = self._analyze_transactions(txs, address)
@@ -126,10 +133,85 @@ class WalletScreeningSkill(BaseSkill):
126
133
 
127
134
  # --- API Helpers ---
128
135
 
129
- def _validate_eth_address(self, address: str) -> bool:
130
- return (
131
- isinstance(address, str) and address.startswith("0x") and len(address) == 42
136
+ @staticmethod
137
+ def normalize_eth_address(address: str) -> Optional[str]:
138
+ """Normalize and validate an Ethereum address (EIP-55 checksum not required)."""
139
+ if not isinstance(address, str):
140
+ return None
141
+ cleaned = address.strip().translate(
142
+ {ord(c): None for c in _ZERO_WIDTH_CHARS}
132
143
  )
144
+ if not cleaned.lower().startswith("0x"):
145
+ return None
146
+ normalized = "0x" + cleaned[2:].lower()
147
+ if _ETH_ADDRESS_RE.match(normalized):
148
+ return normalized
149
+ return None
150
+
151
+ @staticmethod
152
+ def _iter_property_values(properties: Dict, key: str):
153
+ raw = properties.get(key)
154
+ if raw is None:
155
+ return
156
+ if isinstance(raw, list):
157
+ for item in raw:
158
+ if item is not None:
159
+ yield str(item)
160
+ else:
161
+ yield str(raw)
162
+
163
+ def _eth_addresses_from_record(self, record: Dict) -> List[str]:
164
+ """Collect normalized Ethereum addresses from a sanctions or FTM record."""
165
+ found: List[str] = []
166
+ if "addresses" in record and isinstance(record["addresses"], list):
167
+ for a in record["addresses"]:
168
+ norm = self.normalize_eth_address(str(a))
169
+ if norm:
170
+ found.append(norm)
171
+
172
+ properties = record.get("properties") or {}
173
+ if isinstance(properties, dict):
174
+ for key in ("address", "publicKey"):
175
+ for value in self._iter_property_values(properties, key):
176
+ for part in value.split(","):
177
+ norm = self.normalize_eth_address(part)
178
+ if norm:
179
+ found.append(norm)
180
+
181
+ if "address" in record and not isinstance(record["address"], list):
182
+ norm = self.normalize_eth_address(str(record["address"]))
183
+ if norm:
184
+ found.append(norm)
185
+
186
+ return list(dict.fromkeys(found))
187
+
188
+ def _build_sanctions_index(self) -> None:
189
+ """Index normalized ETH addresses from FTM and additional datasets."""
190
+ index: Dict[str, List[Dict]] = {}
191
+ for entity in self.sanctions_entities:
192
+ addrs = self._eth_addresses_from_record(entity)
193
+ if not addrs:
194
+ continue
195
+ tagged = dict(entity)
196
+ tagged["__source_file__"] = "entities.ftm.json"
197
+ for addr in addrs:
198
+ index.setdefault(addr, []).append(tagged)
199
+
200
+ for entry in self.additional_datasets:
201
+ addrs = self._eth_addresses_from_record(entry)
202
+ if not addrs:
203
+ continue
204
+ tagged = dict(entry)
205
+ for addr in addrs:
206
+ index.setdefault(addr, []).append(tagged)
207
+
208
+ self._sanctions_index = index
209
+
210
+ def _lookup_sanctions_hits(self, address: str) -> List[Dict]:
211
+ normalized = self.normalize_eth_address(address)
212
+ if not normalized:
213
+ return []
214
+ return list(self._sanctions_index.get(normalized, []))
133
215
 
134
216
  def _get_price(self, url: str, currency: str) -> float:
135
217
  try:
@@ -179,42 +261,6 @@ class WalletScreeningSkill(BaseSkill):
179
261
 
180
262
  # --- Logic Helpers ---
181
263
 
182
- def _check_against_sanctions(self, address: str) -> List[Dict]:
183
- hits = []
184
- lower_addr = address.lower()
185
- for entity in self.sanctions_entities:
186
- matches = False
187
- if "addresses" in entity:
188
- if any(a.lower() == lower_addr for a in entity["addresses"]):
189
- matches = True
190
- elif "properties" in entity and "address" in entity["properties"]:
191
- if entity["properties"]["address"].lower() == lower_addr:
192
- matches = True
193
-
194
- if matches:
195
- entity["__source_file__"] = "entities.ftm.json"
196
- hits.append(entity)
197
- return hits
198
-
199
- def _check_against_additional_sanctions(self, address: str) -> List[Dict]:
200
- hits = []
201
- lower_addr = address.lower()
202
- for entry in self.additional_datasets:
203
- addr = None
204
- if "address" in entry:
205
- addr = entry["address"]
206
- elif "properties" in entry and "address" in entry["properties"]:
207
- addr = entry["properties"]["address"]
208
- elif "addresses" in entry and isinstance(entry["addresses"], list):
209
- for a in entry["addresses"]:
210
- if a.lower() == lower_addr:
211
- addr = a
212
- break
213
-
214
- if addr and addr.lower() == lower_addr:
215
- hits.append(entry)
216
- return hits
217
-
218
264
  def _analyze_transactions(
219
265
  self, txs: List[Dict], wallet_addr: str
220
266
  ) -> Dict[str, Any]:
@@ -1,6 +1,7 @@
1
1
  name: pdf_form_filler
2
2
  version: 0.1.0
3
3
  description: Fills PDF forms based on natural language instructions.
4
+ short_description: "Fills PDF forms from natural language instructions using Claude."
4
5
  issuer:
5
6
  name: Ross Peili
6
7
  email: vpeilivanidis@gmail.com
@@ -1,6 +1,7 @@
1
1
  name: "optimization/prompt_rewriter"
2
2
  version: "0.1.0"
3
3
  description: "A powerful middleware skill that aggressively compresses massive, bloated prompts into fewer tokens while retaining 100% of the semantic meaning and instructions."
4
+ short_description: "Compresses bloated prompts into fewer tokens without losing meaning."
4
5
  issuer:
5
6
  name: Ross Peili
6
7
  email: vpeilivanidis@gmail.com
@@ -0,0 +1,299 @@
1
+ import argparse
2
+ import yaml
3
+ from pathlib import Path
4
+ from typing import List, Dict, Any, Optional
5
+
6
+ from skillware.core.loader import SkillLoader
7
+
8
+ TABLE_STYLE = "bold #C7CEEA" # lavender - headers
9
+ CATEGORY_STYLE = "bold #FFDAC1" # peach - category column
10
+ ID_STYLE = "#B5EAD7" # mint - skill ID column
11
+ BORDER_STYLE = "#C7CEEA" # lavender - table border
12
+
13
+ SPLASH_STYLE = "#C7CEEA" # lavender - skillware splash color
14
+ MENU_STYLE = "#FFDAC1" # peach - menu category
15
+
16
+
17
+ def _get_skill_roots(skills_root_override: Optional[Path] = None) -> List[Path]:
18
+ """Return the list of roots to search for skills, mirrors SkillLoader resolution order."""
19
+ if skills_root_override is not None:
20
+ if skills_root_override.exists():
21
+ return [skills_root_override]
22
+ return []
23
+
24
+ roots = []
25
+ seen = set()
26
+
27
+ for root in (
28
+ SkillLoader._env_skill_roots()
29
+ + SkillLoader._cwd_skill_roots()
30
+ + [SkillLoader._bundled_skills_root()]
31
+ ):
32
+ resolved = root.resolve()
33
+ if resolved not in seen and resolved.exists():
34
+ seen.add(resolved)
35
+ roots.append(resolved)
36
+
37
+ return roots
38
+
39
+
40
+ def _short_description(data: Dict[str, Any], max_len: int = 80) -> str:
41
+ """Return short_description if present, else first sentence of description truncated."""
42
+ short = data.get("short_description", "").strip()
43
+ if short:
44
+ return short[:max_len] + ("…" if len(short) > max_len else "")
45
+
46
+ desc = data.get("description", "").strip()
47
+
48
+ seps = [".", "!", "?"]
49
+
50
+ for sep in seps:
51
+ idx = desc.find(sep)
52
+ if idx != -1:
53
+ desc = desc[: idx + 1]
54
+ break
55
+
56
+ return desc[:max_len] + ("…" if len(desc) > max_len else "")
57
+
58
+
59
+ def _discover_skills(
60
+ skills_root_override: Optional[Path] = None,
61
+ ) -> List[Dict[str, Any]]:
62
+ """Walk all skill roots and return a list of dicts with each skill's metadata."""
63
+ roots = _get_skill_roots(skills_root_override)
64
+
65
+ skills = []
66
+ seen_ids = set()
67
+
68
+ for root in roots:
69
+ for manifest_path in root.glob("*/*/manifest.yaml"):
70
+
71
+ if not SkillLoader._is_skill_dir(manifest_path.parent):
72
+ continue
73
+
74
+ with open(manifest_path, encoding="utf-8") as f:
75
+ data = yaml.safe_load(f) or {}
76
+
77
+ skill_id = f"{manifest_path.parent.parent.name}/{manifest_path.parent.name}"
78
+
79
+ # skip duplicates found in multiple roots
80
+ if skill_id in seen_ids:
81
+ continue
82
+ seen_ids.add(skill_id)
83
+
84
+ issuer = data.get("issuer") or {}
85
+
86
+ skills.append(
87
+ {
88
+ "id": skill_id,
89
+ "category": manifest_path.parent.parent.name,
90
+ "name": manifest_path.parent.name,
91
+ "version": data.get("version", "?").strip(),
92
+ "description": _short_description(data),
93
+ "requirements": ", ".join(data.get("requirements") or []).strip(),
94
+ "issuer": issuer.get("github") or issuer.get("name") or "",
95
+ }
96
+ )
97
+
98
+ return skills
99
+
100
+
101
+ def cmd_list(
102
+ skills_root_override: Optional[Path] = None,
103
+ category_filter: Optional[str] = None,
104
+ issuer_filter: Optional[str] = None,
105
+ console=None,
106
+ ) -> None:
107
+ """Print a formatted table of all available skills."""
108
+ try:
109
+ from rich.table import Table
110
+ from rich.console import Console
111
+ from rich import box
112
+ except ImportError:
113
+ raise SystemExit(
114
+ "rich is required for the CLI. Install it with: pip install 'skillware[cli]'"
115
+ )
116
+
117
+ if console is None:
118
+ console = Console()
119
+
120
+ skills = _discover_skills(skills_root_override)
121
+
122
+ if category_filter:
123
+ skills = [s for s in skills if s["category"] == category_filter]
124
+
125
+ if issuer_filter:
126
+ skills = [s for s in skills if s["issuer"] == issuer_filter]
127
+
128
+ if not skills:
129
+ console.print("No skills found.")
130
+ return
131
+
132
+ table = Table(
133
+ box=box.SIMPLE_HEAVY,
134
+ border_style=BORDER_STYLE,
135
+ header_style=TABLE_STYLE,
136
+ expand=True,
137
+ )
138
+
139
+ table.add_column("ID", style=ID_STYLE, no_wrap=True, ratio=2)
140
+ table.add_column("VERSION", style="dim", no_wrap=True, ratio=1)
141
+ table.add_column("CATEGORY", style=CATEGORY_STYLE, no_wrap=True, ratio=1)
142
+ table.add_column("ISSUER", style="dim", no_wrap=True, ratio=1)
143
+ table.add_column("DESCRIPTION", ratio=3)
144
+ table.add_column("REQUIREMENTS", style="dim", ratio=2)
145
+
146
+ for skill in skills:
147
+ table.add_row(
148
+ skill["id"],
149
+ skill["version"],
150
+ skill["category"],
151
+ skill["issuer"],
152
+ skill["description"],
153
+ skill["requirements"],
154
+ )
155
+
156
+ console.print(table)
157
+
158
+
159
+ def _print_menu(console, menu) -> None:
160
+ for num, name, desc in menu:
161
+ console.print(f" [{num}] {name:<20}— {desc}", style=MENU_STYLE)
162
+
163
+ console.print()
164
+
165
+
166
+ def cmd_interactive(console=None, parser=None) -> None:
167
+ """Launch ASCII splash screen and interactive menu."""
168
+ try:
169
+ from rich.console import Console
170
+ from rich.text import Text
171
+ except ImportError:
172
+ raise SystemExit(
173
+ "rich is required for the CLI. Install it with: pip install 'skillware[cli]'"
174
+ )
175
+
176
+ import importlib.metadata
177
+
178
+ if console is None:
179
+ console = Console()
180
+
181
+ try:
182
+ version = importlib.metadata.version("skillware")
183
+ except importlib.metadata.PackageNotFoundError:
184
+ version = "dev"
185
+
186
+ splash = r"""
187
+ ███████╗██╗ ██╗██╗██╗ ██╗ ██╗ █████╗ ██████╗ ███████╗
188
+ ██╔════╝██║ ██╔╝██║██║ ██║ ██║██╔══██╗██╔══██╗██╔════╝
189
+ ███████╗█████╔╝ ██║██║ ██║ █╗ ██║███████║██████╔╝█████╗
190
+ ╚════██║██╔═██╗ ██║██║ ██║███╗██║██╔══██║██╔══██╗██╔══╝
191
+ ███████║██║ ██╗██║███████╗╚███╔███╔╝██║ ██║██║ ██║███████╗
192
+ ╚══════╝╚═╝ ╚═╝╚═╝╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝"""
193
+
194
+ console.print(Text(splash, style=SPLASH_STYLE))
195
+ console.print(
196
+ Text(
197
+ f" Skill Management Framework — v{version}",
198
+ style=f"dim {SPLASH_STYLE}",
199
+ )
200
+ )
201
+
202
+ console.print(
203
+ Text(
204
+ " https://skillware.site · https://github.com/arpahls/skillware\n",
205
+ style=f"dim {SPLASH_STYLE}",
206
+ )
207
+ )
208
+
209
+ menu = [
210
+ ("1", "list", "discover and display all locally installed skills"),
211
+ ("2", "paths (soon, #81)", "show and repair skill directory resolution paths"),
212
+ ("3", "test (soon, #83)", "run test_skill.py for one or all skills"),
213
+ ("4", "help", "usage guide for any command"),
214
+ ]
215
+
216
+ commands = {
217
+ "1": "list",
218
+ "list": "list",
219
+ "2": "paths",
220
+ "paths": "paths",
221
+ "3": "test",
222
+ "test": "test",
223
+ "4": "help",
224
+ "help": "help",
225
+ }
226
+
227
+ _print_menu(console, menu)
228
+
229
+ while True:
230
+ try:
231
+ choice = input(" > ").strip().lower()
232
+ except (KeyboardInterrupt, EOFError):
233
+ console.print("\n Bye.", style="dim")
234
+ return
235
+
236
+ if choice == "q":
237
+ console.print(" Bye.", style="dim")
238
+ return
239
+
240
+ command = commands.get(choice)
241
+
242
+ if command == "list":
243
+ cmd_list(console=console)
244
+ elif command in ("paths", "test"):
245
+ console.print(
246
+ f" '{command}' is not yet implemented. Coming in a future release.",
247
+ style="dim",
248
+ )
249
+ elif command == "help":
250
+ if parser:
251
+ parser.print_help()
252
+ else:
253
+ console.print(
254
+ " Run 'skillware --help' for usage information.", style="dim"
255
+ )
256
+ else:
257
+ console.print(f" Unknown command: '{choice}'", style="dim #FF9AA2")
258
+
259
+ console.print()
260
+ _print_menu(console, menu)
261
+
262
+
263
+ def main() -> None:
264
+ """CLI entry point."""
265
+ parser = argparse.ArgumentParser(prog="skillware")
266
+ subparsers = parser.add_subparsers(dest="command")
267
+
268
+ list_parser = subparsers.add_parser("list", help="List all available skills.")
269
+ list_parser.add_argument(
270
+ "--skills-root",
271
+ type=Path,
272
+ default=None,
273
+ help="Override the skills directory path.",
274
+ )
275
+ list_parser.add_argument(
276
+ "--category",
277
+ default=None,
278
+ help="Filter skills by category.",
279
+ )
280
+ list_parser.add_argument(
281
+ "--issuer",
282
+ default=None,
283
+ help="Filter skills by issuer GitHub handle or name.",
284
+ )
285
+
286
+ args = parser.parse_args()
287
+
288
+ if args.command == "list":
289
+ cmd_list(
290
+ skills_root_override=args.skills_root,
291
+ category_filter=args.category,
292
+ issuer_filter=args.issuer,
293
+ )
294
+ else:
295
+ cmd_interactive(parser=parser)
296
+
297
+
298
+ if __name__ == "__main__":
299
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skillware
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: A framework for modular, self-contained AI skills.
5
5
  Author-email: ARPA Hellenic Logical Systems <skillware-os@arpacorp.net>
6
6
  License: MIT License
@@ -81,6 +81,7 @@ Dynamic: license-file
81
81
  <a href="#architecture">Architecture</a> •
82
82
  <a href="#quick-start">Quick Start</a> •
83
83
  <a href="#documentation">Documentation</a> •
84
+ <a href="CHANGELOG.md">Changelog</a> •
84
85
  <a href="#contributing">Contributing</a> •
85
86
  <a href="#comparison">Comparison</a> •
86
87
  <a href="#contact">Contact</a>
@@ -163,7 +164,7 @@ pip install "skillware[cli]"
163
164
  skillware list
164
165
  ```
165
166
 
166
- This prints a table of all locally available skills and confirms the install and path resolution are working.
167
+ 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.
167
168
 
168
169
  ### 3. Configuration
169
170
 
@@ -240,6 +241,7 @@ For other providers and shared integration patterns, see the [usage guides index
240
241
  | Topic | Link |
241
242
  | :--- | :--- |
242
243
  | Introduction | [docs/introduction.md](docs/introduction.md) |
244
+ | Changelog | [CHANGELOG.md](CHANGELOG.md) |
243
245
  | Testing | [docs/TESTING.md](docs/TESTING.md) |
244
246
  | Skill library | [docs/skills/README.md](docs/skills/README.md) |
245
247
  | Usage guides | [index](docs/usage/README.md) — [Gemini](docs/usage/gemini.md) · [Claude](docs/usage/claude.md) · [OpenAI](docs/usage/openai.md) · [DeepSeek](docs/usage/deepseek.md) · [Ollama](docs/usage/ollama.md); [agent loops](docs/usage/agent_loops.md); [API keys](docs/usage/api_keys.md) |
@@ -1,4 +1,9 @@
1
- from skillware.cli import _discover_skills, cmd_list
1
+ from skillware.cli import (
2
+ _discover_skills,
3
+ cmd_list,
4
+ cmd_interactive,
5
+ _short_description,
6
+ )
2
7
 
3
8
 
4
9
  def test_discover_skills_returns_skills(tmp_path):
@@ -138,3 +143,79 @@ def test_cmd_list_filter_by_category(tmp_path):
138
143
  output = buf.getvalue()
139
144
  assert "office" in output
140
145
  assert "finance" not in output
146
+
147
+
148
+ def test_short_description_uses_short_description_field():
149
+ """short_description field takes priority over description."""
150
+ data = {
151
+ "short_description": "Short one.",
152
+ "description": "This is a much longer description that should not appear.",
153
+ }
154
+ assert _short_description(data) == "Short one."
155
+
156
+
157
+ def test_short_description_truncates_at_80_chars():
158
+ """short_description longer than 80 chars should be truncated with …"""
159
+ data = {"short_description": "A" * 90}
160
+ result = _short_description(data)
161
+ assert len(result) == 81 # 80 + "…"
162
+ assert result.endswith("…")
163
+
164
+
165
+ def test_short_description_falls_back_to_first_sentence():
166
+ """Without short_description, use first sentence of description."""
167
+ data = {"description": "First sentence. Second sentence follows."}
168
+ assert _short_description(data) == "First sentence."
169
+
170
+
171
+ def test_short_description_empty_manifest():
172
+ """Empty manifest should return empty string."""
173
+ assert _short_description({}) == ""
174
+
175
+
176
+ def test_cmd_interactive_exits_on_q(monkeypatch):
177
+ """Entering q should exit cleanly."""
178
+ import io
179
+ from rich.console import Console
180
+
181
+ monkeypatch.setattr("builtins.input", lambda _: "q")
182
+ buf = io.StringIO()
183
+ cmd_interactive(console=Console(file=buf, force_terminal=False))
184
+ assert "Bye" in buf.getvalue()
185
+
186
+
187
+ def test_cmd_interactive_unknown_command(monkeypatch):
188
+ """Unknown command should print error then exit on q."""
189
+ import io
190
+ from rich.console import Console
191
+
192
+ responses = iter(["unknown_cmd", "q"])
193
+ monkeypatch.setattr("builtins.input", lambda _: next(responses))
194
+ buf = io.StringIO()
195
+ cmd_interactive(console=Console(file=buf, force_terminal=False))
196
+ assert "Unknown command" in buf.getvalue()
197
+
198
+
199
+ def test_cmd_interactive_list_dispatch(tmp_path, monkeypatch):
200
+ """Entering 1 or list should dispatch to cmd_list."""
201
+ import io
202
+ from rich.console import Console
203
+
204
+ skill_dir = tmp_path / "office" / "test_skill"
205
+ skill_dir.mkdir(parents=True)
206
+ (skill_dir / "skill.py").touch()
207
+ (skill_dir / "manifest.yaml").write_text(
208
+ "name: test_skill\nversion: 0.1.0\ndescription: Test.\n"
209
+ "short_description: Test skill.\n"
210
+ )
211
+
212
+ responses = iter(["1", "q"])
213
+ monkeypatch.setattr("builtins.input", lambda _: next(responses))
214
+ monkeypatch.chdir(tmp_path)
215
+
216
+ buf = io.StringIO()
217
+ console = Console(file=buf, force_terminal=False)
218
+ cmd_interactive(console=console)
219
+
220
+ output = buf.getvalue()
221
+ assert "test_skill" in output
@@ -1,162 +0,0 @@
1
- import argparse
2
- import yaml
3
- from pathlib import Path
4
- from typing import List, Dict, Any, Optional
5
-
6
- from skillware.core.loader import SkillLoader
7
-
8
-
9
- def _get_skill_roots(skills_root_override: Optional[Path] = None) -> List[Path]:
10
- """Return the list of roots to search for skills, mirrors SkillLoader resolution order."""
11
- if skills_root_override is not None:
12
- if skills_root_override.exists():
13
- return [skills_root_override]
14
- return []
15
-
16
- roots = []
17
- seen = set()
18
-
19
- for root in (
20
- SkillLoader._env_skill_roots()
21
- + SkillLoader._cwd_skill_roots()
22
- + [SkillLoader._bundled_skills_root()]
23
- ):
24
- resolved = root.resolve()
25
- if resolved not in seen and resolved.exists():
26
- seen.add(resolved)
27
- roots.append(resolved)
28
-
29
- return roots
30
-
31
-
32
- def _discover_skills(
33
- skills_root_override: Optional[Path] = None,
34
- ) -> List[Dict[str, Any]]:
35
- """Walk all skill roots and return a list of dicts with each skill's metadata."""
36
- roots = _get_skill_roots(skills_root_override)
37
-
38
- skills = []
39
- seen_ids = set()
40
-
41
- for root in roots:
42
- for manifest_path in root.glob("*/*/manifest.yaml"):
43
-
44
- if not SkillLoader._is_skill_dir(manifest_path.parent):
45
- continue
46
-
47
- with open(manifest_path, encoding="utf-8") as f:
48
- data = yaml.safe_load(f) or {}
49
-
50
- skill_id = f"{manifest_path.parent.parent.name}/{manifest_path.parent.name}"
51
-
52
- # skip duplicates found in multiple roots
53
- if skill_id in seen_ids:
54
- continue
55
- seen_ids.add(skill_id)
56
-
57
- issuer = data.get("issuer") or {}
58
-
59
- skills.append(
60
- {
61
- "id": skill_id,
62
- "category": manifest_path.parent.parent.name,
63
- "name": manifest_path.parent.name,
64
- "version": data.get("version", "?").strip(),
65
- "description": data.get("description", "").strip(),
66
- "requirements": ", ".join(data.get("requirements") or []).strip(),
67
- "issuer": issuer.get("github") or issuer.get("name") or "",
68
- }
69
- )
70
-
71
- return skills
72
-
73
-
74
- def cmd_list(
75
- skills_root_override: Optional[Path] = None,
76
- category_filter: Optional[str] = None,
77
- issuer_filter: Optional[str] = None,
78
- console=None,
79
- ) -> None:
80
- """Print a formatted table of all available skills."""
81
- try:
82
- from rich.table import Table
83
- from rich.console import Console
84
- except ImportError:
85
- raise SystemExit(
86
- "rich is required for the CLI. Install it with: pip install 'skillware[cli]'"
87
- )
88
-
89
- if console is None:
90
- console = Console()
91
-
92
- skills = _discover_skills(skills_root_override)
93
-
94
- if category_filter:
95
- skills = [s for s in skills if s["category"] == category_filter]
96
-
97
- if issuer_filter:
98
- skills = [s for s in skills if s["issuer"] == issuer_filter]
99
-
100
- if not skills:
101
- console.print("No skills found.")
102
- return
103
-
104
- table = Table()
105
-
106
- table.add_column("ID")
107
- table.add_column("VERSION")
108
- table.add_column("CATEGORY")
109
- table.add_column("ISSUER")
110
- table.add_column("DESCRIPTION")
111
- table.add_column("REQUIREMENTS")
112
-
113
- for skill in skills:
114
- table.add_row(
115
- skill["id"],
116
- skill["version"],
117
- skill["category"],
118
- skill["issuer"],
119
- skill["description"],
120
- skill["requirements"],
121
- )
122
-
123
- console.print(table)
124
-
125
-
126
- def main() -> None:
127
- """CLI entry point."""
128
- parser = argparse.ArgumentParser(prog="skillware")
129
- subparsers = parser.add_subparsers(dest="command")
130
-
131
- list_parser = subparsers.add_parser("list", help="List all available skills.")
132
- list_parser.add_argument(
133
- "--skills-root",
134
- type=Path,
135
- default=None,
136
- help="Override the skills directory path.",
137
- )
138
- list_parser.add_argument(
139
- "--category",
140
- default=None,
141
- help="Filter skills by category.",
142
- )
143
- list_parser.add_argument(
144
- "--issuer",
145
- default=None,
146
- help="Filter skills by issuer GitHub handle or name.",
147
- )
148
-
149
- args = parser.parse_args()
150
-
151
- if args.command == "list":
152
- cmd_list(
153
- skills_root_override=args.skills_root,
154
- category_filter=args.category,
155
- issuer_filter=args.issuer,
156
- )
157
- else:
158
- parser.print_help()
159
-
160
-
161
- if __name__ == "__main__":
162
- main()
File without changes
File without changes
File without changes
File without changes