keepassxc-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- keepassxc_cli/__init__.py +1 -0
- keepassxc_cli/__main__.py +77 -0
- keepassxc_cli/commands/__init__.py +1 -0
- keepassxc_cli/commands/add.py +48 -0
- keepassxc_cli/commands/clip.py +61 -0
- keepassxc_cli/commands/edit.py +56 -0
- keepassxc_cli/commands/generate.py +47 -0
- keepassxc_cli/commands/lock.py +32 -0
- keepassxc_cli/commands/ls.py +33 -0
- keepassxc_cli/commands/mkdir.py +33 -0
- keepassxc_cli/commands/rm.py +40 -0
- keepassxc_cli/commands/search.py +41 -0
- keepassxc_cli/commands/setup.py +33 -0
- keepassxc_cli/commands/show.py +37 -0
- keepassxc_cli/commands/status.py +49 -0
- keepassxc_cli/commands/totp.py +33 -0
- keepassxc_cli/config.py +50 -0
- keepassxc_cli/output.py +167 -0
- keepassxc_cli-0.1.0.dist-info/METADATA +226 -0
- keepassxc_cli-0.1.0.dist-info/RECORD +24 -0
- keepassxc_cli-0.1.0.dist-info/WHEEL +5 -0
- keepassxc_cli-0.1.0.dist-info/entry_points.txt +2 -0
- keepassxc_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- keepassxc_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""CLI entry point for keepassxc-cli."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
11
|
+
from keepassxc_browser_api.exceptions import KeePassXCError, ConnectionError
|
|
12
|
+
|
|
13
|
+
from .config import CliConfig, DEFAULT_CLI_CONFIG_PATH
|
|
14
|
+
from .commands import setup, status, show, search, ls, add, edit, rm, totp, clip, generate, lock, mkdir
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
prog="keepassxc-cli",
|
|
20
|
+
description="CLI for KeePassXC using the browser extension protocol with biometric unlock",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--config",
|
|
24
|
+
default=str(DEFAULT_CLI_CONFIG_PATH),
|
|
25
|
+
help="Path to CLI config file (default: %(default)s)",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--browser-api-config",
|
|
29
|
+
default=None,
|
|
30
|
+
help="Path to browser API config file",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--format",
|
|
34
|
+
choices=["table", "json", "tsv"],
|
|
35
|
+
default=None,
|
|
36
|
+
dest="fmt",
|
|
37
|
+
help="Output format (default: table)",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose logging")
|
|
40
|
+
|
|
41
|
+
subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
|
|
42
|
+
subparsers.required = True
|
|
43
|
+
|
|
44
|
+
setup.add_parser(subparsers)
|
|
45
|
+
status.add_parser(subparsers)
|
|
46
|
+
show.add_parser(subparsers)
|
|
47
|
+
search.add_parser(subparsers)
|
|
48
|
+
ls.add_parser(subparsers)
|
|
49
|
+
add.add_parser(subparsers)
|
|
50
|
+
edit.add_parser(subparsers)
|
|
51
|
+
rm.add_parser(subparsers)
|
|
52
|
+
totp.add_parser(subparsers)
|
|
53
|
+
clip.add_parser(subparsers)
|
|
54
|
+
generate.add_parser(subparsers)
|
|
55
|
+
lock.add_parser(subparsers)
|
|
56
|
+
mkdir.add_parser(subparsers)
|
|
57
|
+
|
|
58
|
+
args = parser.parse_args()
|
|
59
|
+
|
|
60
|
+
if args.verbose:
|
|
61
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
62
|
+
|
|
63
|
+
cli_config_path = Path(args.config)
|
|
64
|
+
cli_config = CliConfig.load(cli_config_path)
|
|
65
|
+
|
|
66
|
+
browser_api_config_path = Path(args.browser_api_config or cli_config.browser_api_config_path)
|
|
67
|
+
browser_config = BrowserConfig.load(browser_api_config_path)
|
|
68
|
+
|
|
69
|
+
fmt = args.fmt or cli_config.default_format
|
|
70
|
+
|
|
71
|
+
client = BrowserClient(browser_config)
|
|
72
|
+
try:
|
|
73
|
+
rc = args.func(client, args, cli_config, browser_config, browser_api_config_path, fmt=fmt)
|
|
74
|
+
except (KeePassXCError, ConnectionError, OSError, json.JSONDecodeError) as e:
|
|
75
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
76
|
+
rc = 1
|
|
77
|
+
sys.exit(rc)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import getpass
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
9
|
+
|
|
10
|
+
from keepassxc_cli.config import CliConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
14
|
+
p = subparsers.add_parser("add", help="Add a new entry")
|
|
15
|
+
p.add_argument("--url", required=True, help="Entry URL")
|
|
16
|
+
p.add_argument("--username", required=True, help="Username")
|
|
17
|
+
p.add_argument("--password", default=None, help="Password (prompted if omitted)")
|
|
18
|
+
p.add_argument("--title", default="", help="Entry title")
|
|
19
|
+
p.add_argument("--group-uuid", default="", help="Target group UUID")
|
|
20
|
+
p.set_defaults(func=run)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run(
|
|
24
|
+
client: BrowserClient,
|
|
25
|
+
args: argparse.Namespace,
|
|
26
|
+
cli_config: CliConfig,
|
|
27
|
+
browser_config: BrowserConfig,
|
|
28
|
+
browser_config_path: Path,
|
|
29
|
+
*,
|
|
30
|
+
fmt: str = "table",
|
|
31
|
+
) -> int:
|
|
32
|
+
password = args.password
|
|
33
|
+
if password is None:
|
|
34
|
+
password = getpass.getpass("Password: ")
|
|
35
|
+
|
|
36
|
+
success = client.set_login(
|
|
37
|
+
url=args.url,
|
|
38
|
+
username=args.username,
|
|
39
|
+
password=password,
|
|
40
|
+
title=args.title,
|
|
41
|
+
group_uuid=args.group_uuid,
|
|
42
|
+
)
|
|
43
|
+
if success:
|
|
44
|
+
print("Entry added successfully.")
|
|
45
|
+
return 0
|
|
46
|
+
else:
|
|
47
|
+
print("Failed to add entry.", file=sys.stderr)
|
|
48
|
+
return 1
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
8
|
+
|
|
9
|
+
from keepassxc_cli.config import CliConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
13
|
+
p = subparsers.add_parser("clip", help="Copy a field to clipboard")
|
|
14
|
+
p.add_argument("url", help="URL to look up")
|
|
15
|
+
p.add_argument(
|
|
16
|
+
"--field",
|
|
17
|
+
choices=["password", "username", "totp"],
|
|
18
|
+
default="password",
|
|
19
|
+
help="Field to copy (default: password)",
|
|
20
|
+
)
|
|
21
|
+
p.set_defaults(func=run)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run(
|
|
25
|
+
client: BrowserClient,
|
|
26
|
+
args: argparse.Namespace,
|
|
27
|
+
cli_config: CliConfig,
|
|
28
|
+
browser_config: BrowserConfig,
|
|
29
|
+
browser_config_path: Path,
|
|
30
|
+
*,
|
|
31
|
+
fmt: str = "table",
|
|
32
|
+
) -> int:
|
|
33
|
+
try:
|
|
34
|
+
import pyperclip
|
|
35
|
+
except ImportError:
|
|
36
|
+
print("Error: pyperclip is required for clipboard support. Install it with: pip install pyperclip", file=sys.stderr)
|
|
37
|
+
return 1
|
|
38
|
+
|
|
39
|
+
entries = client.get_logins(args.url)
|
|
40
|
+
if not entries:
|
|
41
|
+
print(f"No entries found for: {args.url}", file=sys.stderr)
|
|
42
|
+
return 1
|
|
43
|
+
|
|
44
|
+
entry = entries[0]
|
|
45
|
+
|
|
46
|
+
if args.field == "password":
|
|
47
|
+
value = entry.password
|
|
48
|
+
elif args.field == "username":
|
|
49
|
+
value = entry.login
|
|
50
|
+
elif args.field == "totp":
|
|
51
|
+
value = client.get_totp(entry.uuid)
|
|
52
|
+
if value is None:
|
|
53
|
+
print(f"No TOTP for entry: {entry.uuid}", file=sys.stderr)
|
|
54
|
+
return 1
|
|
55
|
+
else:
|
|
56
|
+
print(f"Unknown field: {args.field}", file=sys.stderr)
|
|
57
|
+
return 1
|
|
58
|
+
|
|
59
|
+
pyperclip.copy(value)
|
|
60
|
+
print(f"Copied {args.field} to clipboard.")
|
|
61
|
+
return 0
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
8
|
+
|
|
9
|
+
from keepassxc_cli.config import CliConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
13
|
+
p = subparsers.add_parser("edit", help="Edit an existing entry by UUID")
|
|
14
|
+
p.add_argument("uuid", help="UUID of the entry to edit")
|
|
15
|
+
p.add_argument("--url", default=None, help="New URL")
|
|
16
|
+
p.add_argument("--username", default=None, help="New username")
|
|
17
|
+
p.add_argument("--password", default=None, help="New password")
|
|
18
|
+
p.add_argument("--title", default=None, help="New title")
|
|
19
|
+
p.set_defaults(func=run)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run(
|
|
23
|
+
client: BrowserClient,
|
|
24
|
+
args: argparse.Namespace,
|
|
25
|
+
cli_config: CliConfig,
|
|
26
|
+
browser_config: BrowserConfig,
|
|
27
|
+
browser_config_path: Path,
|
|
28
|
+
*,
|
|
29
|
+
fmt: str = "table",
|
|
30
|
+
) -> int:
|
|
31
|
+
all_entries = client.get_database_entries()
|
|
32
|
+
entry = next((e for e in all_entries if e.uuid == args.uuid), None)
|
|
33
|
+
if entry is None:
|
|
34
|
+
print(f"Entry not found: {args.uuid}", file=sys.stderr)
|
|
35
|
+
return 1
|
|
36
|
+
|
|
37
|
+
# Determine the URL to use for set_login (required by the API)
|
|
38
|
+
url = args.url or next(
|
|
39
|
+
(sf.get("KPH: url", "") for sf in entry.string_fields if "KPH: url" in sf),
|
|
40
|
+
"",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
success = client.set_login(
|
|
44
|
+
url=url,
|
|
45
|
+
username=args.username if args.username is not None else entry.login,
|
|
46
|
+
password=args.password if args.password is not None else entry.password,
|
|
47
|
+
title=args.title if args.title is not None else entry.name,
|
|
48
|
+
uuid=entry.uuid,
|
|
49
|
+
group_uuid=entry.group_uuid,
|
|
50
|
+
)
|
|
51
|
+
if success:
|
|
52
|
+
print("Entry updated successfully.")
|
|
53
|
+
return 0
|
|
54
|
+
else:
|
|
55
|
+
print("Failed to update entry.", file=sys.stderr)
|
|
56
|
+
return 1
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
8
|
+
|
|
9
|
+
from keepassxc_cli.config import CliConfig
|
|
10
|
+
from keepassxc_cli.output import print_password
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
14
|
+
p = subparsers.add_parser(
|
|
15
|
+
"generate",
|
|
16
|
+
help="Generate a password using KeePassXC's configured password profile",
|
|
17
|
+
)
|
|
18
|
+
p.add_argument("--clip", action="store_true", help="Copy to clipboard instead of printing")
|
|
19
|
+
p.set_defaults(func=run)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run(
|
|
23
|
+
client: BrowserClient,
|
|
24
|
+
args: argparse.Namespace,
|
|
25
|
+
cli_config: CliConfig,
|
|
26
|
+
browser_config: BrowserConfig,
|
|
27
|
+
browser_config_path: Path,
|
|
28
|
+
*,
|
|
29
|
+
fmt: str = "table",
|
|
30
|
+
) -> int:
|
|
31
|
+
password = client.generate_password()
|
|
32
|
+
if password is None:
|
|
33
|
+
print("Failed to generate password.", file=sys.stderr)
|
|
34
|
+
return 1
|
|
35
|
+
|
|
36
|
+
if args.clip:
|
|
37
|
+
try:
|
|
38
|
+
import pyperclip
|
|
39
|
+
pyperclip.copy(password)
|
|
40
|
+
print("Password copied to clipboard.")
|
|
41
|
+
except ImportError:
|
|
42
|
+
print("Error: pyperclip is required for clipboard support. Install it with: pip install pyperclip", file=sys.stderr)
|
|
43
|
+
return 1
|
|
44
|
+
else:
|
|
45
|
+
print_password(password, fmt)
|
|
46
|
+
|
|
47
|
+
return 0
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
8
|
+
|
|
9
|
+
from keepassxc_cli.config import CliConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
13
|
+
p = subparsers.add_parser("lock", help="Lock the KeePassXC database")
|
|
14
|
+
p.set_defaults(func=run)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run(
|
|
18
|
+
client: BrowserClient,
|
|
19
|
+
args: argparse.Namespace,
|
|
20
|
+
cli_config: CliConfig,
|
|
21
|
+
browser_config: BrowserConfig,
|
|
22
|
+
browser_config_path: Path,
|
|
23
|
+
*,
|
|
24
|
+
fmt: str = "table",
|
|
25
|
+
) -> int:
|
|
26
|
+
success = client.lock_database()
|
|
27
|
+
if success:
|
|
28
|
+
print("Database locked.")
|
|
29
|
+
return 0
|
|
30
|
+
else:
|
|
31
|
+
print("Failed to lock database.", file=sys.stderr)
|
|
32
|
+
return 1
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
7
|
+
|
|
8
|
+
from keepassxc_cli.config import CliConfig
|
|
9
|
+
from keepassxc_cli.output import print_entries, print_groups
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
13
|
+
p = subparsers.add_parser("ls", help="List database entries or groups")
|
|
14
|
+
p.add_argument("--groups", action="store_true", help="List groups instead of entries")
|
|
15
|
+
p.set_defaults(func=run)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run(
|
|
19
|
+
client: BrowserClient,
|
|
20
|
+
args: argparse.Namespace,
|
|
21
|
+
cli_config: CliConfig,
|
|
22
|
+
browser_config: BrowserConfig,
|
|
23
|
+
browser_config_path: Path,
|
|
24
|
+
*,
|
|
25
|
+
fmt: str = "table",
|
|
26
|
+
) -> int:
|
|
27
|
+
if args.groups:
|
|
28
|
+
groups = client.get_database_groups()
|
|
29
|
+
print_groups(groups, fmt)
|
|
30
|
+
else:
|
|
31
|
+
entries = client.get_database_entries()
|
|
32
|
+
print_entries(entries, fmt)
|
|
33
|
+
return 0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
8
|
+
|
|
9
|
+
from keepassxc_cli.config import CliConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
13
|
+
p = subparsers.add_parser("mkdir", help="Create a new group")
|
|
14
|
+
p.add_argument("name", help="Group name")
|
|
15
|
+
p.add_argument("--parent-uuid", default="", help="Parent group UUID")
|
|
16
|
+
p.set_defaults(func=run)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run(
|
|
20
|
+
client: BrowserClient,
|
|
21
|
+
args: argparse.Namespace,
|
|
22
|
+
cli_config: CliConfig,
|
|
23
|
+
browser_config: BrowserConfig,
|
|
24
|
+
browser_config_path: Path,
|
|
25
|
+
*,
|
|
26
|
+
fmt: str = "table",
|
|
27
|
+
) -> int:
|
|
28
|
+
group = client.create_group(args.name, parent_group_uuid=args.parent_uuid)
|
|
29
|
+
if group is None:
|
|
30
|
+
print("Failed to create group.", file=sys.stderr)
|
|
31
|
+
return 1
|
|
32
|
+
print(f"Group created: {group.name} [{group.uuid}]")
|
|
33
|
+
return 0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
8
|
+
|
|
9
|
+
from keepassxc_cli.config import CliConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
13
|
+
p = subparsers.add_parser("rm", help="Delete an entry by UUID")
|
|
14
|
+
p.add_argument("uuid", help="UUID of the entry to delete")
|
|
15
|
+
p.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompt")
|
|
16
|
+
p.set_defaults(func=run)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run(
|
|
20
|
+
client: BrowserClient,
|
|
21
|
+
args: argparse.Namespace,
|
|
22
|
+
cli_config: CliConfig,
|
|
23
|
+
browser_config: BrowserConfig,
|
|
24
|
+
browser_config_path: Path,
|
|
25
|
+
*,
|
|
26
|
+
fmt: str = "table",
|
|
27
|
+
) -> int:
|
|
28
|
+
if not args.yes:
|
|
29
|
+
answer = input(f"Delete entry {args.uuid}? [y/N] ").strip().lower()
|
|
30
|
+
if answer != "y":
|
|
31
|
+
print("Aborted.")
|
|
32
|
+
return 1
|
|
33
|
+
|
|
34
|
+
success = client.delete_entry(args.uuid)
|
|
35
|
+
if success:
|
|
36
|
+
print("Entry deleted.")
|
|
37
|
+
return 0
|
|
38
|
+
else:
|
|
39
|
+
print("Failed to delete entry.", file=sys.stderr)
|
|
40
|
+
return 1
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
8
|
+
|
|
9
|
+
from keepassxc_cli.config import CliConfig
|
|
10
|
+
from keepassxc_cli.output import print_entries
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
14
|
+
p = subparsers.add_parser("search", help="Search all database entries")
|
|
15
|
+
p.add_argument("query", help="Search query (case-insensitive match on title, username, or URL)")
|
|
16
|
+
p.add_argument("-p", "--show-password", action="store_true", help="Reveal password")
|
|
17
|
+
p.set_defaults(func=run)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run(
|
|
21
|
+
client: BrowserClient,
|
|
22
|
+
args: argparse.Namespace,
|
|
23
|
+
cli_config: CliConfig,
|
|
24
|
+
browser_config: BrowserConfig,
|
|
25
|
+
browser_config_path: Path,
|
|
26
|
+
*,
|
|
27
|
+
fmt: str = "table",
|
|
28
|
+
) -> int:
|
|
29
|
+
query = args.query.lower()
|
|
30
|
+
all_entries = client.get_database_entries()
|
|
31
|
+
matches = [
|
|
32
|
+
e for e in all_entries
|
|
33
|
+
if query in e.name.lower()
|
|
34
|
+
or query in e.login.lower()
|
|
35
|
+
or any(query in v.lower() for sf in e.string_fields for v in sf.values() if v is not None)
|
|
36
|
+
]
|
|
37
|
+
if not matches:
|
|
38
|
+
print(f"No entries found matching: {args.query}", file=sys.stderr)
|
|
39
|
+
return 1
|
|
40
|
+
print_entries(matches, fmt, show_password=args.show_password)
|
|
41
|
+
return 0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
8
|
+
|
|
9
|
+
from keepassxc_cli.config import CliConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
13
|
+
p = subparsers.add_parser("setup", help="Associate with KeePassXC (key exchange)")
|
|
14
|
+
p.set_defaults(func=run)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run(
|
|
18
|
+
client: BrowserClient,
|
|
19
|
+
args: argparse.Namespace,
|
|
20
|
+
cli_config: CliConfig,
|
|
21
|
+
browser_config: BrowserConfig,
|
|
22
|
+
browser_config_path: Path,
|
|
23
|
+
*,
|
|
24
|
+
fmt: str = "table",
|
|
25
|
+
) -> int:
|
|
26
|
+
success = client.setup()
|
|
27
|
+
if success:
|
|
28
|
+
browser_config.save(browser_config_path)
|
|
29
|
+
print("Successfully associated with KeePassXC.")
|
|
30
|
+
return 0
|
|
31
|
+
else:
|
|
32
|
+
print("Failed to associate with KeePassXC.", file=sys.stderr)
|
|
33
|
+
return 1
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
8
|
+
|
|
9
|
+
from keepassxc_cli.config import CliConfig
|
|
10
|
+
from keepassxc_cli.output import print_entry_detail
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
14
|
+
p = subparsers.add_parser("show", help="Show entries matching a URL")
|
|
15
|
+
p.add_argument("url", help="URL or search string")
|
|
16
|
+
p.add_argument("-p", "--show-password", action="store_true", help="Reveal password")
|
|
17
|
+
p.set_defaults(func=run)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run(
|
|
21
|
+
client: BrowserClient,
|
|
22
|
+
args: argparse.Namespace,
|
|
23
|
+
cli_config: CliConfig,
|
|
24
|
+
browser_config: BrowserConfig,
|
|
25
|
+
browser_config_path: Path,
|
|
26
|
+
*,
|
|
27
|
+
fmt: str = "table",
|
|
28
|
+
) -> int:
|
|
29
|
+
entries = client.get_logins(args.url)
|
|
30
|
+
if not entries:
|
|
31
|
+
print(f"No entries found for: {args.url}", file=sys.stderr)
|
|
32
|
+
return 1
|
|
33
|
+
for entry in entries:
|
|
34
|
+
print_entry_detail(entry, fmt, show_password=args.show_password)
|
|
35
|
+
if fmt == "table" and len(entries) > 1:
|
|
36
|
+
print()
|
|
37
|
+
return 0
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
7
|
+
from keepassxc_browser_api.exceptions import KeePassXCError
|
|
8
|
+
|
|
9
|
+
from keepassxc_cli.config import CliConfig
|
|
10
|
+
from keepassxc_cli.output import print_status
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
14
|
+
p = subparsers.add_parser("status", help="Show KeePassXC connection and association status")
|
|
15
|
+
p.set_defaults(func=run)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run(
|
|
19
|
+
client: BrowserClient,
|
|
20
|
+
args: argparse.Namespace,
|
|
21
|
+
cli_config: CliConfig,
|
|
22
|
+
browser_config: BrowserConfig,
|
|
23
|
+
browser_config_path: Path,
|
|
24
|
+
*,
|
|
25
|
+
fmt: str = "table",
|
|
26
|
+
) -> int:
|
|
27
|
+
info: dict = {}
|
|
28
|
+
|
|
29
|
+
connected = client.connect()
|
|
30
|
+
info["Connected"] = "yes" if connected else "no"
|
|
31
|
+
|
|
32
|
+
if not connected:
|
|
33
|
+
print_status(info, fmt)
|
|
34
|
+
return 1
|
|
35
|
+
|
|
36
|
+
associated = False
|
|
37
|
+
if browser_config.associations:
|
|
38
|
+
try:
|
|
39
|
+
association = next(iter(browser_config.associations.values()))
|
|
40
|
+
associated = client.test_associate(association)
|
|
41
|
+
except KeePassXCError:
|
|
42
|
+
associated = False
|
|
43
|
+
|
|
44
|
+
info["Associated"] = "yes" if associated else "no"
|
|
45
|
+
info["Unlock timeout"] = str(browser_config.unlock_timeout)
|
|
46
|
+
|
|
47
|
+
client.disconnect()
|
|
48
|
+
print_status(info, fmt)
|
|
49
|
+
return 0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
8
|
+
|
|
9
|
+
from keepassxc_cli.config import CliConfig
|
|
10
|
+
from keepassxc_cli.output import print_totp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
14
|
+
p = subparsers.add_parser("totp", help="Get TOTP code for an entry")
|
|
15
|
+
p.add_argument("uuid", help="UUID of the entry")
|
|
16
|
+
p.set_defaults(func=run)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run(
|
|
20
|
+
client: BrowserClient,
|
|
21
|
+
args: argparse.Namespace,
|
|
22
|
+
cli_config: CliConfig,
|
|
23
|
+
browser_config: BrowserConfig,
|
|
24
|
+
browser_config_path: Path,
|
|
25
|
+
*,
|
|
26
|
+
fmt: str = "table",
|
|
27
|
+
) -> int:
|
|
28
|
+
totp = client.get_totp(args.uuid)
|
|
29
|
+
if totp is None:
|
|
30
|
+
print(f"No TOTP for entry: {args.uuid}", file=sys.stderr)
|
|
31
|
+
return 1
|
|
32
|
+
print_totp(totp, fmt)
|
|
33
|
+
return 0
|
keepassxc_cli/config.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
DEFAULT_BROWSER_API_CONFIG_PATH = str(Path.home() / ".keepassxc" / "browser-api.json")
|
|
9
|
+
DEFAULT_CLI_CONFIG_PATH = Path.home() / ".keepassxc" / "cli.json"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class CliConfig:
|
|
14
|
+
browser_api_config_path: str = field(default_factory=lambda: DEFAULT_BROWSER_API_CONFIG_PATH)
|
|
15
|
+
default_format: str = "table"
|
|
16
|
+
|
|
17
|
+
def to_dict(self) -> dict:
|
|
18
|
+
d: dict = {}
|
|
19
|
+
if self.browser_api_config_path != DEFAULT_BROWSER_API_CONFIG_PATH:
|
|
20
|
+
d["browser_api_config_path"] = self.browser_api_config_path
|
|
21
|
+
if self.default_format != "table":
|
|
22
|
+
d["default_format"] = self.default_format
|
|
23
|
+
return d
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_dict(cls, d: dict) -> CliConfig:
|
|
27
|
+
return cls(
|
|
28
|
+
browser_api_config_path=d.get("browser_api_config_path", DEFAULT_BROWSER_API_CONFIG_PATH),
|
|
29
|
+
default_format=d.get("default_format", "table"),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def save(self, path: Path | str) -> None:
|
|
33
|
+
path = Path(path)
|
|
34
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
data = json.dumps(self.to_dict(), indent=2)
|
|
36
|
+
# Write with restricted permissions (0o600)
|
|
37
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
38
|
+
try:
|
|
39
|
+
os.write(fd, data.encode())
|
|
40
|
+
finally:
|
|
41
|
+
os.close(fd)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def load(cls, path: Path | str) -> CliConfig:
|
|
45
|
+
path = Path(path)
|
|
46
|
+
if not path.exists():
|
|
47
|
+
return cls()
|
|
48
|
+
with open(path) as f:
|
|
49
|
+
d = json.load(f)
|
|
50
|
+
return cls.from_dict(d)
|
keepassxc_cli/output.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from keepassxc_browser_api import Entry, Group
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _truncate(s: str, n: int) -> str:
|
|
9
|
+
if len(s) > n:
|
|
10
|
+
return s[: n - 1] + "…"
|
|
11
|
+
return s
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _table_row(cols: list[str], widths: list[int]) -> str:
|
|
15
|
+
return "| " + " | ".join(v.ljust(w) for v, w in zip(cols, widths)) + " |"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _table_sep(widths: list[int]) -> str:
|
|
19
|
+
return "+-" + "-+-".join("-" * w for w in widths) + "-+"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _print_table(headers: list[str], rows: list[list[str]]) -> None:
|
|
23
|
+
widths = [len(h) for h in headers]
|
|
24
|
+
for row in rows:
|
|
25
|
+
for i, cell in enumerate(row):
|
|
26
|
+
widths[i] = max(widths[i], len(cell))
|
|
27
|
+
sep = _table_sep(widths)
|
|
28
|
+
print(sep)
|
|
29
|
+
print(_table_row(headers, widths))
|
|
30
|
+
print(sep)
|
|
31
|
+
for row in rows:
|
|
32
|
+
print(_table_row(row, widths))
|
|
33
|
+
print(sep)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def print_entries(entries: list[Entry], fmt: str = "table", show_password: bool = False) -> None:
|
|
37
|
+
if fmt == "json":
|
|
38
|
+
data = [
|
|
39
|
+
{
|
|
40
|
+
"uuid": e.uuid,
|
|
41
|
+
"name": e.name,
|
|
42
|
+
"login": e.login,
|
|
43
|
+
"password": e.password if show_password else "***",
|
|
44
|
+
"url": next((sf.get("KPH: url", "") for sf in e.string_fields if "KPH: url" in sf), ""),
|
|
45
|
+
"group": e.group,
|
|
46
|
+
}
|
|
47
|
+
for e in entries
|
|
48
|
+
]
|
|
49
|
+
print(json.dumps(data, indent=2))
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
if fmt == "tsv":
|
|
53
|
+
headers = ["UUID", "Title", "Username", "URL", "Group"]
|
|
54
|
+
print("\t".join(headers))
|
|
55
|
+
for e in entries:
|
|
56
|
+
url = next((sf.get("KPH: url", "") for sf in e.string_fields if "KPH: url" in sf), "")
|
|
57
|
+
print("\t".join([e.uuid, e.name, e.login, url, e.group]))
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# table
|
|
61
|
+
headers = ["UUID", "Title", "Username", "URL", "Group"]
|
|
62
|
+
rows = []
|
|
63
|
+
for e in entries:
|
|
64
|
+
url = next((sf.get("KPH: url", "") for sf in e.string_fields if "KPH: url" in sf), "")
|
|
65
|
+
rows.append([_truncate(e.uuid, 9), e.name, e.login, url, e.group])
|
|
66
|
+
_print_table(headers, rows)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _print_group_tree(group: Group, indent: int = 0) -> None:
|
|
70
|
+
prefix = " " * indent
|
|
71
|
+
print(f"{prefix}{group.name} [{_truncate(group.uuid, 9)}]")
|
|
72
|
+
for child in group.children:
|
|
73
|
+
_print_group_tree(child, indent + 1)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def print_groups(groups: list[Group], fmt: str = "table") -> None:
|
|
77
|
+
if fmt == "json":
|
|
78
|
+
def _g2d(g: Group) -> dict:
|
|
79
|
+
return {"uuid": g.uuid, "name": g.name, "children": [_g2d(c) for c in g.children]}
|
|
80
|
+
print(json.dumps([_g2d(g) for g in groups], indent=2))
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
if fmt == "tsv":
|
|
84
|
+
print("UUID\tName")
|
|
85
|
+
for g in groups:
|
|
86
|
+
for flat in g.flat_list():
|
|
87
|
+
print(f"{flat.uuid}\t{flat.name}")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
for g in groups:
|
|
91
|
+
_print_group_tree(g)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def print_entry_detail(entry: Entry, fmt: str = "table", show_password: bool = False) -> None:
|
|
95
|
+
password = entry.password if show_password else "***"
|
|
96
|
+
if fmt == "json":
|
|
97
|
+
data = {
|
|
98
|
+
"uuid": entry.uuid,
|
|
99
|
+
"name": entry.name,
|
|
100
|
+
"login": entry.login,
|
|
101
|
+
"password": password,
|
|
102
|
+
"totp": entry.totp,
|
|
103
|
+
"group": entry.group,
|
|
104
|
+
"group_uuid": entry.group_uuid,
|
|
105
|
+
"string_fields": entry.string_fields,
|
|
106
|
+
}
|
|
107
|
+
print(json.dumps(data, indent=2))
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
if fmt == "tsv":
|
|
111
|
+
print("Field\tValue")
|
|
112
|
+
print(f"UUID\t{entry.uuid}")
|
|
113
|
+
print(f"Title\t{entry.name}")
|
|
114
|
+
print(f"Username\t{entry.login}")
|
|
115
|
+
print(f"Password\t{password}")
|
|
116
|
+
print(f"TOTP\t{entry.totp}")
|
|
117
|
+
print(f"Group\t{entry.group}")
|
|
118
|
+
print(f"Group UUID\t{entry.group_uuid}")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
print(f"UUID: {entry.uuid}")
|
|
122
|
+
print(f"Title: {entry.name}")
|
|
123
|
+
print(f"Username: {entry.login}")
|
|
124
|
+
print(f"Password: {password}")
|
|
125
|
+
if entry.totp:
|
|
126
|
+
print(f"TOTP: {entry.totp}")
|
|
127
|
+
if entry.group:
|
|
128
|
+
print(f"Group: {entry.group}")
|
|
129
|
+
if entry.group_uuid:
|
|
130
|
+
print(f"Group UUID: {entry.group_uuid}")
|
|
131
|
+
if entry.string_fields:
|
|
132
|
+
for sf in entry.string_fields:
|
|
133
|
+
for k, v in sf.items():
|
|
134
|
+
print(f"{k}: {v}")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def print_totp(totp: str, fmt: str = "table") -> None:
|
|
138
|
+
if fmt == "json":
|
|
139
|
+
print(json.dumps({"totp": totp}, indent=2))
|
|
140
|
+
return
|
|
141
|
+
if fmt == "tsv":
|
|
142
|
+
print(f"TOTP\t{totp}")
|
|
143
|
+
return
|
|
144
|
+
print(totp)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def print_password(password: str, fmt: str = "table") -> None:
|
|
148
|
+
if fmt == "json":
|
|
149
|
+
print(json.dumps({"password": password}, indent=2))
|
|
150
|
+
return
|
|
151
|
+
if fmt == "tsv":
|
|
152
|
+
print(f"Password\t{password}")
|
|
153
|
+
return
|
|
154
|
+
print(password)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def print_status(info: dict, fmt: str = "table") -> None:
|
|
158
|
+
if fmt == "json":
|
|
159
|
+
print(json.dumps(info, indent=2))
|
|
160
|
+
return
|
|
161
|
+
if fmt == "tsv":
|
|
162
|
+
print("Key\tValue")
|
|
163
|
+
for k, v in info.items():
|
|
164
|
+
print(f"{k}\t{v}")
|
|
165
|
+
return
|
|
166
|
+
for k, v in info.items():
|
|
167
|
+
print(f"{k}: {v}")
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keepassxc-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for KeePassXC using the browser extension protocol with biometric unlock
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: keepassxc-browser-api==0.1.0
|
|
10
|
+
Requires-Dist: pyperclip>=1.8.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# keepassxc-cli
|
|
17
|
+
|
|
18
|
+
A command-line interface for [KeePassXC](https://keepassxc.org/) that communicates via the browser extension protocol, supporting biometric (TouchID/fingerprint) unlock on supported platforms.
|
|
19
|
+
|
|
20
|
+
## What it is
|
|
21
|
+
|
|
22
|
+
`keepassxc-cli` talks to a running KeePassXC instance using the same native messaging protocol used by the KeePassXC Browser extension. This means:
|
|
23
|
+
|
|
24
|
+
- **Biometric unlock**: On macOS with TouchID (or similar) configured in KeePassXC, you can authenticate via fingerprint rather than typing your master password.
|
|
25
|
+
- **No master password in shell history**: Authentication happens through KeePassXC's GUI, not the terminal.
|
|
26
|
+
- **Full CRUD**: List, search, add, edit, delete entries and groups.
|
|
27
|
+
- **TOTP**: Retrieve time-based one-time passwords.
|
|
28
|
+
- **Clipboard**: Copy credentials directly to the clipboard.
|
|
29
|
+
|
|
30
|
+
## Prerequisites
|
|
31
|
+
|
|
32
|
+
1. **KeePassXC** ≥ 2.7 with the **Browser Integration** feature enabled:
|
|
33
|
+
- Open KeePassXC → Tools → Settings → Browser Integration
|
|
34
|
+
- Enable "Enable browser integration"
|
|
35
|
+
2. A KeePassXC database must be open (or KeePassXC must be running with auto-open configured).
|
|
36
|
+
3. Python ≥ 3.10
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pipx install keepassxc-cli
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or with pip:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install keepassxc-cli
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Setup
|
|
51
|
+
|
|
52
|
+
Before using `keepassxc-cli`, associate it with your KeePassXC instance:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
keepassxc-cli setup
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This performs a key exchange with KeePassXC (you will be prompted to allow the association in the KeePassXC GUI). The association is saved to `~/.keepassxc/browser-api.json`.
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
### Global options
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
keepassxc-cli [--config PATH] [--browser-api-config PATH] [--format {table,json,tsv}] [-v] COMMAND
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Option | Description |
|
|
69
|
+
|--------|-------------|
|
|
70
|
+
| `--config` | Path to CLI config file (default: `~/.keepassxc/cli.json`) |
|
|
71
|
+
| `--browser-api-config` | Path to browser API config file (default: `~/.keepassxc/browser-api.json`) |
|
|
72
|
+
| `--format` | Output format: `table` (default), `json`, or `tsv` |
|
|
73
|
+
| `-v, --verbose` | Enable verbose/debug logging |
|
|
74
|
+
|
|
75
|
+
### Commands
|
|
76
|
+
|
|
77
|
+
#### `setup` — Associate with KeePassXC
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
keepassxc-cli setup
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### `status` — Connection and association status
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
keepassxc-cli status
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### `ls` — List entries or groups
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
keepassxc-cli ls # list all entries (includes UUID column)
|
|
93
|
+
keepassxc-cli ls --groups # list groups (tree view)
|
|
94
|
+
keepassxc-cli ls --format json # output as JSON
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
UUIDs shown in the output are needed for `edit`, `rm`, `totp`, and `clip --field totp`.
|
|
98
|
+
|
|
99
|
+
#### `search` — Search entries
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
keepassxc-cli search github
|
|
103
|
+
keepassxc-cli search "my bank" --show-password
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Searches case-insensitively across title, username, and URL fields.
|
|
107
|
+
|
|
108
|
+
#### `show` — Show entries for a URL
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
keepassxc-cli show https://github.com
|
|
112
|
+
keepassxc-cli show https://github.com -p # reveal password
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### `add` — Add a new entry
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
keepassxc-cli add --url https://example.com --username user@example.com --title "Example"
|
|
119
|
+
# Password will be prompted securely if --password is not given
|
|
120
|
+
keepassxc-cli add --url https://example.com --username user --password mypass
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### `edit` — Edit an entry
|
|
124
|
+
|
|
125
|
+
> **Finding a UUID**: Use `keepassxc-cli ls` or `keepassxc-cli search <query>` to list entries with their UUIDs.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
keepassxc-cli edit <uuid> --username newuser
|
|
129
|
+
keepassxc-cli edit <uuid> --password newpass --title "New Title"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### `rm` — Delete an entry
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
keepassxc-cli rm <uuid> # prompts for confirmation
|
|
136
|
+
keepassxc-cli rm <uuid> --yes # skip confirmation
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### `totp` — Get TOTP code
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
keepassxc-cli totp <uuid>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### `clip` — Copy to clipboard
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
keepassxc-cli clip https://github.com # copies password
|
|
149
|
+
keepassxc-cli clip https://github.com --field username
|
|
150
|
+
keepassxc-cli clip https://github.com --field totp
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### `generate` — Generate a password
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
keepassxc-cli generate
|
|
157
|
+
keepassxc-cli generate --length 32 --symbols
|
|
158
|
+
keepassxc-cli generate --no-numbers --no-uppercase
|
|
159
|
+
keepassxc-cli generate --clip # copy to clipboard instead of printing
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### `lock` — Lock the database
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
keepassxc-cli lock
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### `mkdir` — Create a group
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
keepassxc-cli mkdir "Work"
|
|
172
|
+
keepassxc-cli mkdir "Projects" --parent-uuid <parent-group-uuid>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Configuration
|
|
176
|
+
|
|
177
|
+
### CLI config (`~/.keepassxc/cli.json`)
|
|
178
|
+
|
|
179
|
+
Only non-default values are stored. Available options:
|
|
180
|
+
|
|
181
|
+
| Key | Default | Description |
|
|
182
|
+
|-----|---------|-------------|
|
|
183
|
+
| `browser_api_config_path` | `~/.keepassxc/browser-api.json` | Path to the browser API config |
|
|
184
|
+
| `default_format` | `table` | Default output format (`table`, `json`, `tsv`) |
|
|
185
|
+
|
|
186
|
+
Example `~/.keepassxc/cli.json`:
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"default_format": "json"
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Browser API config (`~/.keepassxc/browser-api.json`)
|
|
194
|
+
|
|
195
|
+
Shared with `keepassxc-browser-api`. Contains the association keys created during `keepassxc-cli setup`. This file is automatically created and updated by the `setup` command.
|
|
196
|
+
|
|
197
|
+
Both config files are stored with `0o600` permissions (owner read/write only).
|
|
198
|
+
|
|
199
|
+
## Development
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
git clone https://github.com/mietzen/keepassxc-cli
|
|
203
|
+
cd keepassxc-cli
|
|
204
|
+
|
|
205
|
+
python3 -m venv .venv
|
|
206
|
+
source .venv/bin/activate
|
|
207
|
+
|
|
208
|
+
# Install local keepassxc-browser-api dependency first
|
|
209
|
+
pip install ../mietzen-keepassxc-browser-api/
|
|
210
|
+
|
|
211
|
+
# Install in editable mode with dev dependencies
|
|
212
|
+
pip install -e ".[dev]"
|
|
213
|
+
|
|
214
|
+
# Run tests
|
|
215
|
+
pytest --tb=short -q
|
|
216
|
+
|
|
217
|
+
# Run linter
|
|
218
|
+
ruff check --ignore=E501 --exclude=__init__.py ./keepassxc_cli
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Known Limitations
|
|
222
|
+
|
|
223
|
+
- Requires KeePassXC to be **running** and the database to be **open** (or biometric auto-unlock configured).
|
|
224
|
+
- The `clip` and `generate --clip` commands require `pyperclip` and a working clipboard (e.g., `xclip`/`xsel` on Linux, built-in on macOS/Windows).
|
|
225
|
+
- The browser integration protocol does not support moving entries between groups directly.
|
|
226
|
+
- Entry URLs in the database are stored as `KPH: url` string fields; entries without a URL field may not appear in `show` results.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
keepassxc_cli/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
2
|
+
keepassxc_cli/__main__.py,sha256=kVXXp8Ts8LmotfJw0nL71M6kfe_3YRTekrvyVK-KNkg,2469
|
|
3
|
+
keepassxc_cli/config.py,sha256=bgSllV9Vz6j-Xou_BLAvkoi_TWhXEeBsLlWteZAgxXc,1653
|
|
4
|
+
keepassxc_cli/output.py,sha256=RXgmXSnyNyd6gr59_TdHFkR_xR-jcyBuITUopXuBdWE,5019
|
|
5
|
+
keepassxc_cli/commands/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
6
|
+
keepassxc_cli/commands/add.py,sha256=wAJ_V0sH1kXE3Dkvt7uEiBh2J7ZAKbgTveq7Eppib8I,1359
|
|
7
|
+
keepassxc_cli/commands/clip.py,sha256=PvLT2ZKnr7phphPgQEZ0mTU7EwLDFiEbIaywF1XIXjw,1658
|
|
8
|
+
keepassxc_cli/commands/edit.py,sha256=HnzyDZ7_6pxWBhh_alNtA48sUcTCtkC1-LREKcHJlSs,1816
|
|
9
|
+
keepassxc_cli/commands/generate.py,sha256=dY1gE3aPHGVq09V0Vo-3TRlI7wsj-ZO84_2_frhZKac,1312
|
|
10
|
+
keepassxc_cli/commands/lock.py,sha256=kHKd_H4z-gtCsyl_Tf9Tk8cbm2oeamyFFcgv-Nh7LZk,757
|
|
11
|
+
keepassxc_cli/commands/ls.py,sha256=ZuQcxTrDt9CHzHBVTTXBbf9gq-27UL-JV613ffap1gg,917
|
|
12
|
+
keepassxc_cli/commands/mkdir.py,sha256=mk8Fj2_QurccoLSjncZUxzpR9CkfYL7B2EkK1DtWqFs,925
|
|
13
|
+
keepassxc_cli/commands/rm.py,sha256=r-iRFlID6f41pmKhAK9YAFjE-v7iW7ofSTWsfmf_6Vw,1082
|
|
14
|
+
keepassxc_cli/commands/search.py,sha256=Wvdzb3L56mIA-XtophNlswuJBmVG081Us_eiB82dkks,1299
|
|
15
|
+
keepassxc_cli/commands/setup.py,sha256=m8hyRpyPaRY07-iQItQXkrgbu5E6dwauplyxjI4oR8E,845
|
|
16
|
+
keepassxc_cli/commands/show.py,sha256=AxhWLyCSFc3drFkU1iUySZuoKRqoifPuQHV4Nl1B55Q,1090
|
|
17
|
+
keepassxc_cli/commands/status.py,sha256=f-5DJK1yAUWzaGe_EnqOWliwhlQo8e7Ugy1fqmxXO24,1320
|
|
18
|
+
keepassxc_cli/commands/totp.py,sha256=jE6C8azlqMpuHoQa1y0JV27bVo0Pj-wewvP_WRdsfuM,844
|
|
19
|
+
keepassxc_cli-0.1.0.dist-info/licenses/LICENSE,sha256=i7l1iI-cTPEtMrRCNzPD2_ZMZV0LQna7gPvYG0lVoCs,1061
|
|
20
|
+
keepassxc_cli-0.1.0.dist-info/METADATA,sha256=dgvHoyFe4oc6RMdGPsAHN45XmWBRURasWSdYfJS0gFA,6471
|
|
21
|
+
keepassxc_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
22
|
+
keepassxc_cli-0.1.0.dist-info/entry_points.txt,sha256=nWY3_lURlCrN2WmUwkYzgEQByM204jRVL0NRlfpm2LM,62
|
|
23
|
+
keepassxc_cli-0.1.0.dist-info/top_level.txt,sha256=Ac6eDBVHsCdXRS6v48vVgGyU0xVGoXXIc4DuNtFc6CY,14
|
|
24
|
+
keepassxc_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nils
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
keepassxc_cli
|