mirror of
https://github.com/HKUDS/CLI-Anything.git
synced 2026-05-06 14:11:57 +08:00
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:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -6,84 +6,141 @@
|
||||
[](https://github.com/HKUDS/CLI-Anything)
|
||||
[](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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
89
n8n/agent-harness/setup.py
Normal file
89
n8n/agent-harness/setup.py
Normal 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",
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user