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
7 changes: 6 additions & 1 deletion skills/fessctl/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: fessctl
description: Operate Fess via fessctl CLI (admin API client). Use when managing webconfigs, file/data configs, schedulers, users/roles/groups, dictionaries, access tokens, or any Fess admin resource from Claude. Covers 25 resource types and cross-feature workflows like initial crawl setup.
description: Operate Fess via fessctl CLI (admin API client). Use when managing webconfigs, file/data configs, schedulers, users/roles/groups, dictionaries, access tokens, or any Fess admin resource from Claude. Covers 30 resource types and cross-feature workflows like initial crawl setup.
license: Apache-2.0
version: 0.1.0
---
Expand Down Expand Up @@ -70,3 +70,8 @@ Each file documents one Fess admin feature: what it is, when to use it, fessctl
| Path mapping | references/features/pathmap.md |
| Duplicate host | references/features/duplicatehost.md |
| Request header | references/features/reqheader.md |
| System info | references/features/systeminfo.md |
| Server stats | references/features/stats.md |
| Log files | references/features/log.md |
| Backup files | references/features/backup.md |
| Suggest words | references/features/suggest.md |
48 changes: 48 additions & 0 deletions skills/fessctl/references/features/backup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Backup files (fessctl `backup`)

## What it is
The files the Admin UI backup page offers for download: index dumps in Elasticsearch/OpenSearch bulk format (`fess_config.bulk`, `fess_basic_config.bulk`, `fess_user.bulk`), the `system.properties` file, the index mappings (`fess.json`, `doc.json`) and the search, click, favorite, user-info and chat logs as NDJSON. The admin API lists the ids and streams one file. It is download only; restoring a backup is not available through the admin API.

Available in every version fessctl supports (14.19.2, 15.6.1, 15.7.0, 15.8.0, 15.9.0), with differences noted below.

## When to use
- Take a copy of the Fess configuration (crawl configs, labels, users, system properties) before an upgrade or a risky change.
- Export the search log for analysis outside Fess.

## Subcommand surface
| Subcommand | Purpose | Required arguments |
| --- | --- | --- |
| `list` | List the downloadable files. | none |
| `download` | Write one file to a local file. Refuses to overwrite unless `--force` is given. | `<backup_id> --dest PATH` |

Always reconfirm with `fessctl backup <sub> --help`.

## Resource JSON shape
```json
{
"response": {
"status": 0,
"total": 2,
"files": [
{"name": "fess_config.bulk", "id": "fess_config.bulk"},
{"name": "search_log.ndjson", "id": "search_log.ndjson"}
]
}
}
```
The id equals the name. `download` with `--output json` returns `{"response": {"status": 0, "path": "...", "size": 6463}}`. An unknown id returns `status` 1 with `Could not find any backup index.` (`Could not find index for backup.` in 14.x) and creates no file.

## Gotchas
- The dumps contain everything stored in the index, including access tokens and crawl credentials. Treat the files as secrets.
- An empty file is a valid result: a search log with no entries downloads as 0 bytes.
- Before 15.9.0 `fess.json` and `doc.json` are listed but download as empty files (the mapping export was added in 15.9.0), and `chat_log.ndjson` is listed only from 15.9.0.
- Fess answers a path whose last segment has an extension (`.../backup/file/fess_config.bulk`) with a bare `404 Not Found.` in 15.9 and a redirect in older versions, instead of running the action. fessctl requests the id with a trailing slash, which Fess serves.
- The file is written to a temporary `.part` file next to the destination and renamed on success, so a failed transfer leaves no partial file. Large dumps are streamed; the read timeout applies per chunk (60 s).

## Examples
```bash
fessctl backup list
fessctl backup download fess_config.bulk --dest ./fess_config.bulk
fessctl backup download system.properties --dest ./system.properties --force
fessctl backup download search_log.ndjson --dest ./search_log.ndjson -o json
```
45 changes: 45 additions & 0 deletions skills/fessctl/references/features/log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Log files (fessctl `log`)

## What it is
The log files in the log directory of the Fess server (`audit.log`, `fess.log`, `fess-crawler.log`, ... plus rotated `*.log.gz`), as listed on the Admin UI log files page. The admin API lists them and streams one file.

Available in every version fessctl supports (14.19.2, 15.6.1, 15.7.0, 15.8.0, 15.9.0). Only names ending in `.log` or `.log.gz` are served.

## When to use
- Fetch the server logs for a bug report without shell access to the server.
- Check the crawler, thumbnail or audit log after a failed job.

## Subcommand surface
| Subcommand | Purpose | Required arguments |
| --- | --- | --- |
| `list` | List the log files with their id and last-modified time. | none |
| `download` | Write one log file to a local file. Refuses to overwrite unless `--force` is given. | `<log_id> --dest PATH` |

Always reconfirm with `fessctl log <sub> --help`.

## Resource JSON shape
```json
{
"response": {
"status": 0,
"total": 2,
"files": [
{"name": "fess.log", "id": "ZmVzcy5sb2c=", "lastModified": "2026-10-16T12:23:38"},
{"name": "audit.log", "id": "YXVkaXQubG9n", "lastModified": "2026-10-16T12:23:49"}
]
}
}
```
The `id` is the URL-safe Base64 of the file name. `lastModified` is in the server's local time without a zone (and camelCase, unlike the rest of the API). `download` with `--output json` returns `{"response": {"status": 0, "path": "...", "size": 981}}`.

## Gotchas
- An empty log file downloads as an empty file; a log that Fess has not written to yet is common. Fess also answers an id that names no log file with an empty download, so `download` checks the id against `list` first and fails with `not found` instead.
- Fess decodes the id as standard Base64 while `list` hands out URL-safe Base64. The two differ only for `-` and `_` in an id, which appear only for file names containing characters such as `>`, `?`, `~` or non-ASCII; such a log file cannot be downloaded through the API (`Could not find ...`).
- The file is written to a temporary `.part` file next to the destination and renamed on success, so a failed transfer leaves no partial file.

## Examples
```bash
fessctl log list
fessctl log download "$(fessctl log list -o json | jq -r '.response.files[] | select(.name == "fess.log") | .id')" --dest ./fess.log
fessctl log download YXVkaXQubG9n --dest ./audit.log --force
```
46 changes: 46 additions & 0 deletions skills/fessctl/references/features/stats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Stats (fessctl `stats`)

## What it is
Runtime statistics of the Fess server: JVM memory, garbage collection and threads, OS memory and load, process file descriptors and CPU, the search engine cluster health, and the filesystem roots.

Read only. Available in every version fessctl supports (14.19.2, 15.6.1, 15.7.0, 15.8.0, 15.9.0).

## When to use
- Check heap usage, thread counts or the load average of the Fess process.
- Check the search engine cluster status (`engine.status`) and shard counts, or the free disk space (`fs`).

## Subcommand surface
`stats` takes no subcommand; it takes `--output text|json|yaml`.

| Command | Purpose |
| --- | --- |
| `fessctl stats` | Show the statistics. Text output is a `NAME \| VALUE` table of dotted names such as `jvm.memory.heap.used`; list items are named by their key or path (`jvm.gc[young].count`, `fs[/].free`). `json`/`yaml` return the response unchanged. |

Always reconfirm with `fessctl stats --help`.

## Resource JSON shape
```json
{
"response": {
"status": 0,
"stats": {
"jvm": {"memory": {"heap": {"used": 366768360, "max": 536870912, "percent": 68}}, "gc": [{"key": "young", "count": 10, "time": 48}], "threads": {"count": 37, "peak": 37}, "uptime": 26797},
"os": {"memory": {"physical": {"free": 5921939456, "total": 50425147392}}, "cpu": {"percent": 0}, "load_averages": [2.12, 0.98, 0.86]},
"process": {"file_fescriptor": {"open": 221, "max": 1048576}},
"engine": {"status": "green", "cluster_name": "fess-search", "number_of_nodes": 1, "active_shards": 65, "unassigned_shards": 0},
"fs": [{"path": "/", "used": 182361546752, "free": 101632278528, "total": 270233210880, "percent": 67}]
}
}
}
```
Sizes are bytes, times are milliseconds. A few keys are spelled oddly by the server and passed through unchanged (for example `process.file_fescriptor`).

## Gotchas
- When Fess cannot reach the search engine, the call still succeeds: `engine.status` is `red` and `engine.exception` carries the message. Check the field, not the exit code.

## Examples
```bash
fessctl stats
fessctl stats -o json | jq '.response.stats.engine.status'
fessctl stats -o json | jq '.response.stats.jvm.memory.heap.percent'
```
45 changes: 45 additions & 0 deletions skills/fessctl/references/features/suggest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Suggest words (fessctl `suggest`)

## What it is
The suggest index holds the words behind the search box auto-complete. Fess fills it from two sources: the content of crawled documents (document words) and the queries users searched for (query words). The Admin UI Suggest page shows the counts and offers the deletes; the admin API offers the same.

Available in every version fessctl supports (14.19.2, 15.6.1, 15.7.0, 15.8.0, 15.9.0).

## When to use
- Check whether suggestions exist (`info`): the counts are 0 until the **Suggest Indexer** scheduler job has run.
- Remove words after a change in the crawl targets, or remove query words that should not be suggested.

## Subcommand surface
| Subcommand | Purpose | Required arguments |
| --- | --- | --- |
| `info` | Show `total_words_num`, `document_words_num` and `query_words_num`. | none |
| `delete-all` | Delete every word. Fess re-adds the elevate words afterwards. Asks for confirmation unless `--yes` is given. | none |
| `delete-document` | Delete the words that come from documents. Asks for confirmation unless `--yes` is given. | none |
| `delete-query` | Delete the words that come from search queries. Asks for confirmation unless `--yes` is given. | none |

Always reconfirm with `fessctl suggest <sub> --help`.

## Resource JSON shape
```json
{
"response": {
"status": 0,
"setting": {"total_words_num": 17, "document_words_num": 17, "query_words_num": 4}
}
}
```
The delete commands return only `status` (and `version`).

## Gotchas
- A word can come from both sources, so `document_words_num` plus `query_words_num` can exceed `total_words_num`; `delete-document` removes the document origin of such a word and `delete-query` the query origin.
- Deleted words come back: the **Suggest Indexer** job (`fessctl scheduler start suggest_indexer`) re-creates the document words from the index and the query words from the search log.
- The counts are read from the search engine and can lag a moment behind an index run.
- None of these APIs needs the CodeLibs search engine plugins (the 15.9 `404` for plugin-less engines applies to the dictionary APIs only).

## Examples
```bash
fessctl suggest info
fessctl suggest delete-query --yes
fessctl suggest delete-all --yes --output json
fessctl scheduler start suggest_indexer
```
44 changes: 44 additions & 0 deletions skills/fessctl/references/features/systeminfo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# System info (fessctl `systeminfo`)

## What it is
The system information of the running Fess server, the data of the Admin UI system information page: the properties to attach to a bug report, the environment variables, the `fess_config.properties` values in effect, and the JVM system properties.

Read only. Available in every version fessctl supports (14.19.2, 15.6.1, 15.7.0, 15.8.0, 15.9.0).

## When to use
- Attach the server environment to a bug report or support request.
- Check which configuration value Fess is really using (`fess_props`) or which environment variable reached the JVM (`env_props`).

## Subcommand surface
`systeminfo` takes no subcommand; it takes `--output text|json|yaml`.

| Command | Purpose |
| --- | --- |
| `fessctl systeminfo` | Show the four property groups. Text output is one `NAME \| VALUE` table per group (the Fess group alone has several hundred rows); `json`/`yaml` return the full response. |

Always reconfirm with `fessctl systeminfo --help`.

## Resource JSON shape
```json
{
"response": {
"status": 0,
"version": "15.9",
"bug_report_props": [{"label": "java.runtime.version", "value": "21.0.12.1+1-LTS"}],
"env_props": [{"label": "FESS_PORT", "value": "8080"}],
"fess_props": [{"label": "domain.title", "value": "Fess"}],
"system_props": [{"label": "java.specification.version", "value": "21"}]
}
}
```

## Gotchas
- The output includes environment variables and configuration values of the server; review it before pasting it into a public issue.
- Use `--output json` with `jq` to pick one value: `fessctl systeminfo -o json | jq -r '.response.fess_props[] | select(.label == "domain.title") | .value'`.

## Examples
```bash
fessctl systeminfo
fessctl systeminfo --output yaml
fessctl systeminfo -o json | jq -r '.response.bug_report_props[] | "\(.label)=\(.value)"'
```
2 changes: 1 addition & 1 deletion skills/fessctl/references/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,4 @@ fessctl badword create --suggest-word internal-test

## Out of scope (today)

These recipes intentionally restrict themselves to features fessctl currently wraps. Operations that depend on `failureurl`, `plugin`, `backup`, `searchlist`, `stats`, `storage`, `suggest`, `systeminfo`, or `documents` are not represented here because fessctl 0.1.0 does not expose those subcommands. When fessctl adds them, update this file with the additional recipes.
These recipes intentionally restrict themselves to features fessctl currently wraps. Operations that depend on `failureurl`, `plugin`, `searchlist`, `storage`, or `documents` are not represented here because fessctl 0.1.0 does not expose those subcommands. When fessctl adds them, update this file with the additional recipes.
120 changes: 120 additions & 0 deletions src/fessctl/api/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
from enum import Enum
from pathlib import Path
from typing import Dict, List, Optional

import httpx
Expand Down Expand Up @@ -129,6 +130,45 @@ def send_request(
content=f"Invalid JSON response (HTTP {response.status_code}): {response.text}"
) from e

def _download(self, url: str, dest: Path) -> dict:
"""
Streams a file download to ``dest``.

Fess answers a failed download with a JSON error body, which is returned like any other
response; a successful download returns ``{"response": {"status": 0, "path": ..., "size": ...}}``.
The file is written next to ``dest`` first, so a failed transfer leaves no partial file.
"""
part = dest.with_name(dest.name + ".part")
headers = {"Authorization": self.admin_api_headers["Authorization"]}
try:
with httpx.stream(
"GET", url, headers=headers, timeout=httpx.Timeout(self.timeout, read=60.0)
) as response:
# A backup may itself be application/x-ndjson, so match the JSON error type exactly.
if response.status_code != 200 or response.headers.get("content-type", "").startswith("application/json"):
response.read()
try:
return response.json()
except json.decoder.JSONDecodeError as e:
raise FessAPIClientError(
status_code=response.status_code,
content=f"Invalid JSON response (HTTP {response.status_code}): {response.text}"
) from e
size = 0
with part.open("wb") as f:
for chunk in response.iter_bytes():
f.write(chunk)
size += len(chunk)
part.replace(dest)
except httpx.HTTPError as e:
raise FessAPIClientError(
status_code=-1,
content=f"Network error: {str(e)}"
) from e
finally:
part.unlink(missing_ok=True)
return {"response": {"status": 0, "path": str(dest), "size": size}}

def ping(self) -> dict:
"""
Sends a GET request to the health endpoint of the API to check the service status.
Expand Down Expand Up @@ -712,6 +752,86 @@ def install_theme(self, name: str, version: str) -> dict:
url = f"{self.base_url}/api/admin/theme"
return self.send_request(Action.CREATE, url, json_data={"name": name, "version": version})

# SystemInfo / Stats APIs

def get_systeminfo(self) -> dict:
"""
Retrieves the system information (bug report, environment, Fess and system properties).
"""
url = f"{self.base_url}/api/admin/systeminfo"
return self.send_request(Action.GET, url)

def get_stats(self) -> dict:
"""
Retrieves the server statistics (JVM, OS, process, engine and filesystem).
"""
url = f"{self.base_url}/api/admin/stats"
return self.send_request(Action.GET, url)

# Log APIs

def get_log_files(self) -> dict:
"""
Retrieves the log files in the log directory of the Fess server.
"""
url = f"{self.base_url}/api/admin/log/files"
return self.send_request(Action.GET, url)

def download_log_file(self, log_id: str, dest: Path) -> dict:
"""
Downloads a log file to ``dest``. ``log_id`` is the ``id`` returned by get_log_files.
"""
url = f"{self.base_url}/api/admin/log/file/{log_id}"
return self._download(url, dest)

# Backup APIs

def get_backup_files(self) -> dict:
"""
Retrieves the files that can be downloaded as a backup.
"""
url = f"{self.base_url}/api/admin/backup/files"
return self.send_request(Action.GET, url)

def download_backup_file(self, backup_id: str, dest: Path) -> dict:
"""
Downloads a backup file to ``dest``. ``backup_id`` is the ``id`` returned by get_backup_files.
"""
# An id such as "fess_config.bulk" ends in an extension, and Fess answers such a path
# with a bare "404 Not Found." instead of running the action; the trailing slash avoids it.
url = f"{self.base_url}/api/admin/backup/file/{backup_id}/"
return self._download(url, dest)

# Suggest APIs

def get_suggest_info(self) -> dict:
"""
Retrieves the number of words in the suggest index.
"""
url = f"{self.base_url}/api/admin/suggest"
return self.send_request(Action.GET, url)

def delete_all_suggest_words(self) -> dict:
"""
Deletes every word from the suggest index.
"""
url = f"{self.base_url}/api/admin/suggest/all"
return self.send_request(Action.DELETE, url)

def delete_document_suggest_words(self) -> dict:
"""
Deletes the words that come from documents from the suggest index.
"""
url = f"{self.base_url}/api/admin/suggest/document"
return self.send_request(Action.DELETE, url)

def delete_query_suggest_words(self) -> dict:
"""
Deletes the words that come from search queries from the suggest index.
"""
url = f"{self.base_url}/api/admin/suggest/query"
return self.send_request(Action.DELETE, url)

# WebAuth APIs

def create_webauth(self, config: dict) -> dict:
Expand Down
Loading
Loading