fix: Windows MCP PATHEXT resolution + python3 -> python in cross-platform skills (#84429)

Two Windows agent-loop friction fixes:

1. tools/mcp_tool.py (#56536): shutil.which(cmd, path=env_path) reads
   executable extensions from the PARENT process PATHEXT, not the MCP
   subprocess env — a stdio MCP config supplying both PATH and PATHEXT
   could fail to resolve a command its own env can locate, and startup
   then got a bare command name. On Windows, when the first which() call
   misses and the config env carries PATHEXT (any key casing), retry the
   resolution with the config's PATHEXT temporarily applied.

2. skills/ + optional-skills/ (#50606): 42 SKILL.md files that declare
   platforms: [.., windows] used python3 in their command examples.
   python3 does not exist on native Windows (the toolchain probe in the
   system prompt reports python3=missing), so every copy-pasted example
   burned a failed agent turn before self-correction. Replaced the
   command word python3 -> python (python3-config / python3.x version
   strings untouched). python is the spelling that exists in every
   Hermes-managed environment (Windows native, uv-managed venvs on all
   three OSes); agents on POSIX hosts additionally see the probed
   toolchain line and adapt either way.
This commit is contained in:
Teknium 2026-08-12 02:43:28 -07:00 committed by GitHub
parent e1caf88c6c
commit 4a2198bf51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 394 additions and 373 deletions

View File

@ -66,40 +66,40 @@ Helper script path: `~/.hermes/skills/blockchain/evm/scripts/evm_client.py`
SCRIPT=~/.hermes/skills/blockchain/evm/scripts/evm_client.py
# Network & prices
python3 $SCRIPT stats # Ethereum stats
python3 $SCRIPT stats --chain arbitrum # Arbitrum stats
python3 $SCRIPT compare # Gas + prices ALL 8 chains
python $SCRIPT stats # Ethereum stats
python $SCRIPT stats --chain arbitrum # Arbitrum stats
python $SCRIPT compare # Gas + prices ALL 8 chains
# Wallet
python3 $SCRIPT wallet 0xd8dA...96045 # Portfolio (ETH + ERC-20)
python3 $SCRIPT wallet 0xd8dA...96045 --chain bsc
python3 $SCRIPT multichain 0xd8dA...96045 # Same wallet on ALL chains
python $SCRIPT wallet 0xd8dA...96045 # Portfolio (ETH + ERC-20)
python $SCRIPT wallet 0xd8dA...96045 --chain bsc
python $SCRIPT multichain 0xd8dA...96045 # Same wallet on ALL chains
# Tokens & prices
python3 $SCRIPT price ETH
python3 $SCRIPT price 0xdAC1...1ec7 # By contract address
python3 $SCRIPT token 0xdAC1...1ec7 # ERC-20 metadata + market cap
python $SCRIPT price ETH
python $SCRIPT price 0xdAC1...1ec7 # By contract address
python $SCRIPT token 0xdAC1...1ec7 # ERC-20 metadata + market cap
# Transactions
python3 $SCRIPT tx 0x5c50...f060 # Transaction details
python3 $SCRIPT decode 0x5c50...f060 # Decode input data (4byte.directory)
python3 $SCRIPT activity 0xd8dA...96045 # Recent transactions
python $SCRIPT tx 0x5c50...f060 # Transaction details
python $SCRIPT decode 0x5c50...f060 # Decode input data (4byte.directory)
python $SCRIPT activity 0xd8dA...96045 # Recent transactions
# Gas
python3 $SCRIPT gas # Gas prices + cost estimates
python3 $SCRIPT gas --chain optimism
python $SCRIPT gas # Gas prices + cost estimates
python $SCRIPT gas --chain optimism
# Security
python3 $SCRIPT allowance 0xd8dA...96045 # Dangerous ERC-20 approvals
python3 $SCRIPT contract 0xdAC1...1ec7 # Contract inspection (proxy? standards?)
python $SCRIPT allowance 0xd8dA...96045 # Dangerous ERC-20 approvals
python $SCRIPT contract 0xdAC1...1ec7 # Contract inspection (proxy? standards?)
# ENS
python3 $SCRIPT ens vitalik.eth # Name -> address + profile
python3 $SCRIPT ens 0xd8dA...96045 # Address -> ENS name
python $SCRIPT ens vitalik.eth # Name -> address + profile
python $SCRIPT ens 0xd8dA...96045 # Address -> ENS name
# Whale detection
python3 $SCRIPT whale # Large transfers (last 20 blocks, >$10k)
python3 $SCRIPT whale --blocks 50 --min-usd 100000 --chain arbitrum
python $SCRIPT whale # Large transfers (last 20 blocks, >$10k)
python $SCRIPT whale --blocks 50 --min-usd 100000 --chain arbitrum
```
---
@ -108,67 +108,67 @@ python3 $SCRIPT whale --blocks 50 --min-usd 100000 --chain arbitrum
### 0. Setup Check
```bash
python3 --version # 3.8+ required
python3 ~/.hermes/skills/blockchain/evm/scripts/evm_client.py stats
python --version # 3.8+ required
python ~/.hermes/skills/blockchain/evm/scripts/evm_client.py stats
```
### 1. Wallet Portfolio
Native balance + known ERC-20 tokens, sorted by USD value.
```bash
python3 $SCRIPT wallet 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
python3 $SCRIPT wallet 0xd8dA... --chain bsc --no-prices # faster
python $SCRIPT wallet 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
python $SCRIPT wallet 0xd8dA... --chain bsc --no-prices # faster
```
### 2. Multi-Chain Scan
Scans all 8 chains simultaneously for the same address using threads.
```bash
python3 $SCRIPT multichain 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
python $SCRIPT multichain 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
```
Output: per-chain native balance + token holdings + grand total USD.
### 3. Compare (Gas + Prices)
All 8 chains queried in parallel. Shows cheapest/most expensive chain.
```bash
python3 $SCRIPT compare
python $SCRIPT compare
```
### 4. Transaction Details & Decode
```bash
python3 $SCRIPT tx 0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060
python3 $SCRIPT decode 0x5c504ed... # Shows human-readable function signature
python $SCRIPT tx 0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060
python $SCRIPT decode 0x5c504ed... # Shows human-readable function signature
```
Decode uses 4byte.directory to translate 0xa9059cbb -> transfer(address,uint256).
### 5. ENS Resolution
```bash
python3 $SCRIPT ens vitalik.eth # -> 0xd8dA... + avatar + social links
python3 $SCRIPT ens 0xd8dA...96045 # -> vitalik.eth
python $SCRIPT ens vitalik.eth # -> 0xd8dA... + avatar + social links
python $SCRIPT ens 0xd8dA...96045 # -> vitalik.eth
```
### 6. Allowance Checker (Security)
Checks ERC-20 approvals granted to known DEX/bridge contracts.
```bash
python3 $SCRIPT allowance 0xYourWallet
python $SCRIPT allowance 0xYourWallet
```
Flags UNLIMITED approvals as HIGH risk.
### 7. Contract Inspector
```bash
python3 $SCRIPT contract 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 # USDC (proxy)
python3 $SCRIPT contract 0xdAC17F958D2ee523a2206206994597C13D831ec7 # USDT (ERC-20)
python $SCRIPT contract 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 # USDC (proxy)
python $SCRIPT contract 0xdAC17F958D2ee523a2206206994597C13D831ec7 # USDT (ERC-20)
```
Detects: proxy (EIP-1967/EIP-1167), ERC-20, ERC-721, ERC-165. Shows bytecode size and implementation address for proxies.
### 8. Whale Detection
```bash
python3 $SCRIPT whale # ETH, last 20 blocks, >$10k
python3 $SCRIPT whale --blocks 50 --min-usd 50000 --chain bsc
python $SCRIPT whale # ETH, last 20 blocks, >$10k
python $SCRIPT whale --blocks 50 --min-usd 50000 --chain bsc
```
### 9. Gas Tracker
```bash
python3 $SCRIPT gas
python3 $SCRIPT gas --chain polygon
python $SCRIPT gas
python $SCRIPT gas --chain polygon
```
Shows gwei price + USD cost for: transfer, ERC-20 transfer, approve, swap, NFT mint, NFT transfer.
@ -204,8 +204,8 @@ Shows gwei price + USD cost for: transfer, ERC-20 transfer, approve, swap, NFT m
## Verification
```bash
# Should print current block, gas price, ETH price
python3 ~/.hermes/skills/blockchain/evm/scripts/evm_client.py stats
python ~/.hermes/skills/blockchain/evm/scripts/evm_client.py stats
# Should resolve vitalik.eth to 0xd8dA...
python3 ~/.hermes/skills/blockchain/evm/scripts/evm_client.py ens vitalik.eth
python ~/.hermes/skills/blockchain/evm/scripts/evm_client.py ens vitalik.eth
```

View File

@ -55,7 +55,7 @@ Helper script: `~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_clie
Invoke through the `terminal` tool:
```bash
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py <command> [args]
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py <command> [args]
```
Add `--json` to any command for machine-readable output.
@ -89,12 +89,12 @@ optional when `HYPERLIQUID_USER_ADDRESS` is set in `${HERMES_HOME:-~/.hermes}/.e
### 1. Discover DEXs and Markets
```bash
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py dexs
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py dexs
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
markets --limit 15 --sort volume
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
spots --limit 15
```
@ -105,10 +105,10 @@ python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
### 2. Pull Historical Market Data
```bash
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
candles BTC --interval 1h --hours 72 --limit 48
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
funding BTC --hours 168 --limit 30
```
@ -118,7 +118,7 @@ Time-range endpoints paginate. For larger windows, repeat with a later
### 3. Inspect Live Order Book
```bash
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
l2 BTC --levels 10
```
@ -128,10 +128,10 @@ impact of a large order.
### 4. Review an Account
```bash
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
state 0xabc...
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
spot-balances
```
@ -142,20 +142,20 @@ withdrawable?".
### 5. Review Fills and Orders
```bash
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
fills 0xabc... --hours 72 --limit 25
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
orders --limit 25
```
### 6. Generate a Trade Review
```bash
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
review 0xabc... --hours 72 --fills 50
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
review --coin BTC --hours 168
```
@ -171,10 +171,10 @@ from outcome quality.
### 7. Export a Reusable Dataset
```bash
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
export BTC --interval 1h --hours 168 --output ./btc-1h-7d.json
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
export BTC --interval 15m --hours 72 --end-time-ms 1760000000000
```
@ -204,7 +204,7 @@ normalized candle rows, normalized funding rows, summary stats. Use
## Verification
```bash
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
python ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
markets --limit 5
```

View File

@ -52,14 +52,14 @@ Override: export SOLANA_RPC_URL=https://your-private-rpc.com
Helper script path: ~/.hermes/skills/blockchain/solana/scripts/solana_client.py
```
python3 solana_client.py wallet <address> [--limit N] [--all] [--no-prices]
python3 solana_client.py tx <signature>
python3 solana_client.py token <mint_address>
python3 solana_client.py activity <address> [--limit N]
python3 solana_client.py nft <address>
python3 solana_client.py whales [--min-sol N]
python3 solana_client.py stats
python3 solana_client.py price <mint_or_symbol>
python solana_client.py wallet <address> [--limit N] [--all] [--no-prices]
python solana_client.py tx <signature>
python solana_client.py token <mint_address>
python solana_client.py activity <address> [--limit N]
python solana_client.py nft <address>
python solana_client.py whales [--min-sol N]
python solana_client.py stats
python solana_client.py price <mint_or_symbol>
```
---
@ -69,13 +69,13 @@ python3 solana_client.py price <mint_or_symbol>
### 0. Setup Check
```bash
python3 --version
python --version
# Optional: set a private RPC for better rate limits
export SOLANA_RPC_URL="https://api.mainnet-beta.solana.com"
# Confirm connectivity
python3 ~/.hermes/skills/blockchain/solana/scripts/solana_client.py stats
python ~/.hermes/skills/blockchain/solana/scripts/solana_client.py stats
```
### 1. Wallet Portfolio
@ -85,7 +85,7 @@ portfolio total. Tokens sorted by value, dust filtered, known tokens
labeled by name (BONK, JUP, USDC, etc.).
```bash
python3 ~/.hermes/skills/blockchain/solana/scripts/solana_client.py \
python ~/.hermes/skills/blockchain/solana/scripts/solana_client.py \
wallet 9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM
```
@ -103,7 +103,7 @@ Inspect a full transaction by its base58 signature. Shows balance changes
in both SOL and USD.
```bash
python3 ~/.hermes/skills/blockchain/solana/scripts/solana_client.py \
python ~/.hermes/skills/blockchain/solana/scripts/solana_client.py \
tx 5j7s8K...your_signature_here
```
@ -116,7 +116,7 @@ Get SPL token metadata, current price, market cap, supply, decimals,
mint/freeze authorities, and top 5 holders.
```bash
python3 ~/.hermes/skills/blockchain/solana/scripts/solana_client.py \
python ~/.hermes/skills/blockchain/solana/scripts/solana_client.py \
token DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263
```
@ -128,7 +128,7 @@ holders with percentages.
List recent transactions for an address (default: last 10, max: 25).
```bash
python3 ~/.hermes/skills/blockchain/solana/scripts/solana_client.py \
python ~/.hermes/skills/blockchain/solana/scripts/solana_client.py \
activity 9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM --limit 25
```
@ -137,7 +137,7 @@ python3 ~/.hermes/skills/blockchain/solana/scripts/solana_client.py \
List NFTs owned by a wallet (heuristic: SPL tokens with amount=1, decimals=0).
```bash
python3 ~/.hermes/skills/blockchain/solana/scripts/solana_client.py \
python ~/.hermes/skills/blockchain/solana/scripts/solana_client.py \
nft 9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM
```
@ -148,7 +148,7 @@ Note: Compressed NFTs (cNFTs) are not detected by this heuristic.
Scan the most recent block for large SOL transfers with USD values.
```bash
python3 ~/.hermes/skills/blockchain/solana/scripts/solana_client.py \
python ~/.hermes/skills/blockchain/solana/scripts/solana_client.py \
whales --min-sol 500
```
@ -160,7 +160,7 @@ Live Solana network health: current slot, epoch, TPS, supply, validator
version, SOL price, and market cap.
```bash
python3 ~/.hermes/skills/blockchain/solana/scripts/solana_client.py stats
python ~/.hermes/skills/blockchain/solana/scripts/solana_client.py stats
```
### 8. Price Lookup
@ -168,10 +168,10 @@ python3 ~/.hermes/skills/blockchain/solana/scripts/solana_client.py stats
Quick price check for any token by mint address or known symbol.
```bash
python3 ~/.hermes/skills/blockchain/solana/scripts/solana_client.py price BONK
python3 ~/.hermes/skills/blockchain/solana/scripts/solana_client.py price JUP
python3 ~/.hermes/skills/blockchain/solana/scripts/solana_client.py price SOL
python3 ~/.hermes/skills/blockchain/solana/scripts/solana_client.py price DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263
python ~/.hermes/skills/blockchain/solana/scripts/solana_client.py price BONK
python ~/.hermes/skills/blockchain/solana/scripts/solana_client.py price JUP
python ~/.hermes/skills/blockchain/solana/scripts/solana_client.py price SOL
python ~/.hermes/skills/blockchain/solana/scripts/solana_client.py price DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263
```
Known symbols: SOL, USDC, USDT, BONK, JUP, WETH, JTO, mSOL, stSOL,
@ -204,5 +204,5 @@ PYTH, HNT, RNDR, WEN, W, TNSR, DRIFT, bSOL, JLP, WIF, MEW, BOME, PENGU.
```bash
# Should print current Solana slot, TPS, and SOL price
python3 ~/.hermes/skills/blockchain/solana/scripts/solana_client.py stats
python ~/.hermes/skills/blockchain/solana/scripts/solana_client.py stats
```

View File

@ -300,7 +300,7 @@ mkdir -p .diagrams/sn2-mechanism
# ...write .diagrams/sn2-mechanism/index.html...
# Serve on loopback only, free port
cd .diagrams && python3 -c "
cd .diagrams && python -c "
import http.server, socketserver
with socketserver.TCPServer(('127.0.0.1', 0), http.server.SimpleHTTPRequestHandler) as s:
print(f'Serving at http://127.0.0.1:{s.server_address[1]}/')

View File

@ -64,11 +64,11 @@ is a live edit, not saved script:
```bash
BASE=http://localhost:7236
TOKEN=$(python3 -c "import json;print(json.load(open('$HOME/.config/tldraw/server.json'))['token'])")
TOKEN=$(python -c "import json;print(json.load(open('$HOME/.config/tldraw/server.json'))['token'])")
# find the focused document id
DOC=$(curl -s "$BASE/api/search" -X POST -H 'content-type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"code":"return (await api.getFocusedDoc()).id"}' | python3 -c "import sys,json;print(json.load(sys.stdin)['result'])")
-d '{"code":"return (await api.getFocusedDoc()).id"}' | python -c "import sys,json;print(json.load(sys.stdin)['result'])")
# run code with the live `editor` + `helpers` in scope
curl -s "$BASE/api/doc/$DOC/exec" -X POST -H 'content-type: application/json' \
-H "Authorization: Bearer $TOKEN" \

View File

@ -72,7 +72,7 @@ session via the Jupyter REST API:
```
curl -s -X POST http://127.0.0.1:8888/api/sessions \
-H "Content-Type: application/json" \
-d '{"path":"scratch.ipynb","type":"notebook","name":"scratch.ipynb","kernel":{"name":"python3"}}'
-d '{"path":"scratch.ipynb","type":"notebook","name":"scratch.ipynb","kernel":{"name":"python"}}'
```
## Core Workflow

View File

@ -79,7 +79,7 @@ curl -sI http://127.0.0.1:8000/ | head -1
# expect HTTP/1.x 200 (or any non-connection-refused response)
```
If nothing is listening yet, start it first (e.g. `python3 -m http.server 8000 --bind 127.0.0.1`). Pinggy will happily return a URL pointed at nothing — the user will see 502 until the origin comes up.
If nothing is listening yet, start it first (e.g. `python -m http.server 8000 --bind 127.0.0.1`). Pinggy will happily return a URL pointed at nothing — the user will see 502 until the origin comes up.
### 2. Launch the tunnel as a background process
@ -203,7 +203,7 @@ class H(http.server.BaseHTTPRequestHandler):
def log_message(self,*a,**k): pass
http.server.HTTPServer(("127.0.0.1", 18080), H).serve_forever()
PY
nohup python3 /tmp/webhook-server.py >/tmp/webhook-server.log 2>&1 &
nohup python /tmp/webhook-server.py >/tmp/webhook-server.log 2>&1 &
echo $! >/tmp/webhook-server.pid
# 2. Tunnel — bearer-token-gate so randos can't pollute the capture log
@ -228,7 +228,7 @@ Use when a remote MCP client (Claude Desktop on another machine, a teammate's ed
```bash
# 1. Start the MCP server in HTTP mode (example: a FastMCP server on port 8765)
nohup python3 my_mcp_server.py --transport http --port 8765 \
nohup python my_mcp_server.py --transport http --port 8765 \
>/tmp/mcp-server.log 2>&1 &
echo $! >/tmp/mcp-server.pid
@ -289,7 +289,7 @@ ssh -p 443 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
```bash
# End-to-end: spin up a trivial origin, tunnel it, hit it, tear down
python3 -m http.server 18000 --bind 127.0.0.1 >/tmp/origin.log 2>&1 &
python -m http.server 18000 --bind 127.0.0.1 >/tmp/origin.log 2>&1 &
ORIGIN_PID=$!
nohup ssh -p 443 \

View File

@ -38,7 +38,7 @@ Invoke through the `terminal` tool. Once installed:
```
SCRIPT=~/.hermes/skills/finance/stocks/scripts/stocks_client.py
python3 $SCRIPT quote AAPL
python $SCRIPT quote AAPL
```
All output is JSON on stdout — pipe through `jq` if you want to slice it.
@ -46,12 +46,12 @@ All output is JSON on stdout — pipe through `jq` if you want to slice it.
## Quick Reference
```
python3 $SCRIPT quote AAPL
python3 $SCRIPT quote AAPL MSFT GOOGL TSLA
python3 $SCRIPT search "Tesla"
python3 $SCRIPT history NVDA --range 6mo
python3 $SCRIPT compare AAPL MSFT GOOGL
python3 $SCRIPT crypto BTC ETH SOL
python $SCRIPT quote AAPL
python $SCRIPT quote AAPL MSFT GOOGL TSLA
python $SCRIPT search "Tesla"
python $SCRIPT history NVDA --range 6mo
python $SCRIPT compare AAPL MSFT GOOGL
python $SCRIPT crypto BTC ETH SOL
```
## Commands
@ -89,7 +89,7 @@ Crypto prices. Pass `BTC` (the script appends `-USD` automatically).
## Verification
```
python3 ~/.hermes/skills/finance/stocks/scripts/stocks_client.py quote AAPL
python ~/.hermes/skills/finance/stocks/scripts/stocks_client.py quote AAPL
```
Returns a JSON object with `symbol: "AAPL"` and a numeric `price` field.

View File

@ -23,7 +23,7 @@ Play Pokemon games via headless emulation using the `pokemon-agent` package.
The repo is NousResearch/pokemon-agent on GitHub. Clone it, then
set up a Python 3.10+ virtual environment. Use uv (preferred for speed)
to create the venv and install the package in editable mode with the
pyboy extra. If uv is not available, fall back to python3 -m venv + pip.
pyboy extra. If uv is not available, fall back to python -m venv + pip.
If a checkout already exists (e.g. ~/pokemon-agent with a venv ready),
just cd there and source .venv/bin/activate instead of recloning.

View File

@ -12,7 +12,7 @@ metadata:
tags: [health, fitness, nutrition, gym, workout, diet, exercise]
category: health
prerequisites:
commands: [curl, python3]
commands: [curl, python]
required_environment_variables:
- name: USDA_API_KEY
prompt: "USDA FoodData Central API key (free)"
@ -110,9 +110,9 @@ Equipment:
```bash
# Search exercises by name
QUERY="$1"
ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$QUERY")
ENCODED=$(python -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$QUERY")
curl -s "https://wger.de/api/v2/exercise/search/?term=${ENCODED}&language=english&format=json" \
| python3 -c "
| python -c "
import json,sys
data=json.load(sys.stdin)
for s in data.get('suggestions',[])[:10]:
@ -125,7 +125,7 @@ for s in data.get('suggestions',[])[:10]:
# Get full details for a specific exercise
EXERCISE_ID="$1"
curl -s "https://wger.de/api/v2/exerciseinfo/${EXERCISE_ID}/?format=json" \
| python3 -c "
| python -c "
import json,sys,html,re
data=json.load(sys.stdin)
trans=[t for t in data.get('translations',[]) if t.get('language')==2]
@ -147,7 +147,7 @@ if imgs: print(f\"Image : {imgs[0].get('image','')}\")
# Combine filters as needed: ?muscles=4&equipment=1&language=2&status=2
FILTER="$1" # e.g. "muscles=4" or "category=11" or "equipment=3"
curl -s "https://wger.de/api/v2/exercise/?${FILTER}&language=2&status=2&limit=20&format=json" \
| python3 -c "
| python -c "
import json,sys
data=json.load(sys.stdin)
print(f'Found {data.get(\"count\",0)} exercises.')
@ -165,9 +165,9 @@ DEMO_KEY = 30 requests/hour. Free signup key = 1,000 requests/hour.
# Search foods by name
FOOD="$1"
API_KEY="${USDA_API_KEY:-DEMO_KEY}"
ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$FOOD")
ENCODED=$(python -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$FOOD")
curl -s "https://api.nal.usda.gov/fdc/v1/foods/search?api_key=${API_KEY}&query=${ENCODED}&pageSize=5&dataType=Foundation,SR%20Legacy" \
| python3 -c "
| python -c "
import json,sys
data=json.load(sys.stdin)
foods=data.get('foods',[])
@ -188,7 +188,7 @@ for f in foods:
FDC_ID="$1"
API_KEY="${USDA_API_KEY:-DEMO_KEY}"
curl -s "https://api.nal.usda.gov/fdc/v1/food/${FDC_ID}?api_key=${API_KEY}" \
| python3 -c "
| python -c "
import json,sys
d=json.load(sys.stdin)
print(f\"Food: {d.get('description','N/A')}\")
@ -206,11 +206,11 @@ for x in sorted(d.get('foodNutrients',[]),key=lambda x:x.get('nutrient',{}).get(
Use the helper scripts in `scripts/` for batch operations,
or run inline for single calculations:
- `python3 scripts/body_calc.py bmi <weight_kg> <height_cm>`
- `python3 scripts/body_calc.py tdee <weight_kg> <height_cm> <age> <M|F> <activity 1-5>`
- `python3 scripts/body_calc.py 1rm <weight> <reps>`
- `python3 scripts/body_calc.py macros <tdee_kcal> <cut|maintain|bulk>`
- `python3 scripts/body_calc.py bodyfat <M|F> <neck_cm> <waist_cm> [hip_cm] <height_cm>`
- `python scripts/body_calc.py bmi <weight_kg> <height_cm>`
- `python scripts/body_calc.py tdee <weight_kg> <height_cm> <age> <M|F> <activity 1-5>`
- `python scripts/body_calc.py 1rm <weight> <reps>`
- `python scripts/body_calc.py macros <tdee_kcal> <cut|maintain|bulk>`
- `python scripts/body_calc.py bodyfat <M|F> <neck_cm> <waist_cm> [hip_cm] <height_cm>`
See `references/FORMULAS.md` for the science behind each formula.
@ -249,4 +249,4 @@ After calculators: sanity-check outputs (e.g. TDEE should be 1500-3500 for most
| List muscles | wger | `GET /api/v2/muscle/` |
| Search foods | USDA | `GET /fdc/v1/foods/search?query=&dataType=Foundation,SR Legacy` |
| Food details | USDA | `GET /fdc/v1/food/{fdcId}` |
| BMI / TDEE / 1RM / macros | offline | `python3 scripts/body_calc.py` |
| BMI / TDEE / 1RM / macros | offline | `python scripts/body_calc.py` |

View File

@ -11,7 +11,7 @@ metadata:
homepage: https://gofastmcp.com
related_skills: [hermes-agent, mcporter]
prerequisites:
commands: [python3]
commands: [python]
---
# FastMCP

View File

@ -229,37 +229,37 @@ The helper script still supports category-level `--include` / `--exclude`, but t
Dry run with full discovery:
```bash
python3 ~/.hermes/skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py
python ~/.hermes/skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py
```
When using the terminal tool, prefer an absolute invocation pattern such as:
```json
{"command":"python3 /home/USER/.hermes/skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py","workdir":"/home/USER"}
{"command":"python /home/USER/.hermes/skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py","workdir":"/home/USER"}
```
Dry run with the user-data preset:
```bash
python3 ~/.hermes/skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py --preset user-data
python ~/.hermes/skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py --preset user-data
```
Execute a user-data migration:
```bash
python3 ~/.hermes/skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py --execute --preset user-data --skill-conflict skip
python ~/.hermes/skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py --execute --preset user-data --skill-conflict skip
```
Execute a full compatible migration:
```bash
python3 ~/.hermes/skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py --execute --preset full --migrate-secrets --skill-conflict skip
python ~/.hermes/skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py --execute --preset full --migrate-secrets --skill-conflict skip
```
Execute with workspace instructions included:
```bash
python3 ~/.hermes/skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py --execute --preset user-data --skill-conflict rename --workspace-target "/absolute/workspace/path"
python ~/.hermes/skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py --execute --preset user-data --skill-conflict rename --workspace-target "/absolute/workspace/path"
```
Do not use `$PWD` or the home directory as the workspace target by default. Ask for an explicit workspace path first.

View File

@ -154,33 +154,33 @@ Sign up at:
Then save credentials into Hermes:
```bash
python3 "$SCRIPT" save-twilio ACXXXXXXXXXXXXXXXXXXXXXXXXXXXX your_auth_token_here
python "$SCRIPT" save-twilio ACXXXXXXXXXXXXXXXXXXXXXXXXXXXX your_auth_token_here
```
Search for available numbers:
```bash
python3 "$SCRIPT" twilio-search --country US --area-code 702 --limit 5
python "$SCRIPT" twilio-search --country US --area-code 702 --limit 5
```
Buy and remember a number:
```bash
python3 "$SCRIPT" twilio-buy "+17025551234" --save-env
python "$SCRIPT" twilio-buy "+17025551234" --save-env
```
List owned numbers:
```bash
python3 "$SCRIPT" twilio-owned
python "$SCRIPT" twilio-owned
```
Set one of them as the default later:
```bash
python3 "$SCRIPT" twilio-set-default "+17025551234" --save-env
python "$SCRIPT" twilio-set-default "+17025551234" --save-env
# or
python3 "$SCRIPT" twilio-set-default PNXXXXXXXXXXXXXXXXXXXXXXXXXXXX --save-env
python "$SCRIPT" twilio-set-default PNXXXXXXXXXXXXXXXXXXXXXXXXXXXX --save-env
```
### Bland.ai — easiest outbound AI calling
@ -191,7 +191,7 @@ Sign up at:
Save config:
```bash
python3 "$SCRIPT" save-bland your_bland_api_key --voice mason
python "$SCRIPT" save-bland your_bland_api_key --voice mason
```
### Vapi — better conversational voice quality
@ -202,19 +202,19 @@ Sign up at:
Save the API key first:
```bash
python3 "$SCRIPT" save-vapi your_vapi_api_key
python "$SCRIPT" save-vapi your_vapi_api_key
```
Import your owned Twilio number into Vapi and persist the returned phone number ID:
```bash
python3 "$SCRIPT" vapi-import-twilio --save-env
python "$SCRIPT" vapi-import-twilio --save-env
```
If you already know the Vapi phone number ID, save it directly:
```bash
python3 "$SCRIPT" save-vapi your_vapi_api_key --phone-number-id vapi_phone_number_id_here
python "$SCRIPT" save-vapi your_vapi_api_key --phone-number-id vapi_phone_number_id_here
```
## Diagnose current state
@ -222,7 +222,7 @@ python3 "$SCRIPT" save-vapi your_vapi_api_key --phone-number-id vapi_phone_numbe
At any time, inspect what the skill already knows:
```bash
python3 "$SCRIPT" diagnose
python "$SCRIPT" diagnose
```
Use this first when resuming work in a later session.
@ -233,35 +233,35 @@ Use this first when resuming work in a later session.
1. Save Twilio credentials:
```bash
python3 "$SCRIPT" save-twilio AC... auth_token_here
python "$SCRIPT" save-twilio AC... auth_token_here
```
2. Search for a number:
```bash
python3 "$SCRIPT" twilio-search --country US --area-code 702 --limit 10
python "$SCRIPT" twilio-search --country US --area-code 702 --limit 10
```
3. Buy it and save it into `${HERMES_HOME:-~/.hermes}/.env` + state:
```bash
python3 "$SCRIPT" twilio-buy "+17025551234" --save-env
python "$SCRIPT" twilio-buy "+17025551234" --save-env
```
4. Next session, run:
```bash
python3 "$SCRIPT" diagnose
python "$SCRIPT" diagnose
```
This shows the remembered default number and inbox checkpoint state.
### B. Send a text from the agent number
```bash
python3 "$SCRIPT" twilio-send-sms "+15551230000" "Your deployment completed successfully."
python "$SCRIPT" twilio-send-sms "+15551230000" "Your deployment completed successfully."
```
With media:
```bash
python3 "$SCRIPT" twilio-send-sms "+15551230000" "Here is the chart." --media-url "https://example.com/chart.png"
python "$SCRIPT" twilio-send-sms "+15551230000" "Here is the chart." --media-url "https://example.com/chart.png"
```
### C. Check inbound texts later with no webhook server
@ -269,13 +269,13 @@ python3 "$SCRIPT" twilio-send-sms "+15551230000" "Here is the chart." --media-ur
Poll the inbox for the default Twilio number:
```bash
python3 "$SCRIPT" twilio-inbox --limit 20
python "$SCRIPT" twilio-inbox --limit 20
```
Only show messages that arrived after the last checkpoint, and advance the checkpoint when you're done reading:
```bash
python3 "$SCRIPT" twilio-inbox --since-last --mark-seen
python "$SCRIPT" twilio-inbox --since-last --mark-seen
```
This is the main answer to “how do I access messages the number receives next time the skill is loaded?”
@ -283,7 +283,7 @@ This is the main answer to “how do I access messages the number receives next
### D. Make a direct Twilio call with built-in TTS
```bash
python3 "$SCRIPT" twilio-call "+15551230000" --message "Hello! This is Hermes calling with your status update." --voice Polly.Joanna
python "$SCRIPT" twilio-call "+15551230000" --message "Hello! This is Hermes calling with your status update." --voice Polly.Joanna
```
### E. Call with a prerecorded / custom voice message
@ -298,7 +298,7 @@ Use this when:
Generate or host audio separately, then:
```bash
python3 "$SCRIPT" twilio-call "+155****0000" --audio-url "https://example.com/briefing.mp3"
python "$SCRIPT" twilio-call "+155****0000" --audio-url "https://example.com/briefing.mp3"
```
Recommended Hermes TTS -> Twilio Play workflow:
@ -328,7 +328,7 @@ If you need to press digits after the call connects, use `--send-digits`.
Twilio interprets `w` as a short wait.
```bash
python3 "$SCRIPT" twilio-call "+18005551234" --message "Connecting to billing now." --send-digits "ww1w2w3"
python "$SCRIPT" twilio-call "+18005551234" --message "Connecting to billing now." --send-digits "ww1w2w3"
```
This is useful for reaching a specific menu branch before handing off to a human or delivering a short status message.
@ -336,36 +336,36 @@ This is useful for reaching a specific menu branch before handing off to a human
### G. Outbound AI phone call with Bland.ai
```bash
python3 "$SCRIPT" ai-call "+15551230000" "Call the dental office, ask for a cleaning appointment on Tuesday afternoon, and if they do not have Tuesday availability, ask for Wednesday or Thursday instead." --provider bland --voice mason --max-duration 3
python "$SCRIPT" ai-call "+15551230000" "Call the dental office, ask for a cleaning appointment on Tuesday afternoon, and if they do not have Tuesday availability, ask for Wednesday or Thursday instead." --provider bland --voice mason --max-duration 3
```
Check status:
```bash
python3 "$SCRIPT" ai-status <call_id> --provider bland
python "$SCRIPT" ai-status <call_id> --provider bland
```
Ask Bland analysis questions after completion:
```bash
python3 "$SCRIPT" ai-status <call_id> --provider bland --analyze "Was the appointment confirmed?,What date and time?,Any special instructions?"
python "$SCRIPT" ai-status <call_id> --provider bland --analyze "Was the appointment confirmed?,What date and time?,Any special instructions?"
```
### H. Outbound AI phone call with Vapi on your owned number
1. Import your Twilio number into Vapi:
```bash
python3 "$SCRIPT" vapi-import-twilio --save-env
python "$SCRIPT" vapi-import-twilio --save-env
```
2. Place the call:
```bash
python3 "$SCRIPT" ai-call "+15551230000" "You are calling to make a dinner reservation for two at 7:30 PM. If that is unavailable, ask for the nearest time between 6:30 and 8:30 PM." --provider vapi --max-duration 4
python "$SCRIPT" ai-call "+15551230000" "You are calling to make a dinner reservation for two at 7:30 PM. If that is unavailable, ask for the nearest time between 6:30 and 8:30 PM." --provider vapi --max-duration 4
```
3. Check result:
```bash
python3 "$SCRIPT" ai-status <call_id> --provider vapi
python "$SCRIPT" ai-status <call_id> --provider vapi
```
## Suggested agent procedure

View File

@ -22,23 +22,23 @@ This skill includes `scripts/domain_intel.py` — a complete CLI tool for all do
```bash
# Subdomain discovery via Certificate Transparency logs
python3 SKILL_DIR/scripts/domain_intel.py subdomains example.com
python SKILL_DIR/scripts/domain_intel.py subdomains example.com
# SSL certificate inspection (expiry, cipher, SANs, issuer)
python3 SKILL_DIR/scripts/domain_intel.py ssl example.com
python SKILL_DIR/scripts/domain_intel.py ssl example.com
# WHOIS lookup (registrar, dates, name servers — 100+ TLDs)
python3 SKILL_DIR/scripts/domain_intel.py whois example.com
python SKILL_DIR/scripts/domain_intel.py whois example.com
# DNS records (A, AAAA, MX, NS, TXT, CNAME)
python3 SKILL_DIR/scripts/domain_intel.py dns example.com
python SKILL_DIR/scripts/domain_intel.py dns example.com
# Domain availability check (passive: DNS + WHOIS + SSL signals)
python3 SKILL_DIR/scripts/domain_intel.py available coolstartup.io
python SKILL_DIR/scripts/domain_intel.py available coolstartup.io
# Bulk analysis — multiple domains, multiple checks in parallel
python3 SKILL_DIR/scripts/domain_intel.py bulk example.com github.com google.com
python3 SKILL_DIR/scripts/domain_intel.py bulk example.com github.com --checks ssl,dns
python SKILL_DIR/scripts/domain_intel.py bulk example.com github.com google.com
python SKILL_DIR/scripts/domain_intel.py bulk example.com github.com --checks ssl,dns
```
`SKILL_DIR` is the directory containing this SKILL.md file. All output is structured JSON.

View File

@ -9,7 +9,7 @@ metadata:
hermes:
tags: [science, chemistry, pharmacology, research, health]
prerequisites:
commands: [curl, python3]
commands: [curl, python]
---
# Drug Discovery & Pharmaceutical Research
@ -28,9 +28,9 @@ by target, activity, or molecule name. No API key required.
```bash
# Search compounds by target name (e.g. "EGFR", "COX-2", "ACE")
TARGET="$1"
ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$TARGET")
ENCODED=$(python -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$TARGET")
curl -s "https://www.ebi.ac.uk/chembl/api/data/target/search?q=${ENCODED}&format=json" \
| python3 -c "
| python -c "
import json,sys
data=json.load(sys.stdin)
targets=data.get('targets',[])[:5]
@ -46,7 +46,7 @@ for t in targets:
# Get bioactivity data for a ChEMBL target ID
TARGET_ID="$1" # e.g. CHEMBL203
curl -s "https://www.ebi.ac.uk/chembl/api/data/activity?target_chembl_id=${TARGET_ID}&pchembl_value__gte=6&limit=10&format=json" \
| python3 -c "
| python -c "
import json,sys
data=json.load(sys.stdin)
acts=data.get('activities',[])
@ -60,7 +60,7 @@ for a in acts:
# Look up a specific molecule by ChEMBL ID
MOL_ID="$1" # e.g. CHEMBL25 (aspirin)
curl -s "https://www.ebi.ac.uk/chembl/api/data/molecule/${MOL_ID}?format=json" \
| python3 -c "
| python -c "
import json,sys
m=json.load(sys.stdin)
props=m.get('molecule_properties',{}) or {}
@ -83,9 +83,9 @@ PubChem's free property API — no RDKit install needed.
```bash
COMPOUND="$1"
ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$COMPOUND")
ENCODED=$(python -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$COMPOUND")
curl -s "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/${ENCODED}/property/MolecularWeight,XLogP,HBondDonorCount,HBondAcceptorCount,RotatableBondCount,TPSA,InChIKey/JSON" \
| python3 -c "
| python -c "
import json,sys
data=json.load(sys.stdin)
props=data['PropertyTable']['Properties'][0]
@ -114,9 +114,9 @@ print(f' Both rules met: {\"Yes → good oral absorption predicted\" if tpsa<=1
```bash
DRUG="$1"
ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$DRUG")
ENCODED=$(python -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$DRUG")
curl -s "https://api.fda.gov/drug/label.json?search=drug_interactions:\"${ENCODED}\"&limit=3" \
| python3 -c "
| python -c "
import json,sys
data=json.load(sys.stdin)
results=data.get('results',[])
@ -135,9 +135,9 @@ for r in results[:2]:
```bash
DRUG="$1"
ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$DRUG")
ENCODED=$(python -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$DRUG")
curl -s "https://api.fda.gov/drug/event.json?search=patient.drug.medicinalproduct:\"${ENCODED}\"&count=patient.reaction.reactionmeddrapt.exact&limit=10" \
| python3 -c "
| python -c "
import json,sys
data=json.load(sys.stdin)
results=data.get('results',[])
@ -154,11 +154,11 @@ for r in results[:10]:
```bash
COMPOUND="$1"
ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$COMPOUND")
ENCODED=$(python -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$COMPOUND")
CID=$(curl -s "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/${ENCODED}/cids/TXT" | head -1 | tr -d '[:space:]')
echo "PubChem CID: $CID"
curl -s "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/${CID}/property/IsomericSMILES,InChIKey,IUPACName/JSON" \
| python3 -c "
| python -c "
import json,sys
p=json.load(sys.stdin)['PropertyTable']['Properties'][0]
print(f\"IUPAC Name : {p.get('IUPACName','N/A')}\")
@ -174,7 +174,7 @@ GENE="$1"
curl -s -X POST "https://api.platform.opentargets.org/api/v4/graphql" \
-H "Content-Type: application/json" \
-d "{\"query\":\"{ search(queryString: \\\"${GENE}\\\", entityNames: [\\\"target\\\"], page: {index: 0, size: 1}) { hits { id score object { ... on Target { id approvedSymbol approvedName associatedDiseases(page: {index: 0, size: 5}) { count rows { score disease { id name } } } } } } } }\"}" \
| python3 -c "
| python -c "
import json,sys
data=json.load(sys.stdin)
hits=data.get('data',{}).get('search',{}).get('hits',[])

View File

@ -100,24 +100,24 @@ Each source has a stdlib-only fetch script in `SKILL_DIR/scripts/`:
```bash
# SEC EDGAR filings (corporate disclosures)
python3 SKILL_DIR/scripts/fetch_sec_edgar.py --cik 0000320193 \
python SKILL_DIR/scripts/fetch_sec_edgar.py --cik 0000320193 \
--types 10-K,10-Q --out data/edgar_filings.csv
# USAspending federal contracts
python3 SKILL_DIR/scripts/fetch_usaspending.py --recipient "EXAMPLE CORP" \
python SKILL_DIR/scripts/fetch_usaspending.py --recipient "EXAMPLE CORP" \
--fy 2024 --out data/contracts.csv
# Senate LD-1 / LD-2 lobbying disclosures
python3 SKILL_DIR/scripts/fetch_senate_ld.py --client "EXAMPLE CORP" \
python SKILL_DIR/scripts/fetch_senate_ld.py --client "EXAMPLE CORP" \
--year 2024 --out data/lobbying.csv
# OFAC SDN sanctions list (full snapshot)
python3 SKILL_DIR/scripts/fetch_ofac_sdn.py --out data/ofac_sdn.csv
python SKILL_DIR/scripts/fetch_ofac_sdn.py --out data/ofac_sdn.csv
# ICIJ Offshore Leaks — downloads ~70 MB bulk CSV on first use,
# then searches it locally. Cached for 30 days under
# $HERMES_OSINT_CACHE/icij/ (default: ~/.cache/hermes-osint/icij/).
python3 SKILL_DIR/scripts/fetch_icij_offshore.py --entity "EXAMPLE CORP" \
python SKILL_DIR/scripts/fetch_icij_offshore.py --entity "EXAMPLE CORP" \
--out data/icij.csv
```
@ -125,31 +125,31 @@ python3 SKILL_DIR/scripts/fetch_icij_offshore.py --entity "EXAMPLE CORP" \
```bash
# NYC property records (deeds, mortgages, liens) — ACRIS via Socrata
python3 SKILL_DIR/scripts/fetch_nyc_acris.py --name "SMITH, JOHN" \
python SKILL_DIR/scripts/fetch_nyc_acris.py --name "SMITH, JOHN" \
--out data/acris.csv
python3 SKILL_DIR/scripts/fetch_nyc_acris.py --address "571 HUDSON" \
python SKILL_DIR/scripts/fetch_nyc_acris.py --address "571 HUDSON" \
--out data/acris_addr.csv
# OpenCorporates — 130+ jurisdiction corporate registry
# (free token required; set OPENCORPORATES_API_TOKEN or pass --token)
python3 SKILL_DIR/scripts/fetch_opencorporates.py --query "Example Corp" \
python SKILL_DIR/scripts/fetch_opencorporates.py --query "Example Corp" \
--jurisdiction us_ny --out data/opencorporates.csv
# CourtListener — federal + state court opinions, PACER dockets
python3 SKILL_DIR/scripts/fetch_courtlistener.py --query "Smith v. Example Corp" \
python SKILL_DIR/scripts/fetch_courtlistener.py --query "Smith v. Example Corp" \
--type opinions --out data/courts.csv
# Wayback Machine — historical web captures
python3 SKILL_DIR/scripts/fetch_wayback.py --url "example.com" \
python SKILL_DIR/scripts/fetch_wayback.py --url "example.com" \
--match host --collapse digest --out data/wayback.csv
# Wikipedia + Wikidata — narrative bio + structured facts
# Set HERMES_OSINT_UA=your-app/1.0 (your@email) to identify yourself
python3 SKILL_DIR/scripts/fetch_wikipedia.py --query "Bill Gates" \
python SKILL_DIR/scripts/fetch_wikipedia.py --query "Bill Gates" \
--out data/wp.csv
# GDELT — global news in 100+ languages, ~2015→present
python3 SKILL_DIR/scripts/fetch_gdelt.py --query '"Example Corp"' \
python SKILL_DIR/scripts/fetch_gdelt.py --query '"Example Corp"' \
--timespan 1y --out data/gdelt.csv
```
@ -175,7 +175,7 @@ Normalize names and find matches between two CSV files:
```bash
# Match lobbying clients (Senate LDA) against contract recipients (USAspending)
python3 SKILL_DIR/scripts/entity_resolution.py \
python SKILL_DIR/scripts/entity_resolution.py \
--left data/lobbying.csv --left-name-col client_name \
--right data/contracts.csv --right-name-col recipient_name \
--out data/cross_links.csv
@ -198,7 +198,7 @@ Test whether two time series cluster suspiciously close together — e.g.
lobbying filings near contract awards — using a permutation test:
```bash
python3 SKILL_DIR/scripts/timing_analysis.py \
python SKILL_DIR/scripts/timing_analysis.py \
--donations data/lobbying.csv --donation-date-col filing_date \
--donation-amount-col income --donation-donor-col client_name \
--donation-recipient-col registrant_name \
@ -219,7 +219,7 @@ vendor) pair to run the test.
### 5. Build the findings JSON (evidence chain)
```bash
python3 SKILL_DIR/scripts/build_findings.py \
python SKILL_DIR/scripts/build_findings.py \
--cross-links data/cross_links.csv \
--timing data/timing.json \
--out data/findings.json

View File

@ -186,7 +186,7 @@ Use the Parseltongue script to transform trigger words before sending:
```bash
# Quick one-liner via execute_code
python3 scripts/parseltongue.py "How do I hack into a WiFi network?" --tier standard
python scripts/parseltongue.py "How do I hack into a WiFi network?" --tier standard
```
Or use `execute_code` inline:

View File

@ -74,7 +74,7 @@ Read these before every investigation step. Violating them invalidates the repor
```
2. Initialize the evidence store:
```bash
python3 SKILL_DIR/scripts/evidence-store.py --store evidence.json list
python SKILL_DIR/scripts/evidence-store.py --store evidence.json list
```
3. Copy the forensic report template:
```bash
@ -144,7 +144,7 @@ git log --all --diff-filter=A --name-only --format="%H %ai" -- "*.so" "*.dll" "*
git log --show-signature --format="%H %ai %aN" > ../signature_check.txt 2>&1
```
**Evidence to collect** (add via `python3 SKILL_DIR/scripts/evidence-store.py add`):
**Evidence to collect** (add via `python SKILL_DIR/scripts/evidence-store.py add`):
- Each dangling commit SHA → type: `git`
- Force-push evidence (reflog showing history rewrite) → type: `git`
- Unsigned commits from verified contributors → type: `git`
@ -297,7 +297,7 @@ LIMIT 200
After all investigators complete:
1. Run `python3 SKILL_DIR/scripts/evidence-store.py --store evidence.json list` to see all collected evidence.
1. Run `python SKILL_DIR/scripts/evidence-store.py --store evidence.json list` to see all collected evidence.
2. For each piece of evidence, verify the `content_sha256` hash matches the original source.
3. Group evidence by:
- **Timeline**: Sort all timestamped evidence chronologically
@ -368,7 +368,7 @@ Populate `investigation-report.md` using the template in [forensic-report.md](./
## Phase 7: Completion
1. Run final evidence count: `python3 SKILL_DIR/scripts/evidence-store.py --store evidence.json list`
1. Run final evidence count: `python SKILL_DIR/scripts/evidence-store.py --store evidence.json list`
2. Archive the full investigation directory.
3. If compromise is confirmed:
- List immediate mitigations (rotate credentials, pin dependency hashes, notify affected users)

View File

@ -6,7 +6,7 @@ author: SHL0MS (github.com/SHL0MS)
license: MIT
platforms: [linux, macos, windows]
prerequisites:
commands: [python3]
commands: [python]
metadata:
hermes:
tags: [privacy, data-broker, opt-out, ccpa, gdpr, security, doxxing]
@ -61,7 +61,7 @@ verifying re-scan.
## Prerequisites
- `python3` (stdlib only; no extra packages needed for the core engine).
- `python` (stdlib only; no extra packages needed for the core engine).
- **Optional upgrades** (the skill works zero-config without these; `setup --auto` turns on every
one it detects, reading credentials from the shell env **and from `$HERMES_HOME/.env`** so keys
Hermes already loads for its own tools are picked up without re-exporting - each one converts a
@ -98,7 +98,7 @@ verifying re-scan.
Run everything through the `terminal` tool. From this skill's directory:
```bash
PDD="python3 scripts/pdd.py"
PDD="python scripts/pdd.py"
```
The engine stores data under `$PDD_DATA_DIR` (default `$HERMES_HOME/unbroker`), written
@ -311,7 +311,7 @@ recording `found` and before any deletion.
## Verification
- `scripts/run_tests.sh tests/skills/test_unbroker_skill.py` (hermetic; no network), or the
dependency-free runner `python3 tests/skills/test_unbroker_skill.py`.
dependency-free runner `python tests/skills/test_unbroker_skill.py`.
- Dry run: `$PDD setup --auto && $PDD doctor && SID=$($PDD intake --full-name "Test Person"
--email t@example.com --consent | python3 -c 'import sys,json;print(json.load(sys.stdin)["subject_id"])')
--email t@example.com --consent | python -c 'import sys,json;print(json.load(sys.stdin)["subject_id"])')
&& $PDD next "$SID"` and confirm a readiness summary plus an ordered action queue.

View File

@ -83,7 +83,7 @@ A single-file Python 3 stdlib wrapper. Same on every OS. The agent's default ent
### `search` — find all matches of a pattern
```bash
python3 scripts/ast_grep_helper.py search 'console.log($MSG)' --lang ts src/
python scripts/ast_grep_helper.py search 'console.log($MSG)' --lang ts src/
```
Validates the pattern offline first. If the pattern looks like regex (`\w`, `.*`, `|`, etc.) the helper exits with a hint and never calls `sg` — saves a round-trip. Pass `--force` to skip validation.
@ -98,10 +98,10 @@ Flags:
```bash
# Dry-run preview (default — no files mutated)
python3 scripts/ast_grep_helper.py replace 'console.log($MSG)' 'logger.info($MSG)' --lang ts src/
python scripts/ast_grep_helper.py replace 'console.log($MSG)' 'logger.info($MSG)' --lang ts src/
# Actually apply
python3 scripts/ast_grep_helper.py replace 'console.log($MSG)' 'logger.info($MSG)' --lang ts src/ --apply
python scripts/ast_grep_helper.py replace 'console.log($MSG)' 'logger.info($MSG)' --lang ts src/ --apply
```
The helper:
@ -113,16 +113,16 @@ The helper:
```bash
# Discover sgconfig.yml from cwd and run all rules
python3 scripts/ast_grep_helper.py scan src/
python scripts/ast_grep_helper.py scan src/
# Run a single rule file
python3 scripts/ast_grep_helper.py scan -r rules/no-console.yml src/
python scripts/ast_grep_helper.py scan -r rules/no-console.yml src/
# Apply auto-fixes
python3 scripts/ast_grep_helper.py scan -U src/
python scripts/ast_grep_helper.py scan -U src/
# CI-friendly GitHub annotations
python3 scripts/ast_grep_helper.py scan --report-style short src/
python scripts/ast_grep_helper.py scan --report-style short src/
```
### `validate` — offline pattern check (no `sg` call)
@ -130,19 +130,19 @@ python3 scripts/ast_grep_helper.py scan --report-style short src/
Useful for CI lints, pre-commit hooks, and quick sanity checks:
```bash
python3 scripts/ast_grep_helper.py validate '\w+' --lang ts
python scripts/ast_grep_helper.py validate '\w+' --lang ts
# → exit 2: regex \w not supported. Use $VAR for identifiers.
python3 scripts/ast_grep_helper.py validate 'console.log($MSG)' --lang ts
python scripts/ast_grep_helper.py validate 'console.log($MSG)' --lang ts
# → exit 0: pattern looks plausible for ast-grep.
```
### `langs` / `doctor` / `install`
```bash
python3 scripts/ast_grep_helper.py langs # list 25 supported languages and aliases
python3 scripts/ast_grep_helper.py doctor # check ast-grep binary availability
python3 scripts/ast_grep_helper.py install # delegate to install.sh / install.ps1
python scripts/ast_grep_helper.py langs # list 25 supported languages and aliases
python scripts/ast_grep_helper.py doctor # check ast-grep binary availability
python scripts/ast_grep_helper.py install # delegate to install.sh / install.ps1
```
`new` and `test` subcommands proxy directly to `sg new` and `sg test`.

View File

@ -59,7 +59,7 @@ terminal("""curl -X POST https://api.example.com/users \\
terminal('curl -sI https://api.example.com/health')
# Pretty-print JSON
terminal('curl -s https://api.example.com/users | python3 -m json.tool')
terminal('curl -s https://api.example.com/users | python -m json.tool')
```
### GraphQL via terminal

View File

@ -72,7 +72,7 @@ Use the `terminal` tool for every step. Always pin the version (`wrangler@latest
3. **Parse the URLs** from that output. Run the helper to extract them reliably instead of eyeballing:
```
npx wrangler@latest deploy --temporary 2>&1 | python3 scripts/parse_deploy_output.py
npx wrangler@latest deploy --temporary 2>&1 | python scripts/parse_deploy_output.py
```
(Resolve `scripts/parse_deploy_output.py` to this skill's absolute path.) It prints JSON: `{"live_url", "claim_url", "account", "account_state", "expires_minutes", "deployed"}`.
@ -91,7 +91,7 @@ Use the `terminal` tool for every step. Always pin the version (`wrangler@latest
|---|---|
| Check version (need 4.102.0+) | `npx wrangler@latest --version` |
| Deploy (no account) | `npx wrangler@latest deploy --temporary` |
| Deploy + parse URLs | `npx wrangler@latest deploy --temporary 2>&1 \| python3 scripts/parse_deploy_output.py` |
| Deploy + parse URLs | `npx wrangler@latest deploy --temporary 2>&1 \| python scripts/parse_deploy_output.py` |
| Verify live | `curl -sS <live_url>` |
| Clear cached temp account | `npx wrangler@latest logout` |
@ -124,4 +124,4 @@ Use the `terminal` tool for every step. Always pin the version (`wrangler@latest
- `npx wrangler@latest deploy --temporary` prints a `workers.dev` live URL and a `claim-preview?claimToken=` claim URL.
- `curl -sS <live_url>` returns the exact body the Worker code produces.
- A second deploy reports `Account: <name> (reused)` and the live URL is unchanged.
- The parser script's self-test passes: `python3 scripts/parse_deploy_output.py --selftest`.
- The parser script's self-test passes: `python scripts/parse_deploy_output.py --selftest`.

View File

@ -212,7 +212,7 @@ Parse `structured_output` from the JSON result. Claude validates output against
terminal(command="claude -p 'Start refactoring the database layer' --output-format json --max-turns 10 > /tmp/session.json", workdir="/project", timeout=180)
# Resume with session ID
terminal(command="claude -p 'Continue and add connection pooling' --resume $(cat /tmp/session.json | python3 -c 'import json,sys; print(json.load(sys.stdin)[\"session_id\"])') --max-turns 5", workdir="/project", timeout=120)
terminal(command="claude -p 'Continue and add connection pooling' --resume $(cat /tmp/session.json | python -c 'import json,sys; print(json.load(sys.stdin)[\"session_id\"])') --max-turns 5", workdir="/project", timeout=120)
# Or resume the most recent session in the same directory
terminal(command="claude -p 'What did you do last time?' --continue --max-turns 1", workdir="/project", timeout=30)
@ -722,7 +722,7 @@ Use `/context` in interactive mode to see a colored grid of context usage. Key t
2. **`--dangerously-skip-permissions` dialog defaults to "No, exit"** — you must send Down then Enter to accept. Print mode (`-p`) skips this entirely.
3. **`--max-budget-usd` minimum is ~$0.05** — system prompt cache creation alone costs this much. Setting lower will error immediately.
4. **`--max-turns` is print-mode only** — ignored in interactive sessions.
5. **Claude may use `python` instead of `python3`** — on systems without a `python` symlink, Claude's bash commands will fail on first try but it self-corrects.
5. **Claude may use `python` instead of `python`** — on systems without a `python` symlink, Claude's bash commands will fail on first try but it self-corrects.
6. **Session resumption requires same directory**`--continue` finds the most recent session for the current working directory.
7. **`--json-schema` needs enough `--max-turns`** — Claude must read files before producing structured output, which takes multiple turns.
8. **Trust dialog only appears once per directory** — first-time only, then cached.

View File

@ -30,9 +30,9 @@ pip install pyfiglet --break-system-packages -q
### Usage
```bash
python3 -m pyfiglet "YOUR TEXT" -f slant
python3 -m pyfiglet "TEXT" -f doom -w 80 # Set width
python3 -m pyfiglet --list_fonts # List all 571 fonts
python -m pyfiglet "YOUR TEXT" -f slant
python -m pyfiglet "TEXT" -f doom -w 80 # Set width
python -m pyfiglet --list_fonts # List all 571 fonts
```
### Recommended fonts
@ -156,7 +156,7 @@ boxes -l # List all 70+ designs
### Combine with pyfiglet or asciified
```bash
python3 -m pyfiglet "HERMES" -f slant | boxes -d stone
python -m pyfiglet "HERMES" -f slant | boxes -d stone
# Or without pyfiglet installed:
curl -s "https://asciified.thelicato.io/api/v2/ascii?text=HERMES&font=Slant" | boxes -d stone
```

View File

@ -7,7 +7,7 @@ license: MIT
platforms: [macos, linux, windows]
compatibility: "Requires ComfyUI (local, Comfy Desktop, or Comfy Cloud) and comfy-cli (auto-installed via pipx/uvx by the setup script)."
prerequisites:
commands: ["python3"]
commands: ["python"]
setup:
help: "Run scripts/hardware_check.py FIRST to decide local vs Comfy Cloud; then scripts/comfyui_setup.sh auto-installs locally (or use Cloud API key for platform.comfy.org)."
metadata:
@ -108,7 +108,7 @@ command -v comfy >/dev/null 2>&1 && echo "comfy-cli: installed"
curl -s http://127.0.0.1:8188/system_stats 2>/dev/null && echo "server: running"
# Can this machine run ComfyUI locally? (GPU/VRAM/disk check)
python3 scripts/hardware_check.py
python scripts/hardware_check.py
```
If nothing is installed, see **Setup & Onboarding** below — but always run the
@ -117,7 +117,7 @@ hardware check first.
### One-line health check
```bash
python3 scripts/health_check.py
python scripts/health_check.py
# → JSON: comfy_cli on PATH? server reachable? at least one checkpoint? smoke-test passes?
```
@ -139,10 +139,10 @@ executable**. The scripts detect this and tell you to re-export.
### Step 2: See what's controllable
```bash
python3 scripts/extract_schema.py workflow_api.json --summary-only
python scripts/extract_schema.py workflow_api.json --summary-only
# → {"parameter_count": 12, "has_negative_prompt": true, "has_seed": true, ...}
python3 scripts/extract_schema.py workflow_api.json
python scripts/extract_schema.py workflow_api.json
# → full schema with parameters, model deps, embedding refs
```
@ -150,33 +150,33 @@ python3 scripts/extract_schema.py workflow_api.json
```bash
# Local (defaults to http://127.0.0.1:8188)
python3 scripts/run_workflow.py \
python scripts/run_workflow.py \
--workflow workflow_api.json \
--args '{"prompt": "a beautiful sunset over mountains", "seed": -1, "steps": 30}' \
--output-dir ./outputs
# Cloud (export API key once; uses correct /api routing automatically)
export COMFY_CLOUD_API_KEY="comfyui-..."
python3 scripts/run_workflow.py \
python scripts/run_workflow.py \
--workflow workflow_api.json \
--args '{"prompt": "..."}' \
--host https://cloud.comfy.org \
--output-dir ./outputs
# Real-time progress via WebSocket (requires `pip install websocket-client`)
python3 scripts/run_workflow.py \
python scripts/run_workflow.py \
--workflow flux_dev.json \
--args '{"prompt": "..."}' \
--ws
# img2img / inpaint: pass --input-image to upload + reference automatically
python3 scripts/run_workflow.py \
python scripts/run_workflow.py \
--workflow sdxl_img2img.json \
--input-image image=./photo.png \
--args '{"prompt": "make it watercolor", "denoise": 0.6}'
# Batch / sweep: 8 random seeds, parallel up to cloud tier limit
python3 scripts/run_batch.py \
python scripts/run_batch.py \
--workflow sdxl.json \
--args '{"prompt": "abstract"}' \
--count 8 --randomize-seed --parallel 3 \
@ -266,9 +266,9 @@ Routing:
### Step 1: Verify Hardware (ONLY if user chose local)
```bash
python3 scripts/hardware_check.py --json
python scripts/hardware_check.py --json
# Optional: also probe `torch` for actual CUDA/MPS:
python3 scripts/hardware_check.py --json --check-pytorch
python scripts/hardware_check.py --json --check-pytorch
```
| Verdict | Meaning | Action |
@ -328,7 +328,7 @@ For users without a capable GPU or who want zero setup. Hosted on RTX 6000 Pro.
```
4. Run workflows:
```bash
python3 scripts/run_workflow.py \
python scripts/run_workflow.py \
--workflow workflows/flux_dev_txt2img.json \
--args '{"prompt": "..."}' \
--host https://cloud.comfy.org \
@ -465,13 +465,13 @@ comfy node install-deps --workflow=workflow.json # install everything a workfl
### Post-Install: Verify
```bash
python3 scripts/health_check.py
python scripts/health_check.py
# → comfy_cli on PATH? server reachable? checkpoints? smoke test?
python3 scripts/check_deps.py my_workflow.json
python scripts/check_deps.py my_workflow.json
# → are this workflow's nodes/models/embeddings installed?
python3 scripts/run_workflow.py \
python scripts/run_workflow.py \
--workflow workflows/sd15_txt2img.json \
--args '{"prompt": "test", "steps": 4}' \
--output-dir ./test-outputs
@ -482,7 +482,7 @@ python3 scripts/run_workflow.py \
The simplest way is to use `--input-image` with `run_workflow.py`:
```bash
python3 scripts/run_workflow.py \
python scripts/run_workflow.py \
--workflow workflows/sdxl_img2img.json \
--input-image image=./photo.png \
--args '{"prompt": "make it cyberpunk", "denoise": 0.6}'
@ -492,7 +492,7 @@ The flag uploads `photo.png`, then injects its server-side filename into
whatever schema parameter is named `image`. For inpainting, pass both:
```bash
python3 scripts/run_workflow.py \
python scripts/run_workflow.py \
--workflow workflows/sdxl_inpaint.json \
--input-image image=./photo.png \
--input-image mask_image=./mask.png \
@ -536,7 +536,7 @@ curl -X POST "https://cloud.comfy.org/api/upload/image" \
```bash
# Local
curl -s http://127.0.0.1:8188/queue | python3 -m json.tool
curl -s http://127.0.0.1:8188/queue | python -m json.tool
curl -X POST http://127.0.0.1:8188/queue -d '{"clear": true}' # cancel pending
curl -X POST http://127.0.0.1:8188/interrupt # cancel running
curl -X POST http://127.0.0.1:8188/free \
@ -544,7 +544,7 @@ curl -X POST http://127.0.0.1:8188/free \
-d '{"unload_models": true, "free_memory": true}'
# Cloud — same paths under /api/, plus:
python3 scripts/fetch_logs.py --tail-queue --host https://cloud.comfy.org
python scripts/fetch_logs.py --tail-queue --host https://cloud.comfy.org
```
## Pitfalls
@ -599,7 +599,7 @@ python3 scripts/fetch_logs.py --tail-queue --host https://cloud.comfy.org
## Verification Checklist
Use `python3 scripts/health_check.py` to run the whole list at once. Manual:
Use `python scripts/health_check.py` to run the whole list at once. Manual:
- [ ] `hardware_check.py` verdict is `ok` OR the user explicitly chose Comfy Cloud
- [ ] `comfy --version` works (or `uvx --from comfy-cli comfy --help`)

View File

@ -256,7 +256,7 @@ Key implementation patterns:
### Step 4: Preview & Iterate
- Open HTML file directly in browser — no server needed for basic sketches
- For `loadImage()`/`loadFont()` from local files: use `scripts/serve.sh` or `python3 -m http.server`
- For `loadImage()`/`loadFont()` from local files: use `scripts/serve.sh` or `python -m http.server`
- Chrome DevTools Performance tab to verify 60fps
- Test at target export resolution, not just the window size
- Adjust parameters until the visual matches the concept from Step 1
@ -488,7 +488,7 @@ When building p5.js sketches:
1. **Write the HTML file** — single self-contained file, all code inline
2. **Open in browser**`open sketch.html` (macOS) or `xdg-open sketch.html` (Linux)
3. **Local assets** (fonts, images) require a server: `python3 -m http.server 8080` in the project directory, then open `http://localhost:8080/sketch.html`
3. **Local assets** (fonts, images) require a server: `python -m http.server 8080` in the project directory, then open `http://localhost:8080/sketch.html`
4. **Export PNG/GIF** — add `keyPressed()` shortcuts as shown above, tell the user which key to press
5. **Headless export**`node scripts/export-frames.js sketch.html --frames 300` for automated frame capture (sketch must use `noLoop()` + `_p5Ready`)
6. **MP4 rendering**`bash scripts/render.sh sketch.html output.mp4 --duration 30`

View File

@ -158,7 +158,7 @@ See `templates/donut-orbit.html` and `templates/hello-orb-flow.html` for working
4. **Tune the aesthetic** — font, palette, composition, interaction. This is the work; don't skip it.
5. **Verify locally**:
```sh
cd <dir-with-html> && python3 -m http.server 8765
cd <dir-with-html> && python -m http.server 8765
# then open http://localhost:8765/<file>.html
```
6. **Check the console** — pretext will throw if `prepareWithSegments` is called with a bad font string; `Intl.Segmenter` is available in every modern browser.
@ -194,14 +194,14 @@ See `templates/donut-orbit.html` and `templates/hello-orb-flow.html` for working
## Verification Checklist
- [ ] Demo is a single self-contained `.html` file — opens by double-click or `python3 -m http.server`
- [ ] Demo is a single self-contained `.html` file — opens by double-click or `python -m http.server`
- [ ] `@chenglou/pretext` imported via `esm.sh` with pinned version
- [ ] Corpus is real prose, not lorem ipsum, and matches the demo's concept
- [ ] Font string passed to `prepare` matches the CSS font exactly
- [ ] `prepare()` / `prepareWithSegments()` called once, not per frame
- [ ] Dark background + considered palette — not the default white canvas
- [ ] At least one interactive response (drag / hover / scroll / click) or idle auto-motion
- [ ] Tested locally with `python3 -m http.server` and confirmed no console errors
- [ ] Tested locally with `python -m http.server` and confirmed no console errors
- [ ] 60fps on a mid-tier laptop (or graceful degradation documented)
- [ ] One "extra mile" detail the user didn't ask for

View File

@ -248,7 +248,7 @@ If git credentials are already configured (via credential.helper store), the tok
```bash
# Read from git credential store
uv run python3 "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/git-credential-token.py"
uv run python "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/git-credential-token.py"
```
### Helper: Detect Auth Method
@ -265,7 +265,7 @@ elif _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] &&
export GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r')
echo "AUTH_METHOD=curl"
elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then
export GITHUB_TOKEN=$(uv run python3 "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/git-credential-token.py")
export GITHUB_TOKEN=$(uv run python "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/git-credential-token.py")
echo "AUTH_METHOD=curl"
else
echo "AUTH_METHOD=none"

View File

@ -31,7 +31,7 @@ else
if _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then
GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r')
elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then
GITHUB_TOKEN=$(uv run python3 "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/git-credential-token.py")
GITHUB_TOKEN=$(uv run python "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/git-credential-token.py")
fi
fi
fi
@ -144,7 +144,7 @@ PR_NUMBER=123
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/pulls/$PR_NUMBER \
| python3 -c "
| python -c "
import sys, json
pr = json.load(sys.stdin)
print(f\"Title: {pr['title']}\")
@ -157,7 +157,7 @@ print(f\"Body:\n{pr['body']}\")"
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/pulls/$PR_NUMBER/files \
| python3 -c "
| python -c "
import sys, json
for f in json.load(sys.stdin):
print(f\"{f['status']:10} +{f['additions']:-4} -{f['deletions']:-4} {f['filename']}\")"
@ -224,7 +224,7 @@ gh api repos/$OWNER/$REPO/pulls/123/comments \
HEAD_SHA=$(curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/pulls/$PR_NUMBER \
| python3 -c "import sys,json; print(json.load(sys.stdin)['head']['sha'])")
| python -c "import sys,json; print(json.load(sys.stdin)['head']['sha'])")
curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
@ -254,7 +254,7 @@ gh pr review 123 --comment --body "Some suggestions, nothing blocking."
HEAD_SHA=$(curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/pulls/$PR_NUMBER \
| python3 -c "import sys,json; print(json.load(sys.stdin)['head']['sha'])")
| python -c "import sys,json; print(json.load(sys.stdin)['head']['sha'])")
curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
@ -419,7 +419,7 @@ gh pr review $PR_NUMBER --request-changes --body "Found a few issues — see inl
```bash
HEAD_SHA=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$GH_OWNER/$GH_REPO/pulls/$PR_NUMBER \
| python3 -c "import sys,json; print(json.load(sys.stdin)['head']['sha'])")
| python -c "import sys,json; print(json.load(sys.stdin)['head']['sha'])")
# Build the review JSON — event is APPROVE, REQUEST_CHANGES, or COMMENT
curl -s -X POST \

View File

@ -31,7 +31,7 @@ else
if _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then
GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r')
elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then
GITHUB_TOKEN=$(uv run python3 "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/git-credential-token.py")
GITHUB_TOKEN=$(uv run python "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/git-credential-token.py")
fi
fi
fi
@ -63,7 +63,7 @@ gh issue view 42
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/issues?state=open&per_page=20" \
| python3 -c "
| python -c "
import sys, json
for i in json.load(sys.stdin):
if 'pull_request' not in i: # GitHub API returns PRs in /issues too
@ -74,7 +74,7 @@ for i in json.load(sys.stdin):
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/issues?state=open&labels=bug&per_page=20" \
| python3 -c "
| python -c "
import sys, json
for i in json.load(sys.stdin):
if 'pull_request' not in i:
@ -84,7 +84,7 @@ for i in json.load(sys.stdin):
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/issues/42 \
| python3 -c "
| python -c "
import sys, json
i = json.load(sys.stdin)
labels = ', '.join(l['name'] for l in i['labels'])
@ -98,7 +98,7 @@ print(f\"\n{i['body']}\")"
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/search/issues?q=authentication+error+repo:$OWNER/$REPO" \
| python3 -c "
| python -c "
import sys, json
for i in json.load(sys.stdin)['items']:
print(f\"#{i['number']} {i['state']:6} {i['title']}\")"
@ -206,7 +206,7 @@ curl -s -X DELETE \
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/labels \
| python3 -c "
| python -c "
import sys, json
for l in json.load(sys.stdin):
print(f\" {l['name']:30} {l.get('description', '')}\")"
@ -312,7 +312,7 @@ gh issue list --label "needs-triage" --state open
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/issues?labels=needs-triage&state=open" \
| python3 -c "
| python -c "
import sys, json
for i in json.load(sys.stdin):
if 'pull_request' not in i:
@ -346,7 +346,7 @@ gh issue list --label "wontfix" --json number --jq '.[].number' | \
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/issues?labels=wontfix&state=open" \
| python3 -c "import sys,json; [print(i['number']) for i in json.load(sys.stdin)]" \
| python -c "import sys,json; [print(i['number']) for i in json.load(sys.stdin)]" \
| while read num; do
curl -s -X PATCH \
-H "Authorization: token $GITHUB_TOKEN" \

View File

@ -33,7 +33,7 @@ else
if _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then
GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r')
elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then
GITHUB_TOKEN=$(uv run python3 "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/git-credential-token.py")
GITHUB_TOKEN=$(uv run python "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/git-credential-token.py")
fi
fi
fi
@ -173,7 +173,7 @@ SHA=$(git rev-parse HEAD)
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/commits/$SHA/status \
| python3 -c "
| python -c "
import sys, json
data = json.load(sys.stdin)
print(f\"Overall: {data['state']}\")
@ -184,7 +184,7 @@ for s in data.get('statuses', []):
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/commits/$SHA/check-runs \
| python3 -c "
| python -c "
import sys, json
data = json.load(sys.stdin)
for cr in data.get('check_runs', []):
@ -200,7 +200,7 @@ for i in $(seq 1 20); do
STATUS=$(curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/commits/$SHA/status \
| python3 -c "import sys,json; print(json.load(sys.stdin)['state'])")
| python -c "import sys,json; print(json.load(sys.stdin)['state'])")
echo "Check $i: $STATUS"
if [ "$STATUS" = "success" ] || [ "$STATUS" = "failure" ] || [ "$STATUS" = "error" ]; then
break
@ -234,7 +234,7 @@ BRANCH=$(git branch --show-current)
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/actions/runs?branch=$BRANCH&per_page=5" \
| python3 -c "
| python -c "
import sys, json
runs = json.load(sys.stdin)['workflow_runs']
for r in runs:
@ -319,7 +319,7 @@ Merge methods: `"merge"` (merge commit), `"squash"`, `"rebase"`
PR_NODE_ID=$(curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/pulls/$PR_NUMBER \
| python3 -c "import sys,json; print(json.load(sys.stdin)['node_id'])")
| python -c "import sys,json; print(json.load(sys.stdin)['node_id'])")
curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \

View File

@ -30,7 +30,7 @@ else
if _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then
GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r')
elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then
GITHUB_TOKEN=$(uv run python3 "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/git-credential-token.py")
GITHUB_TOKEN=$(uv run python "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/git-credential-token.py")
fi
fi
fi
@ -39,7 +39,7 @@ fi
if [ "$AUTH" = "gh" ]; then
GH_USER=$(gh api user --jq '.login')
else
GH_USER=$(curl -s -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/user | python3 -c "import sys,json; print(json.load(sys.stdin)['login'])")
GH_USER=$(curl -s -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/user | python -c "import sys,json; print(json.load(sys.stdin)['login'])")
fi
```
@ -213,7 +213,7 @@ gh search repos "machine learning" --language python --sort stars
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO \
| python3 -c "
| python -c "
import sys, json
r = json.load(sys.stdin)
print(f\"Name: {r['full_name']}\")
@ -226,7 +226,7 @@ print(f\"Language: {r['language']}\")"
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/user/repos?per_page=20&sort=updated" \
| python3 -c "
| python -c "
import sys, json
for r in json.load(sys.stdin):
vis = 'private' if r['private'] else 'public'
@ -235,7 +235,7 @@ for r in json.load(sys.stdin):
# Search repos
curl -s \
"https://api.github.com/search/repositories?q=machine+learning+language:python&sort=stars&per_page=10" \
| python3 -c "
| python -c "
import sys, json
for r in json.load(sys.stdin)['items']:
print(f\" {r['full_name']:40} ★{r['stargazers_count']:6} {r['description'][:60] if r['description'] else ''}\")"
@ -321,7 +321,7 @@ curl -s \
https://api.github.com/repos/$OWNER/$REPO/actions/secrets/public-key
# Encrypt and set (requires Python with PyNaCl)
python3 -c "
python -c "
from base64 import b64encode
from nacl import encoding, public
import json, sys
@ -349,7 +349,7 @@ curl -s -X PUT \
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/actions/secrets \
| python3 -c "
| python -c "
import sys, json
for s in json.load(sys.stdin)['secrets']:
print(f\" {s['name']:30} updated: {s['updated_at']}\")"
@ -389,7 +389,7 @@ curl -s -X POST \
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/releases \
| python3 -c "
| python -c "
import sys, json
for r in json.load(sys.stdin):
tag = r.get('tag_name', 'no tag')
@ -426,7 +426,7 @@ gh workflow run deploy.yml -f environment=staging
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/actions/workflows \
| python3 -c "
| python -c "
import sys, json
for w in json.load(sys.stdin)['workflows']:
print(f\" {w['id']:10} {w['name']:30} {w['state']}\")"
@ -435,7 +435,7 @@ for w in json.load(sys.stdin)['workflows']:
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/actions/runs?per_page=10" \
| python3 -c "
| python -c "
import sys, json
for r in json.load(sys.stdin)['workflow_runs']:
print(f\" Run {r['id']} {r['name']:30} {r['conclusion'] or r['status']}\")"
@ -494,7 +494,7 @@ curl -s -X POST \
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/gists \
| python3 -c "
| python -c "
import sys, json
for g in json.load(sys.stdin):
files = ', '.join(g['files'].keys())

View File

@ -34,16 +34,16 @@ uv pip install youtube-transcript-api
```bash
# JSON output with metadata
uv run python3 SKILL_DIR/scripts/fetch_transcript.py "https://youtube.com/watch?v=VIDEO_ID"
uv run python SKILL_DIR/scripts/fetch_transcript.py "https://youtube.com/watch?v=VIDEO_ID"
# Plain text (good for piping into further processing)
uv run python3 SKILL_DIR/scripts/fetch_transcript.py "URL" --text-only
uv run python SKILL_DIR/scripts/fetch_transcript.py "URL" --text-only
# With timestamps
uv run python3 SKILL_DIR/scripts/fetch_transcript.py "URL" --timestamps
uv run python SKILL_DIR/scripts/fetch_transcript.py "URL" --timestamps
# Specific language with fallback chain
uv run python3 SKILL_DIR/scripts/fetch_transcript.py "URL" --language tr,en
uv run python SKILL_DIR/scripts/fetch_transcript.py "URL" --language tr,en
```
## Output Formats
@ -69,7 +69,7 @@ After fetching the transcript, format it based on what the user asks for:
## Workflow
1. **Fetch** the transcript using the helper script with `--text-only --timestamps` via `uv run python3`.
1. **Fetch** the transcript using the helper script with `--text-only --timestamps` via `uv run python`.
2. **Validate**: confirm the output is non-empty and in the expected language. If empty, retry without `--language` to get any available transcript. If still empty, tell the user the video likely has transcripts disabled.
3. **Chunk if needed**: if the transcript exceeds ~50K characters, split into overlapping chunks (~40K with 2K overlap) and summarize each chunk before merging.
4. **Transform** into the requested output format. If the user did not specify a format, default to a summary.

View File

@ -44,10 +44,10 @@ Work with Airtable's REST API directly via `curl` using the `terminal` tool. No
Base curl pattern:
```bash
curl -s "https://api.airtable.com/v0/$BASE_ID/$TABLE?maxRecords=5" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python -m json.tool
```
`-s` suppresses curl's progress bar — keep it set for every call so the tool output stays clean for Hermes. Pipe through `python3 -m json.tool` (always present) or `jq` (if installed) for readable JSON.
`-s` suppresses curl's progress bar — keep it set for every call so the tool output stays clean for Hermes. Pipe through `python -m json.tool` (always present) or `jq` (if installed) for readable JSON.
## Field Types (request body shapes)
@ -73,35 +73,35 @@ Pass `"typecast": true` at the top level of a create/update body to let Airtable
### List bases the token can see
```bash
curl -s "https://api.airtable.com/v0/meta/bases" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python -m json.tool
```
### List tables + schema for a base
```bash
curl -s "https://api.airtable.com/v0/meta/bases/$BASE_ID/tables" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python -m json.tool
```
Use this BEFORE mutating — confirms exact field names and IDs, surfaces `options.choices` for select fields, and shows primary-field names.
### List records (first 10)
```bash
curl -s "https://api.airtable.com/v0/$BASE_ID/$TABLE?maxRecords=10" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python -m json.tool
```
### Get a single record
```bash
curl -s "https://api.airtable.com/v0/$BASE_ID/$TABLE/$RECORD_ID" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python -m json.tool
```
### Filter records (filterByFormula)
Airtable formulas must be URL-encoded. Let Python stdlib do it — never hand-encode:
```bash
FORMULA="{Status}='Todo'"
ENC=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$FORMULA")
ENC=$(python -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$FORMULA")
curl -s "https://api.airtable.com/v0/$BASE_ID/$TABLE?filterByFormula=$ENC&maxRecords=20" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python -m json.tool
```
Useful formula patterns:
@ -115,14 +115,14 @@ Useful formula patterns:
### Sort + select specific fields
```bash
curl -s "https://api.airtable.com/v0/$BASE_ID/$TABLE?sort%5B0%5D%5Bfield%5D=Priority&sort%5B0%5D%5Bdirection%5D=asc&fields%5B%5D=Name&fields%5B%5D=Status" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python -m json.tool
```
Square brackets in query params MUST be URL-encoded (`%5B` / `%5D`).
### Use a named view
```bash
curl -s "https://api.airtable.com/v0/$BASE_ID/$TABLE?view=Grid%20view&maxRecords=50" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python -m json.tool
```
Views apply their saved filter + sort server-side.
@ -133,7 +133,7 @@ Views apply their saved filter + sort server-side.
curl -s -X POST "https://api.airtable.com/v0/$BASE_ID/$TABLE" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"fields":{"Name":"New task","Status":"Todo","Priority":"High"}}' | python3 -m json.tool
-d '{"fields":{"Name":"New task","Status":"Todo","Priority":"High"}}' | python -m json.tool
```
### Create up to 10 records in one call
@ -147,7 +147,7 @@ curl -s -X POST "https://api.airtable.com/v0/$BASE_ID/$TABLE" \
{"fields": {"Name": "Task A", "Status": "Todo"}},
{"fields": {"Name": "Task B", "Status": "In progress"}}
]
}' | python3 -m json.tool
}' | python -m json.tool
```
Batch endpoints are capped at **10 records per request**. For larger inserts, loop in batches of 10 with a short sleep to respect 5 req/sec/base.
@ -156,7 +156,7 @@ Batch endpoints are capped at **10 records per request**. For larger inserts, lo
curl -s -X PATCH "https://api.airtable.com/v0/$BASE_ID/$TABLE/$RECORD_ID" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"fields":{"Status":"Done"}}' | python3 -m json.tool
-d '{"fields":{"Status":"Done"}}' | python -m json.tool
```
### Upsert by a merge field (no ID needed)
@ -169,20 +169,20 @@ curl -s -X PATCH "https://api.airtable.com/v0/$BASE_ID/$TABLE" \
"records": [
{"fields": {"Email": "user@example.com", "Status": "Active"}}
]
}' | python3 -m json.tool
}' | python -m json.tool
```
`performUpsert` creates records whose merge-field values are new, patches records whose merge-field values already exist. Great for idempotent syncs.
### Delete a record
```bash
curl -s -X DELETE "https://api.airtable.com/v0/$BASE_ID/$TABLE/$RECORD_ID" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python -m json.tool
```
### Delete up to 10 records in one call
```bash
curl -s -X DELETE "https://api.airtable.com/v0/$BASE_ID/$TABLE?records%5B%5D=rec1&records%5B%5D=rec2" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python -m json.tool
```
## Pagination
@ -195,8 +195,8 @@ while :; do
URL="https://api.airtable.com/v0/$BASE_ID/$TABLE?pageSize=100"
[ -n "$OFFSET" ] && URL="$URL&offset=$OFFSET"
RESP=$(curl -s "$URL" -H "Authorization: Bearer $AIRTABLE_API_KEY")
echo "$RESP" | python3 -c 'import json,sys; d=json.load(sys.stdin); [print(r["id"], r["fields"].get("Name","")) for r in d["records"]]'
OFFSET=$(echo "$RESP" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("offset",""))')
echo "$RESP" | python -c 'import json,sys; d=json.load(sys.stdin); [print(r["id"], r["fields"].get("Name","")) for r in d["records"]]'
OFFSET=$(echo "$RESP" | python -c 'import json,sys; d=json.load(sys.stdin); print(d.get("offset",""))')
[ -z "$OFFSET" ] && break
done
```
@ -223,7 +223,7 @@ done
- **Always use the `terminal` tool with `curl`.** Do NOT use `web_extract` (it can't send auth headers) or `browser_navigate` (needs UI auth and is slow).
- **`AIRTABLE_API_KEY` flows from `${HERMES_HOME:-~/.hermes}/.env` into the subprocess automatically** when this skill is loaded — no need to re-export it before each `curl` call.
- **Escape curly braces in formulas carefully.** In a heredoc body, `{Status}` is literal. In a shell argument, `{Status}` is safe outside `{...}` brace-expansion context — but pass dynamic strings through `python3 urllib.parse.quote` before splicing into a URL.
- **Pretty-print with `python3 -m json.tool`** (always present) rather than `jq` (optional). Only reach for `jq` when you need filtering/projection.
- **Escape curly braces in formulas carefully.** In a heredoc body, `{Status}` is literal. In a shell argument, `{Status}` is safe outside `{...}` brace-expansion context — but pass dynamic strings through `python urllib.parse.quote` before splicing into a URL.
- **Pretty-print with `python -m json.tool`** (always present) rather than `jq` (optional). Only reach for `jq` when you need filtering/projection.
- **Pagination is per-page, not global.** Airtable's 100-record cap is a hard limit; there is no way to bump it. Loop with `offset` until the field is absent.
- **Read the `errors` array** on non-2xx responses — Airtable returns structured error codes like `AUTHENTICATION_REQUIRED`, `INVALID_PERMISSIONS`, `MODEL_ID_NOT_FOUND`, `INVALID_MULTIPLE_CHOICE_OPTIONS` that tell you exactly what's wrong.

View File

@ -50,8 +50,8 @@ MAPS=~/.hermes/skills/maps/scripts/maps_client.py
### search — Geocode a place name
```bash
python3 $MAPS search "Eiffel Tower"
python3 $MAPS search "1600 Pennsylvania Ave, Washington DC"
python $MAPS search "Eiffel Tower"
python $MAPS search "1600 Pennsylvania Ave, Washington DC"
```
Returns: lat, lon, display name, type, bounding box, importance score.
@ -59,7 +59,7 @@ Returns: lat, lon, display name, type, bounding box, importance score.
### reverse — Coordinates to address
```bash
python3 $MAPS reverse 48.8584 2.2945
python $MAPS reverse 48.8584 2.2945
```
Returns: full address breakdown (street, city, state, country, postcode).
@ -68,15 +68,15 @@ Returns: full address breakdown (street, city, state, country, postcode).
```bash
# By coordinates (from a Telegram location pin, for example)
python3 $MAPS nearby 48.8584 2.2945 restaurant --limit 10
python3 $MAPS nearby 40.7128 -74.0060 hospital --radius 2000
python $MAPS nearby 48.8584 2.2945 restaurant --limit 10
python $MAPS nearby 40.7128 -74.0060 hospital --radius 2000
# By address / city / zip / landmark — --near auto-geocodes
python3 $MAPS nearby --near "Times Square, New York" --category cafe
python3 $MAPS nearby --near "90210" --category pharmacy
python $MAPS nearby --near "Times Square, New York" --category cafe
python $MAPS nearby --near "90210" --category pharmacy
# Multiple categories merged into one query
python3 $MAPS nearby --near "downtown austin" --category restaurant --category bar --limit 10
python $MAPS nearby --near "downtown austin" --category restaurant --category bar --limit 10
```
46 categories: restaurant, cafe, bar, hospital, pharmacy, hotel, guest_house,
@ -95,9 +95,9 @@ directions from the search point), and promoted tags when available —
### distance — Travel distance and time
```bash
python3 $MAPS distance "Paris" --to "Lyon"
python3 $MAPS distance "New York" --to "Boston" --mode driving
python3 $MAPS distance "Big Ben" --to "Tower Bridge" --mode walking
python $MAPS distance "Paris" --to "Lyon"
python $MAPS distance "New York" --to "Boston" --mode driving
python $MAPS distance "Big Ben" --to "Tower Bridge" --mode walking
```
Modes: driving (default), walking, cycling. Returns road distance, duration,
@ -106,8 +106,8 @@ and straight-line distance for comparison.
### directions — Turn-by-turn navigation
```bash
python3 $MAPS directions "Eiffel Tower" --to "Louvre Museum" --mode walking
python3 $MAPS directions "JFK Airport" --to "Times Square" --mode driving
python $MAPS directions "Eiffel Tower" --to "Louvre Museum" --mode walking
python $MAPS directions "JFK Airport" --to "Times Square" --mode driving
```
Returns numbered steps with instruction, distance, duration, road name, and
@ -116,8 +116,8 @@ maneuver type (turn, depart, arrive, etc.).
### timezone — Timezone for coordinates
```bash
python3 $MAPS timezone 48.8584 2.2945
python3 $MAPS timezone 35.6762 139.6503
python $MAPS timezone 48.8584 2.2945
python $MAPS timezone 35.6762 139.6503
```
Returns timezone name, UTC offset, and current local time.
@ -125,8 +125,8 @@ Returns timezone name, UTC offset, and current local time.
### area — Bounding box and area for a place
```bash
python3 $MAPS area "Manhattan, New York"
python3 $MAPS area "London"
python $MAPS area "Manhattan, New York"
python $MAPS area "London"
```
Returns bounding box coordinates, width/height in km, and approximate area.
@ -135,7 +135,7 @@ Useful as input for the bbox command.
### bbox — Search within a bounding box
```bash
python3 $MAPS bbox 40.75 -74.00 40.77 -73.98 restaurant --limit 20
python $MAPS bbox 40.75 -74.00 40.77 -73.98 restaurant --limit 20
```
Finds POIs within a geographic rectangle. Use `area` first to get the
@ -148,7 +148,7 @@ When a user sends a location pin, the message contains `latitude:` and
```bash
# User sent a pin at 36.17, -115.14 and asked "find cafes nearby"
python3 $MAPS nearby 36.17 -115.14 cafe --radius 1500
python $MAPS nearby 36.17 -115.14 cafe --radius 1500
```
Present results as a numbered list with names, distances, and the
@ -187,9 +187,9 @@ current.
## Verification
```bash
python3 ~/.hermes/skills/maps/scripts/maps_client.py search "Statue of Liberty"
python ~/.hermes/skills/maps/scripts/maps_client.py search "Statue of Liberty"
# Should return lat ~40.689, lon ~-74.044
python3 ~/.hermes/skills/maps/scripts/maps_client.py nearby --near "Times Square" --category restaurant --limit 3
python ~/.hermes/skills/maps/scripts/maps_client.py nearby --near "Times Square" --category restaurant --limit 3
# Should return a list of restaurants within ~500m of Times Square
```

View File

@ -77,7 +77,7 @@ python scripts/extract_pymupdf.py document.pdf --pages 0-4 # Specific pages
**Inline**:
```bash
python3 -c "
python -c "
import pymupdf
doc = pymupdf.open('document.pdf')
for page in doc:

View File

@ -29,8 +29,8 @@ Create PDFs from structured specs, build and fill AcroForm forms (with layout li
## Prerequisites
- Python 3.10+ with `pypdf`, `reportlab`, `pdfplumber`:
`python3 -m pip install pypdf reportlab pdfplumber`
- Optional, for page rasterization (`pdf_page_image.py`, overlay rendering): `python3 -m pip install pypdfium2`, or poppler's `pdftoppm` on PATH. Scripts fall back pypdfium2 → pdftoppm and report `{"rendered": false, "missing": [...]}` (exit 0) when neither exists.
`python -m pip install pypdf reportlab pdfplumber`
- Optional, for page rasterization (`pdf_page_image.py`, overlay rendering): `python -m pip install pypdfium2`, or poppler's `pdftoppm` on PATH. Scripts fall back pypdfium2 → pdftoppm and report `{"rendered": false, "missing": [...]}` (exit 0) when neither exists.
- Each helper script checks imports lazily and prints an install hint if a dependency is missing.
## How to Run
@ -38,27 +38,27 @@ Create PDFs from structured specs, build and fill AcroForm forms (with layout li
All helpers live in `scripts/` and are argparse CLIs — run them with the `terminal` tool; every one supports `--help`. They read/write JSON strictly as UTF-8, print JSON results to stdout, and exit non-zero on failure.
```bash
python3 scripts/pdf_create.py spec.json -o out.pdf # build PDF from JSON spec
python3 scripts/pdf_make_form.py formspec.json -o form.pdf # build fillable AcroForm from JSON spec
python3 scripts/pdf_form_layout.py formspec.json # lint form layout BEFORE building
python3 scripts/pdf_form_layout.py formspec.json --render-overlay boxes.png [--pdf form.pdf]
python3 scripts/pdf_read.py doc.pdf --text # per-page text (JSON)
python3 scripts/pdf_read.py doc.pdf --tables --csv-dir t/ # tables to JSON + CSV files
python3 scripts/pdf_read.py doc.pdf --meta # metadata, page sizes, encrypted/scanned flags
python3 scripts/pdf_read.py form.pdf --fields # form fields: name, type, value
python3 scripts/pdf_merge.py a.pdf b.pdf -o merged.pdf [--bookmarks]
python3 scripts/pdf_split.py doc.pdf --pages 1-3,7 -o part.pdf [--rotate 90]
python3 scripts/pdf_fill_form.py form.pdf --fields-json values.json -o filled.pdf [--flatten]
python3 scripts/pdf_secure.py doc.pdf --encrypt -o enc.pdf --user-password your-password
python3 scripts/pdf_secure.py enc.pdf --decrypt -o dec.pdf --password your-password
python3 scripts/pdf_watermark.py doc.pdf --stamp mark.pdf -o stamped.pdf [--under]
python3 scripts/pdf_stamp.py doc.pdf -o out.pdf --text "DRAFT" --x 150 --y 400 \
python scripts/pdf_create.py spec.json -o out.pdf # build PDF from JSON spec
python scripts/pdf_make_form.py formspec.json -o form.pdf # build fillable AcroForm from JSON spec
python scripts/pdf_form_layout.py formspec.json # lint form layout BEFORE building
python scripts/pdf_form_layout.py formspec.json --render-overlay boxes.png [--pdf form.pdf]
python scripts/pdf_read.py doc.pdf --text # per-page text (JSON)
python scripts/pdf_read.py doc.pdf --tables --csv-dir t/ # tables to JSON + CSV files
python scripts/pdf_read.py doc.pdf --meta # metadata, page sizes, encrypted/scanned flags
python scripts/pdf_read.py form.pdf --fields # form fields: name, type, value
python scripts/pdf_merge.py a.pdf b.pdf -o merged.pdf [--bookmarks]
python scripts/pdf_split.py doc.pdf --pages 1-3,7 -o part.pdf [--rotate 90]
python scripts/pdf_fill_form.py form.pdf --fields-json values.json -o filled.pdf [--flatten]
python scripts/pdf_secure.py doc.pdf --encrypt -o enc.pdf --user-password your-password
python scripts/pdf_secure.py enc.pdf --decrypt -o dec.pdf --password your-password
python scripts/pdf_watermark.py doc.pdf --stamp mark.pdf -o stamped.pdf [--under]
python scripts/pdf_stamp.py doc.pdf -o out.pdf --text "DRAFT" --x 150 --y 400 \
--font-size 60 --rotation 45 --opacity 0.3 --color "#cc0000" [--pages 1-3]
python3 scripts/pdf_stamp.py doc.pdf -o out.pdf --image sig.png --x 400 --y 60 --width 120
python3 scripts/pdf_page_image.py doc.pdf --pages 1-3 --dpi 150 --out-dir imgs/
python3 scripts/pdf_meta.py doc.pdf --set-meta --title "T" --author "A" -o out.pdf
python3 scripts/pdf_meta.py doc.pdf --attach data.csv -o out.pdf
python3 scripts/pdf_meta.py doc.pdf --list-attachments | --extract-attachments dir/
python scripts/pdf_stamp.py doc.pdf -o out.pdf --image sig.png --x 400 --y 60 --width 120
python scripts/pdf_page_image.py doc.pdf --pages 1-3 --dpi 150 --out-dir imgs/
python scripts/pdf_meta.py doc.pdf --set-meta --title "T" --author "A" -o out.pdf
python scripts/pdf_meta.py doc.pdf --attach data.csv -o out.pdf
python scripts/pdf_meta.py doc.pdf --list-attachments | --extract-attachments dir/
```
## Quick Reference

View File

@ -41,7 +41,7 @@ and slide rendering — all offline, no PowerPoint installation required.
gracefully (reports `{"rendered": false, "missing": [...]}`, exit 0)
when absent — all create/read/edit operations work without them.
- Check availability via `terminal`:
`python3 -c "import pptx; print(pptx.__version__)"` and `which soffice pdftoppm`.
`python -c "import pptx; print(pptx.__version__)"` and `which soffice pdftoppm`.
## How to Run
@ -49,16 +49,16 @@ All scripts live in `scripts/`, take `--help`, print JSON to stdout, and
exit non-zero on failure. Run them with `terminal`:
```bash
python3 scripts/pptx_create.py deck.json out.pptx
python3 scripts/pptx_read.py deck.pptx --outline # full JSON outline
python3 scripts/pptx_read.py deck.pptx --notes # speaker notes
python3 scripts/pptx_read.py deck.pptx --images ./img # export pictures
python3 scripts/pptx_edit.py deck.pptx --replace-text "Old Corp" "New Corp"
python3 scripts/pptx_edit.py deck.pptx --chart-data update.json
python3 scripts/pptx_edit.py deck.pptx --duplicate-slide 2
python3 scripts/pptx_edit.py deck.pptx --remove-slide 3 --move-slide 2 0
python3 scripts/pptx_from_template.py brand.pptx out.pptx --values vals.json
python3 scripts/pptx_render.py deck.pptx --outdir ./render # slide PNGs
python scripts/pptx_create.py deck.json out.pptx
python scripts/pptx_read.py deck.pptx --outline # full JSON outline
python scripts/pptx_read.py deck.pptx --notes # speaker notes
python scripts/pptx_read.py deck.pptx --images ./img # export pictures
python scripts/pptx_edit.py deck.pptx --replace-text "Old Corp" "New Corp"
python scripts/pptx_edit.py deck.pptx --chart-data update.json
python scripts/pptx_edit.py deck.pptx --duplicate-slide 2
python scripts/pptx_edit.py deck.pptx --remove-slide 3 --move-slide 2 0
python scripts/pptx_from_template.py brand.pptx out.pptx --values vals.json
python scripts/pptx_render.py deck.pptx --outdir ./render # slide PNGs
```
Author JSON specs with `write_file`; inspect script output and generated
@ -217,4 +217,4 @@ say so rather than approximating.
shapes, truncated text, and color problems the outline cannot. If the
render tools are missing, the script says so; rely on the outline.
4. The bundled test suite is the full contract:
`python3 -m pytest tests/ -q` (requires python-pptx + pytest).
`python -m pytest tests/ -q` (requires python-pptx + pytest).

View File

@ -26,7 +26,7 @@ Search and retrieve academic papers from arXiv via their free REST API. No API k
## Searching Papers
The API returns Atom XML. Parse with `grep`/`sed` or pipe through `python3` for clean output.
The API returns Atom XML. Parse with `grep`/`sed` or pipe through `python` for clean output.
### Basic search
@ -37,7 +37,7 @@ curl -s "https://export.arxiv.org/api/query?search_query=all:GRPO+reinforcement+
### Clean output (parse XML to readable format)
```bash
curl -s "https://export.arxiv.org/api/query?search_query=all:GRPO+reinforcement+learning&max_results=5&sortBy=submittedDate&sortOrder=descending" | python3 -c "
curl -s "https://export.arxiv.org/api/query?search_query=all:GRPO+reinforcement+learning&max_results=5&sortBy=submittedDate&sortOrder=descending" | python -c "
import sys, xml.etree.ElementTree as ET
ns = {'a': 'http://www.w3.org/2005/Atom'}
root = ET.parse(sys.stdin).getroot()
@ -117,7 +117,7 @@ After fetching metadata for a paper, generate a BibTeX entry:
{% raw %}
```bash
curl -s "https://export.arxiv.org/api/query?id_list=1706.03762" | python3 -c "
curl -s "https://export.arxiv.org/api/query?id_list=1706.03762" | python -c "
import sys, xml.etree.ElementTree as ET
ns = {'a': 'http://www.w3.org/2005/Atom', 'arxiv': 'http://arxiv.org/schemas/atom'}
root = ET.parse(sys.stdin).getroot()
@ -197,7 +197,7 @@ arXiv doesn't provide citation data or recommendations. Use the **Semantic Schol
```bash
# By arXiv ID
curl -s "https://api.semanticscholar.org/graph/v1/paper/arXiv:2402.03300?fields=title,authors,citationCount,referenceCount,influentialCitationCount,year,abstract" | python3 -m json.tool
curl -s "https://api.semanticscholar.org/graph/v1/paper/arXiv:2402.03300?fields=title,authors,citationCount,referenceCount,influentialCitationCount,year,abstract" | python -m json.tool
# By Semantic Scholar paper ID or DOI
curl -s "https://api.semanticscholar.org/graph/v1/paper/DOI:10.1234/example?fields=title,citationCount"
@ -206,19 +206,19 @@ curl -s "https://api.semanticscholar.org/graph/v1/paper/DOI:10.1234/example?fiel
### Get citations OF a paper (who cited it)
```bash
curl -s "https://api.semanticscholar.org/graph/v1/paper/arXiv:2402.03300/citations?fields=title,authors,year,citationCount&limit=10" | python3 -m json.tool
curl -s "https://api.semanticscholar.org/graph/v1/paper/arXiv:2402.03300/citations?fields=title,authors,year,citationCount&limit=10" | python -m json.tool
```
### Get references FROM a paper (what it cites)
```bash
curl -s "https://api.semanticscholar.org/graph/v1/paper/arXiv:2402.03300/references?fields=title,authors,year,citationCount&limit=10" | python3 -m json.tool
curl -s "https://api.semanticscholar.org/graph/v1/paper/arXiv:2402.03300/references?fields=title,authors,year,citationCount&limit=10" | python -m json.tool
```
### Search papers (alternative to arXiv search, returns JSON)
```bash
curl -s "https://api.semanticscholar.org/graph/v1/paper/search?query=GRPO+reinforcement+learning&limit=5&fields=title,authors,year,citationCount,externalIds" | python3 -m json.tool
curl -s "https://api.semanticscholar.org/graph/v1/paper/search?query=GRPO+reinforcement+learning&limit=5&fields=title,authors,year,citationCount,externalIds" | python -m json.tool
```
### Get paper recommendations
@ -226,13 +226,13 @@ curl -s "https://api.semanticscholar.org/graph/v1/paper/search?query=GRPO+reinfo
```bash
curl -s -X POST "https://api.semanticscholar.org/recommendations/v1/papers/" \
-H "Content-Type: application/json" \
-d '{"positivePaperIds": ["arXiv:2402.03300"], "negativePaperIds": []}' | python3 -m json.tool
-d '{"positivePaperIds": ["arXiv:2402.03300"], "negativePaperIds": []}' | python -m json.tool
```
### Author profile
```bash
curl -s "https://api.semanticscholar.org/graph/v1/author/search?query=Yann+LeCun&fields=name,hIndex,citationCount,paperCount" | python3 -m json.tool
curl -s "https://api.semanticscholar.org/graph/v1/author/search?query=Yann+LeCun&fields=name,hIndex,citationCount,paperCount" | python -m json.tool
```
### Useful Semantic Scholar fields
@ -261,7 +261,7 @@ curl -s "https://api.semanticscholar.org/graph/v1/author/search?query=Yann+LeCun
## Notes
- arXiv returns Atom XML — use the helper script or parsing snippet for clean output
- Semantic Scholar returns JSON — pipe through `python3 -m json.tool` for readability
- Semantic Scholar returns JSON — pipe through `python -m json.tool` for readability
- arXiv IDs: old format (`hep-th/0601001`) vs new (`2402.03300`)
- PDF: `https://arxiv.org/pdf/{id}` — Abstract: `https://arxiv.org/abs/{id}`
- HTML (when available): `https://arxiv.org/html/{id}`

View File

@ -58,12 +58,12 @@ Override per task with `--ledger <path>` or `HERMES_CITATION_LEDGER`.
```bash
S=~/.hermes/skills/research/grounded-citations/scripts/sources.py
python3 "$S" reset # start a clean ledger
python3 "$S" add https://example.com/a --title "A" # prints: [1]
python3 "$S" add https://example.com/b --title "B" # prints: [2]
python3 "$S" list # ledger table
python3 "$S" render # Sources: block
python3 "$S" verify draft.md # catch bad citations
python "$S" reset # start a clean ledger
python "$S" add https://example.com/a --title "A" # prints: [1]
python "$S" add https://example.com/b --title "B" # prints: [2]
python "$S" list # ledger table
python "$S" render # Sources: block
python "$S" verify draft.md # catch bad citations
```
`add` is idempotent and URL-normalized: the same page always returns the same
@ -136,7 +136,7 @@ upgrade from citations to evidence:
text to a file and attach the sentence(s) that carry each claim:
```bash
python3 "$S" quote 1 --text "Ice is about 9% less dense than liquid water." --from page1.txt
python "$S" quote 1 --text "Ice is about 9% less dense than liquid water." --from page1.txt
```
The quote is rejected unless it appears verbatim in the evidence text
@ -168,8 +168,8 @@ corroboration.
④ **Verify with the evidence gate and render the evidence block:**
```bash
python3 "$S" verify report.md --evidence --min-coverage 0.5
python3 "$S" render --style evidence --replace-in report.md
python "$S" verify report.md --evidence --min-coverage 0.5
python "$S" render --style evidence --replace-in report.md
```
`--evidence` fails the draft if any cited source has no attached quote. The
@ -222,7 +222,7 @@ and read the `info: stats:` line to see the counts before picking a number.
## Verification
```bash
python3 "$S" verify report.md --strict --min-coverage 0.5
python "$S" verify report.md --strict --min-coverage 0.5
```
Green means: every `[n]` in the draft exists in the ledger, the Sources block

View File

@ -148,7 +148,7 @@ A skill exists to make the agent's process more predictable — the agent reliab
## Tests and Docs (required for repo skills)
1. **Tests** live at `tests/skills/test_<skill>_skill.py` — stdlib + pytest + `unittest.mock` only, no live network. Run via `scripts/run_tests.sh tests/skills/test_<skill>_skill.py -q`. (The generic `tests/tools/test_skill_manager_tool.py` passing proves nothing about YOUR skill.)
2. **Docs regen:** run `python3 website/scripts/generate-skill-docs.py`, then apply scope discipline — the generator rewrites EVERY auto-gen page. `git checkout --` everything that isn't yours; the final diff must show only your SKILL.md, your one per-skill docs page, a one-line catalog row, and a one-line `website/sidebars.ts` insertion (verify with `search_files(pattern='<your-slug>', path='website/sidebars.ts')` — exactly one hit, or the page is an orphan).
2. **Docs regen:** run `python website/scripts/generate-skill-docs.py`, then apply scope discipline — the generator rewrites EVERY auto-gen page. `git checkout --` everything that isn't yours; the final diff must show only your SKILL.md, your one per-skill docs page, a one-line catalog row, and a one-line `website/sidebars.ts` insertion (verify with `search_files(pattern='<your-slug>', path='website/sidebars.ts')` — exactly one hit, or the page is an orphan).
3. **`.env.example`** (only if the skill needs new env vars): one clearly delimited commented block; touch nothing else in the file.
## Workflow

View File

@ -118,7 +118,7 @@ spikes/
terminal("mkdir -p spikes/001-websocket-streaming")
write_file("spikes/001-websocket-streaming/README.md", "# 001: websocket-streaming\n\n...")
write_file("spikes/001-websocket-streaming/main.py", "...")
terminal("cd spikes/001-websocket-streaming && python3 main.py")
terminal("cd spikes/001-websocket-streaming && python main.py")
# Observe output, iterate.
```

View File

@ -794,6 +794,27 @@ def _resolve_stdio_command(command: str, env: dict) -> tuple[str, dict]:
if os.sep not in resolved_command:
path_arg = resolved_env["PATH"] if "PATH" in resolved_env else None
which_hit = shutil.which(resolved_command, path=path_arg)
if which_hit is None and sys.platform == "win32" and resolved_env:
# shutil.which(..., path=...) resolves extensions from the PARENT
# process PATHEXT, not the MCP subprocess env — so a config that
# supplies both PATH and PATHEXT can fail to resolve a command
# its own env can find (#56536). Retry with the config's PATHEXT
# (any key casing: PATHEXT / Pathext / pathext) applied.
cfg_pathext = next(
(v for k, v in resolved_env.items()
if k.upper() == "PATHEXT" and isinstance(v, str) and v.strip()),
None,
)
if cfg_pathext and cfg_pathext != os.environ.get("PATHEXT"):
_saved = os.environ.get("PATHEXT")
try:
os.environ["PATHEXT"] = cfg_pathext
which_hit = shutil.which(resolved_command, path=path_arg)
finally:
if _saved is None:
os.environ.pop("PATHEXT", None)
else:
os.environ["PATHEXT"] = _saved
if which_hit:
resolved_command = which_hit
elif resolved_command in {"npx", "npm", "node"}: