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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions .claude/skills/fessctl/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
---
name: fessctl
description: Manage Fess search engine via fessctl CLI. Use for CRUD operations on web configs, file configs, data configs, schedulers, users, roles, groups, and more.
---

## Overview

fessctl is a CLI tool for managing Fess search engine via the admin API. It supports 22 resource types with standard CRUD operations.

### Prerequisites

- Python 3.13+
- fessctl installed (`pip install fessctl` or `uv pip install fessctl`)
- Fess server running with API access enabled

## Environment Setup

```bash
export FESS_ENDPOINT="http://localhost:8080" # Fess server URL
export FESS_ACCESS_TOKEN="your-access-token" # API access token
export FESS_VERSION="v1" # API version (default: v1)
```

## Output Formats

- `text` (default) - Markdown tables and structured output, AI-friendly
- `json` - Raw JSON from API
- `yaml` - YAML formatted output

Use `-o json` for programmatic parsing, `-o text` for human/AI readable output.

## Command Reference

| Resource | Commands | Description |
| --- | --- | --- |
| accesstoken | create, update, delete, get, list | API access tokens |
| badword | create, update, delete, get, list | Bad words for suggest |
| boostdoc | create, update, delete, get, list | Document boost rules |
| crawlinginfo | delete, get, list | Crawling session info |
| dataconfig | create, update, delete, get, list | Data store configs |
| duplicatehost | create, update, delete, get, list | Duplicate host mappings |
| elevateword | create, update, delete, get, list | Promoted search words |
| fileauth | create, update, delete, get, list | File auth credentials |
| fileconfig | create, update, delete, get, list | File crawl configs |
| group | create, update, delete, get, getbyname, list | User groups |
| joblog | delete, get, list | Job execution logs |
| keymatch | create, update, delete, get, list | Key match rules |
| labeltype | create, update, delete, get, list | Label types |
| pathmap | create, update, delete, get, list | Path mappings |
| relatedcontent | create, update, delete, get, list | Related content |
| relatedquery | create, update, delete, get, list | Related queries |
| reqheader | create, update, delete, get, list | Request headers |
| role | create, update, delete, get, getbyname, list | User roles |
| scheduler | create, update, delete, get, list, start, stop | Job schedulers |
| user | create, update, delete, get, getbyname, list | Users |
| webauth | create, update, delete, get, list | Web auth credentials |
| webconfig | create, update, delete, get, list | Web crawl configs |

## Common Workflows

### Health Check

```bash
fessctl ping
```

### Web Crawl Setup

```bash
# 1. Create web config
fessctl webconfig create --name "My Site" --url "https://example.com" -o json

# 2. Find the default crawler scheduler
fessctl scheduler list

# 3. Start crawling
fessctl scheduler start <scheduler-id>

# 4. Monitor job logs
fessctl joblog list
```

### User Management

```bash
# Create role
fessctl role create "editor"

# Create group
fessctl group create "team-a"

# Create user with role and group
fessctl user create "john" "password123" --role "editor" --group "team-a"

# Look up user by name
fessctl user getbyname "john"
```

### File Crawl Setup

```bash
# Create file config
fessctl fileconfig create --name "Docs" --path "/data/docs"

# Add file auth if needed
fessctl fileauth create --username "user" --file-config-id <file-config-id> --password "pass"
```

## Response Structure

- `response.status` == 0 means success
- `response.id` contains the resource ID on create
- `response.setting` contains single resource data (get)
- `response.settings` contains resource list (list)
- Exception: crawlinginfo and joblog use `response.log` / `response.logs`

## Important Patterns

- **Update**: internally does GET then PUT (merges existing data)
- **List pagination**: `--page` (default 1) and `--size` (default 100)
- **Permissions**: use `--permission "{role}guest"` format
- **Multi-value fields**: repeat the option (e.g., `--url "http://a" --url "http://b"`)

## Complete Examples

```bash
# Create a web config with multiple URLs and labels
fessctl webconfig create \
--name "Corporate Site" \
--url "https://www.example.com" \
--url "https://blog.example.com" \
--excluded-url "(?i).*(css|js|jpeg|jpg|gif|png)" \
--depth 3 \
--max-access-count 10000 \
--num-of-thread 3 \
--interval-time 5000 \
-o json

# Update a web config
fessctl webconfig update <config-id> --name "Updated Name" --depth 5

# Delete a web config
fessctl webconfig delete <config-id>

# Get details
fessctl webconfig get <config-id>

# List with pagination
fessctl webconfig list --page 1 --size 50

# Create a scheduled job
fessctl scheduler create \
--name "Nightly Crawl" \
--target "all" \
--script-type "groovy" \
--cron-expression "0 0 2 * * ?" \
--script-data "return container.getComponent(\"crawlJob\").execute();"

# Create a boost rule
fessctl boostdoc create \
--url-expr "https://important.example.com/.*" \
--boost-expr "100" \
--sort-order 1

# Create a key match
fessctl keymatch create \
--term "FAQ" \
--query "title:FAQ" \
--max-size 3 \
--boost 10.0 \
--version-no 1
```
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ requires-python = ">=3.13"
dependencies = [
"httpx==0.28.1",
"pyyaml==6.0.2",
"rich==14.0.0",
"typer[all]==0.16.0",
]

Expand Down
17 changes: 5 additions & 12 deletions src/fessctl/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from fessctl.api.client import FessAPIClient
from fessctl.config.settings import Settings
from fessctl.utils import format_result_markdown, output_error
from fessctl.commands.accesstoken import accesstoken_app
# from fessctl.commands.backup import backup_app
from fessctl.commands.badword import badword_app
Expand Down Expand Up @@ -97,25 +98,17 @@ def ping(
typer.echo(yaml.dump(result))
else:
if status == "green" and not timed_out:
typer.secho(
"Fess server is healthy (status: green).", fg=typer.colors.GREEN
)
typer.echo(format_result_markdown(True, "Fess server is healthy (status: green).", "Server", "ping"))
elif status == "yellow":
typer.secho(
f"Fess server status: {status} (timed_out: {timed_out})",
fg=typer.colors.YELLOW,
)
typer.echo(format_result_markdown(True, f"Fess server status: {status} (timed_out: {timed_out})", "Server", "ping"))
else:
message: str = result.get("response", {}).get("message", "")
typer.secho(
f"Fess server status: {status} (timed_out: {timed_out}) {message}",
fg=typer.colors.RED,
)
typer.echo(format_result_markdown(False, f"Fess server status: {status} (timed_out: {timed_out}) {message}", "Server", "ping"))
raise typer.Exit(code=1)
except typer.Exit:
raise
except Exception as e:
typer.secho(str(e), fg=typer.colors.RED)
output_error(output, e, "Server", "ping")
raise typer.Exit(code=1)


Expand Down
115 changes: 32 additions & 83 deletions src/fessctl/commands/accesstoken.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@

import typer
import yaml
from rich.console import Console
from rich.table import Table

from fessctl.api.client import FessAPIClient
from fessctl.config.settings import Settings
from fessctl.utils import to_utc_iso8601
from fessctl.utils import format_detail_markdown, format_list_markdown, format_result_markdown, to_utc_iso8601

accesstoken_app = typer.Typer()

Expand Down Expand Up @@ -66,16 +64,10 @@ def create_accesstoken(
else:
if status == 0:
accesstoken_id = result.get("response", {}).get("id", "")
typer.secho(
f"AccessToken '{accesstoken_id}' created successfully.",
fg=typer.colors.GREEN,
)
typer.echo(format_result_markdown(True, f"AccessToken '{accesstoken_id}' created successfully.", "AccessToken", "create", accesstoken_id))
else:
message: str = result.get("response", {}).get("message", "")
typer.secho(
f"Operation failed. {message} Status code: {status}",
fg=typer.colors.RED,
)
typer.echo(format_result_markdown(False, f"Operation failed. {message} Status code: {status}", "AccessToken", "create"))
raise typer.Exit(code=status)


Expand Down Expand Up @@ -113,10 +105,7 @@ def update_accesstoken(

if result.get("response", {}).get("status", 1) != 0:
message: str = result.get("response", {}).get("message", "")
typer.secho(
f"AccessToken with ID '{accesstoken_id}' not found. {message}",
fg=typer.colors.RED,
)
typer.echo(format_result_markdown(False, f"AccessToken with ID '{accesstoken_id}' not found. {message}", "AccessToken", "update"))
raise typer.Exit(code=1)

config = result.get("response", {}).get("setting", {})
Expand Down Expand Up @@ -145,16 +134,10 @@ def update_accesstoken(
else:
if status == 0:
accesstoken_id = result.get("response", {}).get("id", "")
typer.secho(
f"AccessToken '{accesstoken_id}' updated successfully.",
fg=typer.colors.GREEN,
)
typer.echo(format_result_markdown(True, f"AccessToken '{accesstoken_id}' updated successfully.", "AccessToken", "update", accesstoken_id))
else:
message: str = result.get("response", {}).get("message", "")
typer.secho(
f"Failed to update AccessToken. {message} Status code: {status}",
fg=typer.colors.RED,
)
typer.echo(format_result_markdown(False, f"Failed to update AccessToken. {message} Status code: {status}", "AccessToken", "update"))
raise typer.Exit(code=status)


Expand All @@ -178,16 +161,10 @@ def delete_accesstoken(
typer.echo(yaml.dump(result))
else:
if status == 0:
typer.secho(
f"AccessToken '{accesstoken_id}' deleted successfully.",
fg=typer.colors.GREEN,
)
typer.echo(format_result_markdown(True, f"AccessToken '{accesstoken_id}' deleted successfully.", "AccessToken", "delete", accesstoken_id))
else:
message: str = result.get("response", {}).get("message", "")
typer.secho(
f"Failed to delete AccessToken. {message} Status code: {status}",
fg=typer.colors.RED,
)
typer.echo(format_result_markdown(False, f"Failed to delete AccessToken. {message} Status code: {status}", "AccessToken", "delete"))
raise typer.Exit(code=status)


Expand All @@ -212,41 +189,27 @@ def get_accesstoken(
else:
if status == 0:
accesstoken = result.get("response", {}).get("setting", {})
console = Console()
table = Table(
title=f"AccessToken Details: {accesstoken.get('name', '-')}")
table.add_column("Field", style="cyan", no_wrap=True)
table.add_column("Value", style="magenta")

# Output each field according to the latest AccessToken schema
table.add_row("id", str(accesstoken.get("id", "-")))
table.add_row("updated_by", str(
accesstoken.get("updated_by", "-")))
table.add_row(
"updated_time", to_utc_iso8601(accesstoken.get("updated_time"))
)
table.add_row("version_no", str(
accesstoken.get("version_no", "-")))
table.add_row("name", str(accesstoken.get("name", "-")))
table.add_row("token", str(accesstoken.get("token", "-")))
table.add_row("permissions", str(
accesstoken.get("permissions", "-")))
table.add_row("parameter_name", str(
accesstoken.get("parameter_name", "-")))
table.add_row("expires", str(accesstoken.get("expires", "-")))
table.add_row("created_by", str(
accesstoken.get("created_by", "-")))
table.add_row(
"created_time", to_utc_iso8601(accesstoken.get("created_time"))
)

console.print(table)
typer.echo(format_detail_markdown(
f"AccessToken Details: {accesstoken.get('name', '-')}",
accesstoken,
[
("id", "id"),
("updated_by", "updated_by"),
("updated_time", "updated_time"),
("version_no", "version_no"),
("name", "name"),
("token", "token"),
("permissions", "permissions"),
("parameter_name", "parameter_name"),
("expires", "expires"),
("created_by", "created_by"),
("created_time", "created_time"),
],
transforms={"updated_time": to_utc_iso8601, "created_time": to_utc_iso8601},
))
else:
message: str = result.get("response", {}).get("message", "")
typer.secho(
f"Failed to retrieve AccessToken. {message} Status code: {status}",
fg=typer.colors.RED,
)
typer.echo(format_result_markdown(False, f"Failed to retrieve AccessToken. {message} Status code: {status}", "AccessToken", "get"))
raise typer.Exit(code=status)


Expand All @@ -273,26 +236,12 @@ def list_accesstokens(
if status == 0:
accesstokens = result.get("response", {}).get("settings", [])
if not accesstokens:
typer.secho("No AccessTokens found.", fg=typer.colors.YELLOW)
typer.echo("No AccessTokens found.")
else:
console = Console()
table = Table(title="AccessTokens")
table.add_column("ID", style="cyan", no_wrap=True)
table.add_column("NAME", style="cyan", no_wrap=True)
table.add_column("EXPIRES", style="cyan", no_wrap=True)
table.add_column("PERMISSIONS", style="cyan", no_wrap=False)
for accesstoken in accesstokens:
table.add_row(
accesstoken.get("id", "-"),
accesstoken.get("name", "-"),
accesstoken.get("expires", "-"),
accesstoken.get("permissions", "-"),
)
console.print(table)
typer.echo(format_list_markdown("AccessTokens", accesstokens, [
("ID", "id"), ("NAME", "name"), ("EXPIRES", "expires"), ("PERMISSIONS", "permissions"),
]))
else:
message: str = result.get("response", {}).get("message", "")
typer.secho(
f"Failed to list AccessTokens. {message} Status code: {status}",
fg=typer.colors.RED,
)
typer.echo(format_result_markdown(False, f"Failed to list AccessTokens. {message} Status code: {status}", "AccessToken", "list"))
raise typer.Exit(code=status)
Loading