Skip to content

feat(familiars): icon field on /api/v1/familiars (read + write)#151

Merged
BunsDev merged 1 commit into
mainfrom
feat/familiar-icon-field
May 31, 2026
Merged

feat(familiars): icon field on /api/v1/familiars (read + write)#151
BunsDev merged 1 commit into
mainfrom
feat/familiar-icon-field

Conversation

@BunsDev
Copy link
Copy Markdown
Member

@BunsDev BunsDev commented May 31, 2026

Summary

The daemon now owns the full read+write story for a familiar's glyph.

Adds an optional icon field to ~/.coven/familiars.toml, served at GET /api/v1/familiars and writable via a new PUT /api/v1/familiars/{id}/icon. The Cave's glyph picker (OpenCoven/coven-cave#6, already merged) can stop writing only to Cave-local localStorage and start persisting picks back to the user's TOML — surviving across devices, reinstalls, and other Cave surfaces.

Schema

[[familiar]]
id = "cody"
icon = "ph:lightning-fill"   # Phosphor icon

[[familiar]]
id = "kitten"
icon = "🐈‍⬛"                # emoji literal

The ph: prefix is the wire discriminator. Both shapes round-trip through one field, so the daemon stays neutral about which kind a user wants.

Endpoints

Method Path Body Status Behavior
GET /api/v1/familiars 200 FamiliarDto[] now includes icon when present, omitted when absent
PUT /api/v1/familiars/{id}/icon { "icon": "ph:cat-fill" } 200 Insert or replace the icon
PUT /api/v1/familiars/{id}/icon { "icon": null } or {} or empty body 200 Clear the field
PUT /api/v1/familiars/{id}/icon { "icon": [1,2,3] } 400 Reject non-string/non-null
PUT /api/v1/familiars/{id}/icon any 404 Unknown familiar id — error.code = "familiar_not_found"

Whitespace-only values normalize to None so a " " icon can't escape as an empty glyph.

Write safety

  • cockpit_sources::write_familiar_icon() does the edit via toml_edit so existing field order, comments, and surrounding whitespace are preserved.
  • Atomic write: serialize to .familiars.toml.tmp in the same directory, then rename over the target — POSIX-atomic on the same filesystem and Windows-safe. A crash mid-write can leave the temp file behind but never a half-written familiars.toml.

Tests (12 new)

cockpit_sources::tests (6):

  • read_familiars_carries_icon_field_for_both_shapes — TOML round-trip for ph: names, emoji literals, whitespace, absent
  • familiar_dto_skips_serializing_absent_icon — wire shape contract
  • write_familiar_icon_inserts_when_absent_and_preserves_other_fields — surrounding fields + comments survive
  • write_familiar_icon_replaces_existing_value
  • write_familiar_icon_clears_field_when_value_is_none
  • write_familiar_icon_treats_whitespace_as_clear
  • write_familiar_icon_returns_not_found_for_unknown_id
  • write_familiar_icon_leaves_no_tempfile_on_success — atomic-rename hygiene

api::tests (6):

  • put_familiar_icon_updates_existing_value
  • put_familiar_icon_inserts_when_absent — also asserts the other familiar's icon stays untouched
  • put_familiar_icon_clears_when_null
  • put_familiar_icon_returns_404_for_unknown_id
  • put_familiar_icon_rejects_non_string_icon
  • put_familiar_icon_with_empty_body_clears

Pre-existing read_familiars_parses_toml_entries extended to also assert icon = None on fixtures that don't set it.

Dependency

Adds toml_edit = "0.22" (with serde feature) for write-side formatting preservation. toml = "1.1" (read-only) stays.

Test plan

  • cargo fmt --check -p coven-cli clean (the first CI run failed on this — fixed)
  • cargo test -p coven-cli all green, all 12 new tests pass
  • cargo check -p coven-cli clean
  • After merge, set icon = "ph:cat-fill" on a familiar in local ~/.coven/familiars.toml, restart the daemon, hit GET /api/v1/familiars, confirm the field appears
  • Same flow but via PUTcurl -X PUT --unix-socket "$COVEN_SOCK" http://x/api/v1/familiars/cody/icon -d '{"icon":"ph:cat-fill"}' — confirm the TOML is updated and surrounding formatting is preserved

Follow-up

Cave-side daemon-writer: extend cave-glyph-overrides.ts to also PUT to this endpoint when the user picks a glyph (with localStorage as a fast-path mirror that stays valid if the daemon is offline). One small PR on coven-cave.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 31, 2026 06:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Adds an optional `icon` field to `~/.coven/familiars.toml` and serves
it both ways through the daemon HTTP API. Clients with a richer icon
system — CovenCave's glyph picker — can pick either an emoji literal
or a Phosphor icon name and persist it back to the user's TOML so the
pick survives across devices, reinstalls, and other Cave surfaces.

## Schema

    [[familiar]]
    id = "cody"
    icon = "ph:lightning-fill"   # Phosphor icon

    [[familiar]]
    id = "kitten"
    icon = "🐈‍⬛"                # emoji literal

The `ph:` prefix is the wire discriminator — clients with a Phosphor
renderer treat it as an icon name; anything else is rendered as a
literal glyph. Both shapes round-trip through one field, so the
daemon stays neutral about which kind a user wants.

## Read path

- `FamiliarEntry` (TOML reader) gets `icon: Option<String>`.
- `FamiliarDto` (API output) gets `pub icon: Option<String>` with
  `skip_serializing_if = "Option::is_none"` so older clients keep
  their existing payload.
- `read_familiars()` threads `icon` through and normalizes
  whitespace-only values to `None` so a `"   "` icon can't escape as
  an empty glyph.

## Write path

- New `PUT /api/v1/familiars/{id}/icon` endpoint.
- Body shape: `{ "icon": "ph:cat-fill" }`, `{ "icon": "🐈" }`,
  `{ "icon": null }`, or an empty body — null/empty/whitespace clear
  the field, anything else updates or inserts it.
- Non-string, non-null `icon` values return 400 so a typo can't write
  garbage into the file.
- Unknown familiar id returns 404 with `error.code = "familiar_not_found"`.
- New `cockpit_sources::write_familiar_icon()` does the edit via
  `toml_edit` so existing field order, comments, and whitespace in the
  user's TOML are preserved across writes.
- Writes are atomic: write to `.familiars.toml.tmp` in the same
  directory, then `rename` over the target — POSIX-atomic on the
  same filesystem and Windows-safe.

## Tests (12 new)

`cockpit_sources::tests`:
- `read_familiars_carries_icon_field_for_both_shapes` — TOML round-trip
  for `ph:` names, emoji literals, whitespace, and absent.
- `familiar_dto_skips_serializing_absent_icon` — `serde_json` wire
  shape check (absent icon is not in the JSON; present icon is).
- `write_familiar_icon_inserts_when_absent_and_preserves_other_fields`
  — surrounding fields + comments survive the edit.
- `write_familiar_icon_replaces_existing_value`.
- `write_familiar_icon_clears_field_when_value_is_none`.
- `write_familiar_icon_treats_whitespace_as_clear`.
- `write_familiar_icon_returns_not_found_for_unknown_id`.
- `write_familiar_icon_leaves_no_tempfile_on_success` — atomic-rename
  hygiene check.

`api::tests`:
- `put_familiar_icon_updates_existing_value`.
- `put_familiar_icon_inserts_when_absent` — also asserts the other
  familiar's icon stays untouched.
- `put_familiar_icon_clears_when_null`.
- `put_familiar_icon_returns_404_for_unknown_id`.
- `put_familiar_icon_rejects_non_string_icon`.
- `put_familiar_icon_with_empty_body_clears`.

Pre-existing `read_familiars_parses_toml_entries` now also asserts
`icon` defaults to `None` on fixtures that don't set it.

## Dependency

Adds `toml_edit = "0.22"` so writes can preserve formatting and
comments. `toml = "1.1"` (read-only) stays.

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

Co-Authored-By: OpenCoven <noreply@opencoven.ai>
@BunsDev BunsDev changed the title feat(familiars): serve optional icon field from /api/v1/familiars feat(familiars): icon field on /api/v1/familiars (read + write) May 31, 2026
@BunsDev BunsDev force-pushed the feat/familiar-icon-field branch from ffcc82a to 40490da Compare May 31, 2026 07:29
@BunsDev BunsDev merged commit 7b90eaf into main May 31, 2026
10 checks passed
@BunsDev BunsDev deleted the feat/familiar-icon-field branch May 31, 2026 07:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants