44 files of prose — CLAUDE.md, AGENTS.md, TODO.md, 20 docs, both plugin design
documents, and the comment surface the earlier steps could not reach.
Applied against an explicit keep-list, not swept, because the word turned out to
have SIX meanings in this repository rather than the three the offscale doc
recorded:
permissions renamed (steps 1–2)
$OFFICER_ROOT/capabilities/ KEPT — the item store, and now the only thing
the word means that is ours
sidecar routing keys renamed to `handles` (step 3)
Lightning wallet KEPT — a domain term, and on the wire to the mobile apps
terminfo queries KEPT — XTGETTCAP, in the pty sidecar
InvoiceShelf KEPT — per-resource { write, bulkDelete } flags
The sweep still falsified two things, both caught by checking rather than by
review, and both in prose that discusses more than one meaning at once:
CLAUDE.md began claiming the item store lives at `$OFFICER_ROOT/permissions`.
It does not; that directory is on disk and full of skills and tools.
And the offscale doc's own note about the collision became
"Named `permissions`, NOT `permissions`" — a sentence that had eaten the thing
it existed to warn about.
Both restored, and the note rewritten to say what is now true: capability means
one thing of ours, and three that belong to somebody else's vocabulary.
Verified live after restart: self and admin permission endpoints 200, gated
route 200, agent-status 200, 9 grants intact with 6 permissions offered.
tsgo clean, 797 tests, 787 pass, same 7.
The rename is done. Four steps, no data lost, no client break that survived
the step it was introduced in.
104 lines
6.2 KiB
TypeScript
104 lines
6.2 KiB
TypeScript
import type { PluginManifest } from '@@/plugins/manifest';
|
|
|
|
// Music — the library, the player, and the phone and tablet apps that stream from it.
|
|
//
|
|
// The second plugin extracted from the platform, on 2026-08-15. Bigger than offscale and, unlike it, not
|
|
// a clean cut. It took two passes: the first left three pieces in the platform, and the second moved two
|
|
// of them here after the owner read the code and asked why the platform still had files named for music.
|
|
// He was right — one of the three "seams" turned out to be dead code holding the door open.
|
|
//
|
|
// api/router.ts the sidecar proxy, built here — thin, and it must never grow music knowledge
|
|
// cliamp/ the second playback path, parked
|
|
// widgets/ the dashboard widget, parked
|
|
// assets/icon.png the dock tile, published to /plugins/music/ on install
|
|
// sidecar/ the whole /api/music contract: indexing, streaming, per-user state
|
|
// db/ music_favorites, _playlists, _playlist_items, _now_playing
|
|
// web/ the library panels; the shell renders the Workspace
|
|
//
|
|
// ── What stayed in the platform, and why ── (nothing. All three moved here.)
|
|
//
|
|
// 1. ~~cliamp~~ — MOVED HERE, all of it, into `./cliamp/`. The sidecar halves, the relay, the file
|
|
// browser's panel and its `Play` action. `src/servers/sidecar/music/`, `src/servers/api/cliamp/` and
|
|
// `src/servers/api/music/` no longer exist, and `server.tsx` has no cliamp anything. It is PARKED, not
|
|
// working: bringing it back needs a plugin to own a websocket, which is a platform gap.
|
|
//
|
|
// 2. ~~The dashboard widget~~ — MOVED HERE, to `./widgets/`, and unregistered from WidgetRegistry.
|
|
// Parked: plugins cannot contribute widgets and that mechanism is not built.
|
|
//
|
|
// 3. ~~The global player overlay~~ — MOVED HERE, to `./web/`. It stayed while the widget pinned
|
|
// `useMusicPlayer` in `officerdev`; once the widget left, the only platform dependency was one line
|
|
// in DashboardLayout. `MusicPlayerHost` now mounts inside the MusicDetail panel, where it owns the
|
|
// audio engine and renders nothing — which is what it already did on /music.
|
|
//
|
|
// `[phase 2]` Leaving /music stops playback. Giving audio a life outside the route needs a shell slot
|
|
// a plugin can contribute to, or the engine hoisted to module scope. Deferred deliberately; nothing
|
|
// breaks meanwhile.
|
|
//
|
|
// ── Host dependencies ──
|
|
//
|
|
// Music is the plugin that made `osDependencies` exist. Offscale was self-sufficient, so until this one
|
|
// there was nothing to declare and no reason to build the field — see ./PLUGIN.md.
|
|
export const manifest: PluginManifest = {
|
|
publisher: 'officerdev',
|
|
version: '1.0.0',
|
|
platform: '>=1.0.0',
|
|
|
|
label: 'Music',
|
|
summary: 'The music library — browse, play, favourites and playlists',
|
|
// No `icon` field: this plugin ships `assets/icon.png` and the file wins. A lucide name could only
|
|
// ever pick from the 106 glyphs the platform happens to bundle, which is a ceiling a plugin from a
|
|
// marketplace cannot see coming — and this one's artwork is a voxel duck in headphones, not a glyph.
|
|
color: '#22c55e',
|
|
|
|
// One permission gating the whole surface, grantable per role at read or write like every other.
|
|
//
|
|
// The key is `music` and that is not incidental: it is the key the platform's own registry used until
|
|
// this extraction, so every existing `role_permissions` grant keeps meaning what it meant, and the
|
|
// overlay's `can('music')` keeps resolving. Renaming it would have been a silent data change.
|
|
//
|
|
// `[open]` What a member's grant MEANS here is this plugin's own job and is not finished. Favourites,
|
|
// playlists and now-playing are already per-caller — the sidecar scopes every one of them by the
|
|
// `X-Officer-User` header the proxy injects — while the library itself is one shared index for the
|
|
// household. So "whose row is this" already has a real, non-uniform answer, which is why the platform's
|
|
// side of it is a uniform read/write and nothing more. Designing the rest belongs in these queries.
|
|
permissions: [
|
|
{
|
|
key: 'music',
|
|
label: 'Music',
|
|
description: 'The music library, playback, and your own favourites and playlists',
|
|
// These are the `personal` paths from the registry entry this replaces, carried across verbatim.
|
|
//
|
|
// They are not read-only — they are genuine writes to the CALLER'S own data, which is what made
|
|
// them safe at read level. The manifest deliberately has no `personal` field, and adding one would
|
|
// be designing the per-user visibility model that is explicitly not this extraction's work. It
|
|
// costs nothing to go without: `isRequestAllowedAtLevel` concatenates `personal` and
|
|
// `readOnlyWrites` into a single allow-list, so the two are the same mechanism under two names and
|
|
// a read grant permits exactly the same four paths it permitted yesterday.
|
|
//
|
|
// `/queue` is here because it was there. No such route exists, in the sidecar or anywhere else.
|
|
readOnlyWrites: ['/favorites', '/now-playing', '/playlists', '/queue'],
|
|
},
|
|
],
|
|
|
|
// Both come from one package everywhere, which is luck rather than a rule — hence a name per manager
|
|
// rather than one canonical name. `packages.sh` records why that indirection was rejected.
|
|
//
|
|
// They are declared SEPARATELY even so, because the platform probes binaries and these two fail
|
|
// differently. Losing `ffprobe` is the quiet one: the indexer catches the spawn error and returns a
|
|
// track carrying its filename and nothing else — no title, artist, album, duration or embedded
|
|
// lyrics — then reports success. Losing `ffmpeg` costs cover art and video poster frames, which is at
|
|
// least visible. Naming both means the owner is told which of the two they are missing.
|
|
osDependencies: [
|
|
{
|
|
binary: 'ffprobe',
|
|
reason: 'Reads tags, duration and embedded lyrics. Without it every track indexes as a bare filename.',
|
|
packages: { apt: 'ffmpeg', pacman: 'ffmpeg', dnf: 'ffmpeg', brew: 'ffmpeg' },
|
|
},
|
|
{
|
|
binary: 'ffmpeg',
|
|
reason: 'Compresses cover art for phones and grabs poster frames from videos.',
|
|
packages: { apt: 'ffmpeg', pacman: 'ffmpeg', dnf: 'ffmpeg', brew: 'ffmpeg' },
|
|
},
|
|
],
|
|
};
|