Skip to content

fix: scope dedup index to org when --bitwarden-org is set - #3

Merged
kjanat merged 14 commits into
masterfrom
fix/org-dedup-index
Feb 26, 2026
Merged

kjanat merged 14 commits into
masterfrom
fix/org-dedup-index

Conversation

@kjanat

@kjanat kjanat commented Feb 24, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • _build_dedup_index() fetched all vault items via /list/object/items with no org filter, so personal-vault entries shadowed the empty org vault and every import was skipped as "already exists"
  • Now passes organizationId query param to the bw serve items endpoint when --bitwarden-org is specified, per the Vault Management API spec
  • No behavior change for personal-vault (non-org) imports

fix: scope dedup index to org when --bitwarden-org is set; collection-aware dedup; stronger BW API types

Problem

  • _build_dedup_index() previously requested /list/object/items with no organization filter. When importing into a Bitwarden organization via --bitwarden-org, personal-vault entries could shadow an empty org vault and cause valid org imports to be skipped as "already exists".

Solution

  • When --bitwarden-org is set, pass organizationId as a query parameter to bw serve's /list/object/items endpoint so the dedup index is limited to the specified organization.
  • Make the dedup index collection-aware and richer: index structure now maps folder_name -> item_name -> BwItemResponse (not simple name sets). This enables detecting items missing collection assignments and performing an in-place PUT to add missing collection(s) instead of skipping.
  • No behavior change for personal-vault (org_id None) imports — those still index all vault items.

Notable changes

  • src/kp2bw/bw_serve.py

    • list_items(..., organization_id: str | None = None) supports organizationId query param.
    • _build_dedup_index(self) now builds a dict[str|None, dict[str, BwItemResponse]] and is called with self._org_id when an org is configured.
    • Added helpers: get_existing_item, entry_exists, update_dedup_entry, refresh_dedup_index and update_item(item_id, item) to PUT full payloads when assigning missing collections.
    • Added _collection_id state to support fixed-collection dedup scoping and related logic.
    • Broadened _request json_body typing to Mapping[str, Any].
  • Strongly-typed Bitwarden API layer

    • Generated OpenAPI-derived TypedDicts: src/kp2bw/_bw_api_types.py.
    • Hand-authored transport types: src/kp2bw/bw_types.py (BwItemCreate, BwItemResponse, BwFolder, BwCollection, BwUri, BwField, BwFido2Credential, etc.).
    • Converted bw_serve.py and convert.py to use these Bw* types (several public/internal signatures changed).
  • src/kp2bw/convert.py

    • Build and import strongly-typed BwItemCreate payloads; typed FIDO2 credential handling.
    • Use collection-aware dedup checks and update logic during import.
    • Integrates Rich progress reporting and final migration summary.
  • CLI, UX, tooling, docs, CI

    • CLI: added -d/--debug and improved logging; standardized metavars (src/kp2bw/cli.py).
    • Shared Rich console: src/kp2bw/_console.py.
    • AGENTS.md updated to document org-scoped dedup behavior.
    • Added scripts/generate-bw-types.sh and a codegen drift check workflow (.github/workflows/codegen-check.yml) to keep generated types in sync.
    • Bumped project version to 3.0.0a2; README and changelog updates; minor help text tweaks.

Impact

  • Fixes dedup behavior for organization-targeted imports: org imports will no longer be incorrectly skipped due to personal-vault items.
  • Improves dedup correctness by allowing assignment of missing collections via update PUTs instead of skipping.
  • Introduces stronger typing for Bitwarden API shapes and changes several method signatures — reviewers should audit public client usages for compatibility and typing implications.

_build_dedup_index() fetched all vault items, so personal-vault
entries shadowed the empty org vault and every import was skipped.
Pass organizationId filter to /list/object/items.
@kjanat kjanat added the bug Something isn't working label Feb 24, 2026
@github-actions

github-actions Bot commented Feb 24, 2026 •

Copy link
Copy Markdown

📦 Test this PR (archived)

Status: ✅ Merged

This PR has been merged. You can still test the final state:

uvx -p3.14 --from git+https://github.com/kjanat/kp2bw@7bc3369e477d25561bd68940ce633bdbed87cf25 kp2bw --help

📋 PR Details

Field Value
Final commit 7bc3369
Commit message fix: merge unreleased changelog into v3.0.0a2, tighten BwField.type to Literal
Branch fix/org-dedup-index

Merge commit: eda8d03

📋 Example usage

# Show version
uvx -p3.14 --from git+https://github.com/kjanat/kp2bw@7bc3369e477d25561bd68940ce633bdbed87cf25 kp2bw --version
# Migrate KeePass DB (interactive password prompts)
uvx -p3.14 --from git+https://github.com/kjanat/kp2bw@7bc3369e477d25561bd68940ce633bdbed87cf25 kp2bw vault.kdbx
# Non-interactive with env vars
KP2BW_KEEPASS_PASSWORD=kppass KP2BW_BITWARDEN_PASSWORD=bwpass uvx -p3.14 --from git+https://github.com/kjanat/kp2bw@7bc3369e477d25561bd68940ce633bdbed87cf25 kp2bw vault.kdbx -y

🤖 Archived on merge

@coderabbitai

coderabbitai Bot commented Feb 24, 2026 •

Copy link
Copy Markdown

Walkthrough

Adds strict TypedDict Bitwarden API types, migrates internal item shapes to those types, scopes deduplication to organization when provided, adds update_item (PUT), introduces generated _bw_api_types.py and codegen script, integrates Rich console/progress and a codegen CI check, and bumps to v3.0.0a2.

Changes

Cohort / File(s) Summary
API Types & Codegen
src/kp2bw/_bw_api_types.py, src/kp2bw/bw_types.py, scripts/generate-bw-types.sh
Add generated OpenAPI TypedDicts and a hand-authored bw_types layer (BwItemCreate, BwItemResponse, BwUri, BwField, BwFido2Credential, BwFolder, BwCollection, etc.). New generator script to (re)produce _bw_api_types.py. Review type fidelity vs. spec and re-export/aliasing.
Dedup / Serve client
src/kp2bw/bw_serve.py
Replace loose dicts with Bw* types; add _collection_id scoping; change dedup index shape to `dict[str
Conversion / Import pipeline
src/kp2bw/convert.py, src/kp2bw/bw_import.py
Conversion now builds and mutates BwItemCreate/BwItemResponse, introduces FieldSpec, typed fido2/uri/field structures, and Rich progress UI. Reference resolution, dedup/collection resolution, and import mappings updated to typed shapes. Removed from __future__ annotation in bw_import.py.
CLI & UX
src/kp2bw/cli.py, README.md
Add rich logging handler and shared console integration; new -d/--debug flag and KP2BW_DEBUG env var docs; add metavar tokens; adjust logging levels and interrupt handling.
Console & Progress UI
src/kp2bw/_console.py
New shared Rich Console(stderr=True) instance for coordinated progress and log output.
Docs & Agents note
src/kp2bw/AGENTS.md, CHANGELOG.md
Document org-scoped dedup when --bitwarden-org is used; add 3.0.0a2 changelog entries (types, codegen check, progress UI, fixes).
Repo & CI
pyproject.toml, .gitattributes, .github/workflows/codegen-check.yml, .github/workflows/pr-comment-bot.yml
Bump version to 3.0.0a2, add rich and codegen dependency groups, add .gitattributes annotations, add GitHub workflow to detect codegen drift by regenerating _bw_api_types.py. Minor newline change in PR bot workflow.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Migrator as Migrator (kp2bw)
    participant BWClient as bw_serve client
    participant BWServer as Bitwarden Serve API
    participant SourceVault as Source Vault
    Migrator->>BWClient: request dedup index (organization_id optional)
    BWClient->>BWServer: GET /list/object/items?organizationId=ORG_ID
    BWServer-->>BWClient: returns list[BwItemResponse]
    BWClient-->>Migrator: org-scoped item list
    Migrator->>Migrator: build dedup index keyed by folder + name
    Migrator->>SourceVault: iterate source entries
    Migrator->>BWClient: create_item(item: BwItemCreate) or update_item(id, item: BwItemResponse)
    BWClient->>BWServer: POST /object/item or PUT /object/item/{id}
    BWServer-->>BWClient: created/updated item id/ack
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Agents.Md Documentation Updated ⚠️ Warning AGENTS.md received only +1/-0 lines while PR introduces substantial architectural changes (new modules, CI workflow, codegen automation, logging overhaul, major refactors) that warrant comprehensive documentation. Update AGENTS.md with new modules in STRUCTURE, new WHERE TO LOOK tasks for type management and progress reporting, CLI logging overhaul details, and expanded NOTES on dedup scoping and codegen workflow.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title uses 'fix:' prefix, is concise (57 characters), and clearly describes the specific organizational scoping fix for the dedup index.
Docstring Coverage ✅ Passed Docstring coverage is 93.94% which is sufficient. The required threshold is 80.00%.
Changelog Update ✅ Passed PR fully satisfies CHANGELOG update requirement with version bump to 3.0.0a2 and comprehensive documentation of all eight source file modifications.
Semver Version Bump Validation ✅ Passed Version bumped from 3.0.0a1 to 3.0.0a2 within pre-release cycle; appropriate for breaking API changes before stable release.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/org-dedup-index

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot]

This comment was marked as resolved.

Replace auto-derived dest-based metavars (KP_PW, BW_ORG, etc.)
with type-descriptive names: PASSWORD, FILE, ID, TAG, N.
Add -d/--debug to README usage synopsis and flags table.
coderabbitai[bot]

This comment was marked as resolved.

API spec (vault-management-api.json) defines the query param as
camelCase 'organizationId' on /list/object/org-collections, matching
/list/object/items. The prior lowercase 'organizationid' was wrong.

Also document org-scoped dedup behavior in src/kp2bw/AGENTS.md.
@kjanat
kjanat force-pushed the fix/org-dedup-index branch from b023802 to 41fe23c Compare February 24, 2026 01:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/kp2bw/bw_serve.py (1)

383-389: ⚠️ Potential issue | 🔴 Critical

organizationId (camelCase) is still wrong — query param must be organizationid (all lowercase), and this is the core of the PR's entire fix.

The Bitwarden CLI uses --organizationid (all lowercase) for bw list org-collections and all list-filter flags. bw serve is a thin Express wrapper that passes query string keys directly to the CLI as flag names — same casing required. folderid at line 386 follows this correctly. organizationId (camelCase) is only valid inside POST/PUT JSON bodies, not as a query parameter. With the wrong casing, the Express server either ignores the parameter or rejects it silently, the filter never applies, and the dedup index still encompasses the entire vault — meaning the original bug is completely unfixed.

🐛 Proposed fix
         if organization_id is not None:
-            params["organizationId"] = organization_id
+            params["organizationid"] = organization_id
#!/bin/bash
# Verify all query-param dict keys for bw serve list endpoints in bw_serve.py
# Expect: all filter keys (folderid, organizationid, etc.) are lowercase
rg -n "params\s*=\s*\{" --type py -A2 src/kp2bw/bw_serve.py
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kp2bw/bw_serve.py` around lines 383 - 389, The query param key
"organizationId" in the params dict is incorrectly camelCase and must be
"organizationid" (all lowercase) so the Express wrapper passes the correct flag
to the Bitwarden CLI; update the params assignment that sets
params["organizationId"] to params["organizationid"] (mirror the existing
folderid usage) in the function that builds the params before calling
self._request("GET", "/list/object/items", params=...) so the organization
filter is applied correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/kp2bw/AGENTS.md`:
- Line 51: Update the documentation note in AGENTS.md to use the correct
lowercase query parameter name per bw serve conventions: change the mention of
`organizationId` to `organizationid` in the description of _build_dedup_index()
behavior (which passes organization_id=self._org_id to list_items()), so readers
know the actual query string key used when filtering org-scoped dedup indexing
versus personal vault indexing.

In `@src/kp2bw/bw_serve.py`:
- Around line 477-481: The call in list_collections is passing the wrong query
key casing so the org filter is dropped; update the params dict in the
self._request call inside list_collections to use the lowercase key
"organizationid" (not "organizationId") so the organization filter is applied
correctly.

---

Duplicate comments:
In `@src/kp2bw/bw_serve.py`:
- Around line 383-389: The query param key "organizationId" in the params dict
is incorrectly camelCase and must be "organizationid" (all lowercase) so the
Express wrapper passes the correct flag to the Bitwarden CLI; update the params
assignment that sets params["organizationId"] to params["organizationid"]
(mirror the existing folderid usage) in the function that builds the params
before calling self._request("GET", "/list/object/items", params=...) so the
organization filter is applied correctly.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d907259 and 41fe23c.

📒 Files selected for processing (2)
  • src/kp2bw/AGENTS.md
  • src/kp2bw/bw_serve.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
src/kp2bw/**/*.py

📄 CodeRabbit inference engine (src/kp2bw/AGENTS.md)

src/kp2bw/**/*.py: Use module logger with logger = logging.getLogger(__name__), never root logger calls
Keep sensitive values out of logs - do not log raw bw commands/output, session values, or password values
Raise project exceptions (BitwardenClientError, ConversionError) instead of bare Exception
Use relative imports for local modules (e.g., from .exceptions import ConversionError)
Python 3.14 comma-form multi-except is valid; do not rewrite it to tuple form just for style
Do not reintroduce subprocess-per-op transport for new work; use bw_serve.py as the active transport path
Do not log decrypted vault content or credential-bearing command strings
Do not break idempotency by bypassing existing-item dedup checks

Never log raw bw command strings or raw command output to preserve security

Files:

  • src/kp2bw/bw_serve.py
src/kp2bw/bw_serve.py

📄 CodeRabbit inference engine (src/kp2bw/AGENTS.md)

src/kp2bw/bw_serve.py: Tune dedup/idempotency behavior in src/kp2bw/bw_serve.py using the existing item index and batch create behavior
Configure attachment behavior in src/kp2bw/bw_serve.py via the async upload path and multipart logic

bw serve is localhost-only and password is passed via environment variable, not CLI argument

Files:

  • src/kp2bw/bw_serve.py
🧠 Learnings (18)
📓 Common learnings
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:335-342
Timestamp: 2026-02-23T21:45:21.228Z
Learning: In the Bitwarden CLI `bw serve` Vault Management API, list endpoints (`/list/object/folders`, `/list/object/items`, `/list/object/org-collections`) return a nested structure where the outer envelope's `data` field contains a list wrapper object `{ "object": "list", "data": [...] }`. Therefore, code that calls `_request(...)` and then does `.get("data", [])` is correctly extracting the actual items array from the list wrapper - this is not a double-extraction bug but the correct handling of the API's response structure.
<!-- </add_learning>
📚 Learning: 2026-02-24T00:45:03.726Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-24T00:45:03.726Z
Learning: If any AGENTS.md file exists anywhere in the repository and the PR modifies architecture, workflows, responsibilities, automation behavior, CLI interfaces, or agent-related logic, then the corresponding AGENTS.md file in the affected directory MUST be updated in the same PR. If relevant code changes occur without updates to AGENTS.md, issue a warning.

Applied to files:

  • src/kp2bw/AGENTS.md
📚 Learning: 2026-02-21T04:16:56.990Z
Learnt from: CR
Repo: kjanat/paperless-mcp PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-21T04:16:56.990Z
Learning: If AGENTS.md file exists and the PR modifies architecture, workflows, responsibilities, automation behavior, CLI interfaces, or agent-related logic, the corresponding AGENTS.md file in the affected directory MUST be updated in the same PR. Issue a warning if relevant code changes occur without AGENTS.md updates.

Applied to files:

  • src/kp2bw/AGENTS.md
📚 Learning: 2026-02-18T19:15:44.159Z
Learnt from: CR
Repo: kjanat/actup-v2 PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-18T19:15:44.159Z
Learning: Applies to **/AGENTS.md : If any AGENTS.md file exists anywhere in the repository and the PR modifies architecture, workflows, responsibilities, automation behavior, CLI interfaces, or agent-related logic, then the corresponding AGENTS.md file in the affected directory MUST be updated in the same PR. If relevant code changes occur without updates to AGENTS.md, issue a warning.

Applied to files:

  • src/kp2bw/AGENTS.md
📚 Learning: 2026-02-24T01:14:19.224Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.224Z
Learning: Keep docs scoped to directory implementation detail in child `AGENTS.md` files; ignore transient dirs in analysis

Applied to files:

  • src/kp2bw/AGENTS.md
📚 Learning: 2026-02-19T01:03:38.622Z
Learnt from: CR
Repo: kjanat/actup-v2 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-19T01:03:38.622Z
Learning: Applies to AGENTS.md : Document agent configuration and behavior in AGENTS.md

Applied to files:

  • src/kp2bw/AGENTS.md
📚 Learning: 2026-02-23T22:57:17.173Z
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: AGENTS.md:3-3
Timestamp: 2026-02-23T22:57:17.173Z
Learning: In AGENTS.md files in the kp2bw repository: the "Generated: [date] Commit: `[hash]` Branch: `[branch]`" header line is intentionally hardcoded to document when the AGENTS.md file was last updated, serving as a staleness indicator for readers. This is a deliberate documentation practice, not an error.

Applied to files:

  • src/kp2bw/AGENTS.md
📚 Learning: 2026-02-24T00:44:49.520Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.520Z
Learning: Applies to src/kp2bw/bw_serve.py : Tune dedup/idempotency behavior in `src/kp2bw/bw_serve.py` using the existing item index and batch create behavior

Applied to files:

  • src/kp2bw/AGENTS.md
  • src/kp2bw/bw_serve.py
📚 Learning: 2026-02-24T00:44:49.520Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.520Z
Learning: Applies to src/kp2bw/**/*.py : Do not reintroduce subprocess-per-op transport for new work; use bw_serve.py as the active transport path

Applied to files:

  • src/kp2bw/AGENTS.md
📚 Learning: 2026-02-24T00:44:49.520Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.520Z
Learning: Applies to src/kp2bw/**/*.py : Do not break idempotency by bypassing existing-item dedup checks

Applied to files:

  • src/kp2bw/AGENTS.md
📚 Learning: 2026-02-23T21:45:22.144Z
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:416-426
Timestamp: 2026-02-23T21:45:22.144Z
Learning: In `src/kp2bw/bw_serve.py` (Python, Bitwarden CLI integration): Bitwarden `bw serve` list endpoints (`/list/object/folders`, `/list/object/items`, `/list/object/org-collections`) return a nested structure: the outer JSON envelope has `{ "success": true, "data": { "object": "list", "data": [...] } }`. The `_request()` method returns `body.get("data", {})` (the middle layer), so callers must extract `data.get("data", [])` to access the actual array. This double extraction is correct and matches the official Vault Management API spec.

Applied to files:

  • src/kp2bw/AGENTS.md
  • src/kp2bw/bw_serve.py
📚 Learning: 2026-02-24T00:44:49.520Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.520Z
Learning: Applies to src/kp2bw/{cli,__main__}.py : Maintain behavior parity for `kp2bw.cli:main` and `python -m kp2bw` entry points

Applied to files:

  • src/kp2bw/AGENTS.md
📚 Learning: 2026-02-24T00:44:49.520Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.520Z
Learning: Applies to src/kp2bw/convert.py : Change import behavior in `src/kp2bw/convert.py` by modifying the top-level `convert()` flow and migration phases

Applied to files:

  • src/kp2bw/AGENTS.md
📚 Learning: 2026-02-24T01:14:19.224Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.224Z
Learning: Applies to +(src/kp2bw|packages/pykeepass-stubs)/**/*.py : Use project-specific exceptions (`BitwardenClientError`, `ConversionError`) instead of bare `Exception`

Applied to files:

  • src/kp2bw/AGENTS.md
📚 Learning: 2026-02-23T21:45:21.228Z
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:335-342
Timestamp: 2026-02-23T21:45:21.228Z
Learning: In the Bitwarden CLI `bw serve` Vault Management API, list endpoints (`/list/object/folders`, `/list/object/items`, `/list/object/org-collections`) return a nested structure where the outer envelope's `data` field contains a list wrapper object `{ "object": "list", "data": [...] }`. Therefore, code that calls `_request(...)` and then does `.get("data", [])` is correctly extracting the actual items array from the list wrapper - this is not a double-extraction bug but the correct handling of the API's response structure.
<!-- </add_learning>

Applied to files:

  • src/kp2bw/AGENTS.md
  • src/kp2bw/bw_serve.py
📚 Learning: 2026-02-24T01:14:19.224Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.224Z
Learning: Applies to src/kp2bw/bw_serve.py : `bw serve` is localhost-only and password is passed via environment variable, not CLI argument

Applied to files:

  • src/kp2bw/AGENTS.md
📚 Learning: 2026-02-24T00:44:49.520Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.520Z
Learning: Applies to src/kp2bw/**/*.py : Raise project exceptions (`BitwardenClientError`, `ConversionError`) instead of bare `Exception`

Applied to files:

  • src/kp2bw/AGENTS.md
📚 Learning: 2026-02-23T21:45:13.630Z
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:306-310
Timestamp: 2026-02-23T21:45:13.630Z
Learning: In src/kp2bw/bw_serve.py, for Bitwarden CLI 'bw serve' Vault Management API endpoints (e.g., /list/object/folders, /list/object/items, /list/object/org-collections), the HTTP response has the shape {"success": true, "data": {"object": "list", "data": [...]}}. Since the outer 'data' is a wrapper object, extract the inner list with payload.get("data", {}).get("data", []) after calling _request(). Example: payload = _request(...); items = payload.get("data", {}).get("data", []) to obtain the actual array.

Applied to files:

  • src/kp2bw/bw_serve.py
🔇 Additional comments (1)
src/kp2bw/bw_serve.py (1)

436-455: LGTM — the dedup logic is correct in intent.

_build_dedup_index correctly delegates to list_items(organization_id=self._org_id), passing None for personal vaults (no filter) and the org ID for org imports. The behaviour here is sound once list_items uses the correct lowercase query param key.

Comment thread src/kp2bw/AGENTS.md

- `bw_serve.py` is the active transport path; `bw_import.py` and `bitwardenclient.py` are legacy/reference.
- Maintain behavior parity for `kp2bw.cli:main` and `python -m kp2bw`.
- Dedup index is org-scoped when `--bitwarden-org` is set: `_build_dedup_index()` passes `organization_id=self._org_id` to `list_items()`, which appends `organizationId` as a query param to `/list/object/items`. When `org_id` is `None` (personal vault), no filter is applied and all vault items are indexed. This prevents personal vault entries from shadowing an empty org vault during migration.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Documentation perpetuates the wrong query-param casing.

The note says organizationId is appended as a query param, but the correct key per the bw serve query-string convention is organizationid (lowercase). This will mislead future contributors.

📝 Proposed fix
-- Dedup index is org-scoped when `--bitwarden-org` is set: `_build_dedup_index()` passes `organization_id=self._org_id` to `list_items()`, which appends `organizationId` as a query param to `/list/object/items`. When `org_id` is `None` (personal vault), no filter is applied and all vault items are indexed. This prevents personal vault entries from shadowing an empty org vault during migration.
+- Dedup index is org-scoped when `--bitwarden-org` is set: `_build_dedup_index()` passes `organization_id=self._org_id` to `list_items()`, which appends `organizationid` as a query param to `/list/object/items`. When `org_id` is `None` (personal vault), no filter is applied and all vault items are indexed. This prevents personal vault entries from shadowing an empty org vault during migration.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Dedup index is org-scoped when `--bitwarden-org` is set: `_build_dedup_index()` passes `organization_id=self._org_id` to `list_items()`, which appends `organizationId` as a query param to `/list/object/items`. When `org_id` is `None` (personal vault), no filter is applied and all vault items are indexed. This prevents personal vault entries from shadowing an empty org vault during migration.
- Dedup index is org-scoped when `--bitwarden-org` is set: `_build_dedup_index()` passes `organization_id=self._org_id` to `list_items()`, which appends `organizationid` as a query param to `/list/object/items`. When `org_id` is `None` (personal vault), no filter is applied and all vault items are indexed. This prevents personal vault entries from shadowing an empty org vault during migration.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kp2bw/AGENTS.md` at line 51, Update the documentation note in AGENTS.md
to use the correct lowercase query parameter name per bw serve conventions:
change the mention of `organizationId` to `organizationid` in the description of
_build_dedup_index() behavior (which passes organization_id=self._org_id to
list_items()), so readers know the actual query string key used when filtering
org-scoped dedup indexing versus personal vault indexing.

Comment thread src/kp2bw/bw_serve.py
Comment on lines 477 to 481
data = self._request(
"GET",
"/list/object/org-collections",
params={"organizationid": self._org_id},
params={"organizationId": self._org_id},
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Regression: list_collections broke what was previously correct.

The previous code at line 480 used {"organizationid": self._org_id} (lowercase — correct). This PR changed it to {"organizationId": self._org_id} (camelCase — wrong), making list_collections silently return all collections instead of the org-filtered set. This is a net-negative change on top of the pre-existing list_items bug.

🐛 Proposed fix
         data = self._request(
             "GET",
             "/list/object/org-collections",
-            params={"organizationId": self._org_id},
+            params={"organizationid": self._org_id},
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kp2bw/bw_serve.py` around lines 477 - 481, The call in list_collections
is passing the wrong query key casing so the org filter is dropped; update the
params dict in the self._request call inside list_collections to use the
lowercase key "organizationid" (not "organizationId") so the organization filter
is applied correctly.

coderabbitai[bot]

This comment was marked as spam.

- bw_types.py: add Literal import; Field→BwField in _BwItemBase.fields;
  BwItemLogin.uris now list[BwUri] (not list[Uris])
- convert.py: replace BwItem/Fido2Credentials/list[Any] with BwItemCreate,
  BwItemResponse, BwFido2Credential, BwItemLogin, BwField, BwUri, FieldSpec;
  dedup block now calls get_existing_item() and issues PUT to add missing
  collection assignments instead of skipping the entry
- CHANGELOG.md: Unreleased entries for both changes
- organizationId casing fix on /list/object/org-collections
- CLI help metavar standardization

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/kp2bw/convert.py (1)

529-562: 🧹 Nitpick | 🔵 Trivial

Use lazy logging for the new dedup messages (ruff G004).
Logic is good; the formatting style is just wasteful.

♻️ Suggested change
-                        logger.info(
-                            f"-- Entry {bw_item['name']!r}: added to "
-                            f"{len(missing)} collection(s)"
-                        )
+                        logger.info(
+                            "-- Entry %r: added to %d collection(s)",
+                            bw_item["name"],
+                            len(missing),
+                        )
@@
-                        logger.info(
-                            f"-- Entry {bw_item['name']!r} already in "
-                            f"folder {folder!r}, skipping"
-                        )
+                        logger.info(
+                            "-- Entry %r already in folder %r, skipping",
+                            bw_item["name"],
+                            folder,
+                        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kp2bw/convert.py` around lines 529 - 562, The logger.info calls inside
the dedup branch (around bw.get_existing_item, bw.update_item, bw_item) use
f-strings which violate lazy logging (ruff G004); replace them with lazy logging
calls using formatting placeholders and separate arguments (e.g. "Entry %r:
added to %d collection(s)" with bw_item["name"], len(missing)) and similarly for
the skip message ("Entry %r already in folder %r, skipping", bw_item["name"],
folder) so interpolation happens only if the message is emitted.
♻️ Duplicate comments (2)
pyproject.toml (1)

3-3: ⚠️ Potential issue | 🟠 Major

Version is still non‑SemVer. Fix it to MAJOR.MINOR.PATCH.
3.0.0a2 violates the required SemVer format. This is a bugfix PR, so bump PATCH (e.g., 3.0.1) and update the changelog header/links to match. I’ll be happy once this is clean.

🔧 Suggested fix
-version         = "3.0.0a2"
+version         = "3.0.1"

Based on learnings "If any source code files (excluding tests, docs, CI, markdown, or comments-only changes) are modified, then a version field MUST be updated in one of the following files if present in the repo: package.json, pyproject.toml, setup.py, Cargo.toml, go.mod, pom.xml, build.gradle, or a VERSION file. The new version MUST follow SemVer (MAJOR.MINOR.PATCH). If the PR introduces breaking changes (removal or renaming of public APIs, changes to function signatures, deleted exported symbols, or incompatible config changes), MAJOR must increment. If it adds backward-compatible functionality, MINOR must increment. If it only fixes bugs without changing public APIs, PATCH must increment. If code changes exist and no version bump is detected, or the bump level does not match the change type, fail."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyproject.toml` at line 3, The version field in pyproject.toml is not SemVer
(version = "3.0.0a2"); because this is a bugfix PR you must update the version
field to a PATCH bump (e.g., change version = "3.0.1") and also update the
changelog header and any related release links/anchors to match the new 3.0.1
identifier so references stay consistent; locate the "version" entry in
pyproject.toml and the changelog header/links (CHANGELOG.md or RELEASE notes)
and edit them accordingly.
src/kp2bw/bw_serve.py (1)

378-390: ⚠️ Potential issue | 🟠 Major

Verify organizationId query casing — this might still be ignored by bw serve.
bw serve list endpoints typically mirror CLI flag casing (lowercase). If the query key is wrong, the org filter silently does nothing and your dedup fix is dead on arrival. Please confirm, then adjust if needed.

🔧 Possible fix (if bw serve expects CLI-style params)
-        if organization_id is not None:
-            params["organizationId"] = organization_id
+        if organization_id is not None:
+            params["organizationid"] = organization_id
@@
-            params={"organizationId": self._org_id},
+            params={"organizationid": self._org_id},
Bitwarden CLI "bw serve" list items query parameter casing organizationid vs organizationId

Also applies to: 489-493

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kp2bw/bw_serve.py` around lines 378 - 390, The query param key for
organization is currently "organizationId" in the list_items method (params
dict) which may not match bw serve's expected lowercase CLI-style key and thus
the organization filter can be ignored; change the params key to the lowercase
form ("organizationid" or whatever bw serve expects) where organization_id is
added, update any other occurrences (the similar block around lines handling
organizationId at the second instance referenced) and verify by testing
list_items and the other function that builds params so the org filter is
actually applied.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/kp2bw/_bw_api_types.py`:
- Around line 1-5: The generated types in _bw_api_types.py have multiple codegen
artifacts: change the singular uris field to a sequence (replace the field typed
as Uris with list[Uris] or list["Uris"] in the model that references it),
replace the hard-coded Literal["visa"] on the brand field with a broader type
(e.g., Literal of allowed brands or plain str) rather than an example value,
remove blanket NotRequired from fields that are actually required by the API by
making required fields (e.g., name and type on ItemTemplate) mandatory in their
TypedDicts, and rename awkward autogenerated identifiers (Data1,
Deviceapprovalproperties, LockunlockSuccess) to meaningful names and update all
references; implement these fixes either by updating the OpenAPI required
annotations and regenerating or by adding a post-generation pass in your codegen
script that applies these substitutions and updates import/type references
(target symbols: ItemTemplate, Uris, brand, Data1, Deviceapprovalproperties,
LockunlockSuccess).
- Line 14: The generated type for the credit card field "brand" is incorrectly
fixed to Literal["visa"]; update the type declaration in
src/kp2bw/_bw_api_types.py so that the "brand" field uses a flexible string type
(NotRequired[str]) instead of NotRequired[Literal["visa"]] — locate the "brand"
entry in the relevant TypedDict/dataclass definition and replace the Literal
with str (or adjust your codegen/post-processing to emit str for that field).
- Around line 108-112: The ItemLogin TypedDict currently declares uris as a
single Uris object but the Bitwarden API returns a list; update the ItemLogin
definition so uris is NotRequired[list[Uris]] (i.e., change the type of the uris
field in class ItemLogin to NotRequired[list[Uris]]). Keep the rest of the
fields unchanged and ensure any existing references to ItemLogin that iterate or
append to uris will now type-check; this mirrors the existing correct
BwItemLogin/list[BwUri] pattern.

In `@src/kp2bw/bw_serve.py`:
- Around line 394-408: The current create_item and update_item functions use
f-strings in logger.log which eagerly format even when the log level is
disabled; change both logging calls to use lazy "%"-style formatting by passing
the format string and values as separate args (e.g., use a format like "Created
item %r → %s" and pass item.get("name", "?") and item_id, and similarly "Updated
item %r (%s)" with item.get("name", "?") and item_id) so the interpolation is
deferred until the message is actually emitted.

In `@src/kp2bw/bw_types.py`:
- Around line 55-63: The docstring for BwUri uses an en dash in "Literal[0–5]"
which triggers ruff RUF002; update the docstring to use a hyphen so it reads
"Literal[0-5]". Locate the BwUri TypedDict and edit its docstring line
containing "Literal[0–5]" to "Literal[0-5]" (the TypedDict and the match field
name remain unchanged).

---

Outside diff comments:
In `@src/kp2bw/convert.py`:
- Around line 529-562: The logger.info calls inside the dedup branch (around
bw.get_existing_item, bw.update_item, bw_item) use f-strings which violate lazy
logging (ruff G004); replace them with lazy logging calls using formatting
placeholders and separate arguments (e.g. "Entry %r: added to %d collection(s)"
with bw_item["name"], len(missing)) and similarly for the skip message ("Entry
%r already in folder %r, skipping", bw_item["name"], folder) so interpolation
happens only if the message is emitted.

---

Duplicate comments:
In `@pyproject.toml`:
- Line 3: The version field in pyproject.toml is not SemVer (version =
"3.0.0a2"); because this is a bugfix PR you must update the version field to a
PATCH bump (e.g., change version = "3.0.1") and also update the changelog header
and any related release links/anchors to match the new 3.0.1 identifier so
references stay consistent; locate the "version" entry in pyproject.toml and the
changelog header/links (CHANGELOG.md or RELEASE notes) and edit them
accordingly.

In `@src/kp2bw/bw_serve.py`:
- Around line 378-390: The query param key for organization is currently
"organizationId" in the list_items method (params dict) which may not match bw
serve's expected lowercase CLI-style key and thus the organization filter can be
ignored; change the params key to the lowercase form ("organizationid" or
whatever bw serve expects) where organization_id is added, update any other
occurrences (the similar block around lines handling organizationId at the
second instance referenced) and verify by testing list_items and the other
function that builds params so the org filter is actually applied.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45f1c7a and d7ebfe2.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • .gitattributes
  • .github/workflows/pr-comment-bot.yml
  • CHANGELOG.md
  • pyproject.toml
  • scripts/generate-bw-types.sh
  • specs/vault-management-api.json
  • src/kp2bw/_bw_api_types.py
  • src/kp2bw/bw_import.py
  • src/kp2bw/bw_serve.py
  • src/kp2bw/bw_types.py
  • src/kp2bw/convert.py
💤 Files with no reviewable changes (1)
  • src/kp2bw/bw_import.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
.github/workflows/*.yml

📄 CodeRabbit inference engine (AGENTS.md)

Workflow check jobs must call scripts/*.mjs via actions/github-script; script output keys are workflow contracts

Files:

  • .github/workflows/pr-comment-bot.yml
src/kp2bw/**/*.py

📄 CodeRabbit inference engine (src/kp2bw/AGENTS.md)

src/kp2bw/**/*.py: Use module logger with logger = logging.getLogger(__name__), never root logger calls
Keep sensitive values out of logs - do not log raw bw commands/output, session values, or password values
Raise project exceptions (BitwardenClientError, ConversionError) instead of bare Exception
Use relative imports for local modules (e.g., from .exceptions import ConversionError)
Python 3.14 comma-form multi-except is valid; do not rewrite it to tuple form just for style
Do not reintroduce subprocess-per-op transport for new work; use bw_serve.py as the active transport path
Do not log decrypted vault content or credential-bearing command strings
Do not break idempotency by bypassing existing-item dedup checks

Never log raw bw command strings or raw command output to preserve security

Files:

  • src/kp2bw/bw_types.py
  • src/kp2bw/bw_serve.py
  • src/kp2bw/_bw_api_types.py
  • src/kp2bw/convert.py
src/kp2bw/bw_serve.py

📄 CodeRabbit inference engine (src/kp2bw/AGENTS.md)

src/kp2bw/bw_serve.py: Tune dedup/idempotency behavior in src/kp2bw/bw_serve.py using the existing item index and batch create behavior
Configure attachment behavior in src/kp2bw/bw_serve.py via the async upload path and multipart logic

bw serve is localhost-only and password is passed via environment variable, not CLI argument

Files:

  • src/kp2bw/bw_serve.py
src/kp2bw/convert.py

📄 CodeRabbit inference engine (src/kp2bw/AGENTS.md)

src/kp2bw/convert.py: Change import behavior in src/kp2bw/convert.py by modifying the top-level convert() flow and migration phases
Map KeePass fields in src/kp2bw/convert.py including entry/custom field/TOTP/passkey mapping

Files:

  • src/kp2bw/convert.py
+(ruff.toml|pyproject.toml)

📄 CodeRabbit inference engine (AGENTS.md)

Ruff configuration must run in preview mode with target py314 for root package; stubs package has separate Ruff config

Files:

  • pyproject.toml
🧠 Learnings (36)
📓 Common learnings
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:335-342
Timestamp: 2026-02-23T21:45:21.228Z
Learning: In the Bitwarden CLI `bw serve` Vault Management API, list endpoints (`/list/object/folders`, `/list/object/items`, `/list/object/org-collections`) return a nested structure where the outer envelope's `data` field contains a list wrapper object `{ "object": "list", "data": [...] }`. Therefore, code that calls `_request(...)` and then does `.get("data", [])` is correctly extracting the actual items array from the list wrapper - this is not a double-extraction bug but the correct handling of the API's response structure.
<!-- </add_learning>
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.520Z
Learning: Applies to src/kp2bw/**/*.py : Do not break idempotency by bypassing existing-item dedup checks
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.520Z
Learning: Applies to src/kp2bw/bw_serve.py : Tune dedup/idempotency behavior in `src/kp2bw/bw_serve.py` using the existing item index and batch create behavior
📚 Learning: 2026-02-24T00:44:49.520Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.520Z
Learning: Applies to src/kp2bw/convert.py : Change import behavior in `src/kp2bw/convert.py` by modifying the top-level `convert()` flow and migration phases

Applied to files:

  • CHANGELOG.md
  • src/kp2bw/convert.py
📚 Learning: 2026-02-19T10:52:05.498Z
Learnt from: CR
Repo: kjanat/actup-v2 PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-19T10:52:05.498Z
Learning: Applies to **/{.github/workflows/**,**/.github/actions/**/}*.{yml,yaml} : Preserve YAML comments, indentation, and quoting when editing workflow/action files; avoid whole-file rewrites

Applied to files:

  • .github/workflows/pr-comment-bot.yml
📚 Learning: 2026-02-12T02:40:30.410Z
Learnt from: CR
Repo: kjanat/glb-compressor PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-12T02:40:30.410Z
Learning: If NO version bump is detected, changes in the PR MUST be added under the existing '## [Unreleased]' section in CHANGELOG.md with entries describing the changes (e.g., Added, Changed, Fixed, Removed)

Applied to files:

  • .github/workflows/pr-comment-bot.yml
📚 Learning: 2026-02-21T04:16:56.990Z
Learnt from: CR
Repo: kjanat/paperless-mcp PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-21T04:16:56.990Z
Learning: If AGENTS.md file exists and the PR modifies architecture, workflows, responsibilities, automation behavior, CLI interfaces, or agent-related logic, the corresponding AGENTS.md file in the affected directory MUST be updated in the same PR. Issue a warning if relevant code changes occur without AGENTS.md updates.

Applied to files:

  • .github/workflows/pr-comment-bot.yml
📚 Learning: 2026-02-05T18:41:54.547Z
Learnt from: kjanat
Repo: kjanat/notso-glb PR: 12
File: server.py:9-16
Timestamp: 2026-02-05T18:41:54.547Z
Learning: In the kjanat/notso-glb repository, code formatting (such as import statement organization and splitting) is handled automatically by the `.github/workflows/autofix.yml` workflow in CI. Do not flag formatting issues that are auto-corrected by this workflow.

Applied to files:

  • .github/workflows/pr-comment-bot.yml
📚 Learning: 2026-02-18T19:15:44.159Z
Learnt from: CR
Repo: kjanat/actup-v2 PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-18T19:15:44.159Z
Learning: Applies to **/CHANGELOG.md : If NO version bump is detected in source code changes, updates in the PR MUST be added under the existing '## [Unreleased]' section of CHANGELOG.md. The entry MUST describe the changes (e.g., Added, Changed, Fixed, Removed).

Applied to files:

  • .github/workflows/pr-comment-bot.yml
📚 Learning: 2026-02-24T00:45:03.726Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-24T00:45:03.726Z
Learning: If any AGENTS.md file exists anywhere in the repository and the PR modifies architecture, workflows, responsibilities, automation behavior, CLI interfaces, or agent-related logic, then the corresponding AGENTS.md file in the affected directory MUST be updated in the same PR. If relevant code changes occur without updates to AGENTS.md, issue a warning.

Applied to files:

  • .github/workflows/pr-comment-bot.yml
📚 Learning: 2026-02-24T00:45:03.726Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-24T00:45:03.726Z
Learning: If any source code files (excluding tests, docs, CI, markdown, or comments-only changes) are modified, CHANGELOG.md MUST also be modified in the same PR. If a version bump is detected, CHANGELOG.md MUST contain a new section header matching the exact new version number in the format: '## [X.Y.Z] - YYYY-MM-DD'. If NO version bump is detected, the changes in this PR MUST be added under the existing '## [Unreleased]' section. The entry MUST describe the changes (e.g., Added, Changed, Fixed, Removed). If code changes are present and CHANGELOG.md is not updated accordingly, fail.

Applied to files:

  • .github/workflows/pr-comment-bot.yml
📚 Learning: 2026-02-24T00:44:49.520Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.520Z
Learning: Applies to src/kp2bw/tests/e2e_vaultwarden_test.py : Update e2e expectations in `tests/e2e_vaultwarden_test.py` when changing conversion behavior

Applied to files:

  • .gitattributes
  • src/kp2bw/convert.py
📚 Learning: 2026-02-24T01:14:19.224Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.224Z
Learning: Applies to +(src/kp2bw|packages/pykeepass-stubs)/**/*.py : Use project-specific exceptions (`BitwardenClientError`, `ConversionError`) instead of bare `Exception`

Applied to files:

  • src/kp2bw/bw_types.py
  • src/kp2bw/convert.py
  • pyproject.toml
📚 Learning: 2026-02-24T00:44:49.520Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.520Z
Learning: Applies to src/kp2bw/**/*.py : Raise project exceptions (`BitwardenClientError`, `ConversionError`) instead of bare `Exception`

Applied to files:

  • src/kp2bw/bw_types.py
  • src/kp2bw/convert.py
  • pyproject.toml
📚 Learning: 2026-02-23T21:45:22.144Z
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:416-426
Timestamp: 2026-02-23T21:45:22.144Z
Learning: In `src/kp2bw/bw_serve.py` (Python, Bitwarden CLI integration): Bitwarden `bw serve` list endpoints (`/list/object/folders`, `/list/object/items`, `/list/object/org-collections`) return a nested structure: the outer JSON envelope has `{ "success": true, "data": { "object": "list", "data": [...] } }`. The `_request()` method returns `body.get("data", {})` (the middle layer), so callers must extract `data.get("data", [])` to access the actual array. This double extraction is correct and matches the official Vault Management API spec.

Applied to files:

  • src/kp2bw/bw_types.py
  • src/kp2bw/bw_serve.py
📚 Learning: 2026-02-23T21:45:13.630Z
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:306-310
Timestamp: 2026-02-23T21:45:13.630Z
Learning: In Python code using the Bitwarden CLI `bw serve` Vault Management API, list endpoints (e.g., `/list/object/folders`, `/list/object/items`, `/list/object/org-collections`) return a nested structure: the HTTP response is `{"success": true, "data": {"object": "list", "data": [...]}}`, where the outer `data` is a list wrapper object containing an inner `data` array. Code that calls `_request()` and receives the outer `data` payload should use `.get("data", [])` to extract the array from the list wrapper object.

Applied to files:

  • src/kp2bw/bw_types.py
📚 Learning: 2026-02-24T00:44:49.520Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.520Z
Learning: Applies to src/kp2bw/bw_serve.py : Tune dedup/idempotency behavior in `src/kp2bw/bw_serve.py` using the existing item index and batch create behavior

Applied to files:

  • src/kp2bw/bw_serve.py
📚 Learning: 2026-02-23T21:45:21.228Z
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:335-342
Timestamp: 2026-02-23T21:45:21.228Z
Learning: In the Bitwarden CLI `bw serve` Vault Management API, list endpoints (`/list/object/folders`, `/list/object/items`, `/list/object/org-collections`) return a nested structure where the outer envelope's `data` field contains a list wrapper object `{ "object": "list", "data": [...] }`. Therefore, code that calls `_request(...)` and then does `.get("data", [])` is correctly extracting the actual items array from the list wrapper - this is not a double-extraction bug but the correct handling of the API's response structure.
<!-- </add_learning>

Applied to files:

  • src/kp2bw/bw_serve.py
📚 Learning: 2026-02-23T21:45:13.630Z
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:306-310
Timestamp: 2026-02-23T21:45:13.630Z
Learning: In src/kp2bw/bw_serve.py, for Bitwarden CLI 'bw serve' Vault Management API endpoints (e.g., /list/object/folders, /list/object/items, /list/object/org-collections), the HTTP response has the shape {"success": true, "data": {"object": "list", "data": [...]}}. Since the outer 'data' is a wrapper object, extract the inner list with payload.get("data", {}).get("data", []) after calling _request(). Example: payload = _request(...); items = payload.get("data", {}).get("data", []) to obtain the actual array.

Applied to files:

  • src/kp2bw/bw_serve.py
📚 Learning: 2026-02-24T00:44:49.520Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.520Z
Learning: Applies to src/kp2bw/convert.py : Map KeePass fields in `src/kp2bw/convert.py` including entry/custom field/TOTP/passkey mapping

Applied to files:

  • src/kp2bw/convert.py
  • pyproject.toml
📚 Learning: 2026-02-24T01:14:19.224Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.224Z
Learning: Applies to +(src/kp2bw|packages/pykeepass-stubs)/**/*.py : Use relative intra-package imports with `from .module import X` syntax

Applied to files:

  • src/kp2bw/convert.py
  • pyproject.toml
📚 Learning: 2026-02-24T00:44:49.520Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.520Z
Learning: Applies to src/kp2bw/**/*.py : Do not break idempotency by bypassing existing-item dedup checks

Applied to files:

  • src/kp2bw/convert.py
📚 Learning: 2026-02-24T00:44:31.533Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.533Z
Learning: Applies to packages/pykeepass-stubs/pyproject.toml : Stubs package metadata and tool configuration must be maintained in pyproject.toml, including version bumping aligned with 'stubs-v*' release tags

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T01:14:19.224Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.224Z
Learning: Applies to +(ruff.toml|pyproject.toml) : Ruff configuration must run in preview mode with target `py314` for root package; stubs package has separate Ruff config

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-18T19:15:44.159Z
Learnt from: CR
Repo: kjanat/actup-v2 PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-18T19:15:44.159Z
Learning: Applies to {package.json,pyproject.toml,setup.py,Cargo.toml,go.mod,pom.xml,build.gradle,VERSION} : The new version MUST follow SemVer (MAJOR.MINOR.PATCH). If the PR introduces breaking changes (removal or renaming of public APIs, changes to function signatures, deleted exported symbols, or incompatible config changes), MAJOR must increment. If it adds backward-compatible functionality, MINOR must increment. If it only fixes bugs without changing public APIs, PATCH must increment.

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T00:45:03.726Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-24T00:45:03.726Z
Learning: If any source code files (excluding tests, docs, CI, markdown, or comments-only changes) are modified, then a version field MUST be updated in one of the following files if present in the repo: package.json, pyproject.toml, setup.py, Cargo.toml, go.mod, pom.xml, build.gradle, or a VERSION file. The new version MUST follow SemVer (MAJOR.MINOR.PATCH). If the PR introduces breaking changes (removal or renaming of public APIs, changes to function signatures, deleted exported symbols, or incompatible config changes), MAJOR must increment. If it adds backward-compatible functionality, MINOR must increment. If it only fixes bugs without changing public APIs, PATCH must increment. If code changes exist and no version bump is detected, or the bump level does not match the change type, fail.

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-12T02:40:30.410Z
Learnt from: CR
Repo: kjanat/glb-compressor PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-12T02:40:30.410Z
Learning: SemVer Version Bump: If PR only fixes bugs without changing public APIs, PATCH version must increment

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-21T04:16:56.990Z
Learnt from: CR
Repo: kjanat/paperless-mcp PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-21T04:16:56.990Z
Learning: When modifying source code files, ensure the version bump in package.json, pyproject.toml, setup.py, Cargo.toml, go.mod, pom.xml, build.gradle, or VERSION file follows SemVer: increment MAJOR for breaking changes (removal/renaming of public APIs, function signature changes, deleted exported symbols, incompatible config changes), MINOR for backward-compatible functionality additions, and PATCH for bug fixes without public API changes.

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-12T02:40:30.410Z
Learnt from: CR
Repo: kjanat/glb-compressor PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-12T02:40:30.410Z
Learning: SemVer Version Bump: If PR introduces breaking changes (removal or renaming of public APIs, changes to function signatures, deleted exported symbols, or incompatible config changes), MAJOR version must increment

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-12T02:40:30.410Z
Learnt from: CR
Repo: kjanat/glb-compressor PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-12T02:40:30.410Z
Learning: SemVer Version Bump: If PR adds backward-compatible functionality, MINOR version must increment

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-21T04:16:56.990Z
Learnt from: CR
Repo: kjanat/paperless-mcp PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-21T04:16:56.990Z
Learning: If any source code files (excluding tests, docs, CI, markdown, or comments-only changes) are modified, a version field MUST be updated in one of the following files if present: package.json, pyproject.toml, setup.py, Cargo.toml, go.mod, pom.xml, build.gradle, or VERSION file. The new version MUST follow SemVer (MAJOR.MINOR.PATCH) format.

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T00:44:31.533Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.533Z
Learning: Applies to packages/pykeepass-stubs/pyproject.toml : Python baseline for pykeepass-stubs package is >=3.11 (differs from runtime package >=3.14)

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T01:14:19.224Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.224Z
Learning: Applies to +(pyproject.toml|packages/pykeepass-stubs/pyproject.toml) : Python baseline is `>=3.14` for root package; stubs package targets `>=3.11`

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T00:44:31.533Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.533Z
Learning: Applies to packages/pykeepass-stubs/pyproject.toml : Stub package must include 'Typing :: Stubs Only' classifier and include 'py.typed' marker in package configuration

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T00:44:31.533Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.533Z
Learning: Applies to packages/pykeepass-stubs/src/pykeepass-stubs/py.typed : Maintain 'py.typed' marker with 'partial' designation in the shipped stubs package path for partial-stub semantics

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T00:44:31.533Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.533Z
Learning: Applies to packages/pykeepass-stubs/src/pykeepass-stubs/*.pyi : Type stub (.pyi) file signatures must be kept aligned with actual pykeepass module behavior

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T00:44:31.533Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.533Z
Learning: Applies to packages/pykeepass-stubs/** : Keep Ruff and Pyright configuration local to the stubs package; maintain separate settings from root and runtime package configurations

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T00:44:31.533Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.533Z
Learning: Applies to packages/pykeepass-stubs/src/pykeepass-stubs/**/*.py : Pykeepass-stubs package must remain type-only with no runtime module logic

Applied to files:

  • pyproject.toml
🧬 Code graph analysis (3)
src/kp2bw/bw_types.py (1)
src/kp2bw/_bw_api_types.py (7)
  • Group (62-65)
  • ItemCard (12-18)
  • ItemIdentity (21-39)
  • ItemSecureNote (8-9)
  • LockunlockSuccess (93-95)
  • Status (81-83)
  • Uris (42-44)
src/kp2bw/bw_serve.py (1)
src/kp2bw/bw_types.py (4)
  • BwCollection (169-177)
  • BwFolder (161-166)
  • BwItemCreate (139-140)
  • BwItemResponse (143-153)
src/kp2bw/convert.py (1)
src/kp2bw/bw_types.py (5)
  • BwFido2Credential (79-94)
  • BwField (66-71)
  • BwItemLogin (102-114)
  • BwItemResponse (143-153)
  • BwUri (55-63)
🪛 Ruff (0.15.2)
src/kp2bw/bw_types.py

[warning] 58-58: Docstring contains ambiguous – (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF002)

src/kp2bw/bw_serve.py

[warning] 398-398: Logging statement uses f-string

(G004)


[warning] 408-408: Logging statement uses f-string

(G004)

src/kp2bw/convert.py

[warning] 554-555: Logging statement uses f-string

(G004)


[warning] 559-560: Logging statement uses f-string

(G004)

🔇 Additional comments (21)
.github/workflows/pr-comment-bot.yml (1)

160-160: No functional change. Just a trailing newline—nothing to review.

pyproject.toml (1)

33-47: Dependency-group reshuffle looks tidy.

scripts/generate-bw-types.sh (1)

1-22: Nice, deterministic codegen script. Strict mode + explicit output path = ❤️.

.gitattributes (1)

1-3: Good call marking generated/vendored files.

CHANGELOG.md (1)

11-39: Changelog entry is thorough and readable.

Also applies to: 344-345

src/kp2bw/bw_serve.py (5)

18-78: Typed dict imports + cache typing are aligned. Clean upgrade.


352-353: Typed folder parsing is fine.


412-433: Nice: folderId binding avoids mutating input items.


445-466: Dedup index storing full items is a solid move. Makes collection updates cheap.


470-474: get_existing_item helper is clean and useful.

src/kp2bw/bw_types.py (2)

1-47: Docstring + re-export structure is clear.


66-177: TypedDict definitions look consistent with the new API shapes.

src/kp2bw/convert.py (8)

10-33: Typed imports + new aliases are coherent.


96-133: Passkey extraction now lands in typed credentials cleanly.


143-177: Typed item construction looks solid.


215-256: FieldSpec mapping for metadata/custom fields is tidy.


335-343: Ref-value extraction is now type-consistent.


431-464: REF resolution + URI merge still reads well.


479-485: Unpack helper stays clean with the new types.


491-505: Collection resolution now matches BwItemCreate cleanly.

src/kp2bw/_bw_api_types.py (1)

139-152: ItemTemplate structure looks correct — nice to see organizationId and collectionIds properly represented.

This is the key type for the PR's org-scoped item creation. The literal values 1, 2, 3, 4 for item types are correct per the Bitwarden API. Happy to see this in place.


class ItemCard(TypedDict):
cardholderName: NotRequired[str]
brand: NotRequired[Literal["visa"]]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

brand locked to Literal["visa"] is a codegen artifact — should be str.

The OpenAPI spec example value leaked into the generated type. Bitwarden supports many card brands (Mastercard, Amex, Discover, etc.). Since this file is checked in and consumed by other modules, fix the codegen output or post-process it.

Proposed fix
-    brand: NotRequired[Literal["visa"]]
+    brand: NotRequired[str]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
brand: NotRequired[Literal["visa"]]
brand: NotRequired[str]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kp2bw/_bw_api_types.py` at line 14, The generated type for the credit
card field "brand" is incorrectly fixed to Literal["visa"]; update the type
declaration in src/kp2bw/_bw_api_types.py so that the "brand" field uses a
flexible string type (NotRequired[str]) instead of NotRequired[Literal["visa"]]
— locate the "brand" entry in the relevant TypedDict/dataclass definition and
replace the Literal with str (or adjust your codegen/post-processing to emit str
for that field).

Comment on lines +108 to +112
class ItemLogin(TypedDict):
uris: NotRequired[Uris]
username: NotRequired[str]
password: NotRequired[str]
totp: NotRequired[str]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for uris usage in the codebase to see how it's treated
rg -n "uris" src/kp2bw/ --type=py -A 2 -B 2

Repository: kjanat/kp2bw

Length of output: 2377


🏁 Script executed:

#!/bin/bash
# Check if there are any test files or examples showing ItemLogin structure
find . -type f \( -name "*test*" -o -name "*example*" -o -name "*fixture*" \) -exec rg -l "uris" {} \;

Repository: kjanat/kp2bw

Length of output: 87


🏁 Script executed:

#!/bin/bash
# Look at the Uris TypedDict definition to understand the structure
rg -n "class Uris" src/kp2bw/ --type=py -A 5

Repository: kjanat/kp2bw

Length of output: 360


🏁 Script executed:

#!/bin/bash
# Check for any direct API response parsing or sample responses
rg -n "login" src/kp2bw/ --type=py -C 3 | head -80

Repository: kjanat/kp2bw

Length of output: 4803


ItemLogin.uris must be list[Uris], not Uris.

The Bitwarden API returns login.uris as an array of URI objects. The type in _bw_api_types.py is wrong — any downstream code expecting to append to or iterate over uris will fail. The parallel BwItemLogin in bw_types.py already has the correct type (list[BwUri]) with an explicit docstring noting this is a correction to the spec gap.

Proposed fix
 class ItemLogin(TypedDict):
-    uris: NotRequired[Uris]
+    uris: NotRequired[list[Uris]]
     username: NotRequired[str]
     password: NotRequired[str]
     totp: NotRequired[str]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class ItemLogin(TypedDict):
uris: NotRequired[Uris]
username: NotRequired[str]
password: NotRequired[str]
totp: NotRequired[str]
class ItemLogin(TypedDict):
uris: NotRequired[list[Uris]]
username: NotRequired[str]
password: NotRequired[str]
totp: NotRequired[str]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kp2bw/_bw_api_types.py` around lines 108 - 112, The ItemLogin TypedDict
currently declares uris as a single Uris object but the Bitwarden API returns a
list; update the ItemLogin definition so uris is NotRequired[list[Uris]] (i.e.,
change the type of the uris field in class ItemLogin to
NotRequired[list[Uris]]). Keep the rest of the fields unchanged and ensure any
existing references to ItemLogin that iterate or append to uris will now
type-check; this mirrors the existing correct BwItemLogin/list[BwUri] pattern.

Comment thread src/kp2bw/bw_serve.py
Comment on lines +394 to +408
def create_item(self, item: BwItemCreate) -> str:
"""Create a single vault item via HTTP and return its ID."""
data = self._request("POST", "/object/item", json_body=item)
data = self._request("POST", "/object/item", json_body=item) # type: ignore[arg-type]
item_id: str = data["id"]
logger.log(VERBOSE, f"Created item {item.get('name', '?')!r} → {item_id}")
return item_id

def update_item(self, item_id: str, item: BwItemResponse) -> None:
"""Replace an existing vault item via ``PUT /object/item/{id}``.

The API requires the full object in the request body — partial updates
are not supported.
"""
self._request("PUT", f"/object/item/{item_id}", json_body=item) # type: ignore[arg-type]
logger.log(VERBOSE, f"Updated item {item.get('name', '?')!r} ({item_id})")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Please use lazy logging here (ruff G004).
F-strings format even when the log level is off. Use %-style args.

♻️ Suggested change
-        logger.log(VERBOSE, f"Created item {item.get('name', '?')!r} → {item_id}")
+        logger.log(VERBOSE, "Created item %r → %s", item.get("name", "?"), item_id)
@@
-        logger.log(VERBOSE, f"Updated item {item.get('name', '?')!r} ({item_id})")
+        logger.log(VERBOSE, "Updated item %r (%s)", item.get("name", "?"), item_id)
🧰 Tools
🪛 Ruff (0.15.2)

[warning] 398-398: Logging statement uses f-string

(G004)


[warning] 408-408: Logging statement uses f-string

(G004)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kp2bw/bw_serve.py` around lines 394 - 408, The current create_item and
update_item functions use f-strings in logger.log which eagerly format even when
the log level is disabled; change both logging calls to use lazy "%"-style
formatting by passing the format string and values as separate args (e.g., use a
format like "Created item %r → %s" and pass item.get("name", "?") and item_id,
and similarly "Updated item %r (%s)" with item.get("name", "?") and item_id) so
the interpolation is deferred until the message is actually emitted.

Comment thread src/kp2bw/bw_types.py
Comment on lines +55 to +63
class BwUri(TypedDict):
"""Single URI entry on a login item.

The spec types ``match`` as ``Literal[0–5]``; ``None`` is also valid and
means "use Bitwarden's default match detection".
"""

uri: str
match: NotRequired[Literal[0, 1, 2, 3, 4, 5] | None]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Swap the en dash for a hyphen (ruff RUF002). Tiny, but it keeps lint clean.

🔧 Suggested tweak
-    The spec types ``match`` as ``Literal[0–5]``; ``None`` is also valid and
+    The spec types ``match`` as ``Literal[0-5]``; ``None`` is also valid and
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class BwUri(TypedDict):
"""Single URI entry on a login item.
The spec types ``match`` as ``Literal[0–5]``; ``None`` is also valid and
means "use Bitwarden's default match detection".
"""
uri: str
match: NotRequired[Literal[0, 1, 2, 3, 4, 5] | None]
class BwUri(TypedDict):
"""Single URI entry on a login item.
The spec types ``match`` as ``Literal[0-5]``; ``None`` is also valid and
means "use Bitwarden's default match detection".
"""
uri: str
match: NotRequired[Literal[0, 1, 2, 3, 4, 5] | None]
🧰 Tools
🪛 Ruff (0.15.2)

[warning] 58-58: Docstring contains ambiguous – (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF002)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kp2bw/bw_types.py` around lines 55 - 63, The docstring for BwUri uses an
en dash in "Literal[0–5]" which triggers ruff RUF002; update the docstring to
use a hyphen so it reads "Literal[0-5]". Locate the BwUri TypedDict and edit its
docstring line containing "Literal[0–5]" to "Literal[0-5]" (the TypedDict and
the match field name remain unchanged).

- update_dedup_entry() prevents redundant PUTs on shared (folder, name)
- json_body widened to Mapping[str, Any]; removes type: ignore
- _BwItemCommon: notes/collectionIds nullable; login split to subclasses
- BwItemCreate.login required; BwItemResponse.login NotRequired
- copy.copy() replaces TypedDict spread ({**x, key: val})
- _find_referenced_value: explicit dict avoids dynamic key type: ignore
- _unpack_entry_value: direct tuple destructuring
- codegen-check.yml: CI fails PR if generated types drift from spec
…on <id> is set

Items present in other org collections were matching the dedup index and
being silently skipped instead of being imported into the target collection.

- BitwardenServeClient gains collection_id param; stored as _collection_id
- _build_dedup_index passes collection_id to list_items when set, restricting
  the index to items already in the target collection
- list_items gains collection_id param → appends ?collectionId= query param
- Converter passes the fixed collection ID (not 'auto') to the client; 'auto'
  mode retains org-wide dedup with collection-aware PUT updates
- Add rich>=13.0.0 dependency
- _console.py: shared Console(stderr=True) instance so RichHandler and
  Progress coordinate rendering and don't garble each other
- cli.py: replace basicConfig with RichHandler in non-debug modes;
  silence httpx logger at WARNING in normal/verbose modes (only -d shows it)
- bw_serve.py: create_items_batch gains on_item_created callback;
  removes per-batch 'Created X/Y items' log
- convert.py: Progress bar covering phase 1 (entry dedup loop), phase 2
  (item creation), phase 3 (attachments); per-entry INFO logs demoted to
  VERBOSE; final summary prints elapsed time + created/skipped/updated/
  attachment counts
bw_serve's signal handler re-raises via the previous handler, which
causes KeyboardInterrupt to unwind the with-stack (closes bw serve,
stops the progress bar) before reaching main(). Catch it there, print
a terse message, and exit 130 (128 + SIGINT). Remove the now-redundant
'All done.' print; the summary already prints completion.
…e passkey logs

bw serve returns collectionIds=null on listed items regardless of their
actual collection membership. In fixed-collection mode the dedup index is
already scoped to the target collection (collectionId filter), so any dedup
hit means the item is already there — no PUT needed, just skip.

The collection-aware PUT path is retained for auto/no-collection mode where
items may genuinely be missing from their resolved target collection.

Also demote 'Migrating passkey for entry:' from INFO to VERBOSE so it only
appears with -v.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/kp2bw/bw_serve.py (1)

519-529: ⚠️ Potential issue | 🔴 Critical

list_collections also uses wrong casing for organizationId query param.

Line 526: params={"organizationId": self._org_id} — same issue. Must be lowercase organizationid to match CLI flag casing.

🐛 Fix
         data = self._request(
             "GET",
             "/list/object/org-collections",
-            params={"organizationId": self._org_id},
+            params={"organizationid": self._org_id},
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kp2bw/bw_serve.py` around lines 519 - 529, The list_collections method
uses the wrong query param casing: change the params in BwServe.list_collections
(params={"organizationId": self._org_id}) to use the lowercase key
"organizationid" so it matches the CLI/API expectation; update the params dict
in the list_collections function accordingly.
♻️ Duplicate comments (5)
pyproject.toml (1)

3-3: ⚠️ Potential issue | 🟠 Major

Line 3 still uses a non-SemVer prerelease string and should be a patch bump for this bugfix PR.

3.0.0a2 is not strict MAJOR.MINOR.PATCH. For this bugfix PR, bump to a SemVer patch (for example, 3.0.1).

Suggested fix
-version         = "3.0.0a2"
+version         = "3.0.1"

Based on learnings, "The new version MUST follow SemVer (MAJOR.MINOR.PATCH)... If it only fixes bugs without changing public APIs, PATCH must increment."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyproject.toml` at line 3, The version in pyproject.toml is a non-SemVer
prerelease ("version = \"3.0.0a2\"); update the version key to a SemVer patch
release appropriate for a bugfix PR (e.g., change to "3.0.1") so it follows
MAJOR.MINOR.PATCH; edit the version line in pyproject.toml (the version = "...")
to the new patch value and save.
src/kp2bw/bw_serve.py (1)

401-415: 🧹 Nitpick | 🔵 Trivial

f-string in logging statements (ruff G004) — still not fixed.

Lines 405 and 415 use f-strings which format even when the log level is disabled. Use lazy %-style formatting. This was flagged in the previous review.

♻️ Use lazy logging
-        logger.log(VERBOSE, f"Created item {item.get('name', '?')!r} → {item_id}")
+        logger.log(VERBOSE, "Created item %r → %s", item.get("name", "?"), item_id)
...
-        logger.log(VERBOSE, f"Updated item {item.get('name', '?')!r} ({item_id})")
+        logger.log(VERBOSE, "Updated item %r (%s)", item.get("name", "?"), item_id)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kp2bw/bw_serve.py` around lines 401 - 415, The logger calls in
create_item and update_item use f-strings which evaluate even when the log level
is disabled; change them to lazy %-style logging by passing a format string and
the values as separate arguments to logger.log (e.g., in create_item replace the
f-string call to logger.log(VERBOSE, "Created item %r → %s", item.get('name',
'?'), item_id) and in update_item similarly use logger.log(VERBOSE, "Updated
item %r (%s)", item.get('name', '?'), item_id)); update the calls in functions
create_item and update_item to use this pattern so formatting is deferred until
the message is emitted.
src/kp2bw/bw_types.py (1)

55-63: ⚠️ Potential issue | 🟡 Minor

En-dash instead of hyphen in docstring (RUF002) — still not fixed from last review.

Line 58 has Literal[0–5] with an en-dash. It should be Literal[0-5] with a hyphen. Linters are annoying, but they're right.

🔧 Fix
-    The spec types ``match`` as ``Literal[0–5]``; ``None`` is also valid and
+    The spec types ``match`` as ``Literal[0-5]``; ``None`` is also valid and
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kp2bw/bw_types.py` around lines 55 - 63, The docstring for the BwUri
TypedDict uses an en-dash in "Literal[0–5]" which triggers the linter; update
the docstring to use a hyphen so it reads "Literal[0-5]". Edit the docstring
near the BwUri class (referencing BwUri, uri, match) to replace the en-dash with
a regular hyphen and save the file.
src/kp2bw/_bw_api_types.py (2)

107-111: ⚠️ Potential issue | 🟡 Minor

ItemLogin.uris is still singular Uris instead of list[Uris].

Also flagged before. The API returns an array. bw_types.py works around this with BwItemLogin.uris: list[BwUri], so runtime code is correct, but the generated types are misleading.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kp2bw/_bw_api_types.py` around lines 107 - 111, ItemLogin.uris is
incorrectly typed as a single Uris object; change the declaration of
ItemLogin.uris from NotRequired[Uris] to NotRequired[list[Uris]] (i.e. a list of
Uris) so the generated types match the actual API response and align with
BwItemLogin.uris / BwUri handling in bw_types.py; ensure any necessary typing
imports or syntax (built-in list[...] or typing.List) are updated accordingly.

13-13: ⚠️ Potential issue | 🟡 Minor

brand: Literal["visa"] is still a codegen artifact.

This was flagged before. Example value leaked into the type. Since this is generated, fix the spec or add post-processing to the codegen script. bw_types.py doesn't re-export ItemCard, so downstream code isn't affected, but it's still wrong.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kp2bw/_bw_api_types.py` at line 13, The generated type for the card
"brand" field is wrong (brand: NotRequired[Literal["visa"]]) because an example
value leaked into the codegen output; update the generator or add a
post-processing step so the field is typed as a generic string rather than a
single-example Literal (i.e., replace brand: NotRequired[Literal["visa"]] with
brand: NotRequired[str] or with a proper union of allowed brands), and ensure
the codegen step that emits _bw_api_types.py maps example values to the broader
type (refer to the "brand" field in the generated ItemCard/related model to
locate the site to change).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/kp2bw/_bw_api_types.py`:
- Around line 1-4: The generated types file src/kp2bw/_bw_api_types.py is out of
sync with the OpenAPI spec causing the CI codegen drift failure; run the
provided regeneration script (bash scripts/generate-bw-types.sh) to regenerate
_bw_api_types.py (and any other generated files), review the changes, and commit
the updated generated outputs so the CI check passes.

In `@src/kp2bw/convert.py`:
- Line 298: Replace logging of decrypted vault content with opaque identifiers:
where the code calls logger.log(VERBOSE, f"  Migrating passkey for entry:
{entry.title}") (and the other similar logger.log calls that include collection
labels, folder names, or entry titles), stop emitting
entry.title/collection.label/folder.name and instead log a non-sensitive
reference such as entry.id, a migration index/counter, or a truncated hash of
the id. Update the logger calls in the functions/methods that perform migration
(references: the logger.log uses with VERBOSE around migrating
entries/collections/folders) to format messages using the chosen opaque ref
(e.g., entry.id or migration_counter) and ensure no decrypted strings from the
vault are included in any log message.

---

Outside diff comments:
In `@src/kp2bw/bw_serve.py`:
- Around line 519-529: The list_collections method uses the wrong query param
casing: change the params in BwServe.list_collections (params={"organizationId":
self._org_id}) to use the lowercase key "organizationid" so it matches the
CLI/API expectation; update the params dict in the list_collections function
accordingly.

---

Duplicate comments:
In `@pyproject.toml`:
- Line 3: The version in pyproject.toml is a non-SemVer prerelease ("version =
\"3.0.0a2\"); update the version key to a SemVer patch release appropriate for a
bugfix PR (e.g., change to "3.0.1") so it follows MAJOR.MINOR.PATCH; edit the
version line in pyproject.toml (the version = "...") to the new patch value and
save.

In `@src/kp2bw/_bw_api_types.py`:
- Around line 107-111: ItemLogin.uris is incorrectly typed as a single Uris
object; change the declaration of ItemLogin.uris from NotRequired[Uris] to
NotRequired[list[Uris]] (i.e. a list of Uris) so the generated types match the
actual API response and align with BwItemLogin.uris / BwUri handling in
bw_types.py; ensure any necessary typing imports or syntax (built-in list[...]
or typing.List) are updated accordingly.
- Line 13: The generated type for the card "brand" field is wrong (brand:
NotRequired[Literal["visa"]]) because an example value leaked into the codegen
output; update the generator or add a post-processing step so the field is typed
as a generic string rather than a single-example Literal (i.e., replace brand:
NotRequired[Literal["visa"]] with brand: NotRequired[str] or with a proper union
of allowed brands), and ensure the codegen step that emits _bw_api_types.py maps
example values to the broader type (refer to the "brand" field in the generated
ItemCard/related model to locate the site to change).

In `@src/kp2bw/bw_serve.py`:
- Around line 401-415: The logger calls in create_item and update_item use
f-strings which evaluate even when the log level is disabled; change them to
lazy %-style logging by passing a format string and the values as separate
arguments to logger.log (e.g., in create_item replace the f-string call to
logger.log(VERBOSE, "Created item %r → %s", item.get('name', '?'), item_id) and
in update_item similarly use logger.log(VERBOSE, "Updated item %r (%s)",
item.get('name', '?'), item_id)); update the calls in functions create_item and
update_item to use this pattern so formatting is deferred until the message is
emitted.

In `@src/kp2bw/bw_types.py`:
- Around line 55-63: The docstring for the BwUri TypedDict uses an en-dash in
"Literal[0–5]" which triggers the linter; update the docstring to use a hyphen
so it reads "Literal[0-5]". Edit the docstring near the BwUri class (referencing
BwUri, uri, match) to replace the en-dash with a regular hyphen and save the
file.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7ebfe2 and 34dcc7e.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • .github/workflows/codegen-check.yml
  • CHANGELOG.md
  • pyproject.toml
  • scripts/generate-bw-types.sh
  • src/kp2bw/_bw_api_types.py
  • src/kp2bw/_console.py
  • src/kp2bw/bw_serve.py
  • src/kp2bw/bw_types.py
  • src/kp2bw/cli.py
  • src/kp2bw/convert.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (7)
src/kp2bw/**/*.py

📄 CodeRabbit inference engine (src/kp2bw/AGENTS.md)

src/kp2bw/**/*.py: Use module logger with logger = logging.getLogger(__name__), never root logger calls
Keep sensitive values out of logs - do not log raw bw commands/output, session values, or password values
Raise project exceptions (BitwardenClientError, ConversionError) instead of bare Exception
Use relative imports for local modules (e.g., from .exceptions import ConversionError)
Python 3.14 comma-form multi-except is valid; do not rewrite it to tuple form just for style
Do not reintroduce subprocess-per-op transport for new work; use bw_serve.py as the active transport path
Do not log decrypted vault content or credential-bearing command strings
Do not break idempotency by bypassing existing-item dedup checks

Never log raw bw command strings or raw command output to preserve security

Files:

  • src/kp2bw/_console.py
  • src/kp2bw/cli.py
  • src/kp2bw/bw_serve.py
  • src/kp2bw/bw_types.py
  • src/kp2bw/convert.py
  • src/kp2bw/_bw_api_types.py
src/kp2bw/cli.py

📄 CodeRabbit inference engine (src/kp2bw/AGENTS.md)

Add or adjust CLI flags in src/kp2bw/cli.py in the main() function and argument parser

Files:

  • src/kp2bw/cli.py
src/kp2bw/{cli,__main__}.py

📄 CodeRabbit inference engine (src/kp2bw/AGENTS.md)

Maintain behavior parity for kp2bw.cli:main and python -m kp2bw entry points

Files:

  • src/kp2bw/cli.py
.github/workflows/*.yml

📄 CodeRabbit inference engine (AGENTS.md)

Workflow check jobs must call scripts/*.mjs via actions/github-script; script output keys are workflow contracts

Files:

  • .github/workflows/codegen-check.yml
src/kp2bw/bw_serve.py

📄 CodeRabbit inference engine (src/kp2bw/AGENTS.md)

src/kp2bw/bw_serve.py: Tune dedup/idempotency behavior in src/kp2bw/bw_serve.py using the existing item index and batch create behavior
Configure attachment behavior in src/kp2bw/bw_serve.py via the async upload path and multipart logic

bw serve is localhost-only and password is passed via environment variable, not CLI argument

Files:

  • src/kp2bw/bw_serve.py
src/kp2bw/convert.py

📄 CodeRabbit inference engine (src/kp2bw/AGENTS.md)

src/kp2bw/convert.py: Change import behavior in src/kp2bw/convert.py by modifying the top-level convert() flow and migration phases
Map KeePass fields in src/kp2bw/convert.py including entry/custom field/TOTP/passkey mapping

Files:

  • src/kp2bw/convert.py
+(ruff.toml|pyproject.toml)

📄 CodeRabbit inference engine (AGENTS.md)

Ruff configuration must run in preview mode with target py314 for root package; stubs package has separate Ruff config

Files:

  • pyproject.toml
🧠 Learnings (40)
📓 Common learnings
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:335-342
Timestamp: 2026-02-23T21:45:26.732Z
Learning: In the Bitwarden CLI `bw serve` Vault Management API, list endpoints (`/list/object/folders`, `/list/object/items`, `/list/object/org-collections`) return a nested structure where the outer envelope's `data` field contains a list wrapper object `{ "object": "list", "data": [...] }`. Therefore, code that calls `_request(...)` and then does `.get("data", [])` is correctly extracting the actual items array from the list wrapper - this is not a double-extraction bug but the correct handling of the API's response structure.
<!-- </add_learning>
📚 Learning: 2026-02-24T01:14:19.237Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.237Z
Learning: CLI flags, prompts, and environment variable handling are defined in `src/kp2bw/cli.py`

Applied to files:

  • src/kp2bw/cli.py
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/convert.py : Change import behavior in `src/kp2bw/convert.py` by modifying the top-level `convert()` flow and migration phases

Applied to files:

  • src/kp2bw/cli.py
  • src/kp2bw/bw_serve.py
  • src/kp2bw/convert.py
  • CHANGELOG.md
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/cli.py : Add or adjust CLI flags in `src/kp2bw/cli.py` in the `main()` function and argument parser

Applied to files:

  • src/kp2bw/cli.py
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/{cli,__main__}.py : Maintain behavior parity for `kp2bw.cli:main` and `python -m kp2bw` entry points

Applied to files:

  • src/kp2bw/cli.py
📚 Learning: 2026-02-24T01:14:19.237Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.237Z
Learning: Applies to +(src/kp2bw|packages/pykeepass-stubs)/**/*.py : Use module logger instead of root logger; never use bare `logging.info(...)` etc.

Applied to files:

  • src/kp2bw/cli.py
  • src/kp2bw/bw_serve.py
📚 Learning: 2026-02-24T01:14:19.237Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.237Z
Learning: Applies to src/kp2bw/**/*.py : Never log raw `bw` command strings or raw command output to preserve security

Applied to files:

  • src/kp2bw/cli.py
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/**/*.py : Keep sensitive values out of logs - do not log raw `bw` commands/output, session values, or password values

Applied to files:

  • src/kp2bw/cli.py
📚 Learning: 2026-02-24T01:14:19.237Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.237Z
Learning: Applies to .github/workflows/*.yml : Workflow check jobs must call `scripts/*.mjs` via `actions/github-script`; script output keys are workflow contracts

Applied to files:

  • .github/workflows/codegen-check.yml
📚 Learning: 2026-02-21T04:16:56.990Z
Learnt from: CR
Repo: kjanat/paperless-mcp PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-21T04:16:56.990Z
Learning: If AGENTS.md file exists and the PR modifies architecture, workflows, responsibilities, automation behavior, CLI interfaces, or agent-related logic, the corresponding AGENTS.md file in the affected directory MUST be updated in the same PR. Issue a warning if relevant code changes occur without AGENTS.md updates.

Applied to files:

  • .github/workflows/codegen-check.yml
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/bw_serve.py : Tune dedup/idempotency behavior in `src/kp2bw/bw_serve.py` using the existing item index and batch create behavior

Applied to files:

  • src/kp2bw/bw_serve.py
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/**/*.py : Do not break idempotency by bypassing existing-item dedup checks

Applied to files:

  • src/kp2bw/bw_serve.py
📚 Learning: 2026-02-23T21:45:29.282Z
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:416-426
Timestamp: 2026-02-23T21:45:29.282Z
Learning: In `src/kp2bw/bw_serve.py` (Python, Bitwarden CLI integration): Bitwarden `bw serve` list endpoints (`/list/object/folders`, `/list/object/items`, `/list/object/org-collections`) return a nested structure: the outer JSON envelope has `{ "success": true, "data": { "object": "list", "data": [...] } }`. The `_request()` method returns `body.get("data", {})` (the middle layer), so callers must extract `data.get("data", [])` to access the actual array. This double extraction is correct and matches the official Vault Management API spec.

Applied to files:

  • src/kp2bw/bw_serve.py
  • src/kp2bw/bw_types.py
  • CHANGELOG.md
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/**/*.py : Do not reintroduce subprocess-per-op transport for new work; use bw_serve.py as the active transport path

Applied to files:

  • src/kp2bw/bw_serve.py
📚 Learning: 2026-02-23T21:45:26.732Z
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:335-342
Timestamp: 2026-02-23T21:45:26.732Z
Learning: In the Bitwarden CLI `bw serve` Vault Management API, list endpoints (`/list/object/folders`, `/list/object/items`, `/list/object/org-collections`) return a nested structure where the outer envelope's `data` field contains a list wrapper object `{ "object": "list", "data": [...] }`. Therefore, code that calls `_request(...)` and then does `.get("data", [])` is correctly extracting the actual items array from the list wrapper - this is not a double-extraction bug but the correct handling of the API's response structure.
<!-- </add_learning>

Applied to files:

  • src/kp2bw/bw_serve.py
  • CHANGELOG.md
📚 Learning: 2026-02-23T21:45:13.630Z
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:306-310
Timestamp: 2026-02-23T21:45:13.630Z
Learning: In src/kp2bw/bw_serve.py, for Bitwarden CLI 'bw serve' Vault Management API endpoints (e.g., /list/object/folders, /list/object/items, /list/object/org-collections), the HTTP response has the shape {"success": true, "data": {"object": "list", "data": [...]}}. Since the outer 'data' is a wrapper object, extract the inner list with payload.get("data", {}).get("data", []) after calling _request(). Example: payload = _request(...); items = payload.get("data", {}).get("data", []) to obtain the actual array.

Applied to files:

  • src/kp2bw/bw_serve.py
📚 Learning: 2026-02-23T21:45:21.713Z
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:306-310
Timestamp: 2026-02-23T21:45:21.713Z
Learning: In Python code using the Bitwarden CLI `bw serve` Vault Management API, list endpoints (e.g., `/list/object/folders`, `/list/object/items`, `/list/object/org-collections`) return a nested structure: the HTTP response is `{"success": true, "data": {"object": "list", "data": [...]}}`, where the outer `data` is a list wrapper object containing an inner `data` array. Code that calls `_request()` and receives the outer `data` payload should use `.get("data", [])` to extract the array from the list wrapper object.

Applied to files:

  • src/kp2bw/bw_types.py
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/convert.py : Map KeePass fields in `src/kp2bw/convert.py` including entry/custom field/TOTP/passkey mapping

Applied to files:

  • src/kp2bw/convert.py
  • pyproject.toml
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/tests/e2e_vaultwarden_test.py : Update e2e expectations in `tests/e2e_vaultwarden_test.py` when changing conversion behavior

Applied to files:

  • src/kp2bw/convert.py
📚 Learning: 2026-02-24T01:14:19.237Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.237Z
Learning: Applies to +(src/kp2bw|packages/pykeepass-stubs)/**/*.py : Use project-specific exceptions (`BitwardenClientError`, `ConversionError`) instead of bare `Exception`

Applied to files:

  • src/kp2bw/convert.py
  • pyproject.toml
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/**/*.py : Raise project exceptions (`BitwardenClientError`, `ConversionError`) instead of bare `Exception`

Applied to files:

  • src/kp2bw/convert.py
  • pyproject.toml
📚 Learning: 2026-02-24T00:44:57.384Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: tests/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:57.384Z
Learning: Applies to tests/e2e_vaultwarden_test.py : Full migration e2e tests must run conversion twice and assert idempotency

Applied to files:

  • src/kp2bw/convert.py
📚 Learning: 2026-02-24T00:44:31.548Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.548Z
Learning: Applies to packages/pykeepass-stubs/pyproject.toml : Stubs package metadata and tool configuration must be maintained in pyproject.toml, including version bumping aligned with 'stubs-v*' release tags

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T01:14:19.237Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.237Z
Learning: Applies to +(ruff.toml|pyproject.toml) : Ruff configuration must run in preview mode with target `py314` for root package; stubs package has separate Ruff config

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T00:44:31.548Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.548Z
Learning: Applies to packages/pykeepass-stubs/pyproject.toml : Python baseline for pykeepass-stubs package is >=3.11 (differs from runtime package >=3.14)

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-18T19:15:44.159Z
Learnt from: CR
Repo: kjanat/actup-v2 PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-18T19:15:44.159Z
Learning: Applies to {package.json,pyproject.toml,setup.py,Cargo.toml,go.mod,pom.xml,build.gradle,VERSION} : The new version MUST follow SemVer (MAJOR.MINOR.PATCH). If the PR introduces breaking changes (removal or renaming of public APIs, changes to function signatures, deleted exported symbols, or incompatible config changes), MAJOR must increment. If it adds backward-compatible functionality, MINOR must increment. If it only fixes bugs without changing public APIs, PATCH must increment.

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T00:45:03.739Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-24T00:45:03.739Z
Learning: If any source code files (excluding tests, docs, CI, markdown, or comments-only changes) are modified, then a version field MUST be updated in one of the following files if present in the repo: package.json, pyproject.toml, setup.py, Cargo.toml, go.mod, pom.xml, build.gradle, or a VERSION file. The new version MUST follow SemVer (MAJOR.MINOR.PATCH). If the PR introduces breaking changes (removal or renaming of public APIs, changes to function signatures, deleted exported symbols, or incompatible config changes), MAJOR must increment. If it adds backward-compatible functionality, MINOR must increment. If it only fixes bugs without changing public APIs, PATCH must increment. If code changes exist and no version bump is detected, or the bump level does not match the change type, fail.

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-12T02:40:30.410Z
Learnt from: CR
Repo: kjanat/glb-compressor PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-12T02:40:30.410Z
Learning: SemVer Version Bump: If PR only fixes bugs without changing public APIs, PATCH version must increment

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-21T04:16:56.990Z
Learnt from: CR
Repo: kjanat/paperless-mcp PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-21T04:16:56.990Z
Learning: When modifying source code files, ensure the version bump in package.json, pyproject.toml, setup.py, Cargo.toml, go.mod, pom.xml, build.gradle, or VERSION file follows SemVer: increment MAJOR for breaking changes (removal/renaming of public APIs, function signature changes, deleted exported symbols, incompatible config changes), MINOR for backward-compatible functionality additions, and PATCH for bug fixes without public API changes.

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-25T07:25:08.781Z
Learnt from: CR
Repo: kjanat/404 PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-25T07:25:08.781Z
Learning: Applies to {package.json,pyproject.toml,setup.py,Cargo.toml,go.mod,pom.xml,build.gradle,VERSION} : If any source code files (excluding tests, docs, CI, markdown, or comments-only changes) are modified, then a version field MUST be updated in one of the following files if present in the repo: package.json, pyproject.toml, setup.py, Cargo.toml, go.mod, pom.xml, build.gradle, or a VERSION file. Determine the previous version from the base branch and compare to the PR version. The new version MUST follow SemVer (MAJOR.MINOR.PATCH). If the PR introduces breaking changes (removal or renaming of public APIs, changes to function signatures, deleted exported symbols, or incompatible config changes), MAJOR must increment. If it adds backward-compatible functionality, MINOR must increment. If it only fixes bugs without changing public APIs, PATCH must increment.

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-12T02:40:30.410Z
Learnt from: CR
Repo: kjanat/glb-compressor PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-12T02:40:30.410Z
Learning: SemVer Version Bump: If PR introduces breaking changes (removal or renaming of public APIs, changes to function signatures, deleted exported symbols, or incompatible config changes), MAJOR version must increment

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-12T02:40:30.410Z
Learnt from: CR
Repo: kjanat/glb-compressor PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-12T02:40:30.410Z
Learning: SemVer Version Bump: If PR adds backward-compatible functionality, MINOR version must increment

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-21T04:16:56.990Z
Learnt from: CR
Repo: kjanat/paperless-mcp PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-21T04:16:56.990Z
Learning: If any source code files (excluding tests, docs, CI, markdown, or comments-only changes) are modified, a version field MUST be updated in one of the following files if present: package.json, pyproject.toml, setup.py, Cargo.toml, go.mod, pom.xml, build.gradle, or VERSION file. The new version MUST follow SemVer (MAJOR.MINOR.PATCH) format.

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T01:14:19.237Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.237Z
Learning: Applies to +(pyproject.toml|packages/pykeepass-stubs/pyproject.toml) : Python baseline is `>=3.14` for root package; stubs package targets `>=3.11`

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T01:14:19.237Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.237Z
Learning: Applies to +(src/kp2bw|packages/pykeepass-stubs)/**/*.py : Use relative intra-package imports with `from .module import X` syntax

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T00:44:31.548Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.548Z
Learning: Applies to packages/pykeepass-stubs/pyproject.toml : Stub package must include 'Typing :: Stubs Only' classifier and include 'py.typed' marker in package configuration

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T00:44:31.548Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.548Z
Learning: Applies to packages/pykeepass-stubs/src/pykeepass-stubs/py.typed : Maintain 'py.typed' marker with 'partial' designation in the shipped stubs package path for partial-stub semantics

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T00:44:31.548Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.548Z
Learning: Applies to packages/pykeepass-stubs/src/pykeepass-stubs/*.pyi : Type stub (.pyi) file signatures must be kept aligned with actual pykeepass module behavior

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T00:44:31.548Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.548Z
Learning: Applies to packages/pykeepass-stubs/** : Keep Ruff and Pyright configuration local to the stubs package; maintain separate settings from root and runtime package configurations

Applied to files:

  • pyproject.toml
📚 Learning: 2026-02-24T00:44:31.548Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.548Z
Learning: Applies to packages/pykeepass-stubs/src/pykeepass-stubs/**/*.py : Pykeepass-stubs package must remain type-only with no runtime module logic

Applied to files:

  • pyproject.toml
🧬 Code graph analysis (5)
src/kp2bw/cli.py (1)
src/kp2bw/convert.py (1)
  • convert (707-716)
src/kp2bw/bw_serve.py (2)
src/kp2bw/bw_types.py (4)
  • BwCollection (187-195)
  • BwFolder (179-184)
  • BwItemCreate (150-156)
  • BwItemResponse (159-171)
src/kp2bw/bitwardenclient.py (1)
  • create_folder (159-173)
src/kp2bw/bw_types.py (1)
src/kp2bw/_bw_api_types.py (7)
  • Group (61-64)
  • ItemCard (11-17)
  • ItemIdentity (20-38)
  • ItemSecureNote (7-8)
  • LockunlockSuccess (92-94)
  • Status (80-82)
  • Uris (41-43)
src/kp2bw/convert.py (1)
src/kp2bw/bw_types.py (5)
  • BwFido2Credential (79-94)
  • BwField (66-71)
  • BwItemCreate (150-156)
  • BwItemLogin (102-114)
  • BwUri (55-63)
src/kp2bw/_bw_api_types.py (1)
packages/pykeepass-stubs/src/pykeepass-stubs/pykeepass.pyi (1)
  • groups (62-62)
🪛 GitHub Actions: Codegen drift check
src/kp2bw/_bw_api_types.py

[error] 1-1: Generated file is out of sync with specs/vault-management-api.json. Run 'bash scripts/generate-bw-types.sh' and commit the result.

🪛 Ruff (0.15.2)
src/kp2bw/bw_serve.py

[warning] 405-405: Logging statement uses f-string

(G004)


[warning] 415-415: Logging statement uses f-string

(G004)

src/kp2bw/bw_types.py

[warning] 58-58: Docstring contains ambiguous – (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF002)

src/kp2bw/convert.py

[warning] 298-298: Logging statement uses f-string

(G004)


[warning] 534-534: Logging statement uses f-string

(G004)


[warning] 625-626: Logging statement uses f-string

(G004)


[warning] 643-644: Logging statement uses f-string

(G004)


[warning] 650-651: Logging statement uses f-string

(G004)


[warning] 685-686: Logging statement uses f-string

(G004)

🪛 YAMLlint (1.38.0)
.github/workflows/codegen-check.yml

[warning] 4-4: truthy value should be one of [false, true]

(truthy)


[error] 5-5: too many spaces inside braces

(braces)


[error] 5-5: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 6-6: too many spaces inside braces

(braces)


[error] 10-10: too many spaces inside braces

(braces)


[error] 10-10: too many spaces inside braces

(braces)

🔇 Additional comments (16)
pyproject.toml (1)

33-47: Dependency-group refactor looks clean.

The dev composition with include-group and dedicated codegen group is tidy and easier to maintain.

src/kp2bw/_console.py (1)

1-8: Finally, someone who understands stdout vs stderr separation. LGTM!

Shared console on stderr means progress bars won't corrupt piped output. The docstring actually explains why it exists. I'm... dare I say... pleased.

CHANGELOG.md (1)

11-59: Comprehensive changelog entries that actually document what changed. Rare and appreciated.

The distinction between "Unreleased" and "3.0.0a2" sections is correct. The org dedup fix description (lines 47-51) precisely matches the PR objective. Love it when documentation doesn't lie.

src/kp2bw/bw_types.py (2)

79-95: BwFido2Credential schema captures passkey fields well.

All the FIDO2/WebAuthn fields are present. Good that creationDate is typed as str | None since it can be absent on older credentials.


122-171: Excellent TypedDict inheritance pattern for shared fields.

_BwItemCommon as a private base with BwItemCreate and BwItemResponse extending it avoids field override issues. The docstrings explaining why login is in subclasses instead of the base shows actual thought went into this.

scripts/generate-bw-types.sh (1)

1-23: Clean codegen script with proper error handling.

set -euo pipefail ensures failures don't silently continue. --disable-timestamp prevents diff churn. --reuse-model and --collapse-root-models keep output tidy. This is how you do codegen.

src/kp2bw/bw_serve.py (4)

454-488: Dedup scoping logic is well-designed — once the casing is fixed.

The docstring clearly explains the three scoping rules (fixed collection > org-only > personal vault). The asymmetric behavior note is honest. This is good defensive documentation.

The logic itself is correct: filter by collection if set, else by org if set, else get everything. Just... the query params need lowercase keys to actually work.


77-82: New _collection_id state and constructor parameter for collection-scoped dedup.

Clean addition. The type annotation dict[str | None, dict[str, BwItemResponse]] is more precise than the old dict[str | None, set[str]]. Storing full item responses enables collection-aware updates without extra GETs.

Also applies to: 94-105


503-513: update_dedup_entry keeps cache fresh after PUT — nice touch.

Avoids full refresh_dedup_index() round-trip when only one item's metadata changed. The docstring explains exactly when to call it. This is how you maintain cache coherence without being wasteful.


382-399: The query parameter casing in lines 394 and 396 is correct — this was already fixed.

The CHANGELOG explicitly documents that organizationId casing was corrected from lowercase organizationid (which the API silently ignored) to the camelCase form required by the spec. Lines 394 and 396 properly use organizationId and collectionId as documented. The org-scoping does work as intended.

The only mild inconsistency is that line 392 uses lowercase folderid while the organization and collection filters use camelCase — but this isn't evidence that the camelCase params are wrong; it may reflect different API endpoint behavior for different parameters.

Likely an incorrect or invalid review comment.

src/kp2bw/cli.py (4)

8-11: Rich logging integration imports look correct.

RichHandler from rich.logging and shared console from ._console — coordinated output without import cycles.


295-311: Logging configuration with RichHandler is properly structured.

Debug mode bypasses Rich for raw output. Normal mode uses RichHandler with the shared console. httpx is silenced to WARNING to avoid noise. VERBOSE level is correctly applied only to the kp2bw logger, not globally.

This follows the -v / -d distinction documented in the changelog. Good separation of concerns.


353-357: KeyboardInterrupt handling with exit code 130 — Unix-correct.

130 = 128 + SIGINT(2). The Rich markup [yellow]Interrupted.[/yellow] renders nicely. This prevents stack traces on Ctrl+C, which users appreciate.


72-134: Metavar additions improve --help readability.

FILE, PASSWORD, ID, TAG, N are much clearer than auto-derived KP_PW, BW_ORG. Small change, big UX improvement.

.github/workflows/codegen-check.yml (1)

1-24: 🧹 Nitpick | 🔵 Trivial

Workflow logic is correct, but YAMLlint is unhappy about formatting.

The drift detection logic works. However, the brace spacing on lines 5, 6, 10 triggers YAMLlint errors. I know, I know, "it works" — but CI linting consistency matters.

🧹 Quieter YAML formatting
 on:
-  pull_request: { paths: [src/kp2bw/_bw_api_types.py, specs/vault-management-api.json] }
-  push: { branches: [master], paths: [src/kp2bw/_bw_api_types.py, specs/vault-management-api.json] }
+  pull_request:
+    paths: [src/kp2bw/_bw_api_types.py, specs/vault-management-api.json]
+  push:
+    branches: [master]
+    paths: [src/kp2bw/_bw_api_types.py, specs/vault-management-api.json]
 jobs:
   check-bw-api-types:
     runs-on: ubuntu-latest
-    permissions: { contents: read }
+    permissions:
+      contents: read
     timeout-minutes: 5
⛔ Skipped due to learnings
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.237Z
Learning: Applies to .github/workflows/*.yml : Workflow check jobs must call `scripts/*.mjs` via `actions/github-script`; script output keys are workflow contracts
Learnt from: CR
Repo: kjanat/actup-v2 PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-19T10:52:05.498Z
Learning: Applies to **/{.github/workflows/**,**/.github/actions/**/}*.{yml,yaml} : Preserve YAML comments, indentation, and quoting when editing workflow/action files; avoid whole-file rewrites
Learnt from: CR
Repo: kjanat/actup-v2 PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-19T10:52:05.498Z
Learning: Applies to **/{.github/workflows/**,**/.github/actions/**/}*.{yml,yaml} : Re-parse YAML after editing to confirm validity and ensure `uses:` strings remain syntactically correct
Learnt from: CR
Repo: kjanat/paperless-mcp PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-02-21T04:16:56.990Z
Learning: If AGENTS.md file exists and the PR modifies architecture, workflows, responsibilities, automation behavior, CLI interfaces, or agent-related logic, the corresponding AGENTS.md file in the affected directory MUST be updated in the same PR. Issue a warning if relevant code changes occur without AGENTS.md updates.
Learnt from: kjanat
Repo: kjanat/tools PR: 1
File: .github/actions/get-version/action.yml:23-33
Timestamp: 2026-02-08T04:31:56.655Z
Learning: In kjanat's repositories, do not flag yamllint warnings about spaces inside braces for inline maps (flow style) in YAML files for GitHub Actions workflows. The user prefers inline maps with spaces like `env: { KEY: "value" }` and does not want suggestions to remove the inner spaces or convert to block mappings.
Learnt from: kjanat
Repo: kjanat/notso-glb PR: 12
File: server.py:9-16
Timestamp: 2026-02-05T18:41:54.547Z
Learning: In the kjanat/notso-glb repository, code formatting (such as import statement organization and splitting) is handled automatically by the `.github/workflows/autofix.yml` workflow in CI. Do not flag formatting issues that are auto-corrected by this workflow.
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.237Z
Learning: Applies to scripts/version-check-shared.mjs : Do not change `scripts/version-check-shared.mjs` output shape (`name`, `version`, `pypi_url`) without updating dependent workflows
Learnt from: kjanat
Repo: kjanat/actup-v2 PR: 21
File: extensions/vscode/package.json:83-89
Timestamp: 2026-02-20T13:53:54.853Z
Learning: In actup's VSCode extension, the activation patterns `**/.*/workflows/**/*.yml` and `**/.*/workflows/**/*.yaml` are intentionally broad to match `.github/workflows/`, `.gitea/workflows/`, `.forgejo/workflows/`, and other dot-prefixed directories for multi-provider support. Do not suggest hardcoding `.github` as it would break this design.
Learnt from: kjanat
Repo: kjanat/dreamcli PR: 1
File: src/core/cli/dispatch.ts:199-233
Timestamp: 2026-02-10T16:41:10.111Z
Learning: In code reviews for kjanat/dreamcli, prioritize correctness over conciseness. Verbose or defensive code that prevents bugs is preferred over cleaner but potentially fragile implementations. Focus review effort on catching silent/subtle errors rather than suggesting refactorings that reduce safety checks.
Learnt from: kjanat
Repo: kjanat/actup-v2 PR: 7
File: .github/workflows/release.yml:3-10
Timestamp: 2026-02-02T16:01:24.974Z
Learning: User kjanat prefers inline maps (flow style) in YAML files for GitHub Actions workflows. Do not suggest converting inline maps like `push: { tags: ["v*"] }` or `permissions: { contents: write }` to block mappings to satisfy yamllint warnings.
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: scripts/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:39.719Z
Learning: Run `bun --cwd=scripts typecheck` before committing to validate type behavior for script files
src/kp2bw/convert.py (1)

563-570: Great fix: dedup scoping is now correctly constrained by target scope.

This path cleanly separates fixed-collection behavior from auto/no-collection behavior and matches the PR’s root-cause fix.

Also applies to: 590-594, 620-655

Comment thread src/kp2bw/_bw_api_types.py
Comment thread src/kp2bw/convert.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/kp2bw/convert.py (1)

300-300: ⚠️ Potential issue | 🟠 Major

Stop logging decrypted vault-derived identifiers.

These log lines expose entry titles, folder paths, and collection labels; switch to opaque refs/counters only.

Proposed redaction-oriented diff
-            logger.log(VERBOSE, f"  Migrating passkey for entry: {entry.title}")
+            logger.log(VERBOSE, "  Migrating passkey for entry_ref=%s", str(entry.uuid)[:8])

-                logger.log(VERBOSE, f"Searching Collection {firstlevel}")
+                logger.log(VERBOSE, "Searching collection for entry target")

-                        logger.log(
-                            VERBOSE,
-                            f"-- Entry {bw_item['name']!r} already in "
-                            f"target collection, skipping",
-                        )
+                        logger.log(
+                            VERBOSE,
+                            "-- Entry already in target collection, skipping (entry_ref=%s)",
+                            key[:8],
+                        )

-                            logger.log(
-                                VERBOSE,
-                                f"-- Entry {bw_item['name']!r}: added to "
-                                f"{len(missing)} collection(s)",
-                            )
+                            logger.log(
+                                VERBOSE,
+                                "-- Added existing entry to %s collection(s) (entry_ref=%s)",
+                                len(missing),
+                                key[:8],
+                            )

-                            logger.log(
-                                VERBOSE,
-                                f"-- Entry {bw_item['name']!r} already in "
-                                f"folder {folder!r}, skipping",
-                            )
+                            logger.log(
+                                VERBOSE,
+                                "-- Entry already present in target scope, skipping (entry_ref=%s)",
+                                key[:8],
+                            )

-                        logger.warning(
-                            f"Could not find item ID for {bw_item['name']!r} "
-                            f"in folder {_folder!r} for attachment upload"
-                        )
+                        logger.warning(
+                            "Could not find item ID for attachment upload (entry_ref=%s)",
+                            key[:8],
+                        )
As per coding guidelines, "Keep sensitive values out of logs - do not log raw `bw` commands/output, session values, or password values" and "Do not log decrypted vault content or credential-bearing command strings".

Also applies to: 536-536, 625-629, 643-647, 650-654, 686-689

src/kp2bw/bw_types.py (1)

58-58: ⚠️ Potential issue | 🟡 Minor

Replace the en dash in the Literal range.

Line 58 uses Literal[0–5] with an en dash, which still triggers RUF002.

Proposed fix
-    The spec types ``match`` as ``Literal[0–5]``; ``None`` is also valid and
+    The spec types ``match`` as ``Literal[0-5]``; ``None`` is also valid and
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kp2bw/bw_types.py` at line 58, The Literal range in the type annotation
uses an en dash (Literal[0–5]) which triggers RUF002; update the annotation to
use an ASCII hyphen (Literal[0-5]) wherever that token appears (e.g., the
Literal range in bw_types.py) so the type becomes Literal[0-5] (and ensure any
related union that allows None remains unchanged).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/kp2bw/bw_types.py`:
- Line 58: The Literal range in the type annotation uses an en dash
(Literal[0–5]) which triggers RUF002; update the annotation to use an ASCII
hyphen (Literal[0-5]) wherever that token appears (e.g., the Literal range in
bw_types.py) so the type becomes Literal[0-5] (and ensure any related union that
allows None remains unchanged).

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 34dcc7e and 7bc3369.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/kp2bw/bw_types.py
  • src/kp2bw/convert.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
src/kp2bw/**/*.py

📄 CodeRabbit inference engine (src/kp2bw/AGENTS.md)

src/kp2bw/**/*.py: Use module logger with logger = logging.getLogger(__name__), never root logger calls
Keep sensitive values out of logs - do not log raw bw commands/output, session values, or password values
Raise project exceptions (BitwardenClientError, ConversionError) instead of bare Exception
Use relative imports for local modules (e.g., from .exceptions import ConversionError)
Python 3.14 comma-form multi-except is valid; do not rewrite it to tuple form just for style
Do not reintroduce subprocess-per-op transport for new work; use bw_serve.py as the active transport path
Do not log decrypted vault content or credential-bearing command strings
Do not break idempotency by bypassing existing-item dedup checks

Never log raw bw command strings or raw command output to preserve security

Files:

  • src/kp2bw/convert.py
  • src/kp2bw/bw_types.py
src/kp2bw/convert.py

📄 CodeRabbit inference engine (src/kp2bw/AGENTS.md)

src/kp2bw/convert.py: Change import behavior in src/kp2bw/convert.py by modifying the top-level convert() flow and migration phases
Map KeePass fields in src/kp2bw/convert.py including entry/custom field/TOTP/passkey mapping

Files:

  • src/kp2bw/convert.py
🧠 Learnings (17)
📓 Common learnings
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:335-342
Timestamp: 2026-02-23T21:45:26.732Z
Learning: In the Bitwarden CLI `bw serve` Vault Management API, list endpoints (`/list/object/folders`, `/list/object/items`, `/list/object/org-collections`) return a nested structure where the outer envelope's `data` field contains a list wrapper object `{ "object": "list", "data": [...] }`. Therefore, code that calls `_request(...)` and then does `.get("data", [])` is correctly extracting the actual items array from the list wrapper - this is not a double-extraction bug but the correct handling of the API's response structure.
<!-- </add_learning>
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/convert.py : Change import behavior in `src/kp2bw/convert.py` by modifying the top-level `convert()` flow and migration phases

Applied to files:

  • CHANGELOG.md
  • src/kp2bw/convert.py
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/convert.py : Map KeePass fields in `src/kp2bw/convert.py` including entry/custom field/TOTP/passkey mapping

Applied to files:

  • src/kp2bw/convert.py
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/tests/e2e_vaultwarden_test.py : Update e2e expectations in `tests/e2e_vaultwarden_test.py` when changing conversion behavior

Applied to files:

  • src/kp2bw/convert.py
📚 Learning: 2026-02-24T01:14:19.237Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.237Z
Learning: Applies to +(src/kp2bw|packages/pykeepass-stubs)/**/*.py : Use project-specific exceptions (`BitwardenClientError`, `ConversionError`) instead of bare `Exception`

Applied to files:

  • src/kp2bw/convert.py
  • src/kp2bw/bw_types.py
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/**/*.py : Raise project exceptions (`BitwardenClientError`, `ConversionError`) instead of bare `Exception`

Applied to files:

  • src/kp2bw/convert.py
  • src/kp2bw/bw_types.py
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/**/*.py : Do not log decrypted vault content or credential-bearing command strings

Applied to files:

  • src/kp2bw/convert.py
📚 Learning: 2026-02-24T00:44:49.531Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: src/kp2bw/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:49.531Z
Learning: Applies to src/kp2bw/**/*.py : Keep sensitive values out of logs - do not log raw `bw` commands/output, session values, or password values

Applied to files:

  • src/kp2bw/convert.py
📚 Learning: 2026-02-24T00:44:57.384Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: tests/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:57.384Z
Learning: Applies to tests/e2e_vaultwarden_test.py : E2E command wrapper must redact sensitive arguments and output before logging

Applied to files:

  • src/kp2bw/convert.py
📚 Learning: 2026-02-24T01:14:19.237Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.237Z
Learning: Applies to src/kp2bw/**/*.py : Never log raw `bw` command strings or raw command output to preserve security

Applied to files:

  • src/kp2bw/convert.py
📚 Learning: 2026-02-24T01:14:19.237Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.237Z
Learning: Applies to +(src/kp2bw|packages/pykeepass-stubs)/**/*.py : Use module logger instead of root logger; never use bare `logging.info(...)` etc.

Applied to files:

  • src/kp2bw/convert.py
📚 Learning: 2026-02-24T00:44:57.384Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: tests/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:57.384Z
Learning: Applies to tests/e2e_vaultwarden_test.py : Full migration e2e tests must run conversion twice and assert idempotency

Applied to files:

  • src/kp2bw/convert.py
📚 Learning: 2026-02-24T00:44:31.548Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.548Z
Learning: Applies to packages/pykeepass-stubs/src/pykeepass-stubs/py.typed : Maintain 'py.typed' marker with 'partial' designation in the shipped stubs package path for partial-stub semantics

Applied to files:

  • src/kp2bw/bw_types.py
📚 Learning: 2026-02-24T00:44:31.548Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: packages/pykeepass-stubs/AGENTS.md:0-0
Timestamp: 2026-02-24T00:44:31.548Z
Learning: Applies to packages/pykeepass-stubs/src/pykeepass-stubs/*.pyi : Type stub (.pyi) file signatures must be kept aligned with actual pykeepass module behavior

Applied to files:

  • src/kp2bw/bw_types.py
📚 Learning: 2026-02-24T01:14:19.237Z
Learnt from: CR
Repo: kjanat/kp2bw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-24T01:14:19.237Z
Learning: Applies to +(src/kp2bw|packages/pykeepass-stubs)/**/*.py : Use relative intra-package imports with `from .module import X` syntax

Applied to files:

  • src/kp2bw/bw_types.py
📚 Learning: 2026-02-23T21:45:29.282Z
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:416-426
Timestamp: 2026-02-23T21:45:29.282Z
Learning: In `src/kp2bw/bw_serve.py` (Python, Bitwarden CLI integration): Bitwarden `bw serve` list endpoints (`/list/object/folders`, `/list/object/items`, `/list/object/org-collections`) return a nested structure: the outer JSON envelope has `{ "success": true, "data": { "object": "list", "data": [...] } }`. The `_request()` method returns `body.get("data", {})` (the middle layer), so callers must extract `data.get("data", [])` to access the actual array. This double extraction is correct and matches the official Vault Management API spec.

Applied to files:

  • src/kp2bw/bw_types.py
📚 Learning: 2026-02-23T21:45:21.713Z
Learnt from: kjanat
Repo: kjanat/kp2bw PR: 2
File: src/kp2bw/bw_serve.py:306-310
Timestamp: 2026-02-23T21:45:21.713Z
Learning: In Python code using the Bitwarden CLI `bw serve` Vault Management API, list endpoints (e.g., `/list/object/folders`, `/list/object/items`, `/list/object/org-collections`) return a nested structure: the HTTP response is `{"success": true, "data": {"object": "list", "data": [...]}}`, where the outer `data` is a list wrapper object containing an inner `data` array. Code that calls `_request()` and receives the outer `data` payload should use `.get("data", [])` to extract the array from the list wrapper object.

Applied to files:

  • src/kp2bw/bw_types.py
🧬 Code graph analysis (1)
src/kp2bw/convert.py (1)
src/kp2bw/bw_types.py (5)
  • BwFido2Credential (79-94)
  • BwField (66-71)
  • BwItemCreate (150-156)
  • BwItemLogin (102-114)
  • BwUri (55-63)
🪛 Ruff (0.15.2)
src/kp2bw/convert.py

[warning] 300-300: Logging statement uses f-string

(G004)


[warning] 536-536: Logging statement uses f-string

(G004)


[warning] 627-628: Logging statement uses f-string

(G004)


[warning] 645-646: Logging statement uses f-string

(G004)


[warning] 652-653: Logging statement uses f-string

(G004)


[warning] 687-688: Logging statement uses f-string

(G004)

src/kp2bw/bw_types.py

[warning] 58-58: Docstring contains ambiguous – (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF002)

🔇 Additional comments (2)
CHANGELOG.md (1)

33-37: Good release-note coverage for the org dedup bug.

This clearly documents the root cause and behavior change when --bitwarden-org is set.

src/kp2bw/convert.py (1)

565-596: Collection-scoped dedup client wiring looks correct.

Passing collection_id=fixed_coll_id only for fixed collection mode is a clean way to preserve behavior in other modes.

@kjanat
kjanat merged commit eda8d03 into master Feb 26, 2026
8 checks passed
@kjanat
kjanat deleted the fix/org-dedup-index branch February 26, 2026 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant