157 Commits
Author SHA1 Message Date
Chris CoutinhoandGitHub 43598783e4 Merge pull request #734 from cbcoutinho/fix/calendar-niquests-auth-731
fix(calendar): thread raw credentials to caldav AsyncDAVClient (fixes #731)
2026-04-29 23:04:50 +02:00
Chris CoutinhoandGitHub 478a5ae39d Merge pull request #733 from cbcoutinho/fix/index-php-prefix-732
fix(client): route /apps/* through /index.php (fixes #732)
2026-04-27 23:59:18 +02:00
Chris CoutinhoandClaude Opus 4.7 fc62be08e2 fix(notes): defensively unwrap list-shaped Notes responses (refs #730)
Notes app v5.0.0 has scenarios where the API returns a JSON list where the
MCP server expects a single note object — notably the notes_api#fail
catch-all returning [] for unmatched routes. Without a guard, callers hit
a cryptic Pydantic "argument after ** must be a mapping, not list" from
Note(**payload).

Add a small _expect_note_object helper at the client layer:
- dict → pass through (the healthy case)
- single-element list → unwrap and warn (Notes v5.0.0 quirk)
- empty list, multi-element list, non-dict → raise a diagnostic ValueError
  that names the operation and points at the likely root cause (URL prefix,
  unmatched route, wrong API version)

Wire it into get_note / create_note / update so any list-shaped response
fails clearly instead of cryptically.

Six unit tests pin every branch of the helper.

Note: The 405s the issue reports for update_note / append_content match
Notes v5.0.0's documented routes (PUT /api/v1/notes/{id}) per upstream
appinfo/routes.php. They are most likely a downstream effect of #732
(missing /index.php URL prefix on installs without Pretty URLs) — the fix
in PR #733 should resolve those once it lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:02:19 +02:00
Chris CoutinhoandClaude Opus 4.7 2f1b0d2500 fix(calendar): thread raw credentials to caldav AsyncDAVClient
caldav 3.x lists niquests as a mandatory dependency and prefers it over
httpx. Passing httpx.BasicAuth via the auth= argument breaks under the
niquests backend with "Unexpected non-callable authentication" — see #731.

Switch CalendarClient.__init__ from auth=Auth|None to keyword-only
password/token, and forward them to AsyncDAVClient as password= plus an
explicit auth_type ("basic" or "bearer"). caldav then builds whichever
auth object its active backend needs (niquests.auth.HTTPBasicAuth or
httpx.BasicAuth), so we stay backend-agnostic.

Threaded raw credentials through NextcloudClient — added keyword-only
password/token to its __init__, and updated from_env, from_token, and
the four call sites that build NextcloudClient (context.py basic-auth
and Login Flow paths, auth/userinfo_routes.py, vector/oauth_sync.py).

Four new unit tests pin the construction wiring so the niquests
regression can't recur silently — basic, bearer, no-creds, and
password-precedence cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:52:58 +02:00
Chris CoutinhoandClaude Opus 4.7 3fb3680a83 fix(client): route /apps/* through /index.php for non-pretty-URL installs
Bare /apps/<app>/... URLs return 404 on Nextcloud installs without Pretty
URLs (URL rewriting), which is opt-in and not the default — see #732. The
/index.php/apps/... form is the universal entry point and works regardless
of web-server config, matching how /remote.php/dav and /ocs/v2.php already
have dedicated entry points.

Add a small _resolve_url helper on BaseNextcloudClient that rewrites
/apps/... → /index.php/apps/... at the top of _make_request, so every
current call site (notes, deck, cookbook, news) and any future ones are
covered transparently with no per-client churn.

Other path prefixes (/remote.php, /ocs, absolute URLs, already-prefixed
/index.php/apps) pass through unchanged. New unit tests in
tests/unit/client/test_base.py pin all six cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:33:37 +02:00
Chris CoutinhoandClaude Opus 4.7 c91be45374 fix(contacts): warn on unsupported dict/list email/tel update inputs
Surface the text-merge update path's limitation rather than silently no-op:
when contact_data['email'] or ['tel'] arrives as a dict/list on update, log a
warning at the top of _merge_vcard_properties pointing callers at plain str
or create_contact. Existing EMAIL/TEL lines are still preserved unchanged.

Bring nc_contacts_update_contact docstring into parity with create — the
update tool now documents the same keys plus the explicit single-string
limitation for email/tel and the BDAY validation / URL first-only behaviours.

Three new TestMergeVcardProperties cases pin the warning: dict email warns,
list tel warns, plain str email is silent (no false positives).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:03:29 +02:00
Chris CoutinhoandClaude Opus 4.7 125ab40121 fix(contacts): two PR #719 review bugs
- _merge_vcard_properties: list-form ORG was passed through
  _safe_vcard_value unchanged, emitting a Python repr on the wire. Both
  branches now ;-join list components per RFC 6350 §6.6.4 (ORG is
  Company;Department;…) before interpolation.
- _wrap_contact_field: a dict whose ``type`` was a bare string used to
  hit ``list("WORK")`` and explode into ``["W","O","R","K"]``. Wrap
  bare-string types into a single-element list before the list() call.

Regression tests pin both shapes:
- list-org overwrites and add-new produce ``ORG:Acme;Engineering``
- dict email with ``type="WORK"`` (bare str) emits ``EMAIL;TYPE=WORK:``,
  not ``EMAIL;TYPE=W,O,R,K:``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 03:24:15 +02:00
Chris CoutinhoandClaude Opus 4.7 e1c776716c fix(contacts): close PR #719 second-pass review gaps
- _merge_vcard_properties no longer silently drops the existing EMAIL /
  TEL line when contact_data supplies a dict/list shape: the input is
  unhandled by the text merge, so the original line is preserved
  instead of being consumed and replaced with nothing.
- Extracted _parse_bday so the update path validates ISO format the
  same way create does. Invalid → keep existing BDAY line (or skip on
  add-new) rather than writing a malformed one.
- Added _safe_vcard_value to escape newlines per RFC 6350 §3.4 at every
  interpolation site in _merge_vcard_properties, blocking value-driven
  property injection (e.g. NOTE: containing a literal \n + EMAIL:).
- Removed dead "organization" alias references from _merge_vcard_properties:
  unreachable since update_contact normalises before calling.
- New regression tests pin all four behaviours (dict-email preserves
  existing line, list-tel ditto, invalid-bday-update preserves original,
  invalid-bday-add-new is dropped, newline-in-note no injection).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 02:24:16 +02:00
Chris CoutinhoandClaude Opus 4.7 e2283ff28c refactor(contacts): address PR #719 follow-up review
Pulls the remaining review feedback into one commit:

- Remove the double _normalize_contact_data call: the helper now assumes
  canonical keys, and create_contact normalises before calling it
  (update_contact already did). Docstring states the invariant.
- Drop phone/organization from _SUPPORTED_CONTACT_KEYS; they never reach
  the unknown-key check post-normalisation.
- Tighten generics to dict[str, Any] / list[str] across helpers and
  ContactsClient signatures.
- Comment both URL-merge sites noting only the first URL is written.
- Log a warning when fn is missing from contact_data.
- Test coverage for _wrap_contact_field dropping value-less dicts and
  for the fn-missing warning; _vcard helper now mirrors the real call
  chain (normalise → build).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:04:46 +02:00
Chris CoutinhoandClaude Opus 4.7 0186b494f1 test(contacts): address PR #719 review feedback
- Type-annotate _wrap_contact_field signature; drop stale "url" mention
  from its docstring (url is handled by the list-coercion helper, not
  this one).
- Split the shape-coercion helper so comma-splitting only applies to
  categories: _as_str_list (no split) for org/nickname/url,
  _split_categories (comma split) for CATEGORIES. Fixes the case where
  organization="Smith, Jones & Associates" was mangled into a two-
  component ORG.
- Share _normalize_contact_data between create and update so
  _merge_vcard_properties only sees canonical keys; add URL handlers in
  both update branches so the primary update path no longer drops URL
  silently.
- Annotate the Contact(**kwargs) type:ignore with the reason
  (pythonvCard4 typeshed doesn't accept **dict[str, Any]).
- Add tests/unit/client/test_contacts.py (pure unit, no HTTP) covering
  the #716 round-trip, comma-in-org regression, invalid-bday warning,
  tel/phone precedence, categories string-vs-list behaviour, and direct
  _normalize_contact_data cases.
- Extend the MCP workflow test with an update-with-url step asserting
  the URL handler in _merge_vcard_properties.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:44:05 +02:00
Chris CoutinhoandClaude Opus 4.7 723aee9134 fix(contacts): persist all documented fields on create (fixes #716)
create_contact previously read only fn/email/tel from contact_data and
silently dropped org, organization, note, title, nickname, bday,
categories, url — and didn't accept phone as an alias for tel, so the
reporter's exact call lost every field except fn and email. Introduce
_build_contact_from_data, share it with update_contact's fallback, and
normalise str→list inputs so pythonvCard4 doesn't iterate bare strings
character-by-character for list-typed properties.

Closes #716

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:28:05 +02:00
Chris CoutinhoandClaude Opus 4.6 da380a38c6 fix: convert BDAY datetime.date to string before Pydantic validation
pythonvCard4 parses vCard BDAY fields into datetime.date objects, but
the Contact model expects Optional[str]. This caused a validation error
that crashed the entire contact list. Convert at the client layer
(consistent with the calendar client pattern) with a defensive check
at the server mapping layer.

Closes #672

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 09:11:45 +02:00
Chris CoutinhoandClaude Opus 4.6 52470ea713 fix: address PR review feedback (round 9)
- Fix emoji clearing bug: use _UNSET sentinel in update_collective so
  emoji=None sends {"emoji": null} instead of raising ValueError
- Move collectives_get_trashed_collectives to Read Tools section
- Remove redundant is_trash field from ListTrashedPagesResponse
- Add page lifecycle note to collectives_trash_page docstring
- Add unit test for clearing collective emoji via update_collective
- Add integration test for clearing collective emoji via MCP tool

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 09:19:53 +01:00
Chris CoutinhoandClaude Opus 4.6 cb16060b1b fix: address PR review feedback (round 8)
- Fix inconsistent error code in set_collective_emoji (400 → -32603)
- Allow clearing emoji via set_collective_emoji(emoji=None)
- Remove destructiveHint from trash operations (soft deletes are recoverable)
- Change delete_collective to idempotentHint=False (requires trash precondition)
- Add restore_collective and get_trashed_collectives tools
- Add unit tests for ValueError guard, clear-emoji path, and new tools
- Add integration test for full trash/restore/delete lifecycle
- Verify move_page returns new title in response message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:26:26 +01:00
Chris CoutinhoandClaude Opus 4.6 85119bde91 fix: address PR review feedback (round 6)
- Fix assign_tag sending Content-Type header with no body
- Mark collectives_update_collective as idempotent (no ETag involved)
- Raise OCSError when 'data' key missing instead of silent fallback
- Tighten color validator to 3 or 6 hex chars only
- Add comment explaining null emoji semantics in set_page_emoji

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 14:38:11 +01:00
Chris CoutinhoandClaude Opus 4.6 aa46c6147b fix: address PR review feedback (round 5)
- Validate OCS envelope in trash_collective, delete_collective, trash_page
- Guard _unwrap_ocs against non-OCS responses with informative OCSError
- Remove _get_ocs_headers() indirection, use class constants directly
- Split headers: _OCS_HEADERS (GET) vs _OCS_HEADERS_JSON (with body)
- Fix docstring claiming emoji param is required when it is optional
- Rename misleading test, add test for non-OCS envelope handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 13:49:28 +01:00
Chris CoutinhoandClaude Opus 4.6 95edd9ba8e fix: add trash/delete collective tools and address review feedback (round 4)
Add collectives_trash_collective and collectives_delete_collective MCP
tools with proper destructiveHint annotations. Refactor integration test
fixture to use MCP tools for cleanup instead of direct httpx/OCS calls.
Optimize _get_ocs_headers() to class-level constant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:36:40 +01:00
Chris CoutinhoandClaude Opus 4.6 f3caad122d fix: address PR review feedback (round 3)
Bugs:
- assign_tag/remove_tag now call _unwrap_ocs to surface OCS-level errors
- trash_page changed to idempotentHint=False (trashing twice errors)
- WebDAV path parts stripped of slashes to prevent double-slash paths

Robustness:
- _unwrap_ocs uses ocs.get("data", {}) instead of ocs["data"]
- Unit test added for missing data key in OCS envelope

Minor:
- MCP error codes use -1 (project convention) instead of HTTP status codes
- update_collective docstring notes that emoji is required
- CollectiveTag.color validated as hex format via field_validator

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:25:09 +01:00
Chris CoutinhoandClaude Opus 4.6 44a27bd9e9 fix: address PR review feedback (round 2)
Bug fixes:
- Catch OCSError/HTTPStatusError in all server tools, convert to McpError
- Guard update_collective against empty body (raise ValueError)
- Use restore_page response data in status message

ADR-017 annotation fix:
- Distinguish "remove" (reversible association) from "delete" (permanent):
  remove_tag and deck_remove_label_from_card no longer set destructiveHint
- Update annotation test to exclude "remove" from destructive keywords

Data model improvements:
- Add trashTimestamp field to PageInfo
- Create ListTrashedPagesResponse with is_trash context flag
- Add collective_id to ListTagsResponse

Test robustness:
- Read NC credentials from environment variables (not hardcoded)
- Filter landing page by parentId == 0 instead of assuming pages[0]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:08:13 +01:00
Chris CoutinhoandClaude Opus 4.6 e5ad625a66 fix: address PR review feedback for Collectives support
- Validate OCS envelope status before unwrapping data (raise OCSError on
  statuscode >= 400)
- Fix test data: filePath should be "" for root-level pages, not filename
- Catch specific exceptions (HTTPStatusError, OSError) instead of bare
  Exception in WebDAV content fetch, include error in log message
- Return updated resource data from update_collective, move_page, and
  set_page_emoji instead of discarding API responses
- Fix create_page docstring to mention collectivePath/filePath/fileName
- Remove unused additional_headers parameter from _get_ocs_headers
- Add unit test for OCS error status validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 08:07:37 +01:00
Chris CoutinhoandClaude Opus 4.6 32f5a0fe52 feat: add Nextcloud Collectives app support (#621)
Implement MCP tools for the Collectives wiki/documentation app, enabling
agentic workflows for team knowledge base management.

16 tools covering collectives, pages, tags, search, and trash:
- Read: list collectives, list/get pages (with WebDAV content), search,
  list tags, list trashed pages
- Write: create/update collective, create/move/trash/restore pages,
  set emoji, create/assign/remove tags

Includes Docker hook for app installation, OCS API client with envelope
unwrapping, Pydantic models, unit tests (16), and integration tests (10).

Closes #621

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 07:53:50 +01:00
Chris CoutinhoandClaude Opus 4.6 945b01cbf5 fix: address PR #632 review comments
- Update stale httpx reference to niquests in calendar.py type comment
- Replace inline inspect.isawaitable with _maybe_await helper in tests
- Fix incorrect port number in docker-compose unstructured comment
- Remove commented-out smithery service block (dead code)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 17:03:46 +01:00
Chris CoutinhoandClaude Opus 4.6 e24e49218e fix(caldav): address PR #632 review feedback
- Modernize typing: replace Dict/List/Optional with dict/list/X|None
- Add comment explaining _hacks="insist" mirrors upstream pattern
- Add comments noting caldav v3 raises PutError on HTTP failure
- Narrow except Exception to caldav_error.NotFoundError in delete methods
- Replace private _maybe_await import in tests with stdlib inspect.isawaitable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 22:08:42 +01:00
Chris CoutinhoandClaude Opus 4.6 36a664dda4 fix(caldav): migrate to upstream caldav v3.0.1 to fix href handling (#629)
When Nextcloud stores CalDAV objects, the server-side filename may differ
from the VTODO/VEVENT UID. The caldav fork constructed object URLs from
the UID instead of the actual <d:href> from REPORT responses, causing
list_todos to return wrong hrefs, delete_todo to silently no-op, and
update_todo to fail.

Upstream caldav v3.0.1 fixes this in _async_request_report_build_resultlist
by passing url=self.url.join(url) when constructing result objects.

Key changes:
- Replace caldav fork with upstream caldav>=3.0.1,<4.0
- Update imports to caldav.aio module
- Add _maybe_await() helper for v3's dual-mode methods that return
  either objects or coroutines depending on async context
- Add _async_object_by_uid() to work around upstream's get_object_by_uid
  not being async-aware (it iterates a coroutine synchronously)
- Adapt save_event/save_todo (no longer return tuples)
- Pass url=calendar.url.join(href) in _search_events_by_date
- Pass include_completed=True in list_todos to match previous behavior
- Add integration test for filename != UID scenario

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 18:45:24 +01:00
Chris CoutinhoandClaude Opus 4.6 fdb7b87baf fix: handle pythonvCard4 dict-format fields and missing phone numbers (#601)
Fix three related contacts bugs:
- Parse dict-format vCard fields ({value, type}) that pythonvCard4 returns,
  which previously crashed Pydantic validation expecting plain strings
- Include tel field in client output so phone numbers reach MCP tools
- Clarify addressbook parameter expects URI slug, not displayname

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:32:53 +01:00
Chris CoutinhoandClaude Opus 4.6 a11ae9c027 refactor: enforce PLC0415 (import-outside-top-level) for source code
Enable ruff PLC0415 rule for all source files (tests excluded via
per-file-ignores). Move 136 inline imports to top-level across 33 files.
8 imports suppressed with noqa for legitimate reasons: circular
dependencies (client/__init__.py, context.py), optional dependency
guards (app.py document processors, auth/userinfo_routes.py), and
post-env-setup imports (smithery_main.py).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 08:04:50 +01:00
Chris CoutinhoandClaude Opus 4.6 66ccacdee1 fix: add type: ignore for caldav ssl_verify_cert parameter
caldav types declare ssl_verify_cert as Union[bool, str] but the value
is passed through to httpx which accepts ssl.SSLContext. Add type: ignore
annotation to satisfy the ty type checker in CI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 13:34:05 +01:00
Chris CoutinhoandClaude Opus 4.6 1707b2e6e1 feat: add self-signed SSL certificate support for Nextcloud connections
Add NEXTCLOUD_VERIFY_SSL and NEXTCLOUD_CA_BUNDLE env vars to configure
TLS certificate verification for all outbound Nextcloud connections.
Centralizes SSL config via a new HTTP client factory (http.py) used by
all 27 Nextcloud-bound call sites, including API clients, OIDC endpoints,
OAuth flows, and health checks.

Closes #560

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 09:21:21 +01:00
Chris CoutinhoandClaude Opus 4.6 ec8eab99f3 fix: strip whitespace from category names when splitting
Trim whitespace from comma-separated category values in all three
methods: _create_ical_event, _merge_ical_properties, and
_merge_ical_todo_properties. Prevents leading/trailing spaces in
category names from inputs like "work, meeting".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 23:46:28 +01:00
Chris CoutinhoandClaude Opus 4.6 da104c59ac fix: handle categories, recurrence_rule, attendees, and reminder_minutes in update_event
_merge_ical_properties() only handled a subset of event fields, silently
dropping categories, recurrence_rule, attendees, and reminder_minutes
during updates. These fields were fully supported by _create_ical_event()
and accepted by the MCP tool, but never applied.

Closes #544

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 23:15:37 +01:00
Chris CoutinhoandClaude Opus 4.6 66e32d4705 fix: expand recurring events in date-range queries
PR #539 fixed date-range filtering so events outside the queried range
are excluded. However, recurring events still returned the master event
with its original DTSTART instead of expanded occurrences.

Add <C:expand> element to CalDAV REPORT requests (RFC 4791 §9.6.5) when
both date bounds are provided, so the server returns one VEVENT per
occurrence with the correct DTSTART. Refactor VEVENT parsing into a
shared helper and add _parse_all_ical_events() to handle multi-VEVENT
responses from expanded results.

Closes #538

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 12:43:40 +01:00
Chris CoutinhoandClaude Opus 4.6 b79ac29a9d fix: use CalDAV time-range filter for calendar date range queries
get_calendar_events() accepted start/end datetime parameters but called
calendar.events() which fetches all events, silently discarding the
date filters. This caused nc_calendar_list_events and
nc_calendar_get_upcoming_events to return the entire calendar history.

Add _search_events_by_date() helper that builds a CalDAV REPORT query
with a <time-range> filter (RFC 4791 §9.9) for server-side filtering.
Falls back to calendar.events() when no dates are given.

Closes #538

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 17:35:33 +01:00
Chris CoutinhoandClaude Opus 4.5 21e4d3effd fix(deck): use correct endpoint for reorder_card to fix cross-stack moves
The reorder_card method was using the API route
/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/reorder
which has a parameter conflict: the URL's {stackId} (current stack)
overrides the body's stackId (target stack) in Nextcloud's routing.

This caused cards to stay in their original stack even when the API
reported success.

Switched to the non-API route /cards/{cardId}/reorder which correctly
reads stackId from the request body, matching the behavior of the
working curl command reported in the issue.

Also added the required OCS-APIRequest headers that were missing.

Fixes #469

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 13:29:03 +01:00
Chris CoutinhoandClaude Opus 4.5 a26a470af6 fix(deck): Always preserve fields in update_card for partial updates
The Deck PUT API is a full replacement, not a partial update.
Previously, title and description were conditionally sent, causing:
- 400 errors when title not provided (it's required)
- Description being cleared when not explicitly set

Now all required fields (title, type, owner) and description are
always included in the payload using current card values when not
explicitly provided. This matches the existing pattern for type/owner.

Also simplified owner extraction since DeckCard.validate_owner
already ensures it's always a string.

Fixes #452

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 23:30:01 -06:00
Chris CoutinhoandClaude Sonnet 4.5 e4f3beee01 fix: resolve type checking warnings for CI
- Add type casts for Starlette app state access
- Add assertions for cipher, card, board, stack after initialization
- Add None checks for XML element text attributes
- Handle __package__ being None in tracing setup
- Fix TokenBrokerService initialization to use storage credentials

Resolves 42 type warnings from ty-check, enabling CI linting to pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-18 00:44:58 +01:00
Chris CoutinhoandClaude Sonnet 4.5 d61e33113c fix(news): revert get_item() to use get_items() + filter
Reverts the "perf(news): use direct API endpoint for get_item()" change
from commit 92c4bf3 which incorrectly assumed GET /items/{itemId} exists.

The News API (v1-2, v1-3, v2) does not provide a direct endpoint to
retrieve individual items. The only /items/{itemId} routes are POST
operations for marking items read/unread/starred.

Changes:
- Restore original get_item() implementation that fetches all items
  and filters in Python
- Update exception from HTTPStatusError to ValueError
- Restore documentation explaining API limitation
- Update unit tests to mock get_items() instead of _make_request()
- Add test for ValueError when item not found

Fixes vector processor 405 errors when indexing news items.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 15:47:27 +01:00
Chris CoutinhoandClaude Sonnet 4.5 3f06e2ee77 fix: resolve all type checking errors (8 errors fixed)
Fixed 8 type checker errors across the codebase:

- vector/scanner.py: Handle None scroll results with null-safe iteration
- search/{bm25_hybrid,semantic}.py: Add None checks for result.payload
- auth/{unified_verifier,webhook_routes}.py: Assert non-None auth credentials
- client/webdav.py: Add None checks before int() conversions
- providers/openai.py: Assert embedding_model is not None
- search/algorithms.py: Explicitly type doc_types set and cast values
- observability/logging_config.py: Match parent class signature (log_data)

Also fixed test_create_tag_creates_system_tag to match WebDAV implementation
(was testing OCS API endpoint, now tests correct WebDAV endpoint with
Content-Location header).

Type checker: 0 errors (down from 8), 20 warnings (ignored)
Tests: All 192 unit tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-08 01:09:02 +01:00
Chris CoutinhoandClaude 92c4bf36f6 perf(news): use direct API endpoint for get_item()
Replace O(n) fetch-all-and-filter approach with O(1) direct API call.
The News API v1-3 supports GET /items/{id} for single-item retrieval.

- Update get_item() to use direct endpoint
- Add unit test for get_item() method
- Fixes critical performance issue identified in code review

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 17:22:51 +01:00
Chris CoutinhoandClaude a33f6a2f15 feat(news): add Nextcloud News app integration
Add full integration for the Nextcloud News (RSS/Atom reader) app:

- Add NewsClient with complete CRUD operations for folders, feeds, and items
- Add 8 read-only MCP tools for listing/getting folders, feeds, items
- Add Pydantic models for News entities with camelCase alias support
- Add vector sync support for starred + unread items
- Add HTML to Markdown converter using markdownify for better embeddings
- Add Docker post-install hook to enable News app
- Add 25 unit tests for NewsClient API methods

Vector sync indexes starred and unread items, providing a balanced approach
that captures important (starred) and current (unread) content without
indexing the entire article history.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 14:39:31 +01:00
Chris CoutinhoandClaude 2ab8dad6a5 fix: Use WebDAV for tag creation and add LLM-as-a-judge for RAG tests
- Change create_tag() to use WebDAV POST instead of OCS API which
  returned 404 in some Nextcloud versions
- Add llm_judge() helper that evaluates system output against ground
  truth with simple TRUE/FALSE prompt
- Replace keyword-based assertions in RAG tests with LLM judge for
  more flexible semantic evaluation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 02:24:01 +01:00
Chris CoutinhoandClaude 2896fa1dc9 feat: Add tag management methods to WebDAV client
- Add get_file_info() to get file info including file ID via PROPFIND
- Add create_tag() to create system tags via OCS API
- Add get_or_create_tag() for idempotent tag creation
- Add assign_tag_to_file() to assign tags to files via WebDAV
- Add remove_tag_from_file() to remove tags from files

Also refactors RAG evaluation:
- Add indexed_manual_pdf fixture using existing nc_client/nc_mcp_client
- Remove manual tag creation steps from workflow (now handled by fixture)
- Add comprehensive unit tests for new WebDAV methods

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 01:51:42 +01:00
Chris CoutinhoandClaude 327d843f64 feat: Implement per-chunk vector visualization with context expansion
Major improvements to vector visualization page:
- Refactor PCA to display individual chunks instead of averaged documents
- Add context expansion module for fetching surrounding text from notes and PDFs
- Update deduplication to use (doc_id, doc_type, chunk_start, chunk_end) keys
- Fix Alpine.js rendering with chunk-specific keys including offsets
- Refactor authentication helper to return NextcloudClient for better reuse
- Add async context manager support to NextcloudClient

Technical details:
- viz_routes.py: Fetch specific chunk vectors instead of averaging per document
- context.py: New module supporting both notes and PDF text extraction via PyMuPDF
- search algorithms: Extract page_number, chunk_index, total_chunks from Qdrant
- vector-viz.js/html: Use chunk positions in expansion tracking keys

This enables users to see which specific chunks match their query
and view them with surrounding context in the PCA visualization.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 11:22:20 +01:00
Chris CoutinhoandClaude b8010270c1 fix: Add async/await, PDF metadata, and type safety fixes
This commit addresses multiple issues with async operations, PDF metadata
extraction, and type safety in document processing and search.

## Async/Await Fixes
- processor.py:259 - Added await for chunker.chunk_text(content)
- processor.py:270 - Added await for bm25_service.encode_batch(chunk_texts)
- tests/unit/test_document_chunker.py - Converted all 12 test methods to async

## PDF Metadata Enhancement
- pymupdf.py:143 - Added file_size metadata extraction
- pymupdf.py:145-206 - Refactored to extract text page-by-page
  - Manually loop through pages instead of using page_chunks=True
  - Generate page_boundaries metadata for precise page tracking
  - Works around pymupdf.layout.activate() breaking page_chunks=True
- processor.py:32-66 - Added assign_page_numbers() helper function
  - Assigns page numbers to chunks based on overlap with page boundaries
  - Handles chunks spanning multiple pages
- processor.py:298-300 - Call assign_page_numbers() for PDF files

## Type Safety Fixes
- bm25_hybrid.py:184 - Removed int() conversion of doc_id
- semantic.py:131 - Removed int() conversion of doc_id
- viz_routes.py:275 - Removed int() conversion of doc_id
- Added comments documenting that doc_id can be int (notes) or str (file paths)

## Testing
- All 18 tests passing (12 unit + 6 integration)
- No type errors in modified files
- Container logs show successful processing
- Vector viz searches working correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 02:37:07 +01:00
Chris Coutinho 529daf2b48 ci: temp disable sse in ci 2025-11-16 07:03:18 +01:00
Chris CoutinhoandClaude 137d1d6c75 perf: fix vector viz search performance and visual encoding
This commit addresses critical performance issues with vector visualization
search (reducing time from 40s to ~2s) and improves result visualization
through better visual encoding.

## Performance Fixes

### 1. Fix blocking sleep in retry decorator (base.py:51)
- Changed `time.sleep(5)` to `await anyio.sleep(5)` in @retry_on_429
- Prevents entire event loop from freezing during rate limit retries
- Impact: Reduced search time from 22s to 16s initially

### 2. Add concurrency limiting for verification (verification.py:77-93)
- Added `anyio.Semaphore(20)` to limit concurrent HTTP requests
- Prevents connection pool exhaustion (RequestError) from 90+ simultaneous requests
- Fixes false filtering (was filtering 77/90 results incorrectly)
- Note: Semaphore still in code but verification removed from viz endpoint

### 3. Remove unnecessary verification from viz endpoint (viz_routes.py:483-486)
- Visualization only needs Qdrant metadata (title, excerpt), not full content
- Verification only required for sampling (LLM needs full note content)
- Impact: Reduced search time from 43.7s to ~2s (final fix)

### 4. Restore streaming scanner pattern (scanner.py)
- Process notes one-at-a-time using async generator
- Avoids loading all notes into memory

## Visualization Improvements

### 5. Result-relative score normalization (viz_routes.py:489-504)
- Normalize scores within result set: best=1.0, worst=0.0
- Removes arbitrary RRF normalization (theoretical max didn't make sense)
- Makes visual encoding meaningful regardless of algorithm scores

### 6. Power scaling for marker sizes (userinfo_routes.py:743)
- Changed from linear `8 + (score * 12)` to power `6 + (score² * 14)`
- Creates dramatic visual contrast: 0.0→6px, 0.5→9.5px, 1.0→20px
- Combined with opacity (0.2-1.0) for clear visual hierarchy

### 7. Multi-channel visual encoding (userinfo_routes.py:740-745)
- Size: Exponentially scaled with score²
- Opacity: Linear 0.2-1.0 (keeps all points visible)
- Color: Viridis gradient (blue→yellow)
- Effect: Top results are large/bright/opaque, context results small/dim/transparent

## Result
- Search time: 40s → ~2s (20x faster)
- Visual contrast: Subtle → dramatic (clear result hierarchy)
- No arbitrary cutoffs: All results visible, best naturally highlighted

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 07:01:35 +01:00
Chris CoutinhoandClaude f4759e424d feat: add webhook management UI and BeforeNodeDeletedEvent support
Added comprehensive webhook management capabilities including:

Webhook Client & API:
- Added WebhooksClient for Nextcloud webhooks API integration
- Create, list, update, and delete webhooks programmatically
- Support for event filters in webhook registration

Webhook Presets:
- Added preset system for common webhook configurations
- notes_sync: BeforeNodeDeletedEvent for Notes file operations
- calendar_sync: Calendar events (create, update, delete)
- deck_sync: Deck card operations
- files_sync: File system changes
- forms_sync: Form submissions (conditional)
- Filter presets by installed apps

Admin UI:
- Added multi-pane app view with tabs (User Info, Vector Sync, Webhooks)
- Webhooks tab for admin users only
- Enable/disable preset webhooks via UI
- View currently registered webhooks
- Uses htmx for dynamic loading and Alpine.js for tab state
- Admin permission checking via OCS API

CLI Improvements:
- Refactored CLI to separate module (cli.py)
- Updated entry point in pyproject.toml

BeforeNodeDeletedEvent Fix:
- Updated ADR-010 to document NodeDeletedEvent issue
- BeforeNodeDeletedEvent includes node.id before deletion
- NodeDeletedEvent lacks node.id (file already deleted)
- Implemented per Nextcloud maintainer recommendation

Testing:
- Added comprehensive webhook client tests
- Added webhook preset filtering tests
- Added admin permission tests

Configuration:
- Updated docker-compose.yml Qdrant settings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 20:35:08 +01:00
Chris CoutinhoandClaude a6e5f3d8ff refactor: simplify OpenTelemetry tracing configuration
Simplifies the OpenTelemetry tracing setup by removing the redundant
OTEL_ENABLED flag and using the presence of OTEL_EXPORTER_OTLP_ENDPOINT
to determine if tracing should be enabled. This follows the standard
OpenTelemetry environment variable conventions more closely.

Changes:
- Remove OTEL_ENABLED/tracing_enabled flag in favor of checking if
  OTEL_EXPORTER_OTLP_ENDPOINT is set
- Add OTEL_EXPORTER_VERIFY_SSL configuration option for OTLP endpoints
  with self-signed certificates (defaults to false for development)
- Move HTTPXClientInstrumentor initialization to module level to ensure
  httpx calls are traced across all Nextcloud API requests
- Add tracing spans to vector sync operations (scan_user_documents)
- Fix authorization header logging to only warn about missing headers
  in OAuth mode (BasicAuth mode doesn't use Authorization headers)
- Update observability documentation to reflect simplified configuration
- Refactor Dockerfile to use --no-editable flag for uv sync

Breaking changes:
- OTEL_ENABLED environment variable is removed
- Tracing is now automatically enabled when OTEL_EXPORTER_OTLP_ENDPOINT
  is set

Migration guide:
- Remove OTEL_ENABLED=true from environment configuration
- Tracing will be enabled automatically if OTEL_EXPORTER_OTLP_ENDPOINT
  is configured

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 22:48:37 +01:00
Chris CoutinhoandClaude 640a7818f9 fix: optimize Notes API pagination with pruneBefore parameter
The Nextcloud Notes API intentionally returns all note IDs (with only 'id'
field) in the last chunk to enable deletion detection. Without using the
pruneBefore parameter, this causes duplicates - all notes appear with full
data in chunks, then again with minimal data in the last chunk.

This commit implements proper pruneBefore support:
- NotesClient.get_all_notes() now accepts prune_before timestamp parameter
- Scanner calculates max(indexed_at) from Qdrant to use as prune threshold
- Only notes modified after this timestamp are sent with full data
- Deduplication logic handles the API's deletion detection pattern
- Significantly reduces data transfer for incremental syncs

The behavior is documented in Notes API v1 spec - this is not an API bug,
but a feature we weren't utilizing correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 07:19:26 +01:00
Chris CoutinhoandClaude 578de4d7d6 feat(observability): Add comprehensive monitoring with Prometheus and OpenTelemetry
- Add Prometheus metrics for HTTP, MCP tools, Nextcloud API, OAuth, vector sync, and DB operations
- Add OpenTelemetry distributed tracing with OTLP export
- Add structured JSON logging with trace context correlation
- Add ObservabilityMiddleware for automatic HTTP instrumentation
- Add app_name attribute to all client classes for per-app metrics
- Add configuration for metrics, tracing, and logging via environment variables
- Add documentation in docs/observability.md
- Fix graceful degradation when tracing is disabled (default state)
- Fix uvicorn logging configuration to use observability formatters

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 08:54:04 +01:00
Chris Coutinho 4adb9de5f0 chore: fix typo 2025-11-05 15:36:50 +01:00