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
+15
View File
@@ -1,7 +1,22 @@
import { sdk } from '../sdk'
import { setApiKey } from './setApiKey'
import { setLicense } from './setLicense'
import { setAdminPassword } from './setAdminPassword'
import { setAnthropicApiKey } from './setAnthropicApiKey'
import { setOpenAIApiKey } from './setOpenAIApiKey'
import { setOpenAICompatible } from './setOpenAICompatible'
import { setOllamaUrl } from './setOllamaUrl'
import { setWhisperEndpoint } from './setWhisperEndpoint'
// NOTE: setRelayUrl was removed in 0.2.34. The relay base URL is now
// hardcoded in server/relay-default.js and updated via Recap version
// releases — end users should never see or configure it.
export const actions = sdk.Actions.of()
.addAction(setApiKey)
.addAction(setAnthropicApiKey)
.addAction(setOpenAIApiKey)
.addAction(setOpenAICompatible)
.addAction(setOllamaUrl)
.addAction(setWhisperEndpoint)
.addAction(setLicense)
.addAction(setAdminPassword)
+108
View File
@@ -0,0 +1,108 @@
import { sdk } from '../sdk'
import { configFile } from '../file-models/config.json'
import { randomBytes, scryptSync } from 'crypto'
const { InputSpec, Value } = sdk
const SCRYPT_KEYLEN = 64
const inputSpec = InputSpec.of({
recap_admin_username: Value.text({
name: 'Admin Username',
description:
'Username required at the login screen. Defaults to "admin".',
required: true,
default: 'admin',
minLength: 1,
maxLength: 64,
}),
recap_admin_password: Value.text({
name: 'Admin Password',
description:
'Password required at the login screen. Must be at least 8 characters. Leave blank to disable the login gate.',
required: false,
default: null,
masked: true,
minLength: 0,
maxLength: 256,
}),
recap_admin_password_confirm: Value.text({
name: 'Confirm Password',
description: 'Re-enter the password to confirm.',
required: false,
default: null,
masked: true,
minLength: 0,
maxLength: 256,
}),
})
export const setAdminPassword = sdk.Action.withInput(
'set-admin-password',
async ({ effects }) => ({
name: 'Set Admin Password',
description:
'Set a username and password that gate the Recap web UI. Anyone visiting the site (LAN or clearnet) must log in before reaching the activation screen. Leave the password blank to disable the gate.',
warning: null,
allowedStatuses: 'any',
group: 'Setup',
visibility: 'enabled',
}),
inputSpec,
async ({ effects }) => {
const config = await configFile.read().once()
return {
recap_admin_username: config?.recap_admin_username || 'admin',
recap_admin_password: undefined,
recap_admin_password_confirm: undefined,
}
},
async ({ effects, input }) => {
const username = (input.recap_admin_username || '').trim()
const password = input.recap_admin_password || ''
const confirm = input.recap_admin_password_confirm || ''
if (!username) {
throw new Error('Username is required.')
}
if (password === '' && confirm === '') {
// Disable the gate: clear hash + salt, keep username for next time.
await configFile.merge(effects, {
recap_admin_username: username,
recap_admin_password_hash: '',
recap_admin_password_salt: '',
})
return null
}
if (password !== confirm) {
throw new Error('Password and confirmation do not match.')
}
if (password.length < 8) {
throw new Error('Password must be at least 8 characters.')
}
const salt = randomBytes(16).toString('hex')
const hash = scryptSync(password, salt, SCRYPT_KEYLEN).toString('hex')
const existing = await configFile.read().once()
const sessionSecret =
existing?.recap_admin_session_secret && existing.recap_admin_session_secret.length > 0
? existing.recap_admin_session_secret
: randomBytes(32).toString('hex')
await configFile.merge(effects, {
recap_admin_username: username,
recap_admin_password_hash: hash,
recap_admin_password_salt: salt,
recap_admin_session_secret: sessionSecret,
})
return null
},
)
+45
View File
@@ -0,0 +1,45 @@
import { sdk } from '../sdk'
import { configFile } from '../file-models/config.json'
const { InputSpec, Value } = sdk
const inputSpec = InputSpec.of({
anthropic_api_key: Value.text({
name: 'Anthropic API Key',
description:
'Your Anthropic (Claude) API key. Get one at console.anthropic.com. Required to use Claude models for topic analysis.',
required: true,
default: null,
masked: true,
minLength: 1,
maxLength: 256,
}),
})
export const setAnthropicApiKey = sdk.Action.withInput(
'set-anthropic-api-key',
async ({ effects }) => ({
name: 'Set Anthropic API Key',
description:
'Configure your Anthropic (Claude) API key for topic analysis. Claude does not transcribe audio — pair it with Gemini or OpenAI Whisper for transcription.',
warning: null,
allowedStatuses: 'any',
group: 'AI Providers',
visibility: 'enabled',
}),
inputSpec,
async ({ effects }) => {
const config = await configFile.read().once()
return { anthropic_api_key: config?.anthropic_api_key || undefined }
},
async ({ effects, input }) => {
await configFile.merge(effects, {
anthropic_api_key: (input.anthropic_api_key || '').trim(),
})
return null
},
)
+1 -1
View File
@@ -25,7 +25,7 @@ export const setApiKey = sdk.Action.withInput(
'Configure your Google Gemini API key for transcription and analysis',
warning: null,
allowedStatuses: 'any',
group: null,
group: 'AI Providers',
visibility: 'enabled',
}),
+2 -2
View File
@@ -28,10 +28,10 @@ export const setLicense = sdk.Action.withInput(
async ({ effects }) => ({
name: 'Set Recap License',
description:
'Activate a Recap license to unlock paid features (saved library, channel & podcast subscriptions, auto-queue).',
'Activate a Recap license to unlock paid features (channel & podcast subscriptions, auto-queue, and a monthly allotment of relay credits).',
warning: null,
allowedStatuses: 'any',
group: null,
group: 'Setup',
visibility: 'enabled',
}),
+81
View File
@@ -0,0 +1,81 @@
import { sdk } from '../sdk'
import { configFile } from '../file-models/config.json'
const { InputSpec, Value } = sdk
// Standard Ollama port. Hardcoded because Ollama upstream uses 11434
// universally — its StartOS package preserves this. If a future
// release changes the port we can swap to a runtime
// sdk.serviceInterface.get(...) lookup against ollama's exposed
// interface, but for now hardcode + override-on-mismatch is simpler
// and avoids a guess at the interface ID.
const OLLAMA_DEFAULT_PORT = 11434
const inputSpec = InputSpec.of({
ollama_base_url: Value.text({
name: 'Ollama Base URL',
description:
'URL of your Ollama server. If you have the Ollama StartOS package installed on this server, this field is pre-populated automatically. Override only if you want to point at a different Ollama instance (e.g. on another machine: http://192.168.1.10:11434).',
required: false,
default: 'http://localhost:11434',
minLength: 0,
maxLength: 256,
patterns: [
{
regex: '^(https?://.+)?$',
description: 'Must be empty or start with http:// or https://',
},
],
}),
})
// Best-effort detection of an Ollama instance running on this same
// StartOS server. StartOS exposes every package on its own internal
// `<package-id>.startos` hostname, reachable from any other package's
// container without explicit networking config (per the Service
// Packaging docs). Returns the URL when ollama is installed, null
// otherwise.
async function detectStartOsOllamaUrl(effects: any): Promise<string | null> {
try {
const check = await sdk.checkDependencies(effects, ['ollama'])
if (!check.installedSatisfied('ollama')) return null
return `http://ollama.startos:${OLLAMA_DEFAULT_PORT}`
} catch {
return null
}
}
export const setOllamaUrl = sdk.Action.withInput(
'set-ollama-url',
async ({ effects }) => ({
name: 'Set Ollama Server URL',
description:
'Configure where to reach a local Ollama server for topic analysis. No API key required (Ollama runs locally). Does not transcribe audio. Auto-pre-populates if the Ollama StartOS package is installed on this server.',
warning: null,
allowedStatuses: 'any',
group: 'AI Providers',
visibility: 'enabled',
}),
inputSpec,
async ({ effects }) => {
const config = await configFile.read().once()
// If the user has already set a value, respect it — don't
// overwrite a manual override on every action open.
if (config?.ollama_base_url) {
return { ollama_base_url: config.ollama_base_url }
}
const auto = await detectStartOsOllamaUrl(effects)
if (auto) return { ollama_base_url: auto }
return { ollama_base_url: 'http://localhost:11434' }
},
async ({ effects, input }) => {
await configFile.merge(effects, {
ollama_base_url: (input.ollama_base_url || '').trim(),
})
return null
},
)
+45
View File
@@ -0,0 +1,45 @@
import { sdk } from '../sdk'
import { configFile } from '../file-models/config.json'
const { InputSpec, Value } = sdk
const inputSpec = InputSpec.of({
openai_api_key: Value.text({
name: 'OpenAI API Key',
description:
'Your OpenAI API key. Get one at platform.openai.com. Used for both topic analysis (GPT models) and audio transcription (Whisper).',
required: true,
default: null,
masked: true,
minLength: 1,
maxLength: 256,
}),
})
export const setOpenAIApiKey = sdk.Action.withInput(
'set-openai-api-key',
async ({ effects }) => ({
name: 'Set OpenAI API Key',
description:
'Configure your OpenAI API key. Enables GPT models for topic analysis and Whisper for audio transcription.',
warning: null,
allowedStatuses: 'any',
group: 'AI Providers',
visibility: 'enabled',
}),
inputSpec,
async ({ effects }) => {
const config = await configFile.read().once()
return { openai_api_key: config?.openai_api_key || undefined }
},
async ({ effects, input }) => {
await configFile.merge(effects, {
openai_api_key: (input.openai_api_key || '').trim(),
})
return null
},
)
+64
View File
@@ -0,0 +1,64 @@
import { sdk } from '../sdk'
import { configFile } from '../file-models/config.json'
const { InputSpec, Value } = sdk
const inputSpec = InputSpec.of({
openai_compatible_base_url: Value.text({
name: 'Base URL',
description:
'OpenAI-compatible API endpoint. Examples: https://api.deepseek.com/v1, https://api.together.xyz/v1, https://api.groq.com/openai/v1. Must include the /v1 (or equivalent) path segment.',
required: true,
default: null,
minLength: 1,
maxLength: 512,
patterns: [
{
regex: '^https?://.+',
description: 'Must start with http:// or https://',
},
],
}),
openai_compatible_api_key: Value.text({
name: 'API Key',
description:
'API key for the OpenAI-compatible backend. Some self-hosted backends accept any non-empty value — leave blank for those.',
required: false,
default: null,
masked: true,
minLength: 0,
maxLength: 256,
}),
})
export const setOpenAICompatible = sdk.Action.withInput(
'set-openai-compatible',
async ({ effects }) => ({
name: 'Set OpenAI-Compatible Backend',
description:
'Point Recap at any OpenAI-compatible chat-completions API: DeepSeek, Together, Groq, Fireworks, self-hosted vLLM, etc. Used for topic analysis only — does not transcribe audio.',
warning: null,
allowedStatuses: 'any',
group: 'AI Providers',
visibility: 'enabled',
}),
inputSpec,
async ({ effects }) => {
const config = await configFile.read().once()
return {
openai_compatible_base_url: config?.openai_compatible_base_url || undefined,
openai_compatible_api_key: config?.openai_compatible_api_key || undefined,
}
},
async ({ effects, input }) => {
await configFile.merge(effects, {
openai_compatible_base_url: (input.openai_compatible_base_url || '').trim(),
openai_compatible_api_key: (input.openai_compatible_api_key || '').trim(),
})
return null
},
)
+64
View File
@@ -0,0 +1,64 @@
import { sdk } from '../sdk'
import { configFile } from '../file-models/config.json'
const { InputSpec, Value } = sdk
const inputSpec = InputSpec.of({
whisper_base_url: Value.text({
name: 'Whisper Base URL',
description:
"URL of your Whisper-compatible transcription server. Example: http://whisper.startos:8000 for a local StartOS package, or http://192.168.1.10:9000 for whisper.cpp running on another machine on your LAN. The endpoint must implement OpenAI's /v1/audio/transcriptions wire format.",
required: true,
default: null,
minLength: 1,
maxLength: 512,
patterns: [
{
regex: '^https?://.+',
description: 'Must start with http:// or https://',
},
],
}),
whisper_api_key: Value.text({
name: 'API Key (optional)',
description:
'API key for the Whisper backend. Most self-hosted Whisper servers (whisper.cpp HTTP server, faster-whisper-server) accept any value or none at all — leave blank for those. Cloud Whisper providers (Groq, etc.) require a real key.',
required: false,
default: null,
masked: true,
minLength: 0,
maxLength: 256,
}),
})
export const setWhisperEndpoint = sdk.Action.withInput(
'set-whisper-endpoint',
async ({ effects }) => ({
name: 'Set Whisper Endpoint',
description:
'Point Recap at a self-hosted or third-party Whisper transcription server (whisper.cpp, faster-whisper-server, Groq, etc.). Free alternative to OpenAI Whisper API or Gemini multimodal transcription.',
warning: null,
allowedStatuses: 'any',
group: 'AI Providers',
visibility: 'enabled',
}),
inputSpec,
async ({ effects }) => {
const config = await configFile.read().once()
return {
whisper_base_url: config?.whisper_base_url || undefined,
whisper_api_key: config?.whisper_api_key || undefined,
}
},
async ({ effects, input }) => {
await configFile.merge(effects, {
whisper_base_url: (input.whisper_base_url || '').trim(),
whisper_api_key: (input.whisper_api_key || '').trim(),
})
return null
},
)
+7 -1
View File
@@ -1,6 +1,12 @@
import { sdk } from './sdk'
// Recap has no dependencies on other StartOS services.
// Recap declares Ollama as an OPTIONAL dependency in the manifest.
// We do not return it here because we don't want to enforce a runtime
// requirement on it — Recap runs fine using cloud providers
// (Gemini/Anthropic/OpenAI) when Ollama is not installed. The optional
// declaration in the manifest is what surfaces it as a suggested
// install on the Marketplace; this empty result keeps it from blocking
// startup.
export const setDependencies = sdk.setupDependencies(async ({ effects }) => {
return {}
})
+14
View File
@@ -11,6 +11,20 @@ export const configFile = FileHelper.json(
},
z.object({
gemini_api_key: z.string().default(''),
anthropic_api_key: z.string().default(''),
openai_api_key: z.string().default(''),
openai_compatible_base_url: z.string().default(''),
openai_compatible_api_key: z.string().default(''),
ollama_base_url: z.string().default(''),
whisper_base_url: z.string().default(''),
whisper_api_key: z.string().default(''),
// NOTE: relay_base_url was removed in 0.2.34. The relay endpoint
// is hardcoded in server/relay-default.js and updated via Recap
// version releases — never exposed to end users.
recap_license_key: z.string().default(''),
recap_admin_username: z.string().default(''),
recap_admin_password_hash: z.string().default(''),
recap_admin_password_salt: z.string().default(''),
recap_admin_session_secret: z.string().default(''),
}),
)
+20 -10
View File
@@ -1,22 +1,32 @@
export const short = {
en_US:
'Turn videos and podcasts into structured topic summaries with clickable timestamps.',
'Turn YouTube videos and podcasts into structured topic summaries with clickable timestamps. Free relay credits, bring-your-own API key, or self-host the AI.',
}
export const long = {
en_US:
'Recap downloads audio from YouTube videos and podcast RSS feeds, ' +
'transcribes them using Google Gemini, and produces structured topic-by-topic ' +
'summaries with timestamps. Features include channel and podcast subscriptions ' +
'with automatic new episode detection, a background processing queue with ' +
'configurable delays, auto-download per subscription, organized history with ' +
'folders, and a responsive web interface. ' +
'Requires a Google Gemini API key (free tier available at aistudio.google.com/apikey).',
'Recap downloads audio from YouTube videos and podcast RSS feeds, transcribes them, ' +
'and produces structured topic-by-topic summaries with clickable timestamps. ' +
'Pluggable AI provider system: pair any supported transcription provider with any ' +
'analysis provider per request. Supported: Google Gemini (multimodal — transcription + ' +
'analysis, with speaker labels), Anthropic Claude (analysis), OpenAI (GPT for analysis, ' +
'Whisper for transcription), OpenAI-compatible APIs (DeepSeek, Groq, Together, Fireworks, ' +
'vLLM, etc.), Ollama (local LLMs), and Whisper-compatible endpoints (whisper.cpp, ' +
'faster-whisper-server, NVIDIA Parakeet). ' +
'Free tier ships with a small allotment of relay credits so you can summarize a ' +
'few videos on day one without any setup. Bring your own API key or point at a ' +
'self-hosted model for unlimited use. ' +
'Paid tiers add channel and podcast subscriptions with automatic new-episode detection, ' +
'a background processing queue, auto-download per subscription, organized history with ' +
'folders, and a monthly allotment of relay credits.',
}
export const alertInstall = {
en_US:
'After installing, configure your Google Gemini API key using the "Set Gemini API Key" ' +
'action in the service menu. A free API key is available at aistudio.google.com/apikey. ' +
'After installing, the fastest path is to skip the activation screen and use your free ' +
'relay credits to summarize a few videos. ' +
'For unlimited use: either activate a Recap license (paid features + monthly relay ' +
'credits), or paste your own AI provider API key in Settings → API Keys & Endpoints. ' +
'Set an admin password via the "Set Admin Password" action if you want to gate access. ' +
'Note: The embedded YouTube player will not work if you are connected to a VPN.',
}
+13 -1
View File
@@ -31,5 +31,17 @@ export const manifest = setupManifest({
start: null,
stop: null,
},
dependencies: {},
dependencies: {
// Optional: enables the local-LLM analysis path. When installed on
// the same StartOS server, the "Set Ollama Server URL" action
// auto-pre-populates with the package's container IP + port, so
// users don't have to type anything. Recap works fine without it
// (cloud providers stay available).
ollama: {
description:
'Run local LLMs (Llama, Mistral, etc.) for topic analysis without a cloud API. Recap auto-detects the install and pre-fills its connection URL.',
optional: true,
s9pk: null,
},
},
})
+44 -2
View File
@@ -24,8 +24,50 @@ import { v_0_2_2 } from './v0.2.2'
import { v_0_2_3 } from './v0.2.3'
import { v_0_2_4 } from './v0.2.4'
import { v_0_2_5 } from './v0.2.5'
import { v_0_2_6 } from './v0.2.6'
import { v_0_2_7 } from './v0.2.7'
import { v_0_2_8 } from './v0.2.8'
import { v_0_2_9 } from './v0.2.9'
import { v_0_2_10 } from './v0.2.10'
import { v_0_2_11 } from './v0.2.11'
import { v_0_2_12 } from './v0.2.12'
import { v_0_2_13 } from './v0.2.13'
import { v_0_2_14 } from './v0.2.14'
import { v_0_2_15 } from './v0.2.15'
import { v_0_2_16 } from './v0.2.16'
import { v_0_2_17 } from './v0.2.17'
import { v_0_2_18 } from './v0.2.18'
import { v_0_2_19 } from './v0.2.19'
import { v_0_2_20 } from './v0.2.20'
import { v_0_2_21 } from './v0.2.21'
import { v_0_2_22 } from './v0.2.22'
import { v_0_2_23 } from './v0.2.23'
import { v_0_2_24 } from './v0.2.24'
import { v_0_2_25 } from './v0.2.25'
import { v_0_2_26 } from './v0.2.26'
import { v_0_2_27 } from './v0.2.27'
import { v_0_2_28 } from './v0.2.28'
import { v_0_2_29 } from './v0.2.29'
import { v_0_2_30 } from './v0.2.30'
import { v_0_2_31 } from './v0.2.31'
import { v_0_2_32 } from './v0.2.32'
import { v_0_2_33 } from './v0.2.33'
import { v_0_2_34 } from './v0.2.34'
import { v_0_2_35 } from './v0.2.35'
import { v_0_2_36 } from './v0.2.36'
import { v_0_2_37 } from './v0.2.37'
import { v_0_2_38 } from './v0.2.38'
import { v_0_2_39 } from './v0.2.39'
import { v_0_2_40 } from './v0.2.40'
import { v_0_2_41 } from './v0.2.41'
import { v_0_2_42 } from './v0.2.42'
import { v_0_2_43 } from './v0.2.43'
import { v_0_2_44 } from './v0.2.44'
import { v_0_2_45 } from './v0.2.45'
import { v_0_2_46 } from './v0.2.46'
import { v_0_2_47 } from './v0.2.47'
export const versionGraph = VersionGraph.of({
current: v_0_2_5,
other: [v_0_2_4, v_0_2_3, v_0_2_2, v_0_2_1, v_0_2_0, v_0_1_18, v_0_1_17, v_0_1_16, v_0_1_15, v_0_1_14, v_0_1_13, v_0_1_12, v_0_1_11, v_0_1_10, v_0_1_9, v_0_1_8, v_0_1_7, v_0_1_6, v_0_1_5, v_0_1_4, v_0_1_3, v_0_1_2, v_0_1_1, v_0_1_0],
current: v_0_2_47,
other: [v_0_2_46, v_0_2_45, v_0_2_44, v_0_2_43, v_0_2_42, v_0_2_41, v_0_2_40, v_0_2_39, v_0_2_38, v_0_2_37, v_0_2_36, v_0_2_35, v_0_2_34, v_0_2_33, v_0_2_32, v_0_2_31, v_0_2_30, v_0_2_29, v_0_2_28, v_0_2_27, v_0_2_26, v_0_2_25, v_0_2_24, v_0_2_23, v_0_2_22, v_0_2_21, v_0_2_20, v_0_2_19, v_0_2_18, v_0_2_17, v_0_2_16, v_0_2_15, v_0_2_14, v_0_2_13, v_0_2_12, v_0_2_11, v_0_2_10, v_0_2_9, v_0_2_8, v_0_2_7, v_0_2_6, v_0_2_5, v_0_2_4, v_0_2_3, v_0_2_2, v_0_2_1, v_0_2_0, v_0_1_18, v_0_1_17, v_0_1_16, v_0_1_15, v_0_1_14, v_0_1_13, v_0_1_12, v_0_1_11, v_0_1_10, v_0_1_9, v_0_1_8, v_0_1_7, v_0_1_6, v_0_1_5, v_0_1_4, v_0_1_3, v_0_1_2, v_0_1_1, v_0_1_0],
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_10 = VersionInfo.of({
version: '0.2.10:0',
releaseNotes: {
en_US: 'YouTube captions fast path: when YouTube provides captions for a video (manual or auto-generated), Recap uses them directly and skips the audio download + AI transcription entirely. Much faster, much cheaper. Falls back to AI transcription if captions are missing or unusable. Richer transcription prompt: when we do transcribe, Recap now feeds Gemini the channel name, video description, and YouTube chapter markers — dramatically improving speaker name extraction so transcripts say "Dax:" and "Kristen:" instead of "Host:" and "Guest:". Other 0.2.10 changes: Gemini 2.5 Flash added as a transcription fallback when Gemini 3 Flash gets overloaded; new Test button on each provider in Settings (sends a 3-word ping to confirm your API key works); Ollama and OpenAI-Compatible providers now have a Models field in Settings — type comma-separated model names there and they appear as dropdown options in the picker.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_11 = VersionInfo.of({
version: '0.2.11:0',
releaseNotes: {
en_US: 'YouTube captions toggle: new checkbox in Settings → AI Providers lets you turn off the captions fast-path and force a full AI transcription. Default is on (faster); switch off when you want speaker labels (captions don\'t have them) or higher-quality text. Whisper as a separate transcription provider: configure any OpenAI-Audio-Transcription-API-compatible endpoint (whisper.cpp, faster-whisper-server, Groq\'s Whisper, etc.) via the new "Set Whisper Endpoint" StartOS action — gives you a free local-or-self-hosted transcription option alongside Gemini and OpenAI cloud Whisper. Model fallback hint: the Analysis dropdown now notes that the chosen model falls back through the remaining list if it fails — already worked, now visible.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_12 = VersionInfo.of({
version: '0.2.12:0',
releaseNotes: {
en_US: 'Fixes two bugs from 0.2.11: (1) the in-flight banner\'s poll loop was doing a full re-render every 5 seconds, which was wiping the activity log + YouTube embed and causing the visible flashing — now updates just the elapsed counter in place, with a smooth 1-second tick. (2) Auto-generated YouTube captions are fragmented into 1-3-word entries every 1-3 seconds; for a 30-minute video that\'s 900+ segments, which overwhelmed the analyzer prompt. Captions now get coalesced into ~15-second chunks before analysis, dropping segment counts ~5x while keeping timestamps accurate. Should resolve the "Error in input stream" failures on caption-fast-path videos.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_13 = VersionInfo.of({
version: '0.2.13:0',
releaseNotes: {
en_US: 'Fixes the activity-log flashing bug from 0.2.12 (a stale transition check in the poll loop was triggering a full re-render every 5 seconds — now only re-renders on real presence/cancellation transitions). When processing errors out mid-stream, the YouTube embed now stays visible alongside the error so you can still watch the video and one-click retry. Server-side error logging now captures the full stack + cause + provider names so we can finally see what\'s behind generic messages like "Error in input stream".',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_14 = VersionInfo.of({
version: '0.2.14:0',
releaseNotes: {
en_US: 'Three fixes around the Whisper / Parakeet path: (1) the activity log now says "Whisper at <hostname> (<model>)" instead of the misleading "OpenAI Whisper" when a custom endpoint is configured. (2) The user-defined Models field in Settings now actually reaches the server — previously the frontend kept it client-side only, so the server fell back to whisper-1 in its fallback chain (and tried whisper-1 against your Parakeet wrapper, which doesn\'t have it). Fallback chain now respects your model list. (3) retryAPI now logs the full error body/cause/status to the StartOS service logs on every failed attempt, so generic "500 status code (no body)" failures finally surface what the upstream actually returned.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_15 = VersionInfo.of({
version: '0.2.15:0',
releaseNotes: {
en_US: 'Activity log now uses the actual model name and host for custom transcription endpoints — "Uploading audio to parakeet-tdt-0.6b-v3 at 192.168.1.87:8000…" — instead of misleadingly calling everything "Whisper".',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_16 = VersionInfo.of({
version: '0.2.16:0',
releaseNotes: {
en_US: 'Critical fix: the server has been silently crashing every time the analysis step threw an error, since 0.2.9. Root cause was a JavaScript scoping bug — a constant declared inside the try block was referenced from the catch block, putting it in the Temporal Dead Zone, so the catch handler crashed itself the moment any error reached it. dumb-init then restarted Recap, which is why "Error in input stream" kept appearing — that was the SSE connection being killed by the dying process, not the actual error. Fixed by hoisting the constant to handler scope. After updating, analysis errors will be handled gracefully (logged with full context, surfaced to the UI), not by crashing the daemon.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_17 = VersionInfo.of({
version: '0.2.17:0',
releaseNotes: {
en_US: 'Hotfix for 0.2.16: the captions fast-path was failing with "analysisModel is not defined" because transcriptionModel + analysisModel were still block-scoped inside the audio-download branch — same scoping flavor as the CANCELLED_MARK bug. Hoisted them to handler scope so the captions path (which skips the audio block) can read them. Captions-based summaries should now work cleanly with Gemini.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_18 = VersionInfo.of({
version: '0.2.18:0',
releaseNotes: {
en_US: 'UX cleanup in Settings → API Keys & Endpoints. No more screen flashing on every keystroke — input changes update state silently in the background, no re-render. Each provider section now has its own Save button that flashes a green "✓ Saved" pill for 2.5 seconds so you actually know it landed. Save click is also what refreshes the model dropdown above with any new model names you typed in the Models field — so the flow is: type, click Save, see your new model appear in the picker. Renamed "Whisper (custom endpoint)" to "OpenAI/Whisper-Compatible Endpoint" — clearer that it\'s the wire format (and works for Parakeet, whisper.cpp, etc.), not literally Whisper.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_19 = VersionInfo.of({
version: '0.2.19:0',
releaseNotes: {
en_US: 'Hotfix: Test buttons in Settings → API Keys & Endpoints were returning "license_required" for unlicensed users because /api/providers/* wasn\'t in the license-gate allowlist. Now anyone can test connectivity to their LLM providers (and the provider auto-discovery endpoint) without a license — necessary for trying the app before buying.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_20 = VersionInfo.of({
version: '0.2.20:0',
releaseNotes: {
en_US: 'Settings UX overhaul: provider sections in API Keys & Endpoints are now individually collapsible (click the chevron next to the provider name) — defaults expand only the active transcription + analysis providers plus any with saved data, so the panel stays scannable instead of being one giant wall. Save button is now surgical — it updates the button into a green ✓ Saved pill in place + refreshes only the affected model picker dropdowns up top, without re-rendering the whole settings panel (no more full-screen flash). Whisper/Parakeet endpoint hardening: when the rich transcription request (verbose_json + segment timestamps) fails with a 4xx/5xx, Recap now retries automatically with a bare request shape — handles wrappers that don\'t implement OpenAI\'s optional params. The retry\'s success is logged so you know which path worked.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_21 = VersionInfo.of({
version: '0.2.21:0',
releaseNotes: {
en_US: 'Switching transcription provider to Whisper-compatible (or any provider with a dynamic catalog like Ollama / OpenAI-compatible) now pre-fills the model dropdown from your saved Models field instead of showing the "model name" placeholder. Previously it only looked at the provider\'s hardcoded model list, which is empty for these custom-endpoint providers.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_22 = VersionInfo.of({
version: '0.2.22:0',
releaseNotes: {
en_US: 'Better feedback + faster recovery when Gemini analysis hits an overloaded model. Three changes: (1) the activity log now prints "Retrying analysis... (attempt 2/2)" when a retry actually starts, not just when the previous attempt fails — so the log doesn\'t look frozen while a retry is in flight. (2) Analysis HTTP timeout shortened from 15 minutes to 2 minutes — text analysis shouldn\'t take that long, and we want to move to the next fallback model fast when the chosen one is overloaded. (3) Per-model retry count for analysis dropped from 2 to 1 — model-overload 503s don\'t clear in 5 seconds, so the outer fallback chain (Pro → Pro older → Flash → Flash 2.5) walks through models quickly instead of double-retrying each one.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_23 = VersionInfo.of({
version: '0.2.23:0',
releaseNotes: {
en_US: 'Reverts the aggressive 2-minute analysis timeout from 0.2.22. Long transcripts can legitimately take 3-5+ minutes to analyze, and the 2-min cap was cutting off real work (showing "code:499 The operation was cancelled" — that was us cancelling, not Gemini failing). The actual time-wasting bug from earlier (sitting on an overloaded model for 4 minutes) was already fixed by 0.2.22\'s retries=1 change, which makes 503s fast-fail and the fallback chain walk to the next model in seconds. Analysis now gets the 15-minute SDK default — succeeds on slow-but-working calls, still moves on quickly when a model is truly overloaded.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_24 = VersionInfo.of({
version: '0.2.24:0',
releaseNotes: {
en_US: 'Parakeet (and any Whisper-API endpoint that returns plain text without per-segment timestamps) now produces usable summaries. Previously, the entire transcript landed as one entry at [0:00] — which (a) tripped the truncation detector ("covers 0:00 of 2:15") and triggered a wasted chunked-transcription retry, and (b) gave the analyzer one giant blob so it could only output a single topic. Now Recap detects the single-entry case and splits the transcript into synthetic sentence-based entries with interpolated timestamps. Sentence boundaries (. ! ?) drive the split; very short sentences ("Yeah.") get coalesced into neighbors; timestamps are distributed proportionally by character count across the audio duration. Result: a 2-minute Parakeet transcript becomes ~10-20 entries that the analyzer can carve into proper topic sections.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_25 = VersionInfo.of({
version: '0.2.25:0',
releaseNotes: {
en_US: 'Cancel button now actually interrupts in-flight provider API calls (AbortController wired through transcription + analysis), so cancellation is immediate instead of waiting for the next pipeline checkpoint.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_26 = VersionInfo.of({
version: '0.2.26:0',
releaseNotes: {
en_US: 'Open up /api/process/current and /api/process/cancel for unlicensed users so the in-flight banner clears after the pipeline finishes (previously the banner kept counting forever for free-tier users).',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_27 = VersionInfo.of({
version: '0.2.27:0',
releaseNotes: {
en_US: 'Activity log: each video\'s entries can now be collapsed via the chevron on the group header. After a browser refresh during an in-flight free-tier job, the activity log is repopulated from the server\'s buffered log entries instead of starting blank.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_28 = VersionInfo.of({
version: '0.2.28:0',
releaseNotes: {
en_US: 'Auto-coalesce transcript before analysis on long videos (>400 segments) so the prompt fits in local-model context windows. Transcript display keeps full granularity — only the analyzer sees the coarser view. Flagged to watch for analysis-quality regressions.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_29 = VersionInfo.of({
version: '0.2.29:0',
releaseNotes: {
en_US: 'Activity log now persists across browser refreshes via localStorage (capped at 2000 entries) and has a Clear button in the drawer header. Server-side log rehydrate merges in any in-flight job entries the client missed during disconnection.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_30 = VersionInfo.of({
version: '0.2.30:0',
releaseNotes: {
en_US: 'Gemini transcription picker now lists 3-flash-preview, 2.5-flash, and 2.0-flash. Server fallback chain walks the same order when a model is overloaded.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_31 = VersionInfo.of({
version: '0.2.31:0',
releaseNotes: {
en_US: 'YouTube captions toggle no longer triggers a full re-render — checkbox state was already correct, the render flashed the entire settings screen for no UI benefit.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_32 = VersionInfo.of({
version: '0.2.32:0',
releaseNotes: {
en_US: 'Persistent install-ID: each Recap install now mints a UUID on first boot (stored at /data/install-id) that the upcoming relay backend will use to track comped + paid credits. Surfaced in Settings → Install ID with a Copy button, and at /api/install-id for programmatic access.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_33 = VersionInfo.of({
version: '0.2.33:0',
releaseNotes: {
en_US: 'Relay scaffolding: new "Relay (comped credits)" provider in the picker, /api/relay/status endpoint, server-side credit cache, /data install-id auto-attached to relay calls, X-Recap-Job-Id pairs transcribe + analyze into one credit charge per summary, and a "Set Relay URL" StartOS action. Inactive until the operator sets the relay base URL.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_34 = VersionInfo.of({
version: '0.2.34:0',
releaseNotes: {
en_US: 'Relay endpoint is now hardcoded in server/relay-default.js and no longer user-configurable. Removed the "Set Relay URL" StartOS action and the relay_base_url config field — operator controls relay routing entirely through Recap version updates. End users see only credit balance + tier.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_35 = VersionInfo.of({
version: '0.2.35:0',
releaseNotes: {
en_US: 'Activation screen copy now accurately reflects free tier (library + 5 relay credits + unlimited BYO-key usage; no more "no library" line). Each provider in API Keys & Endpoints has a Delete button that clears credentials from BOTH localStorage AND the StartOS server config; appears only when there\'s a stored value to clear. Save + Delete hidden entirely for providers with no user-configurable fields (e.g. Relay).',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_36 = VersionInfo.of({
version: '0.2.36:0',
releaseNotes: {
en_US: 'Picker UI now shows a per-field "✓ Server-configured" hint under any empty input whose value has been set via the StartOS action — so you can tell at a glance that the key is wired up server-side without it being exposed. Delete button correctly appears whenever EITHER the browser OR the server has a stored value (previously hidden when only the server had it).',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_37 = VersionInfo.of({
version: '0.2.37:0',
releaseNotes: {
en_US: 'Relay balance banner now populates on first page load via a server-side ping to GET /relay/balance (returns credits + tier without charging). Short timeout — if the relay is unreachable the UI falls back to "balance unknown" instead of hanging.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_38 = VersionInfo.of({
version: '0.2.38:0',
releaseNotes: {
en_US: 'Activation copy updated: free tier now offers 10 relay credits (was 5). Pairs with recap-relay 0.2.3, which splits Core lifetime budget into 5 Gemini-served credits + 5 operator-hardware-served credits.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_39 = VersionInfo.of({
version: '0.2.39:0',
releaseNotes: {
en_US: 'StartOS actions are now grouped (Setup / AI Providers) for easier operator navigation. Setup contains Admin Password + Recap License together at the top; AI Providers contains all the per-provider key/endpoint actions.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_40 = VersionInfo.of({
version: '0.2.40:0',
releaseNotes: {
en_US: 'Updated the StartOS package description to reflect current state: multi-provider AI (Gemini, Claude, OpenAI, OpenAI-compatible, Ollama, Whisper/Parakeet, all mix-and-matchable per request), free relay credits on first install, BYO-key for unlimited use, paid-tier feature list aligned with the current subscriptions + auto-queue + monthly-credit model. Install alert rewritten to point new users at relay credits or BYO key rather than Gemini-only setup.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_41 = VersionInfo.of({
version: '0.2.41:0',
releaseNotes: {
en_US: 'Renamed the paid-tier entitlement from "core" to "pro" (with "max" recognized as a parallel). Resolves the naming collision where "Core" is the user-facing name for the free tier but "core" was the server-side flag for paid status. Pro licenses now ship pro + subscriptions + relay_pro; Max licenses ship max + subscriptions + relay_max. No installs in the wild had core-entitled licenses yet, so this is a clean rename — no compat alias needed.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_42 = VersionInfo.of({
version: '0.2.42:0',
releaseNotes: {
en_US: 'Fixes: 1) Relay status pings now surface errors visibly — when /api/relay/status fails to populate (e.g. install-id not initialized, network error), the failure is logged + recorded so the UI shows a real error instead of "balance unknown". 2) Clicking Test on the Relay provider no longer burns a credit — it now hits the relay\'s /balance endpoint and reports "Connected · Tier: X · N credits remaining". 3) Test click auto-expands the provider\'s section so the result is actually visible (previously the result rendered inside a collapsed body and was invisible). 4) Provider sections in API Keys & Endpoints now only auto-expand if the provider is currently selected for transcription or analysis — having saved credentials no longer keeps them sprawled open.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_43 = VersionInfo.of({
version: '0.2.43:0',
releaseNotes: {
en_US: 'Fix: "Unknown provider: relay" error in the relay status pill. resolveProviderOpts() checked PROVIDER_KEY_FIELDS for the provider name first and threw before reaching the relay-specific baseURL/install-id injection. Added an empty relay entry to PROVIDER_KEY_FIELDS so the lookup passes; the injection still happens at the bottom of the function as before.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_44 = VersionInfo.of({
version: '0.2.44:0',
releaseNotes: {
en_US: 'Fresh installs now default to the Relay provider for both transcription and analysis (was Gemini). Adds a one-click "↺ Use comped credits (reset to relay)" link in Settings → AI Providers that swaps both pickers back to relay without touching your saved keys — useful when you previously selected Whisper / Ollama / etc. and want to return to the out-of-the-box experience. Auto-detected Ollama is still surfaced for users who actively want it, just no longer the default.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_45 = VersionInfo.of({
version: '0.2.45:0',
releaseNotes: {
en_US: 'Fix: Summarize button was disabled when Relay was the selected provider but no Gemini key was entered. The submit-disabled check now correctly recognizes Relay (and any other provider with a valid config — including auto-detected Ollama and server-configured keys) instead of insisting on a Gemini key specifically.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_46 = VersionInfo.of({
version: '0.2.46:0',
releaseNotes: {
en_US: 'Removed the Install ID display from Settings. Showing it telegraphs the uninstall-and-reinstall workaround for resetting credits, and there\'s no real user-facing need to surface it. The underlying mechanism is unchanged — Recap still generates an install ID on first boot and sends it to the relay for credit accounting, just doesn\'t display it in the UI.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_47 = VersionInfo.of({
version: '0.2.47:0',
releaseNotes: {
en_US: 'Fix: Summarize button no longer stays disabled after pasting a URL with Relay selected. The previous check waited for the async /api/relay/status call to flip state.relayStatus.configured, which could race against user input. Since the relay URL is hardcoded into the build, the client can assume Relay is runnable the moment it\'s selected — actual reachability errors surface inline at submit time via SSE.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_6 = VersionInfo.of({
version: '0.2.6:0',
releaseNotes: {
en_US: 'Add admin login gate: a new "Set Admin Password" StartOS action puts a username/password screen in front of everything (including the activation screen), so the web UI is no longer wide-open on LAN or clearnet. Leave the password blank to disable the gate.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_7 = VersionInfo.of({
version: '0.2.7:0',
releaseNotes: {
en_US: 'Plumb pluggable AI providers: Claude, OpenAI + Whisper, OpenAI-compatible (DeepSeek/Together/Groq/etc.), and local Ollama join Gemini. Transcription and analysis can independently target any provider per request. New "Set <Provider> Key" StartOS actions for each. Ollama is an optional StartOS dependency — the action auto-pre-populates http://ollama.startos:11434 when Ollama is installed on the same server. Web UI still defaults to Gemini; per-provider picker comes next.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_8 = VersionInfo.of({
version: '0.2.8:0',
releaseNotes: {
en_US: 'Hotfix: Dockerfile now copies server/providers/ into the runtime image — the 0.2.7 build was missing this directory, causing the service to crash on startup with ERR_MODULE_NOT_FOUND. Also lands the new "AI Providers" picker UI in Settings (Transcription + Analysis provider/model dropdowns + per-provider API keys & endpoints) — old single-key flow still works.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+12
View File
@@ -0,0 +1,12 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_9 = VersionInfo.of({
version: '0.2.9:0',
releaseNotes: {
en_US: 'Tier restructure: library + saved history are now free for everyone (the app felt broken without them). The "Free mode" upgrade pitch now leads with auto-queue + clips + relay credits. Refresh-survives-processing: when a free-tier job is in flight, a status banner at the top of the app shows what\'s running + an elapsed-time counter + a Cancel button — even after closing the tab and reloading. Ollama auto-detect: the Settings panel pre-fills http://ollama.startos:11434 when the Ollama StartOS package is installed alongside Recap. Cleaner error messaging when a second free-tier job is submitted while one is in flight (shows what\'s processing). New "Watch on YouTube" link beneath the embed so videos with embedding-disabled (a per-channel YouTube setting) still have a one-click fallback.',
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})