diff --git a/src/password_manager/entry_management.py b/src/password_manager/entry_management.py index d67bda8..260909e 100644 --- a/src/password_manager/entry_management.py +++ b/src/password_manager/entry_management.py @@ -79,6 +79,7 @@ class EntryManager: if "word_count" not in entry and "words" in entry: entry["word_count"] = entry["words"] entry.pop("words", None) + entry.setdefault("tags", []) logger.debug("Index loaded successfully.") return data except Exception as e: @@ -127,6 +128,7 @@ class EntryManager: archived: bool = False, notes: str = "", custom_fields: List[Dict[str, Any]] | None = None, + tags: list[str] | None = None, ) -> int: """ Adds a new entry to the encrypted JSON index file. @@ -154,6 +156,7 @@ class EntryManager: "kind": EntryType.PASSWORD.value, "notes": notes, "custom_fields": custom_fields or [], + "tags": tags or [], } logger.debug(f"Added entry at index {index}: {data['entries'][str(index)]}") @@ -197,6 +200,7 @@ class EntryManager: period: int = 30, digits: int = 6, notes: str = "", + tags: list[str] | None = None, ) -> str: """Add a new TOTP entry and return the provisioning URI.""" entry_id = self.get_next_index() @@ -216,6 +220,7 @@ class EntryManager: "digits": digits, "archived": archived, "notes": notes, + "tags": tags or [], } else: entry = { @@ -227,6 +232,7 @@ class EntryManager: "digits": digits, "archived": archived, "notes": notes, + "tags": tags or [], } data["entries"][str(entry_id)] = entry @@ -248,6 +254,7 @@ class EntryManager: index: int | None = None, notes: str = "", archived: bool = False, + tags: list[str] | None = None, ) -> int: """Add a new SSH key pair entry. @@ -268,6 +275,7 @@ class EntryManager: "label": label, "notes": notes, "archived": archived, + "tags": tags or [], } self._save_index(data) self.update_checksum() @@ -297,6 +305,7 @@ class EntryManager: user_id: str = "", notes: str = "", archived: bool = False, + tags: list[str] | None = None, ) -> int: """Add a new PGP key entry.""" @@ -314,6 +323,7 @@ class EntryManager: "user_id": user_id, "notes": notes, "archived": archived, + "tags": tags or [], } self._save_index(data) self.update_checksum() @@ -347,6 +357,7 @@ class EntryManager: index: int | None = None, notes: str = "", archived: bool = False, + tags: list[str] | None = None, ) -> int: """Add a new Nostr key pair entry.""" @@ -362,6 +373,7 @@ class EntryManager: "label": label, "notes": notes, "archived": archived, + "tags": tags or [], } self._save_index(data) self.update_checksum() @@ -376,6 +388,7 @@ class EntryManager: notes: str = "", custom_fields=None, archived: bool = False, + tags: list[str] | None = None, ) -> int: """Add a new generic key/value entry.""" @@ -391,6 +404,7 @@ class EntryManager: "notes": notes, "archived": archived, "custom_fields": custom_fields or [], + "tags": tags or [], } self._save_index(data) @@ -431,6 +445,7 @@ class EntryManager: words_num: int = 24, notes: str = "", archived: bool = False, + tags: list[str] | None = None, ) -> int: """Add a new derived seed phrase entry.""" @@ -447,6 +462,7 @@ class EntryManager: "word_count": words_num, "notes": notes, "archived": archived, + "tags": tags or [], } self._save_index(data) self.update_checksum() @@ -483,6 +499,7 @@ class EntryManager: index: int | None = None, notes: str = "", archived: bool = False, + tags: list[str] | None = None, ) -> int: """Add a new managed account seed entry. @@ -518,6 +535,7 @@ class EntryManager: "notes": notes, "fingerprint": fingerprint, "archived": archived, + "tags": tags or [], } self._save_index(data) @@ -642,6 +660,7 @@ class EntryManager: digits: Optional[int] = None, value: Optional[str] = None, custom_fields: List[Dict[str, Any]] | None = None, + tags: list[str] | None = None, **legacy, ) -> None: """ @@ -727,6 +746,10 @@ class EntryManager: f"Updated custom fields for index {index}: {custom_fields}" ) + if tags is not None: + entry["tags"] = tags + logger.debug(f"Updated tags for index {index}: {tags}") + data["entries"][str(index)] = entry logger.debug(f"Modified entry at index {index}: {entry}") @@ -890,8 +913,10 @@ class EntryManager: etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value)) label = entry.get("label", entry.get("website", "")) notes = entry.get("notes", "") + tags = entry.get("tags", []) label_match = query_lower in label.lower() notes_match = query_lower in notes.lower() + tags_match = any(query_lower in str(t).lower() for t in tags) if etype == EntryType.PASSWORD.value: username = entry.get("username", "") @@ -908,6 +933,7 @@ class EntryManager: or query_lower in url.lower() or notes_match or custom_match + or tags_match ): results.append( ( @@ -931,6 +957,7 @@ class EntryManager: or query_lower in value_field.lower() or notes_match or custom_match + or tags_match ): results.append( ( @@ -942,7 +969,7 @@ class EntryManager: ) ) else: - if label_match or notes_match: + if label_match or notes_match or tags_match: results.append( ( int(idx), diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index cb502f1..df46bd1 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -981,6 +981,12 @@ class PasswordManager: username = input("Enter the username (optional): ").strip() url = input("Enter the URL (optional): ").strip() notes = input("Enter notes (optional): ").strip() + tags_input = input("Enter tags (comma-separated, optional): ").strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) custom_fields: list[dict[str, object]] = [] while True: @@ -1021,6 +1027,7 @@ class PasswordManager: archived=False, notes=notes, custom_fields=custom_fields, + tags=tags, ) # Mark database as dirty for background sync @@ -1084,6 +1091,14 @@ class PasswordManager: ) continue notes = input("Notes (optional): ").strip() + tags_input = input( + "Enter tags (comma-separated, optional): " + ).strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) totp_index = self.entry_manager.get_next_totp_index() entry_id = self.entry_manager.get_next_index() uri = self.entry_manager.add_totp( @@ -1093,6 +1108,7 @@ class PasswordManager: period=int(period), digits=int(digits), notes=notes, + tags=tags, ) secret = TotpManager.derive_secret(self.parent_seed, totp_index) self.is_dirty = True @@ -1128,6 +1144,14 @@ class PasswordManager: period = int(input("Period (default 30): ").strip() or 30) digits = int(input("Digits (default 6): ").strip() or 6) notes = input("Notes (optional): ").strip() + tags_input = input( + "Enter tags (comma-separated, optional): " + ).strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) entry_id = self.entry_manager.get_next_index() uri = self.entry_manager.add_totp( label, @@ -1136,6 +1160,7 @@ class PasswordManager: period=period, digits=digits, notes=notes, + tags=tags, ) self.is_dirty = True self.last_update = time.time() @@ -1181,7 +1206,15 @@ class PasswordManager: print(colored("Error: Label cannot be empty.", "red")) return notes = input("Notes (optional): ").strip() - index = self.entry_manager.add_ssh_key(label, self.parent_seed, notes=notes) + tags_input = input("Enter tags (comma-separated, optional): ").strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) + index = self.entry_manager.add_ssh_key( + label, self.parent_seed, notes=notes, tags=tags + ) priv_pem, pub_pem = self.entry_manager.get_ssh_key_pair( index, self.parent_seed ) @@ -1230,12 +1263,18 @@ class PasswordManager: return words_input = input("Word count (12 or 24, default 24): ").strip() notes = input("Notes (optional): ").strip() + tags_input = input("Enter tags (comma-separated, optional): ").strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) if words_input and words_input not in {"12", "24"}: print(colored("Invalid word count. Choose 12 or 24.", "red")) return words = int(words_input) if words_input else 24 index = self.entry_manager.add_seed( - label, self.parent_seed, words_num=words, notes=notes + label, self.parent_seed, words_num=words, notes=notes, tags=tags ) phrase = self.entry_manager.get_seed_phrase(index, self.parent_seed) self.is_dirty = True @@ -1296,12 +1335,19 @@ class PasswordManager: ) user_id = input("User ID (optional): ").strip() notes = input("Notes (optional): ").strip() + tags_input = input("Enter tags (comma-separated, optional): ").strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) index = self.entry_manager.add_pgp_key( label, self.parent_seed, key_type=key_type, user_id=user_id, notes=notes, + tags=tags, ) priv_key, fingerprint = self.entry_manager.get_pgp_key( index, self.parent_seed @@ -1350,7 +1396,13 @@ class PasswordManager: print(colored("Error: Label cannot be empty.", "red")) return notes = input("Notes (optional): ").strip() - index = self.entry_manager.add_nostr_key(label, notes=notes) + tags_input = input("Enter tags (comma-separated, optional): ").strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) + index = self.entry_manager.add_nostr_key(label, notes=notes, tags=tags) npub, nsec = self.entry_manager.get_nostr_key_pair(index, self.parent_seed) self.is_dirty = True self.last_update = time.time() @@ -1401,6 +1453,12 @@ class PasswordManager: return value = input("Value: ").strip() notes = input("Notes (optional): ").strip() + tags_input = input("Enter tags (comma-separated, optional): ").strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) custom_fields: list[dict[str, object]] = [] while True: @@ -1419,7 +1477,11 @@ class PasswordManager: ) index = self.entry_manager.add_key_value( - label, value, notes=notes, custom_fields=custom_fields + label, + value, + notes=notes, + custom_fields=custom_fields, + tags=tags, ) self.is_dirty = True self.last_update = time.time() @@ -1465,8 +1527,14 @@ class PasswordManager: print(colored("Error: Label cannot be empty.", "red")) return notes = input("Notes (optional): ").strip() + tags_input = input("Enter tags (comma-separated, optional): ").strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else [] + ) index = self.entry_manager.add_managed_account( - label, self.parent_seed, notes=notes + label, self.parent_seed, notes=notes, tags=tags ) seed = self.entry_manager.get_managed_account_seed(index, self.parent_seed) self.is_dirty = True @@ -2190,6 +2258,15 @@ class PasswordManager: {"label": label, "value": value, "is_hidden": hidden} ) + tags_input = input( + "Enter tags (comma-separated, leave blank to keep current): " + ).strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else None + ) + self.entry_manager.modify_entry( index, archived=new_blacklisted, @@ -2198,6 +2275,7 @@ class PasswordManager: period=new_period, digits=new_digits, custom_fields=custom_fields, + tags=tags, ) elif entry_type in ( EntryType.KEY_VALUE.value, @@ -2273,6 +2351,15 @@ class PasswordManager: {"label": f_label, "value": f_value, "is_hidden": hidden} ) + tags_input = input( + "Enter tags (comma-separated, leave blank to keep current): " + ).strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else None + ) + self.entry_manager.modify_entry( index, archived=new_blacklisted, @@ -2280,6 +2367,7 @@ class PasswordManager: label=new_label, value=new_value, custom_fields=custom_fields, + tags=tags, ) else: website_name = entry.get("label", entry.get("website")) @@ -2366,6 +2454,15 @@ class PasswordManager: {"label": label, "value": value, "is_hidden": hidden} ) + tags_input = input( + "Enter tags (comma-separated, leave blank to keep current): " + ).strip() + tags = ( + [t.strip() for t in tags_input.split(",") if t.strip()] + if tags_input + else None + ) + self.entry_manager.modify_entry( index, new_username, @@ -2374,6 +2471,7 @@ class PasswordManager: notes=new_notes, label=new_label, custom_fields=custom_fields, + tags=tags, ) # Mark database as dirty for background sync diff --git a/src/tests/test_entry_add.py b/src/tests/test_entry_add.py index ea9328e..8ea313d 100644 --- a/src/tests/test_entry_add.py +++ b/src/tests/test_entry_add.py @@ -39,6 +39,7 @@ def test_add_and_retrieve_entry(): "kind": "password", "notes": "", "custom_fields": custom, + "tags": [], } data = enc_mgr.load_json_data(entry_mgr.index_file) diff --git a/src/tests/test_key_value_entry.py b/src/tests/test_key_value_entry.py index 9322d64..895dfbf 100644 --- a/src/tests/test_key_value_entry.py +++ b/src/tests/test_key_value_entry.py @@ -33,6 +33,7 @@ def test_add_and_modify_key_value(): "notes": "token", "archived": False, "custom_fields": [], + "tags": [], } em.modify_entry(idx, value="def456") diff --git a/src/tests/test_manager_add_totp.py b/src/tests/test_manager_add_totp.py index 0038511..c011d3e 100644 --- a/src/tests/test_manager_add_totp.py +++ b/src/tests/test_manager_add_totp.py @@ -48,6 +48,7 @@ def test_handle_add_totp(monkeypatch, capsys): "", # period "", # digits "", # notes + "", # tags ] ) monkeypatch.setattr("builtins.input", lambda *args, **kwargs: next(inputs)) @@ -66,5 +67,6 @@ def test_handle_add_totp(monkeypatch, capsys): "digits": 6, "archived": False, "notes": "", + "tags": [], } assert "ID 0" in out diff --git a/src/tests/test_manager_workflow.py b/src/tests/test_manager_workflow.py index c12f39e..5d2dbd0 100644 --- a/src/tests/test_manager_workflow.py +++ b/src/tests/test_manager_workflow.py @@ -54,6 +54,7 @@ def test_manager_workflow(monkeypatch): "", # username "", # url "", # notes + "", # tags "n", # add custom field "", # length (default) "0", # retrieve index @@ -65,6 +66,7 @@ def test_manager_workflow(monkeypatch): "", # archive status "", # new notes "n", # edit custom fields + "", # tags keep ] ) monkeypatch.setattr("builtins.input", lambda *args, **kwargs: next(inputs)) diff --git a/src/tests/test_nostr_entry.py b/src/tests/test_nostr_entry.py index 569dfeb..c049850 100644 --- a/src/tests/test_nostr_entry.py +++ b/src/tests/test_nostr_entry.py @@ -31,6 +31,7 @@ def test_nostr_key_determinism(): "label": "main", "notes": "", "archived": False, + "tags": [], } npub1, nsec1 = entry_mgr.get_nostr_key_pair(idx, TEST_SEED) diff --git a/src/tests/test_seed_entry.py b/src/tests/test_seed_entry.py index 772fa55..d7d9d36 100644 --- a/src/tests/test_seed_entry.py +++ b/src/tests/test_seed_entry.py @@ -42,6 +42,7 @@ def test_seed_phrase_determinism(): "word_count": 12, "notes": "", "archived": False, + "tags": [], } assert entry24 == { @@ -52,6 +53,7 @@ def test_seed_phrase_determinism(): "word_count": 24, "notes": "", "archived": False, + "tags": [], } assert phrase12_a not in entry12.values() diff --git a/src/tests/test_ssh_entry.py b/src/tests/test_ssh_entry.py index 695034c..f037437 100644 --- a/src/tests/test_ssh_entry.py +++ b/src/tests/test_ssh_entry.py @@ -29,6 +29,7 @@ def test_add_and_retrieve_ssh_key_pair(): "label": "ssh", "notes": "", "archived": False, + "tags": [], } priv1, pub1 = entry_mgr.get_ssh_key_pair(index, TEST_SEED) diff --git a/src/tests/test_totp_entry.py b/src/tests/test_totp_entry.py index b25d610..6eb0b12 100644 --- a/src/tests/test_totp_entry.py +++ b/src/tests/test_totp_entry.py @@ -37,6 +37,7 @@ def test_add_totp_and_get_code(): "digits": 6, "archived": False, "notes": "", + "tags": [], } code = entry_mgr.get_totp_code(0, TEST_SEED, timestamp=0) @@ -76,6 +77,7 @@ def test_add_totp_imported(tmp_path): "digits": 6, "archived": False, "notes": "", + "tags": [], } code = em.get_totp_code(0, timestamp=0) assert code == pyotp.TOTP(secret).at(0)