Pluggable AI providers, relay credit system, picker UX overhaul

Captures roughly forty version bumps (v0.2.6 → v0.2.47) of work that
accumulated without commits.

- Pluggable provider system under server/providers/: gemini, anthropic,
  openai, openai-compatible, ollama, whisper-compatible, relay. Mix and
  match transcription + analysis per request via the picker UI.
- Relay backend integration. Hardcoded relay URL in server/relay-default.js
  (operator-controlled at build time, not user-configurable). New
  /api/relay/{status,policy} endpoints proxy to the relay; balance pings
  populate a cached credit display.
- Per-install identity in server/install-id.js for relay credit accounting.
  Sent to the relay as X-Recap-Install-Id; persists across upgrades, lost
  on a full uninstall + reinstall. Not surfaced in the UI.
- Admin login gate (server/admin-auth.js + setAdminPassword action). Scrypt
  password hash + HMAC-signed session cookie.
- Entitlement scheme rename: pro / max (each paired with subscriptions and
  relay_pro / relay_max), replacing the misleading "core" entitlement
  that conflicted with the user-facing "Core" tier name.
- Activation screen: dynamic credit count pulled from /api/relay/policy,
  "Skip — use free mode" button, accurate paid-feature list.
- Top toolbar: inline credit-balance pill (or "BYO configured" fallback),
  Upgrade + "I have a key" buttons.
- Picker UI: per-provider sections with Save/Test/Delete buttons, sections
  collapsible by chevron, default-collapsed unless currently selected,
  "Use comped credits (reset to relay)" link when the user has strayed,
  green hint under inputs whose values are server-configured.
- Activity log: chevron-collapsible groups per video, refresh-survival via
  localStorage + a 500-entry server-side buffer, explicit Clear button.
- YouTube captions fast-path with user toggle (skips audio download + AI
  transcription when captions are available — uncheck for speaker labels).
- Cancel button: AbortController plumbed through every provider SDK call;
  retryAPI short-circuits on AbortError; cancellation events surface in
  the activity log instead of silent retries.
- Long-video analysis: auto-coalesce transcript entries before building the
  analysis prompt so local-model context windows (32k-ish) don't overflow.
  Original entries preserved for transcript display via an index map; the
  analyzer sees a coarser view but click-to-seek timestamps stay precise.
- StartOS action grouping (Setup / AI Providers) so the actions list is
  navigable.
- Manifest description rewritten to reflect multi-provider support and
  free-tier relay credits.
- Smaller fixes: summarize-button enablement no longer requires a Gemini
  key when other providers are configured; analysis fallback chain handles
  context-length and 503 capacity errors; single-segment expansion for
  providers that don't return per-segment timestamps (Parakeet et al.);
  many other UX polish items.
This commit is contained in:
Keysat
2026-05-11 23:46:20 -05:00
parent 2544cf7dde
commit 373d10595b
79 changed files with 6322 additions and 397 deletions
+44
View File
@@ -89,3 +89,47 @@ export function resolveApiKey(clientKey) {
export function getEnvPath() {
return envPath;
}
// Snapshot of the full StartOS config blob — keys for every provider
// (gemini, anthropic, openai, openai-compatible, ollama) plus the
// admin-auth fields. Each request reads it once and passes it into
// resolveProviderOpts() per provider. Returns {} if the file doesn't
// exist or is unreadable.
export async function getConfigSnapshot() {
try {
const content = await fs.readFile(startosConfigPath, "utf-8");
return JSON.parse(content) || {};
} catch {
return {};
}
}
// Patch the StartOS config file in place. Reads current, merges in the
// given fields, writes atomically (tmp + rename). Used by the picker
// UI's Delete button to clear server-side credentials for a provider.
// The next config poll picks up the changes within CONFIG_POLL_MS;
// resolveProviderOpts already reads getConfigSnapshot per-request, so
// effectively the change is immediate.
//
// `patch` is a plain object of { config_field: value } pairs.
// Pass empty strings to clear a field rather than deleting the key —
// the StartOS schema declares every field with a default of '', so
// empty string is the canonical "unset" representation.
export async function mergeConfig(patch) {
if (!patch || typeof patch !== "object") return;
let current = {};
try {
const content = await fs.readFile(startosConfigPath, "utf-8");
current = JSON.parse(content) || {};
} catch {}
const merged = { ...current, ...patch };
const tmp = startosConfigPath + ".tmp";
await fs.mkdir(path.dirname(startosConfigPath), { recursive: true });
await fs.writeFile(tmp, JSON.stringify(merged, null, 2), { mode: 0o600 });
await fs.rename(tmp, startosConfigPath);
// Re-run the gemini-key refresher so serverApiKey reflects the
// patch immediately (otherwise it'd lag until the poll tick).
if (Object.prototype.hasOwnProperty.call(patch, "gemini_api_key")) {
await refreshServerApiKey("merge config");
}
}