fix: address all 6 reviewer items from PR #188

Blockers (3):
- .gitignore: add Step 5 (/n8n/* and /n8n/.*), clean up misplaced entries
- Versions: unify to 2.4.7 everywhere (n8n_cli, pyproject, registry, e2e test)
- repl_skin.py: replace custom 135-line module with standard ReplSkin class
  (522-line canonical version + n8n coral accent + module-level wrappers)

Should fix (3):
- Rebase onto current upstream/main (1dcf1e0)
- Remove CHANGELOG.md (non-standard, no other harness has one)
- README: English only (removed Spanish section for consistency)

Also:
- Add setup.py following project convention (blender/godot pattern)
- Remove LICENSE and N8N.md (non-standard)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Juan Jose Sanchez Bernal
2026-04-08 07:49:16 +02:00
parent 36aba59d04
commit c9332e4003
8 changed files with 787 additions and 513 deletions

9
.gitignore vendored
View File

@@ -87,6 +87,7 @@
!/renderdoc/
!/cloudcompare/
!/openscreen/
!/n8n/
# Step 5: Inside each software dir, ignore everything (including dotfiles)
/gimp/*
@@ -153,6 +154,8 @@
/wiremock/.*
/exa/*
/exa/.*
/n8n/*
/n8n/.*
# Step 6: ...except agent-harness/
!/gimp/agent-harness/
@@ -190,6 +193,7 @@
!/wiremock/
!/wiremock/agent-harness/
!/exa/agent-harness/
!/n8n/agent-harness/
# Step 7: Ignore build artifacts within allowed dirs
**/__pycache__/
@@ -223,8 +227,5 @@ assets/gen_typing_gif.py
/notebooklm/*
/notebooklm/.*
!/notebooklm/agent-harness/
!/intelwatch/agent-harness/
!/intelwatch/
!/exa/
!/n8n/
!/n8n/agent-harness/
!/intelwatch/agent-harness/

View File

@@ -1,205 +0,0 @@
# Changelog
## [2.2.0] - 2026-04-02
### Hardening (review round 6)
- **Defensive dict access**: Replaced ALL `dict['key']` with `dict.get('key', default)` in display code for external API data (templates, npm nodes, bulk operations). Prevents KeyError crashes from malformed API responses.
- 15+ unsafe dict accesses fixed across template search, node search, node info, bulk activate/deactivate, and scaffold display
## [2.1.9] - 2026-04-02
### Fixes (manual review + round 6)
- **API key in config show**: No longer shows partial key preview — just "configured" or empty
- Consistent with health --diagnostic which already used this approach
## [2.1.8] - 2026-04-02
### Fixes (adversarial security bounty — round 5)
- **REPL quoted args**: REPL now uses `shlex.split()` — handles `'{"name": "my workflow"}'` correctly
- **Error message leaking**: Server error responses no longer leak raw text (only JSON "message" field, truncated to 200 chars)
## [2.1.7] - 2026-04-02
### Critical Fixes (adversarial bug bounty — round 5)
- **Silent data corruption in autofix**: Connection key fix ("0"->"main") now MERGES instead of overwriting existing "main" connections
- **Rollback could reactivate workflows**: `versions rollback` now strips `active` field — use `activate` command explicitly
- **Scaffold pattern mutation**: `get_scaffold()` now returns deep copy — calling code can't corrupt templates
- **Lying test fixed**: `test_export_import_roundtrip` now uses real `_clean_for_api()` instead of reimplementing the logic
## [2.1.6] - 2026-04-02
### Bug Fixes (review round 4)
- **IndexError in `patch --connect`**: Fixed crash when node's "main" connection list exists but is empty
- **AttributeError in `patch --remove-node/--disconnect`**: Connection list items can be None/malformed — now filtered safely
- **JSONDecodeError in config load**: Corrupted `config.json` no longer crashes the entire CLI — falls back to defaults
## [2.1.5] - 2026-04-02
### Critical Bug Fixes (review round 3 — code agent)
- **Data corruption in autofix**: `_set_nested()` now correctly handles bracket notation (`assignments[0].value`) — previously created corrupt keys like `"assignments[0]"` instead of updating list items
- **Crash on malformed workflows**: `autofix()` no longer crashes when `nodes` or `connections` are `None` — handles gracefully
- Hardened all node/connection iteration with type checks
- Added regression tests for both bugs
## [2.1.4] - 2026-04-01
### Security Fixes (review round 3)
- **Prevent accidental workflow activation**: `create`, `update`, `import`, `restore-all`, `template deploy` now explicitly force `active=False`. Use `activate` command to enable.
- **npm registry data validation**: Truncate and validate all fields from untrusted npm responses
- **template deploy**: Force `active=False` on deployed templates
## [2.1.3] - 2026-04-01
### Security Fixes (review round 2 — security agent)
- **Path traversal**: 3 additional file reads in diff/validate/autofix now use centralized `_load_json_arg()` with `Path.resolve()`
- **Webhook URL sanitization**: Strip special chars from webhook path to prevent URL manipulation
- **Error message leaking**: Removed raw server response from error output
## [2.1.2] - 2026-04-01
### Fixes (from review round 2)
- **SQLite race condition**: Added WAL journal mode + 10s timeout for concurrent access
- **Filename encoding**: Safe filename sanitization for workflows with unicode/special chars
- **Watch busy-loop**: Reject `--interval 0` to prevent CPU burn
- **Duplicate dict key**: Removed duplicate `api_key_set` in health diagnostic
- **Versions diff same**: Warn when comparing version with itself
- Added `_safe_filename()` helper for cross-platform filename safety
## [2.1.1] - 2026-04-01
### Security Fixes (from 3-agent code review)
- **Path traversal prevention**: `_load_json_arg()` now resolves paths before opening
- **API key no longer exposed**: `health --diagnostic` shows only "configured/NOT SET"
- **File permissions**: config.json (0600) and versions.db directory (0700) are now restricted
- **Specific exceptions**: Replaced bare `except Exception: pass` with targeted exception types
### Code Quality Fixes
- Extracted `_clean_for_api()` helper — eliminated 7 duplicated filter expressions
- Added `_INTERNAL_FIELDS` constant for workflow metadata fields
- Fixed `versions_diff` missing `@click.pass_context` decorator
- Fixed `_iter_params` to detect expressions inside lists (not just dicts)
- Reviewed by: code-reviewer, security-reviewer, python-reviewer agents
## [2.1.0] - 2026-04-01
### Added
- `node search <query>` — search 26,000+ n8n community node packages on npm
- `node info <package>` — get package details, nodes provided, install command
- New module: `core/nodes.py`
## [2.0.0] - 2026-04-01
### Added
- `workflow scaffold <pattern>` — generate workflows from 5 proven patterns (webhook, api, database, ai-agent, scheduled)
- `workflow patterns` — list available scaffold patterns
- `expression <expr>` — validate n8n expression syntax offline
- New modules: `core/scaffolds.py`, `core/expressions.py`
### Changed
- Version bump to 2.0.0 — the CLI is now feature-complete for the n8n Public API
- 10 core modules, 60+ commands, 80+ tests
## [1.7.0] - 2026-04-01
### Added
- `workflow versions list/show/rollback/diff/prune/stats` — full version tracking with local SQLite
- Auto-snapshot before every write operation (update, patch, autofix --apply)
- Rollback to any previous version with automatic pre-rollback backup
- Version diff between any two stored snapshots
- Storage stats and pruning for disk management
## [1.6.0] - 2026-04-01
### Added
- `workflow autofix` — detect and repair 6 types of common issues (expression format, webhook paths, broken connections, duplicate names, numeric connection keys, unused error outputs). Preview mode by default.
- `workflow patch` — incremental updates: `--rename`, `--enable-node`, `--disable-node`, `--remove-node`, `--connect`, `--disconnect`. No need to send the full workflow JSON.
- `health` — full health check with response time, connectivity test, and `--diagnostic` mode
- New module `core/fixers.py` with pluggable fix engine
### Inspired by
- `WorkflowAutoFixer` and `WorkflowDiffEngine` from [n8n-mcp](https://github.com/czlonkowski/n8n-mcp)
## [1.5.0] - 2026-04-01
### Added
- `template search <query>` — search 2,700+ templates on n8n.io
- `template get <id>` — view template details
- `template deploy <id>` — deploy template directly to your n8n instance
- `workflow validate <id|@file>` — validate workflow structure (nodes, connections, triggers, duplicates)
- `workflow test <id>` — trigger webhook-based workflows with test data
### Inspired by
- Features from [n8n-mcp](https://github.com/czlonkowski/n8n-mcp) adapted for CLI usage
## [1.4.0] - 2026-04-01
### Added
- `workflow backup-all` — backup ALL workflows to a folder (disaster recovery)
- `workflow restore-all` — restore workflows from a backup folder (with `--dry-run`)
- `workflow diff` — compare two workflows or a workflow vs a local file (colored diff)
- `execution errors` — quick view of recent failures with optional `--details`
## [1.3.0] - 2026-04-01
### Added
- `config test` — verify your n8n connection works before doing anything
- `workflow search <query>` — find workflows by name (case-insensitive)
- `workflow bulk-activate --tag X` / `--search X` — activate multiple workflows at once
- `workflow bulk-deactivate --tag X` / `--search X` — deactivate multiple workflows at once
- `completions bash|zsh|fish` — generate shell completion scripts
- Colored status/active columns in table output (green=success, red=error, etc.)
## [1.2.0] - 2026-04-01
### Added
- `workflow export` — save any workflow to a portable JSON file
- `workflow import` — create a workflow from a JSON file (with optional name override)
- `execution watch` — live monitoring of executions with real-time polling
- `status` — dashboard showing workflows, recent executions, and errors at a glance
- REPL tab-completion for all commands and subcommands
- GitHub Actions CI (Python 3.10-3.13)
## [1.1.0] - 2026-03-31
### Changed
- **BREAKING**: Removed `credential list` and `credential update` commands (not supported by n8n Public API v1.1.1)
- **BREAKING**: Removed `execution stop` command (not in public API)
- **BREAKING**: Removed `table` command group (Data Tables not in public API v1.1.1)
- Aligned all endpoints with verified n8n OpenAPI spec v1.1.1
- Version bumped to 1.1.0
### Added
- `workflow set-tags` command to update workflow tags
- `workflow transfer` command to transfer workflows between projects
- `credential transfer` command to transfer credentials between projects
- URL validation in `config set base_url`
- Configurable timeout via `N8N_TIMEOUT` env var (default: 30s)
- Ellipsis indicator when table columns are truncated
- Better JSON error messages (`@file.json` not found, invalid JSON)
### Fixed
- `credential list` no longer crashes with 405 (command removed — API doesn't support it)
- `_load_json_arg` now shows clear error for missing files and invalid JSON
- API key is always masked in `config show`, even with `--json`
### Removed
- `setup.py` (replaced by `pyproject.toml`)
- `core/session.py` (unused REPL state dataclass)
- `core/tables.py` (Data Tables not in n8n Public API)
- `execution stop/stop-all` (not in public API)
- `execution tags/set-tags` (not in public API)
## [1.0.0] - 2026-03-31
### Added
- Initial release
- CLI harness following CLI-Anything pattern
- Workflow management (list, get, create, update, delete, activate, deactivate, tags)
- Execution management (list, get, delete, retry)
- Credential management (create, delete, schema)
- Variable management (CRUD)
- Tag management (CRUD)
- Interactive REPL mode with colored output
- JSON output mode (`--json`)
- Config persistence (`~/.cli-anything/n8n/config.json`)
- 29 unit tests, E2E test suite

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Juan Jose Sanchez Bernal / Webcomunica Soluciones Informaticas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,63 +0,0 @@
# N8N.md — Architecture
## Overview
`cli-anything-n8n` wraps the n8n Public API v1.1.1 into a Click-based CLI, following the CLI-Anything harness pattern by HKUDS. Only verified endpoints from the OpenAPI spec are exposed.
## n8n Compatibility
- **Minimum version**: n8n >= 1.0.0
- **API version**: Public API v1.1.1
- **Verified against**: n8n 2.43.0
- **OpenAPI spec**: fetched from `/api/v1/openapi.yml`
## Architecture
```
cli_anything/n8n/
├── n8n_cli.py # Click CLI (groups, commands, REPL, error handling)
├── core/ # Business logic (thin wrappers over backend)
│ ├── workflows.py # 9 endpoints (CRUD, activate, tags, transfer)
│ ├── executions.py # 4 endpoints (list, get, delete, retry)
│ ├── credentials.py # 4 endpoints (create, delete, schema, transfer)
│ ├── variables.py # 4 endpoints (CRUD)
│ ├── tags.py # 5 endpoints (CRUD)
│ └── project.py # Config load/save
├── utils/
│ ├── n8n_backend.py # HTTP client (sole module making requests)
│ └── repl_skin.py # Banner, colors, table formatting
├── skills/
│ └── SKILL.md # AI agent discovery
└── tests/
├── test_core.py # Unit tests (mocked)
└── test_full_e2e.py # E2E tests (live n8n)
```
## Data Flow
```
User/Agent -> n8n_cli.py (Click) -> core/*.py -> n8n_backend.py -> n8n REST API
```
## Authentication
Header-based: `X-N8N-API-KEY: <key>`
Resolution order: CLI `--api-key` > env `N8N_API_KEY` > config file
## What's NOT in the Public API
These n8n features are NOT available through the public REST API v1.1.1:
- **Data Tables**: no endpoints at all
- **Credential listing/updating**: only create, delete, schema, transfer
- **Execution stop**: only list, get, delete, retry
- **Workflow execute**: not in public API (use internal API or n8n UI)
- **Audit logs**: POST-only, not exposed in CLI
## Conventions
- All core functions accept `base_url` and `api_key` as keyword args
- All commands support `--json` for parseable output
- PEP 420 namespace: `cli_anything/` has NO `__init__.py`
- Entry point: `cli-anything-n8n` -> `cli_anything.n8n.n8n_cli:main`
- Timeout configurable via `N8N_TIMEOUT` env var (default: 30s)

View File

@@ -6,84 +6,141 @@
[![CLI-Anything](https://img.shields.io/badge/CLI--Anything-harness-orange.svg)](https://github.com/HKUDS/CLI-Anything)
[![n8n API v1.1.1](https://img.shields.io/badge/n8n-API%20v1.1.1-EA4B71.svg)](https://docs.n8n.io/api/api-reference/)
> [Leer en Español](#spanish) | [Read in English](#english)
---
<a id="english"></a>
## English
Control your [n8n](https://n8n.io) instance from the terminal. List workflows, check executions, manage tags — all without opening the browser.
Built with the [CLI-Anything](https://github.com/HKUDS/CLI-Anything) pattern, so AI agents can use it too.
### Try it now (2 minutes)
## Installation
```bash
# 1. Install
pip install cli-anything-n8n
```
# 2. Connect to your n8n
## Quick Start
```bash
# Connect to your n8n
export N8N_BASE_URL=https://your-n8n-instance.com
export N8N_API_KEY=your-api-key
# 3. Try it!
# Try it!
cli-anything-n8n workflow list
```
> **Where do I get my API key?** In n8n, go to Settings > API > Create API Key.
### What can I do?
## JSON Output Mode
All commands support `--json` for machine-readable output:
```bash
# List your workflows
cli-anything-n8n workflow list
# See only active workflows
cli-anything-n8n workflow list --active
# Check failed executions
cli-anything-n8n execution list --status error
# Get details of a specific workflow
cli-anything-n8n workflow get ABC123
# Create a tag
cli-anything-n8n tag create "production"
# Get JSON output (for scripts or AI agents)
cli-anything-n8n --json workflow list
cli-anything-n8n --json execution list --status error
```
# Interactive mode — just type commands
## Interactive REPL
```bash
cli-anything-n8n
n8n> workflow list
n8n> tag list
n8n> exit
```
### All commands
## Command Groups
| Group | Commands | What it does |
|-------|----------|--------------|
| **workflow** | list, get, create, update, delete, activate, deactivate, tags, set-tags, transfer | Manage your workflows |
| **execution** | list, get, delete, retry | Check and retry executions |
| **credential** | create, delete, schema, transfer | Manage credentials |
| **variable** | list, create, update, delete | Manage environment variables |
| **tag** | list, get, create, update, delete | Organize with tags |
| **config** | show, set | Save your connection settings |
### Save your connection (so you don't type it every time)
```bash
cli-anything-n8n config set base_url https://your-n8n-instance.com
cli-anything-n8n config set api_key your-api-key
# Now just use it directly
cli-anything-n8n workflow list
### Workflow Management
```
workflow list - List all workflows
workflow get - Get workflow details
workflow create - Create a workflow from JSON
workflow update - Update a workflow from JSON
workflow delete - Delete a workflow
workflow activate - Activate a workflow
workflow deactivate - Deactivate a workflow
workflow tags - List tags on a workflow
workflow set-tags - Set tags on a workflow
workflow transfer - Transfer ownership
workflow export - Export to file
workflow import - Import from file
workflow backup-all - Backup all workflows
workflow restore-all - Restore from backup
workflow diff - Compare two workflows
workflow bulk-activate - Activate multiple workflows
workflow bulk-deactivate - Deactivate multiple workflows
workflow validate - Validate workflow structure
workflow autofix - Auto-fix common issues
workflow patch - Patch workflow fields
workflow test - Test a webhook workflow
workflow scaffold - Create from built-in patterns
workflow patterns - List available patterns
workflow versions - Local version snapshots (list/show/rollback/diff/prune/stats)
```
### Configuration options
### Execution Management
```
execution list - List executions
execution get - Get execution details
execution delete - Delete an execution
execution retry - Retry a failed execution
execution errors - List failed executions
execution watch - Watch executions in real-time
```
### Template Management
```
template search - Search n8n.io templates
template get - Get template details
template deploy - Deploy a template
```
### Node Discovery
```
node search - Search community nodes
node info - Get node package details
```
### Credential Management
```
credential create - Create a credential
credential delete - Delete a credential
credential schema - Show credential schema
credential transfer - Transfer ownership
```
### Variable Management
```
variable list - List variables
variable create - Create a variable
variable update - Update a variable
variable delete - Delete a variable
```
### Tag Management
```
tag list - List tags
tag get - Get tag details
tag create - Create a tag
tag update - Update a tag
tag delete - Delete a tag
```
### Configuration
```
config show - Show current configuration
config set - Set a configuration value
config test - Test the connection
```
### Standalone
```
status - Show n8n instance status
health - Health check
expression - Validate n8n expressions offline
completions - Generate shell completions
```
## Configuration
| Method | Priority | Example |
|--------|----------|---------|
@@ -93,157 +150,50 @@ cli-anything-n8n workflow list
Extra env vars: `N8N_TIMEOUT` (default: 30 seconds)
### For AI agents and scripts
## Running Tests
```bash
# Always use --json for machine-readable output
cli-anything-n8n --json workflow list
# From the agent-harness directory:
# Pass complex data from files
cli-anything-n8n workflow create @my-workflow.json
# Run all unit tests
python3 -m pytest cli_anything/n8n/tests/test_core.py -v
# Check exit codes
cli-anything-n8n workflow get ABC123 && echo "OK" || echo "FAILED"
```
### n8n compatibility
Verified against **n8n 2.43.0** (Public API v1.1.1). Works with any n8n >= 1.0.0.
> **Note**: Some n8n features (Data Tables, credential listing, execution stop) are not available through the public API. This CLI only exposes verified, working endpoints.
### Development
```bash
git clone https://github.com/webcomunicasolutions/cli-n8n.git
cd cli-n8n
pip install -e .
# Run tests (no n8n needed)
pip install pytest
pytest cli_anything/n8n/tests/test_core.py -v
# Run E2E tests (needs a running n8n)
# Run E2E tests (needs a running n8n instance)
export N8N_BASE_URL=https://your-n8n.com
export N8N_API_KEY=your-key
pytest cli_anything/n8n/tests/test_full_e2e.py -v
python3 -m pytest cli_anything/n8n/tests/test_full_e2e.py -v
```
### Project structure
## Architecture
```
cli_anything/n8n/
├── n8n_cli.py # CLI + interactive REPL
├── n8n_cli.py # CLI + interactive REPL (55+ commands)
├── core/ # API wrappers (one file per resource)
│ ├── workflows.py
│ ├── executions.py
│ ├── credentials.py
│ ├── variables.py
│ ├── tags.py
│ ├── templates.py
│ ├── nodes.py
│ ├── versions.py # Local SQLite version snapshots
│ ├── fixers.py # Workflow autofix engine
│ ├── scaffolds.py # Built-in workflow patterns
│ ├── expressions.py # Offline expression validator
│ └── project.py # Config management
├── utils/
│ ├── n8n_backend.py # HTTP client
│ └── repl_skin.py # Terminal UI
│ └── repl_skin.py # Terminal UI (standard cli-anything skin)
├── skills/
│ └── SKILL.md # Agent discovery metadata
└── tests/
├── test_core.py # Unit tests (mocked)
├── test_core.py # Unit tests (mocked HTTP)
└── test_full_e2e.py # E2E tests (live n8n)
```
### License
## n8n Compatibility
MIT - Juan Jose Sanchez Bernal / [Webcomunica Soluciones Informaticas](https://webcomunica.solutions)
Verified against **n8n 2.43.0** (Public API v1.1.1). Works with any n8n >= 1.0.0.
---
<a id="spanish"></a>
## Español
Controla tu instancia de [n8n](https://n8n.io) desde la terminal. Lista workflows, revisa ejecuciones, gestiona tags — todo sin abrir el navegador.
Construido con el patron [CLI-Anything](https://github.com/HKUDS/CLI-Anything), para que agentes IA tambien puedan usarlo.
### Pruebalo ahora (2 minutos)
```bash
# 1. Instalar
pip install cli-anything-n8n
# 2. Conectar a tu n8n
export N8N_BASE_URL=https://tu-instancia-n8n.com
export N8N_API_KEY=tu-api-key
# 3. Probar!
cli-anything-n8n workflow list
```
> **Donde consigo mi API key?** En n8n, ve a Settings > API > Create API Key.
### Que puedo hacer?
```bash
# Listar workflows
cli-anything-n8n workflow list
# Solo los activos
cli-anything-n8n workflow list --active
# Ver ejecuciones fallidas
cli-anything-n8n execution list --status error
# Detalle de un workflow
cli-anything-n8n workflow get ABC123
# Crear un tag
cli-anything-n8n tag create "produccion"
# Salida JSON (para scripts o agentes IA)
cli-anything-n8n --json workflow list
# Modo interactivo
cli-anything-n8n
n8n> workflow list
n8n> tag list
n8n> exit
```
### Todos los comandos
| Grupo | Comandos | Que hace |
|-------|----------|----------|
| **workflow** | list, get, create, update, delete, activate, deactivate, tags, set-tags, transfer | Gestionar workflows |
| **execution** | list, get, delete, retry | Revisar y reintentar ejecuciones |
| **credential** | create, delete, schema, transfer | Gestionar credenciales |
| **variable** | list, create, update, delete | Gestionar variables de entorno |
| **tag** | list, get, create, update, delete | Organizar con tags |
| **config** | show, set | Guardar configuracion de conexion |
### Guardar conexion (para no escribirla cada vez)
```bash
cli-anything-n8n config set base_url https://tu-instancia-n8n.com
cli-anything-n8n config set api_key tu-api-key
# Ahora usalo directamente
cli-anything-n8n workflow list
```
### Compatibilidad con n8n
Verificado contra **n8n 2.43.0** (API publica v1.1.1). Funciona con cualquier n8n >= 1.0.0.
> **Nota**: Algunas funcionalidades de n8n (Data Tables, listar credenciales, parar ejecuciones) no estan disponibles via la API publica. Este CLI solo expone endpoints verificados y funcionales.
### Desarrollo
```bash
git clone https://github.com/webcomunicasolutions/cli-n8n.git
cd cli-n8n
pip install -e .
pip install pytest
pytest cli_anything/n8n/tests/test_core.py -v
```
### Licencia
MIT - Juan Jose Sanchez Bernal / [Webcomunica Soluciones Informaticas](https://webcomunica.solutions)
> **Note**: Some n8n features (Data Tables, credential listing, execution stop) are not available through the public API. This CLI only exposes verified, working endpoints.

View File

@@ -51,7 +51,7 @@ class TestCLISubprocess:
[*_resolve_cli(), "--version"], capture_output=True, text=True, timeout=10,
)
assert result.returncode == 0
assert "2.4.6" in result.stdout
assert "2.4.7" in result.stdout
def test_workflow_help(self):
result = subprocess.run(

View File

@@ -1,41 +1,573 @@
"""REPL UI — banner, prompt, colors, table formatting."""
"""cli-anything REPL Skin — Unified terminal interface for all CLI harnesses.
from __future__ import annotations
Copy this file into your CLI package at:
cli_anything/<software>/utils/repl_skin.py
Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("n8n", version="2.4.7")
skin.print_banner() # auto-detects skills/SKILL.md inside the package
prompt_text = skin.prompt(project_name="my_workflow", modified=True)
skin.success("Workflow activated")
skin.error("Connection failed")
skin.warning("Unsaved changes")
skin.info("Processing 24 workflows...")
skin.status("Status", "Connected")
skin.table(headers, rows)
skin.print_goodbye()
"""
import json
import os
import shutil
import sys
from typing import Any
import click
# ── ANSI color codes (no external deps for core styling) ──────────────
# --- Colors ---
_RESET = "\033[0m"
_BOLD = "\033[1m"
_DIM = "\033[2m"
_ITALIC = "\033[3m"
_UNDERLINE = "\033[4m"
ORANGE = "bright_yellow"
GREEN = "green"
RED = "red"
CYAN = "cyan"
DIM = "bright_black"
# Brand colors
_CYAN = "\033[38;5;80m" # cli-anything brand cyan
_CYAN_BG = "\033[48;5;80m"
_WHITE = "\033[97m"
_GRAY = "\033[38;5;245m"
_DARK_GRAY = "\033[38;5;240m"
_LIGHT_GRAY = "\033[38;5;250m"
# Software accent colors — each software gets a unique accent
_ACCENT_COLORS = {
"gimp": "\033[38;5;214m", # warm orange
"blender": "\033[38;5;208m", # deep orange
"inkscape": "\033[38;5;39m", # bright blue
"audacity": "\033[38;5;33m", # navy blue
"libreoffice": "\033[38;5;40m", # green
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
"n8n": "\033[38;5;203m", # n8n coral/red (#EA4B71)
}
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
# Status colors
_GREEN = "\033[38;5;78m"
_YELLOW = "\033[38;5;220m"
_RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
_ICON = f"{_CYAN}{_BOLD}{_RESET}"
_ICON_SMALL = f"{_CYAN}{_RESET}"
# ── Box drawing characters ────────────────────────────────────────────
_H_LINE = ""
_V_LINE = ""
_TL = ""
_TR = ""
_BL = ""
_BR = ""
_T_DOWN = ""
_T_UP = ""
_T_RIGHT = ""
_T_LEFT = ""
_CROSS = ""
# --- Banner ---
BANNER = r"""
_ __ | (_) __ _ _ __ _ _| |_| |__ (_)_ __ __ _ _ __ | |_ _ __
/ __| | | |___ / _` | '_ \| | | | __| '_ \| | '_ \ / _` | | '_ \| | | '_ \
| (__ | | | | (_| | | | | |_| | |_| | | | | | | | (_| | | | | | |_| | | |
\___|_|_| \__,_|_| |_|\__, |\__|_| |_|_|_| |_|\__, | |_| |_|_|_|_| |_|
|___/ |___/
"""
def _strip_ansi(text: str) -> str:
"""Remove ANSI escape codes for length calculation."""
import re
return re.sub(r"\033\[[^m]*m", "", text)
def print_banner(base_url: str) -> None:
click.secho(BANNER, fg=ORANGE)
click.secho(f" Connected to: {base_url}", fg=GREEN)
click.secho(" Type 'help' for commands, 'exit' to quit.\n", fg=DIM)
def _visible_len(text: str) -> int:
"""Get visible length of text (excluding ANSI codes)."""
return len(_strip_ansi(text))
# --- Output helpers ---
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
Provides consistent branding, prompts, and message formatting
across all CLI harnesses built with the cli-anything methodology.
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
software: Software name (e.g., "gimp", "shotcut", "blender").
version: CLI version string.
history_file: Path for persistent command history.
Defaults to ~/.cli-anything-<software>/history
skill_path: Path to the SKILL.md file for agent discovery.
Auto-detected from the package's skills/ directory if not provided.
Displayed in banner for AI agents to know where to read skill info.
"""
self.software = software.lower().replace("-", "_")
self.display_name = software.replace("_", " ").title()
self.version = version
# Auto-detect skill path from package layout:
# cli_anything/<software>/utils/repl_skin.py (this file)
# cli_anything/<software>/skills/SKILL.md (target)
if skill_path is None:
from pathlib import Path
_auto = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
if _auto.is_file():
skill_path = str(_auto)
self.skill_path = skill_path
self.accent = _ACCENT_COLORS.get(self.software, _DEFAULT_ACCENT)
# History file
if history_file is None:
from pathlib import Path
hist_dir = Path.home() / f".cli-anything-{self.software}"
hist_dir.mkdir(parents=True, exist_ok=True)
self.history_file = str(hist_dir / "history")
else:
self.history_file = history_file
# Detect terminal capabilities
self._color = self._detect_color_support()
def _detect_color_support(self) -> bool:
"""Check if terminal supports color."""
if os.environ.get("NO_COLOR"):
return False
if os.environ.get("CLI_ANYTHING_NO_COLOR"):
return False
if not hasattr(sys.stdout, "isatty"):
return False
return sys.stdout.isatty()
def _c(self, code: str, text: str) -> str:
"""Apply color code if colors are supported."""
if not self._color:
return text
return f"{code}{text}{_RESET}"
# ── Banner ────────────────────────────────────────────────────────
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
pad = inner - _visible_len(content)
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
# Title: ◆ cli-anything · N8N
icon = self._c(_CYAN + _BOLD, "")
brand = self._c(_CYAN + _BOLD, "cli-anything")
dot = self._c(_DARK_GRAY, "·")
name = self._c(self.accent + _BOLD, self.display_name)
title = f" {icon} {brand} {dot} {name}"
ver = f" {self._c(_DARK_GRAY, f' v{self.version}')}"
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
# Skill path for agent discovery
skill_line = None
if self.skill_path:
skill_icon = self._c(_MAGENTA, "")
skill_label = self._c(_DARK_GRAY, " Skill:")
skill_path_display = self._c(_LIGHT_GRAY, self.skill_path)
skill_line = f" {skill_icon} {skill_label} {skill_path_display}"
print(top)
print(_box_line(title))
print(_box_line(ver))
if skill_line:
print(_box_line(skill_line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
print()
# ── Prompt ────────────────────────────────────────────────────────
def prompt(self, project_name: str = "", modified: bool = False,
context: str = "") -> str:
"""Build a styled prompt string for prompt_toolkit or input().
Args:
project_name: Current project name (empty if none open).
modified: Whether the project has unsaved changes.
context: Optional extra context to show in prompt.
Returns:
Formatted prompt string.
"""
parts = []
# Icon
if self._color:
parts.append(f"{_CYAN}{_RESET} ")
else:
parts.append("> ")
# Software name
parts.append(self._c(self.accent + _BOLD, self.software))
# Project context
if project_name or context:
ctx = context or project_name
mod = "*" if modified else ""
parts.append(f" {self._c(_DARK_GRAY, '[')}")
parts.append(self._c(_LIGHT_GRAY, f"{ctx}{mod}"))
parts.append(self._c(_DARK_GRAY, ']'))
parts.append(self._c(_GRAY, " "))
return "".join(parts)
def prompt_tokens(self, project_name: str = "", modified: bool = False,
context: str = ""):
"""Build prompt_toolkit formatted text tokens for the prompt.
Use with prompt_toolkit's FormattedText for proper ANSI handling.
Returns:
list of (style, text) tuples for prompt_toolkit.
"""
tokens = []
tokens.append(("class:icon", ""))
tokens.append(("class:software", self.software))
if project_name or context:
ctx = context or project_name
mod = "*" if modified else ""
tokens.append(("class:bracket", " ["))
tokens.append(("class:context", f"{ctx}{mod}"))
tokens.append(("class:bracket", "]"))
tokens.append(("class:arrow", " "))
return tokens
def get_prompt_style(self):
"""Get a prompt_toolkit Style object matching the skin.
Returns:
prompt_toolkit.styles.Style
"""
try:
from prompt_toolkit.styles import Style
except ImportError:
return None
accent_hex = _ANSI_256_TO_HEX.get(self.accent, "#5fafff")
return Style.from_dict({
"icon": "#5fdfdf bold", # cyan brand color
"software": f"{accent_hex} bold",
"bracket": "#585858",
"context": "#bcbcbc",
"arrow": "#808080",
# Completion menu
"completion-menu.completion": "bg:#303030 #bcbcbc",
"completion-menu.completion.current": f"bg:{accent_hex} #000000",
"completion-menu.meta.completion": "bg:#303030 #808080",
"completion-menu.meta.completion.current": f"bg:{accent_hex} #000000",
# Auto-suggest
"auto-suggest": "#585858",
# Bottom toolbar
"bottom-toolbar": "bg:#1c1c1c #808080",
"bottom-toolbar.text": "#808080",
})
# ── Messages ──────────────────────────────────────────────────────
def success(self, message: str):
"""Print a success message with green checkmark."""
icon = self._c(_GREEN + _BOLD, "")
print(f" {icon} {self._c(_GREEN, message)}")
def error(self, message: str):
"""Print an error message with red cross."""
icon = self._c(_RED + _BOLD, "")
print(f" {icon} {self._c(_RED, message)}", file=sys.stderr)
def warning(self, message: str):
"""Print a warning message with yellow triangle."""
icon = self._c(_YELLOW + _BOLD, "")
print(f" {icon} {self._c(_YELLOW, message)}")
def info(self, message: str):
"""Print an info message with blue dot."""
icon = self._c(_BLUE, "")
print(f" {icon} {self._c(_LIGHT_GRAY, message)}")
def hint(self, message: str):
"""Print a subtle hint message."""
print(f" {self._c(_DARK_GRAY, message)}")
def section(self, title: str):
"""Print a section header."""
print()
print(f" {self._c(self.accent + _BOLD, title)}")
print(f" {self._c(_DARK_GRAY, _H_LINE * len(title))}")
# ── Status display ────────────────────────────────────────────────
def status(self, label: str, value: str):
"""Print a key-value status line."""
lbl = self._c(_GRAY, f" {label}:")
val = self._c(_WHITE, f" {value}")
print(f"{lbl}{val}")
def status_block(self, items: dict[str, str], title: str = ""):
"""Print a block of status key-value pairs.
Args:
items: Dict of label -> value pairs.
title: Optional title for the block.
"""
if title:
self.section(title)
max_key = max(len(k) for k in items) if items else 0
for label, value in items.items():
lbl = self._c(_GRAY, f" {label:<{max_key}}")
val = self._c(_WHITE, f" {value}")
print(f"{lbl}{val}")
def progress(self, current: int, total: int, label: str = ""):
"""Print a simple progress indicator.
Args:
current: Current step number.
total: Total number of steps.
label: Optional label for the progress.
"""
pct = int(current / total * 100) if total > 0 else 0
bar_width = 20
filled = int(bar_width * current / total) if total > 0 else 0
bar = "\u2588" * filled + "\u2591" * (bar_width - filled)
text = f" {self._c(_CYAN, bar)} {self._c(_GRAY, f'{pct:3d}%')}"
if label:
text += f" {self._c(_LIGHT_GRAY, label)}"
print(text)
# ── Table display ─────────────────────────────────────────────────
def table(self, headers: list[str], rows: list[list[str]],
max_col_width: int = 40):
"""Print a formatted table with box-drawing characters.
Args:
headers: Column header strings.
rows: List of rows, each a list of cell strings.
max_col_width: Maximum column width before truncation.
"""
if not headers:
return
# Calculate column widths
col_widths = [min(len(h), max_col_width) for h in headers]
for row in rows:
for i, cell in enumerate(row):
if i < len(col_widths):
col_widths[i] = min(
max(col_widths[i], len(str(cell))), max_col_width
)
def pad(text: str, width: int) -> str:
t = str(text)[:width]
return t + " " * (width - len(t))
# Header
header_cells = [
self._c(_CYAN + _BOLD, pad(h, col_widths[i]))
for i, h in enumerate(headers)
]
sep = self._c(_DARK_GRAY, f" {_V_LINE} ")
header_line = f" {sep.join(header_cells)}"
print(header_line)
# Separator
print(self._c(_DARK_GRAY, f" {'───'.join([_H_LINE * w for w in col_widths])}"))
# Rows
for row in rows:
cells = []
for i, cell in enumerate(row):
if i < len(col_widths):
cells.append(self._c(_LIGHT_GRAY, pad(str(cell), col_widths[i])))
row_sep = self._c(_DARK_GRAY, f" {_V_LINE} ")
print(f" {row_sep.join(cells)}")
# ── Help display ──────────────────────────────────────────────────
def help(self, commands: dict[str, str]):
"""Print a formatted help listing.
Args:
commands: Dict of command -> description pairs.
"""
self.section("Commands")
max_cmd = max(len(c) for c in commands) if commands else 0
for cmd, desc in commands.items():
cmd_styled = self._c(self.accent, f" {cmd:<{max_cmd}}")
desc_styled = self._c(_GRAY, f" {desc}")
print(f"{cmd_styled}{desc_styled}")
print()
# ── Goodbye ───────────────────────────────────────────────────────
def print_goodbye(self):
"""Print a styled goodbye message."""
print(f"\n {_ICON_SMALL} {self._c(_GRAY, 'Goodbye!')}\n")
# ── Prompt toolkit session factory ────────────────────────────────
def create_prompt_session(self):
"""Create a prompt_toolkit PromptSession with skin styling.
Returns:
A configured PromptSession, or None if prompt_toolkit unavailable.
"""
try:
from prompt_toolkit import PromptSession
from prompt_toolkit.history import FileHistory
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
style = self.get_prompt_style()
session = PromptSession(
history=FileHistory(self.history_file),
auto_suggest=AutoSuggestFromHistory(),
style=style,
enable_history_search=True,
)
return session
except ImportError:
return None
def get_input(self, pt_session, project_name: str = "",
modified: bool = False, context: str = "") -> str:
"""Get input from user using prompt_toolkit or fallback.
Args:
pt_session: A prompt_toolkit PromptSession (or None).
project_name: Current project name.
modified: Whether project has unsaved changes.
context: Optional context string.
Returns:
User input string (stripped).
"""
if pt_session is not None:
from prompt_toolkit.formatted_text import FormattedText
tokens = self.prompt_tokens(project_name, modified, context)
return pt_session.prompt(FormattedText(tokens)).strip()
else:
raw_prompt = self.prompt(project_name, modified, context)
return input(raw_prompt).strip()
# ── Toolbar builder ───────────────────────────────────────────────
def bottom_toolbar(self, items: dict[str, str]):
"""Create a bottom toolbar callback for prompt_toolkit.
Args:
items: Dict of label -> value pairs to show in toolbar.
Returns:
A callable that returns FormattedText for the toolbar.
"""
def toolbar():
from prompt_toolkit.formatted_text import FormattedText
parts = []
for i, (k, v) in enumerate(items.items()):
if i > 0:
parts.append(("class:bottom-toolbar.text", ""))
parts.append(("class:bottom-toolbar.text", f" {k}: "))
parts.append(("class:bottom-toolbar", v))
return FormattedText(parts)
return toolbar
# ── ANSI 256-color to hex mapping (for prompt_toolkit styles) ─────────
_ANSI_256_TO_HEX = {
"\033[38;5;33m": "#0087ff", # audacity navy blue
"\033[38;5;35m": "#00af5f", # shotcut teal
"\033[38;5;39m": "#00afff", # inkscape bright blue
"\033[38;5;40m": "#00d700", # libreoffice green
"\033[38;5;55m": "#5f00af", # obs purple
"\033[38;5;69m": "#5f87ff", # kdenlive slate blue
"\033[38;5;75m": "#5fafff", # default sky blue
"\033[38;5;80m": "#5fd7d7", # brand cyan
"\033[38;5;203m": "#ff5f5f", # n8n coral
"\033[38;5;208m": "#ff8700", # blender deep orange
"\033[38;5;214m": "#ffaf00", # gimp warm orange
}
# ── Module-level convenience wrappers ─────────────────────────────────
# These delegate to a lazily-initialized ReplSkin singleton so that
# n8n_cli.py can do `from ...repl_skin import success, error, warn`
# while still routing through the standard ReplSkin class.
_skin: ReplSkin | None = None
def _get_skin() -> ReplSkin:
global _skin
if _skin is None:
# Import VERSION lazily to avoid circular imports
try:
from cli_anything.n8n.n8n_cli import VERSION
except ImportError:
VERSION = "0.0.0"
_skin = ReplSkin("n8n", version=VERSION)
return _skin
def print_banner() -> None:
"""Print the cli-anything branded banner."""
_get_skin().print_banner()
def success(msg: str) -> None:
"""Print a success message."""
_get_skin().success(msg)
def error(msg: str) -> None:
"""Print an error message."""
_get_skin().error(msg)
def warn(msg: str) -> None:
"""Print a warning message."""
_get_skin().warning(msg)
# ── n8n-specific output helpers ───────────────────────────────────────
# These handle --json flag and n8n API response formatting.
# Not part of the standard ReplSkin because they depend on click and
# n8n API response structure (data/nextCursor pagination).
def output(data: Any, as_json: bool) -> None:
"""Print data as JSON or human-readable."""
@@ -47,7 +579,7 @@ def output(data: Any, as_json: bool) -> None:
if "data" in data and isinstance(data["data"], list):
_print_table(data["data"])
if "nextCursor" in data:
click.secho(f"\n Next cursor: {data['nextCursor']}", fg=DIM)
click.secho(f"\n Next cursor: {data['nextCursor']}", fg="bright_black")
else:
_print_dict(data)
elif isinstance(data, list):
@@ -63,18 +595,18 @@ def _print_dict(d: dict[str, Any], indent: int = 0) -> None:
prefix = " " * indent
for k, v in d.items():
if isinstance(v, dict):
click.secho(f"{prefix}{k}:", fg=CYAN)
click.secho(f"{prefix}{k}:", fg="cyan")
_print_dict(v, indent + 1)
elif isinstance(v, list) and v and isinstance(v[0], dict):
click.secho(f"{prefix}{k}:", fg=CYAN)
click.secho(f"{prefix}{k}:", fg="cyan")
_print_table(v)
else:
click.echo(f"{prefix}{click.style(str(k), fg=CYAN)}: {v}")
click.echo(f"{prefix}{click.style(str(k), fg='cyan')}: {v}")
def _print_table(rows: list[dict[str, Any]]) -> None:
if not rows:
click.secho(" (empty)", fg=DIM)
click.secho(" (empty)", fg="bright_black")
return
term_width = shutil.get_terminal_size().columns
@@ -97,14 +629,17 @@ def _print_table(rows: list[dict[str, Any]]) -> None:
max_col = max(10, term_width // len(simple_keys) - 3)
col_widths = {k: min(v, max_col) for k, v in col_widths.items()}
header = " | ".join(click.style(k.ljust(col_widths[k])[:col_widths[k]], fg=CYAN) for k in simple_keys)
header = " | ".join(
click.style(k.ljust(col_widths[k])[:col_widths[k]], fg="cyan")
for k in simple_keys
)
click.echo(header)
click.echo("-+-".join("-" * col_widths[k] for k in simple_keys))
# Color rules for specific columns
color_rules = {
"status": {"success": GREEN, "error": RED, "running": ORANGE, "waiting": CYAN},
"active": {"True": GREEN, "False": DIM},
"status": {"success": "green", "error": "red", "running": "bright_yellow", "waiting": "cyan"},
"active": {"True": "green", "False": "bright_black"},
}
for row in rows:
@@ -121,15 +656,3 @@ def _print_table(rows: list[dict[str, Any]]) -> None:
cell = click.style(cell, fg=color_rules[k][v])
vals.append(cell)
click.echo(" | ".join(vals))
def success(msg: str) -> None:
click.secho(f" OK: {msg}", fg=GREEN)
def error(msg: str) -> None:
click.secho(f" ERROR: {msg}", fg=RED, err=True)
def warn(msg: str) -> None:
click.secho(f" WARN: {msg}", fg=ORANGE)

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""
Setup script for cli-anything-n8n
Install (dev mode):
pip install -e .
Build:
python -m build
Publish:
twine upload dist/*
"""
from pathlib import Path
from setuptools import setup, find_namespace_packages
ROOT = Path(__file__).parent
README = ROOT / "cli_anything/n8n/README.md"
long_description = README.read_text(encoding="utf-8") if README.exists() else ""
setup(
name="cli-anything-n8n",
version="2.4.7",
description="CLI harness for n8n workflow automation — n8n REST API v1.1.1",
long_description=long_description,
long_description_content_type="text/markdown",
author="Juan Jose Sanchez Bernal",
author_email="info@webcomunica.solutions",
url="https://github.com/HKUDS/CLI-Anything",
project_urls={
"Source": "https://github.com/HKUDS/CLI-Anything",
"Tracker": "https://github.com/HKUDS/CLI-Anything/issues",
"PyPI": "https://pypi.org/project/cli-anything-n8n/",
},
license="MIT",
packages=find_namespace_packages(include=("cli_anything.*",)),
python_requires=">=3.10",
install_requires=[
"click>=8.1",
"prompt-toolkit>=3.0",
"requests>=2.28",
],
extras_require={
"dev": [
"pytest>=7",
"pytest-cov>=4",
],
},
entry_points={
"console_scripts": [
"cli-anything-n8n=cli_anything.n8n.n8n_cli:main",
],
},
package_data={
"cli_anything.n8n": ["skills/*.md"],
},
include_package_data=True,
zip_safe=False,
keywords=[
"cli",
"n8n",
"workflow",
"automation",
"cli-anything",
],
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
],
)