keepassxc-cli 1.0.0__tar.gz → 1.2.0__tar.gz

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.

Potentially problematic release.


This version of keepassxc-cli might be problematic. Click here for more details.

Files changed (36) hide show
  1. {keepassxc_cli-1.0.0/keepassxc_cli.egg-info → keepassxc_cli-1.2.0}/PKG-INFO +10 -4
  2. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/README.md +8 -2
  3. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli/commands/add.py +0 -2
  4. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli/commands/mkdir.py +5 -3
  5. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli/commands/show.py +2 -1
  6. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli/output.py +20 -3
  7. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0/keepassxc_cli.egg-info}/PKG-INFO +10 -4
  8. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli.egg-info/requires.txt +1 -1
  9. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/pyproject.toml +1 -1
  10. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/tests/test_commands.py +24 -7
  11. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/tests/test_output.py +15 -0
  12. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/.github/workflows/auto-merge-dependabot.yml +0 -0
  13. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/.github/workflows/auto-release.yml +0 -0
  14. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/.github/workflows/lint_and_test.yml +0 -0
  15. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/.github/workflows/pypi.yml +0 -0
  16. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/.gitignore +0 -0
  17. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/CLAUDE.md +0 -0
  18. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/LICENSE +0 -0
  19. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli/__init__.py +0 -0
  20. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli/__main__.py +0 -0
  21. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli/commands/__init__.py +0 -0
  22. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli/commands/clip.py +0 -0
  23. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli/commands/edit.py +0 -0
  24. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli/commands/lock.py +0 -0
  25. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli/commands/rm.py +0 -0
  26. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli/commands/setup.py +0 -0
  27. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli/commands/status.py +0 -0
  28. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli/commands/totp.py +0 -0
  29. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli/config.py +0 -0
  30. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli.egg-info/SOURCES.txt +0 -0
  31. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli.egg-info/dependency_links.txt +0 -0
  32. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli.egg-info/entry_points.txt +0 -0
  33. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/keepassxc_cli.egg-info/top_level.txt +0 -0
  34. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/setup.cfg +0 -0
  35. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/tests/conftest.py +0 -0
  36. {keepassxc_cli-1.0.0 → keepassxc_cli-1.2.0}/tests/test_config.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keepassxc-cli
3
- Version: 1.0.0
3
+ Version: 1.2.0
4
4
  Summary: CLI for KeePassXC using the browser extension protocol with biometric unlock
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
- Requires-Dist: keepassxc-browser-api==1.0.0
9
+ Requires-Dist: keepassxc-browser-api==1.1.0
10
10
  Requires-Dist: pyperclip==1.8.0
11
11
  Provides-Extra: dev
12
12
  Requires-Dist: pytest>=7.0; extra == "dev"
@@ -121,11 +121,15 @@ keepassxc-cli clip totp https://github.com
121
121
  #### `add` — Add a new entry
122
122
 
123
123
  ```bash
124
- keepassxc-cli add --url https://example.com --username user@example.com --title "Example"
125
124
  # Password is prompted securely if --password is not given
125
+ keepassxc-cli add --url https://example.com --username user@example.com
126
126
  keepassxc-cli add --url https://example.com --username user --password mypass
127
+ # Place the entry in a specific group by UUID
128
+ keepassxc-cli add --url https://example.com --username user --group-uuid <group-uuid>
127
129
  ```
128
130
 
131
+ > **Note**: The entry title is always derived from the URL hostname by KeePassXC. The protocol has no field to set a custom title.
132
+
129
133
  #### `edit` — Edit an entry
130
134
 
131
135
  ```bash
@@ -154,9 +158,11 @@ keepassxc-cli lock
154
158
 
155
159
  ```bash
156
160
  keepassxc-cli mkdir "Work"
157
- keepassxc-cli mkdir "Projects" --parent-uuid <parent-group-uuid>
161
+ keepassxc-cli mkdir "Work/Projects" # create Projects inside Work
158
162
  ```
159
163
 
164
+ Use `/`-separated paths to create nested groups. KeePassXC creates any missing path segments automatically.
165
+
160
166
  ## Configuration
161
167
 
162
168
  ### CLI config (`~/.keepassxc/cli.json`)
@@ -106,11 +106,15 @@ keepassxc-cli clip totp https://github.com
106
106
  #### `add` — Add a new entry
107
107
 
108
108
  ```bash
109
- keepassxc-cli add --url https://example.com --username user@example.com --title "Example"
110
109
  # Password is prompted securely if --password is not given
110
+ keepassxc-cli add --url https://example.com --username user@example.com
111
111
  keepassxc-cli add --url https://example.com --username user --password mypass
112
+ # Place the entry in a specific group by UUID
113
+ keepassxc-cli add --url https://example.com --username user --group-uuid <group-uuid>
112
114
  ```
113
115
 
116
+ > **Note**: The entry title is always derived from the URL hostname by KeePassXC. The protocol has no field to set a custom title.
117
+
114
118
  #### `edit` — Edit an entry
115
119
 
116
120
  ```bash
@@ -139,9 +143,11 @@ keepassxc-cli lock
139
143
 
140
144
  ```bash
141
145
  keepassxc-cli mkdir "Work"
142
- keepassxc-cli mkdir "Projects" --parent-uuid <parent-group-uuid>
146
+ keepassxc-cli mkdir "Work/Projects" # create Projects inside Work
143
147
  ```
144
148
 
149
+ Use `/`-separated paths to create nested groups. KeePassXC creates any missing path segments automatically.
150
+
145
151
  ## Configuration
146
152
 
147
153
  ### CLI config (`~/.keepassxc/cli.json`)
@@ -15,7 +15,6 @@ def add_parser(subparsers: argparse._SubParsersAction) -> None:
15
15
  p.add_argument("--url", required=True, help="Entry URL")
16
16
  p.add_argument("--username", required=True, help="Username")
17
17
  p.add_argument("--password", default=None, help="Password (prompted if omitted)")
18
- p.add_argument("--title", default="", help="Entry title")
19
18
  p.add_argument("--group-uuid", default="", help="Target group UUID")
20
19
  p.set_defaults(func=run)
21
20
 
@@ -37,7 +36,6 @@ def run(
37
36
  url=args.url,
38
37
  username=args.username,
39
38
  password=password,
40
- title=args.title,
41
39
  group_uuid=args.group_uuid,
42
40
  )
43
41
  if success:
@@ -11,8 +11,10 @@ from keepassxc_cli.config import CliConfig
11
11
 
12
12
  def add_parser(subparsers: argparse._SubParsersAction) -> None:
13
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")
14
+ p.add_argument(
15
+ "name",
16
+ help="Group name or path. Use '/' to create nested groups (e.g. 'Work/Projects').",
17
+ )
16
18
  p.set_defaults(func=run)
17
19
 
18
20
 
@@ -25,7 +27,7 @@ def run(
25
27
  *,
26
28
  fmt: str = "table",
27
29
  ) -> int:
28
- group = client.create_group(args.name, parent_group_uuid=args.parent_uuid)
30
+ group = client.create_group(args.name)
29
31
  if group is None:
30
32
  print("Failed to create group.", file=sys.stderr)
31
33
  return 1
@@ -15,6 +15,7 @@ def add_parser(subparsers: argparse._SubParsersAction, fmt_parent: argparse.Argu
15
15
  p = subparsers.add_parser("show", parents=parents, help="Show entries matching a URL")
16
16
  p.add_argument("url", help="URL or search string")
17
17
  p.add_argument("-p", "--show-password", action="store_true", help="Reveal password and TOTP")
18
+ p.add_argument("--show-kph-prefix", action="store_true", help="Keep 'KPH: ' prefix on custom string field names")
18
19
  p.set_defaults(func=run)
19
20
 
20
21
 
@@ -32,7 +33,7 @@ def run(
32
33
  print(f"No entries found for: {args.url}", file=sys.stderr)
33
34
  return 1
34
35
  for entry in entries:
35
- print_entry_detail(entry, fmt, show_password=args.show_password)
36
+ print_entry_detail(entry, fmt, show_password=args.show_password, show_kph_prefix=getattr(args, "show_kph_prefix", False))
36
37
  if fmt == "table" and len(entries) > 1:
37
38
  print()
38
39
  return 0
@@ -4,9 +4,26 @@ import json
4
4
 
5
5
  from keepassxc_browser_api import Entry
6
6
 
7
+ _KPH_PREFIX = "KPH: "
7
8
 
8
- def print_entry_detail(entry: Entry, fmt: str = "table", show_password: bool = False) -> None:
9
+
10
+ def _strip_kph(key: str) -> str:
11
+ return key[len(_KPH_PREFIX):] if key.startswith(_KPH_PREFIX) else key
12
+
13
+
14
+ def print_entry_detail(
15
+ entry: Entry,
16
+ fmt: str = "table",
17
+ show_password: bool = False,
18
+ show_kph_prefix: bool = False,
19
+ ) -> None:
9
20
  totp = entry.totp if show_password else None
21
+
22
+ def _fields() -> list[dict[str, str]]:
23
+ if show_kph_prefix:
24
+ return entry.string_fields
25
+ return [{_strip_kph(k): v for k, v in sf.items()} for sf in entry.string_fields]
26
+
10
27
  if fmt == "json":
11
28
  data = {
12
29
  "uuid": entry.uuid,
@@ -14,7 +31,7 @@ def print_entry_detail(entry: Entry, fmt: str = "table", show_password: bool = F
14
31
  "login": entry.login,
15
32
  "group": entry.group,
16
33
  "group_uuid": entry.group_uuid,
17
- "string_fields": entry.string_fields,
34
+ "string_fields": _fields(),
18
35
  }
19
36
  if show_password:
20
37
  data["password"] = entry.password
@@ -35,7 +52,7 @@ def print_entry_detail(entry: Entry, fmt: str = "table", show_password: bool = F
35
52
  if entry.group_uuid:
36
53
  print(f"Group UUID: {entry.group_uuid}")
37
54
  if entry.string_fields:
38
- for sf in entry.string_fields:
55
+ for sf in _fields():
39
56
  for k, v in sf.items():
40
57
  print(f"{k}: {v}")
41
58
 
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keepassxc-cli
3
- Version: 1.0.0
3
+ Version: 1.2.0
4
4
  Summary: CLI for KeePassXC using the browser extension protocol with biometric unlock
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
- Requires-Dist: keepassxc-browser-api==1.0.0
9
+ Requires-Dist: keepassxc-browser-api==1.1.0
10
10
  Requires-Dist: pyperclip==1.8.0
11
11
  Provides-Extra: dev
12
12
  Requires-Dist: pytest>=7.0; extra == "dev"
@@ -121,11 +121,15 @@ keepassxc-cli clip totp https://github.com
121
121
  #### `add` — Add a new entry
122
122
 
123
123
  ```bash
124
- keepassxc-cli add --url https://example.com --username user@example.com --title "Example"
125
124
  # Password is prompted securely if --password is not given
125
+ keepassxc-cli add --url https://example.com --username user@example.com
126
126
  keepassxc-cli add --url https://example.com --username user --password mypass
127
+ # Place the entry in a specific group by UUID
128
+ keepassxc-cli add --url https://example.com --username user --group-uuid <group-uuid>
127
129
  ```
128
130
 
131
+ > **Note**: The entry title is always derived from the URL hostname by KeePassXC. The protocol has no field to set a custom title.
132
+
129
133
  #### `edit` — Edit an entry
130
134
 
131
135
  ```bash
@@ -154,9 +158,11 @@ keepassxc-cli lock
154
158
 
155
159
  ```bash
156
160
  keepassxc-cli mkdir "Work"
157
- keepassxc-cli mkdir "Projects" --parent-uuid <parent-group-uuid>
161
+ keepassxc-cli mkdir "Work/Projects" # create Projects inside Work
158
162
  ```
159
163
 
164
+ Use `/`-separated paths to create nested groups. KeePassXC creates any missing path segments automatically.
165
+
160
166
  ## Configuration
161
167
 
162
168
  ### CLI config (`~/.keepassxc/cli.json`)
@@ -1,4 +1,4 @@
1
- keepassxc-browser-api==1.0.0
1
+ keepassxc-browser-api==1.1.0
2
2
  pyperclip==1.8.0
3
3
 
4
4
  [dev]
@@ -10,7 +10,7 @@ readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = "MIT"
12
12
  dependencies = [
13
- "keepassxc-browser-api==1.0.0",
13
+ "keepassxc-browser-api==1.1.0",
14
14
  "pyperclip==1.8.0",
15
15
  ]
16
16
 
@@ -32,16 +32,15 @@ def browser_config_path(tmp_path):
32
32
  def make_args(**kwargs) -> argparse.Namespace:
33
33
  defaults = {
34
34
  "show_password": False,
35
+ "show_kph_prefix": False,
35
36
  "yes": False,
36
37
  "field": "password",
37
38
  "url": "https://example.com",
38
39
  "username": "user",
39
40
  "password": "pass",
40
- "title": "Example",
41
41
  "group_uuid": "",
42
42
  "uuid": "abcdef12-0000-0000-0000-000000000000",
43
43
  "name": "NewGroup",
44
- "parent_uuid": "",
45
44
  }
46
45
  defaults.update(kwargs)
47
46
  return argparse.Namespace(**defaults)
@@ -99,7 +98,7 @@ class TestStatusCommand:
99
98
 
100
99
  class TestShowCommand:
101
100
  def test_found_entries(self, mock_client, cli_config, browser_config, browser_config_path, capsys, mock_entry):
102
- entry = mock_entry()
101
+ entry = mock_entry(string_fields=[{"KPH: url": "https://github.com"}])
103
102
  mock_client.get_logins.return_value = [entry]
104
103
  args = make_args(url="https://example.com", show_password=False)
105
104
  rc = show.run(mock_client, args, cli_config, browser_config, browser_config_path)
@@ -107,6 +106,8 @@ class TestShowCommand:
107
106
  out = capsys.readouterr().out
108
107
  assert "Test Entry" in out
109
108
  assert "Password" not in out
109
+ assert "KPH: " not in out
110
+ assert "url: https://github.com" in out
110
111
 
111
112
  def test_found_entries_show_password(self, mock_client, cli_config, browser_config, browser_config_path, capsys, mock_entry):
112
113
  entry = mock_entry()
@@ -118,6 +119,14 @@ class TestShowCommand:
118
119
  assert "Test Entry" in out
119
120
  assert "Password:" in out
120
121
 
122
+ def test_found_entries_show_kph_prefix(self, mock_client, cli_config, browser_config, browser_config_path, capsys, mock_entry):
123
+ entry = mock_entry(string_fields=[{"KPH: url": "https://github.com"}])
124
+ mock_client.get_logins.return_value = [entry]
125
+ args = make_args(url="https://example.com", show_password=False, show_kph_prefix=True)
126
+ rc = show.run(mock_client, args, cli_config, browser_config, browser_config_path)
127
+ assert rc == 0
128
+ assert "KPH: url: https://github.com" in capsys.readouterr().out
129
+
121
130
  def test_no_entries(self, mock_client, cli_config, browser_config, browser_config_path, capsys):
122
131
  mock_client.get_logins.return_value = []
123
132
  args = make_args(url="https://notfound.com")
@@ -131,7 +140,7 @@ class TestShowCommand:
131
140
  class TestAddCommand:
132
141
  def test_success(self, mock_client, cli_config, browser_config, browser_config_path, capsys):
133
142
  mock_client.set_login.return_value = True
134
- args = make_args(url="https://example.com", username="u", password="p", title="T", group_uuid="")
143
+ args = make_args(url="https://example.com", username="u", password="p", group_uuid="")
135
144
  rc = add.run(mock_client, args, cli_config, browser_config, browser_config_path)
136
145
  assert rc == 0
137
146
  mock_client.set_login.assert_called_once()
@@ -139,7 +148,7 @@ class TestAddCommand:
139
148
 
140
149
  def test_failure(self, mock_client, cli_config, browser_config, browser_config_path, capsys):
141
150
  mock_client.set_login.return_value = False
142
- args = make_args(url="https://example.com", username="u", password="p", title="T", group_uuid="")
151
+ args = make_args(url="https://example.com", username="u", password="p", group_uuid="")
143
152
  rc = add.run(mock_client, args, cli_config, browser_config, browser_config_path)
144
153
  assert rc == 1
145
154
 
@@ -259,14 +268,22 @@ class TestMkdirCommand:
259
268
  def test_success(self, mock_client, cli_config, browser_config, browser_config_path, capsys, mock_group):
260
269
  new_group = mock_group(uuid="new-uuid", name="MyGroup")
261
270
  mock_client.create_group.return_value = new_group
262
- args = make_args(name="MyGroup", parent_uuid="")
271
+ args = make_args(name="MyGroup")
263
272
  rc = mkdir.run(mock_client, args, cli_config, browser_config, browser_config_path)
264
273
  assert rc == 0
265
274
  out = capsys.readouterr().out
266
275
  assert "MyGroup" in out
267
276
 
277
+ def test_path_syntax(self, mock_client, cli_config, browser_config, browser_config_path, capsys, mock_group):
278
+ new_group = mock_group(uuid="new-uuid", name="Projects")
279
+ mock_client.create_group.return_value = new_group
280
+ args = make_args(name="Work/Projects")
281
+ rc = mkdir.run(mock_client, args, cli_config, browser_config, browser_config_path)
282
+ assert rc == 0
283
+ mock_client.create_group.assert_called_once_with("Work/Projects")
284
+
268
285
  def test_failure(self, mock_client, cli_config, browser_config, browser_config_path, capsys):
269
286
  mock_client.create_group.return_value = None
270
- args = make_args(name="MyGroup", parent_uuid="")
287
+ args = make_args(name="MyGroup")
271
288
  rc = mkdir.run(mock_client, args, cli_config, browser_config, browser_config_path)
272
289
  assert rc == 1
@@ -34,12 +34,20 @@ class TestPrintEntryDetail:
34
34
  assert "user@example.com" in out
35
35
  assert "Password" not in out
36
36
  assert "s3cr3t" not in out
37
+ assert "KPH: " not in out
38
+ assert "url: https://github.com" in out
37
39
 
38
40
  def test_table_format_show_password(self, capsys, sample_entry):
39
41
  print_entry_detail(sample_entry, fmt="table", show_password=True)
40
42
  out = capsys.readouterr().out
41
43
  assert "s3cr3t" in out
42
44
  assert "Password:" in out
45
+ assert "KPH: " not in out
46
+
47
+ def test_table_format_show_kph_prefix(self, capsys, sample_entry):
48
+ print_entry_detail(sample_entry, fmt="table", show_password=False, show_kph_prefix=True)
49
+ out = capsys.readouterr().out
50
+ assert "KPH: url: https://github.com" in out
43
51
 
44
52
  def test_json_format_hidden(self, capsys, sample_entry):
45
53
  print_entry_detail(sample_entry, fmt="json", show_password=False)
@@ -47,6 +55,7 @@ class TestPrintEntryDetail:
47
55
  data = json.loads(out)
48
56
  assert data["name"] == "GitHub"
49
57
  assert "password" not in data
58
+ assert data["string_fields"] == [{"url": "https://github.com"}]
50
59
 
51
60
  def test_json_format_show_password(self, capsys, sample_entry):
52
61
  print_entry_detail(sample_entry, fmt="json", show_password=True)
@@ -54,6 +63,12 @@ class TestPrintEntryDetail:
54
63
  data = json.loads(out)
55
64
  assert data["name"] == "GitHub"
56
65
  assert data["password"] == "s3cr3t"
66
+ assert data["string_fields"] == [{"url": "https://github.com"}]
67
+
68
+ def test_json_format_show_kph_prefix(self, capsys, sample_entry):
69
+ print_entry_detail(sample_entry, fmt="json", show_password=False, show_kph_prefix=True)
70
+ data = json.loads(capsys.readouterr().out)
71
+ assert data["string_fields"] == [{"KPH: url": "https://github.com"}]
57
72
 
58
73
 
59
74
  class TestPrintTotp:
File without changes
File without changes
File without changes
File without changes