Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.2] - 2026-10-26

### Added

- Native credential dot on the **Muse Code (Meta subscription)** row: the route names
`apiKeyEnv: MUSE_CODE_SUB_TOKEN`; the plugin mirrors the OAuth record's minted key into that
reference (red without a credential, green with one) and re-mints it before Meta's ~24 h
expiry, because an explicit reference is DSH's highest-priority auth override.
- Documented the Meta prompt-cache behavior (`prompt_cache_key` = DSH session id) and the
meaning/consequences of the generic **Edit** and **Delete** row actions.

### Fixed

- The live catalog sync no longer mistakes the empty `models` array that the Settings form
projects for a user-owned catalog, so Muse Wonder keeps being filled into the route.

## [0.2.1] - 2026-10-26

### Added
Expand Down
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cff-version: 1.2.0
message: "If you use this software, please cite it as below."
title: "dsh-muse-code"
version: 0.2.1
version: 0.2.2
date-released: "2026-10-26"
url: "https://github.com/TheStreamCode/dsh-muse-code"
repository-code: "https://github.com/TheStreamCode/dsh-muse-code"
Expand Down
59 changes: 56 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,11 @@ cannot reach the Host falls back to that command.
direct Meta page link, backed by the routes above, plus the `/muse-login` chat command. Both
start DSH's own device-code flow (`llm-pi-ai/meta`) and open the verification page in the
default browser.
- Paints the native credential dot on the **Muse Code (Meta subscription)** row: the route names
`apiKeyEnv: MUSE_CODE_SUB_TOKEN`, and the plugin mirrors the OAuth record's minted key into that
reference (red without a credential, green with one) and re-mints it before Meta's ~24 h expiry.
- Leaves credential handling to DSH: the shipped flow stores the sign-in as an OAuth credential
record and re-mints Meta's short-lived inference key automatically, so the route keeps
record and the plugin keeps the route's credential-reference copy fresh, so the route keeps
working after the key rotates.

## Requirements
Expand Down Expand Up @@ -126,8 +129,9 @@ see the upgrade note under Troubleshooting.
- The local `POST /muse-code/login`, `GET /muse-code/login`, and `GET /muse-code/status` routes
only start the flow and report its state; they transmit no credential and are reachable only
on the Host's own loopback listener.
- The stored credential is the `llm-pi-ai/meta` record in the DSH credential store. It never
appears in configuration or logs.
- The stored credential is the `llm-pi-ai/meta` record in the DSH credential store; the
`MUSE_CODE_SUB_TOKEN` reference the row reads is a copy of its minted key and is refreshed by
the plugin. Neither ever appears in configuration or logs.
- To remove access, remove the bundle, remove the declared provider from **Settings → Models**
(or from the profile's `cordis.patch.yml`), and delete the `llm-pi-ai/meta` record while DSH
is stopped (the file is `$DSH_HOME/.credentials.yaml`), or run the sign-in again to replace
Expand All @@ -137,6 +141,25 @@ see the upgrade note under Troubleshooting.
See [SECURITY.md](SECURITY.md) for vulnerability reporting and
[AUTH_CONTRACT.md](AUTH_CONTRACT.md) for the pinned login contract that DSH implements.

## Prompt caching

The Meta Model API caches the request prefix per `prompt_cache_key`, and a request without that
key gets no reuse at all. DSH's pi-ai adapter sends the session id as the key for every call, so
turns in the same session reuse the prefix Meta already saw. Measured against the live API with
an identical 1.6k-token prefix: same key → 1521 of 1600 tokens cached on the second call;
different key or no key → 0.

The ratio you see in the usage panel therefore depends on how the session is used:

- the cache is per key and per model — switching model or route starts a fresh prefix;
- a short session, or turns spaced far apart, shows a low ratio, because the first call of every
cache window pays full price;
- editing the system prompt or compacting the history invalidates the prefix once.

The plugin does not change the request shape, so this behaves like any other pi-ai route in the
same harness (for example `opencode-go`). DeepSeek's first-party routes report their own cache
counters and are not directly comparable.

## Troubleshooting

### Nothing named Muse Code appears under Settings → Models
Expand Down Expand Up @@ -192,6 +215,36 @@ before it expires.
DSH re-mints the inference key from the stored session. When Meta ends that session, run the
sign-in again (Sign in control or `/muse-login`) to sign in and replace the record.

### The credential dot stays red after signing in

The route's credential reference is a copy of the record's minted key; the plugin mirrors it on
boot and on every record update. If the dot stays red while the card says **Signed in**, restart
DSH and check that the `llm-pi-ai/meta` record still exists in the credential store.

### Requests fail after about a day without the plugin running

The route authenticates through the mirrored reference, and the plugin re-mints the key before
its ~24 h expiry. If DSH ran with the plugin disabled long enough for the key to lapse, sign in
again (Sign in card or `/muse-login`).

### Edit and Delete on the Muse Code row

Both are DSH's generic provider-row actions and a plugin cannot hide them.

- **Edit** writes the route profile into your profile patch: display name, models, and the
`apiKeyEnv` reference. The plugin restores the reference on the next boot and refills an
emptied model list from the account catalog.
- **Delete** removes only that user-layer route: the provider leaves Settings → Models and the
model picker, and a hand-edited model list is lost; the credential record and its mirrored
reference stay. The plugin re-declares the route on the next boot and resyncs the catalog. To
actually remove Muse Code, uninstall the bundle and delete the credential record.

The supported off switch is the plugin itself:

```bash
dsh plugin --profile desktop remove @thestreamcode/dsh-muse-code
```

### Upgrading from 0.1.x

0.1.x inserted a second `@deepseek-ai/dsh-llm-pi-ai` instance, which DSH refuses to activate
Expand Down
4 changes: 2 additions & 2 deletions client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ loader?.load({
function statusView(phase) {
if (phase === "authorized") return { label: "Signed in", color: COLOR.labelSuccess }
if (phase === "pending" || phase === "starting") return { label: "Waiting for Meta…", color: COLOR.labelWarn }
if (phase === "failed") return { label: "Sign-in failed", color: COLOR.labelError }
if (phase === "checking") return { label: "Checking…", color: COLOR.labelTertiary }
return { label: "Not signed in", color: COLOR.labelTertiary }
// Not signed in, or the last attempt failed: the row reads red.
return { label: phase === "failed" ? "Sign-in failed" : "Not signed in", color: COLOR.labelError }
}

function MuseCodeSignIn() {
Expand Down
66 changes: 66 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import type { IncomingMessage, ServerResponse } from "node:http";
export declare const name = "dsh-muse-code";
export declare const inject: string[];
/**
* Credential reference the route reads through `apiKeyEnv`.
*
* DSH paints the Models row's credential dot only for a route that names a
* reference, so the route reads the minted inference key through this reference
* and the plugin keeps it in step with the OAuth record.
*/
export declare const CREDENTIAL_REFERENCE = "MUSE_CODE_SUB_TOKEN";
/** Web route the Settings control calls to start the sign-in. */
export declare const LOGIN_ROUTE = "/muse-code/login";
/** Web route that reports whether the sign-in completed. */
Expand Down Expand Up @@ -84,6 +92,14 @@ type CredentialsFace = {
describeRecord(key: string): Promise<{
configured: boolean;
}>;
/** Stored reference state, used to avoid rewriting an unchanged key. */
resolve?(reference: string): Promise<{
value?: string;
source?: string;
} | undefined>;
set?(reference: string, value: string): Promise<void>;
unset?(reference: string): Promise<void>;
modifyRecord?(key: string, update: (current: CredentialRecord | undefined) => Promise<CredentialRecord | undefined>): Promise<unknown>;
};
type WebRoute = {
kind: "exact" | "prefix";
Expand Down Expand Up @@ -259,6 +275,56 @@ export declare function prettifyModelName(id: string): string;
* @returns the chat model ids, or null on any failure.
*/
export declare function fetchLiveModelIds(accessKey: string, timeoutMs?: number): Promise<string[] | null>;
/** Injected side effects for the credential bridge, overridable in tests. */
export type CredentialSyncOptions = {
logger?: Logger;
fetchImpl?: typeof fetch;
now?: () => number;
refreshWindowMs?: number;
};
/**
* Keep the route's credential reference equal to the OAuth record's minted key
* and re-mint that key before it expires.
*
* The route names `apiKeyEnv: MUSE_CODE_SUB_TOKEN` so DSH paints its credential
* dot; an explicit reference is also the highest-priority auth override, which
* means the plugin must keep it fresh itself. The OAuth record stays the source
* of truth (the shipped flow wrote it, and it carries the non-renewable identity
* token), so this only mirrors its `access` field and rotates it ahead of
* expiry through the pinned key-mint endpoint.
*
* @param credentials - the credential seam.
* @param options - injected side effects, overridable in tests.
* @returns which steps ran.
*/
export declare function syncCredentialState(credentials: CredentialsFace, options?: CredentialSyncOptions): Promise<{
refreshed: boolean;
mirrored: boolean;
}>;
/**
* Copy the record's minted inference key into the route's credential reference,
* or clear the reference when the record is gone (the row then shows red).
* @param credentials - the credential seam.
* @param options - injected side effects.
* @returns whether the stored reference now carries a key.
*/
export declare function mirrorCredentialReference(credentials: CredentialsFace, options?: CredentialSyncOptions): Promise<boolean>;
/**
* Re-mint the inference key from the record's identity token when the current
* key is close to expiry, and store it back into the record.
* @param credentials - the credential seam.
* @param options - injected side effects.
* @returns whether a fresh key was minted and stored.
*/
export declare function refreshCredentialRecord(credentials: CredentialsFace, options?: CredentialSyncOptions): Promise<boolean>;
/**
* Exchange the non-renewable Meta identity token for a fresh inference key.
* @param identityToken - the record's `refresh` field.
* @param fetchImpl - injectable fetch.
* @param timeoutMs - request bound.
* @returns the minted key, or undefined on any failure.
*/
export declare function mintInferenceKey(identityToken: string, fetchImpl?: typeof fetch, timeoutMs?: number): Promise<string | undefined>;
/**
* The page a plain navigation to {@link LOGIN_ROUTE} receives: the code, a link
* to Meta, and a poll of {@link STATUS_ROUTE} until the sign-in completes.
Expand Down
Loading
Loading