@lamplitisles/codex-for-love 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":"1789529084274"}
1
+ {"version":"1789539874385"}
@@ -1,25 +1,25 @@
1
1
  <!doctype html>
2
2
  <html lang="en" data-theme="sticker-messenger">
3
- <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><script>(function(){try{var appearance=localStorage.getItem('lamplitisles.companion.appearance');var language=localStorage.getItem('lamplitisles.companion.language');var systemDark=matchMedia('(prefers-color-scheme: dark)').matches;var dark=appearance==='dark'||(appearance!=='light'&&systemDark);document.documentElement.dataset.theme=dark?'night-voyage':'sticker-messenger';document.documentElement.lang=language==='zh'?'zh-Hans':'en'}catch(_){var systemDark=matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.dataset.theme=systemDark?'night-voyage':'sticker-messenger'}})();</script><link href="/_app/immutable/entry/start.SkCiAosv.js" rel="modulepreload">
4
- <link href="/_app/immutable/chunks/C1lTIQVt.js" rel="modulepreload">
3
+ <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><script>(function(){try{var appearance=localStorage.getItem('lamplitisles.companion.appearance');var language=localStorage.getItem('lamplitisles.companion.language');var systemDark=matchMedia('(prefers-color-scheme: dark)').matches;var dark=appearance==='dark'||(appearance!=='light'&&systemDark);document.documentElement.dataset.theme=dark?'night-voyage':'sticker-messenger';document.documentElement.lang=language==='zh'?'zh-Hans':'en'}catch(_){var systemDark=matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.dataset.theme=systemDark?'night-voyage':'sticker-messenger'}})();</script><link href="/_app/immutable/entry/start.BmMe00-v.js" rel="modulepreload">
4
+ <link href="/_app/immutable/chunks/utqCDLTs.js" rel="modulepreload">
5
5
  <link href="/_app/immutable/chunks/DtqGwW3l.js" rel="modulepreload">
6
- <link href="/_app/immutable/entry/app.BP5g3Nlw.js" rel="modulepreload">
6
+ <link href="/_app/immutable/entry/app.BbmlpP2X.js" rel="modulepreload">
7
7
  <link href="/_app/immutable/chunks/xihTtKlq.js" rel="modulepreload">
8
- <link href="/_app/immutable/nodes/0.C1vi8tBo.js" rel="modulepreload">
8
+ <link href="/_app/immutable/nodes/0.Wk08-krq.js" rel="modulepreload">
9
9
 
10
- <link href="/_app/immutable/assets/0.CGGimnj2.css" rel="stylesheet"></head>
10
+ <link href="/_app/immutable/assets/0.D3rCUG9u.css" rel="stylesheet"></head>
11
11
  <body data-sveltekit-preload-data="hover"><div style="display: contents">
12
12
  <script>
13
13
  {
14
- __sveltekit_1na4qgh = {
14
+ __sveltekit_uz4ce0 = {
15
15
  base: ""
16
16
  };
17
17
 
18
18
  const element = document.currentScript.parentElement;
19
19
 
20
20
  Promise.all([
21
- import("/_app/immutable/entry/start.SkCiAosv.js"),
22
- import("/_app/immutable/entry/app.BP5g3Nlw.js")
21
+ import("/_app/immutable/entry/start.BmMe00-v.js"),
22
+ import("/_app/immutable/entry/app.BbmlpP2X.js")
23
23
  ]).then(([kit, app]) => {
24
24
  kit.start(app, element);
25
25
  });
@@ -21,11 +21,9 @@ model = "gpt-5.6-luna"
21
21
  # local_compaction = true
22
22
  # home = "/path/to/a/Codex-home" # omit to use the existing Codex login
23
23
 
24
- # Optional DashScope STT and tagged TTS; write the DashScope API key using `credential ... speech`.
25
- # [speech]
26
- # endpoint = "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
27
- # [speech.tts]
28
- # provider = "alibaba" # or "bytedance" (uses the separate `tts` credential)
29
- # endpoint = "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
30
- # model = "qwen3-tts-flash"
31
- # voice = "Cherry"
24
+ # Optional DashScope STT and tool-delivered Voice messages.
25
+ [speech]
26
+ [speech.tts]
27
+ provider = "minimax" # or "alibaba" (speech credential), "bytedance" (tts credential)
28
+ voice = "Chinese (Mandarin)_Soft_Girl"
29
+ # MiniMax requires the operator-installed, authenticated `mmx` CLI. No MiniMax secret belongs in this file.
@@ -16,6 +16,8 @@ import { createConnection, isIP } from "net";
16
16
  import * as fs from "node:fs";
17
17
  import { constants, createReadStream } from "node:fs";
18
18
  import { DatabaseSync } from "node:sqlite";
19
+ import { execFile } from "node:child_process";
20
+ import { promisify } from "node:util";
19
21
  import { createServer } from "node:http";
20
22
  import * as qs from "node:querystring";
21
23
  //#region \0rolldown/runtime.js
@@ -5299,7 +5301,7 @@ const parseAsync = /* @__PURE__ */ _parseAsync(ZodRealError);
5299
5301
  const safeParse = /* @__PURE__ */ _safeParse(ZodRealError);
5300
5302
  const safeParseAsync = /* @__PURE__ */ _safeParseAsync(ZodRealError);
5301
5303
  const encode = /* @__PURE__ */ _encode(ZodRealError);
5302
- const decode$1 = /* @__PURE__ */ _decode(ZodRealError);
5304
+ const decode = /* @__PURE__ */ _decode(ZodRealError);
5303
5305
  const encodeAsync = /* @__PURE__ */ _encodeAsync(ZodRealError);
5304
5306
  const decodeAsync = /* @__PURE__ */ _decodeAsync(ZodRealError);
5305
5307
  const safeEncode = /* @__PURE__ */ _safeEncode(ZodRealError);
@@ -5453,7 +5455,7 @@ const ZodType = /*@__PURE__*/ $constructor("ZodType", (inst, def) => {
5453
5455
  return encode(this, data, params, { callee: _encode });
5454
5456
  },
5455
5457
  decode: function _decode(data, params) {
5456
- return decode$1(this, data, params, { callee: _decode });
5458
+ return decode(this, data, params, { callee: _decode });
5457
5459
  },
5458
5460
  encodeAsync: async function _encodeAsync(data, params) {
5459
5461
  return await encodeAsync(this, data, params, { callee: _encodeAsync });
@@ -6196,10 +6198,12 @@ const schema = object({
6196
6198
  speech: object({
6197
6199
  endpoint: url().default("https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"),
6198
6200
  tts: object({
6199
- provider: _enum(["alibaba", "bytedance"]).default("alibaba"),
6200
- endpoint: url().default("https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"),
6201
- model: string().min(1).default("qwen3-tts-flash"),
6202
- voice: string().min(1).default("Maia")
6201
+ provider: _enum([
6202
+ "minimax",
6203
+ "alibaba",
6204
+ "bytedance"
6205
+ ]),
6206
+ voice: string().min(1)
6203
6207
  }).strict().optional()
6204
6208
  }).strict().optional()
6205
6209
  }).strict();
@@ -6208,6 +6212,7 @@ async function loadConfig(path) {
6208
6212
  const base = dirname(resolve(path));
6209
6213
  return {
6210
6214
  ...config,
6215
+ configPath: resolve(path),
6211
6216
  state: resolve(base, config.state),
6212
6217
  persona: resolve(base, config.persona),
6213
6218
  workspace: resolve(base, config.workspace ?? `${config.state}/workspace`),
@@ -6222,7 +6227,7 @@ async function loadConfig(path) {
6222
6227
  }
6223
6228
  };
6224
6229
  }
6225
- /** Only the optional STT adapter has an application-managed secret. */
6230
+ /** Alibaba STT/TTS reuses speech; ByteDance TTS keeps its separate secret. */
6226
6231
  const credentialSchema = object({
6227
6232
  speech: string().min(1).optional(),
6228
6233
  tts: string().min(1).optional()
@@ -62651,12 +62656,16 @@ function sameValue(left, right) {
62651
62656
  return JSON.stringify(left) === JSON.stringify(right);
62652
62657
  }
62653
62658
  /** Keep only CFL's installation-specific Companion endpoint current. */
62654
- function ensureCompanionMcp(config, workspace) {
62659
+ function ensureCompanionMcp(config, workspace, partnerConfigPath) {
62655
62660
  const servers = isRecord$1(config.mcp_servers) ? config.mcp_servers : {};
62656
62661
  let changed = !isRecord$1(config.mcp_servers);
62657
62662
  const required = {
62658
62663
  command: process.execPath,
62659
- args: [fileURLToPath(new URL(`./${companionMcpFileName}`, import.meta.url)), resolve(workspace)]
62664
+ args: [
62665
+ fileURLToPath(new URL(`./${companionMcpFileName}`, import.meta.url)),
62666
+ resolve(workspace),
62667
+ ...partnerConfigPath ? [resolve(partnerConfigPath)] : []
62668
+ ]
62660
62669
  };
62661
62670
  const companion = isRecord$1(servers.companion) ? servers.companion : {};
62662
62671
  for (const [key, value] of Object.entries(required)) if (!sameValue(companion[key], value)) {
@@ -62671,7 +62680,7 @@ function ensureCompanionMcp(config, workspace) {
62671
62680
  return changed;
62672
62681
  }
62673
62682
  /** Add project-owned hooks and the bundled Companion MCP while preserving config. */
62674
- async function ensureHookDeclaration(workspace, contextPath) {
62683
+ async function ensureHookDeclaration(workspace, contextPath, partnerConfigPath) {
62675
62684
  const configPath = join(resolve(workspace), ".codex", "config.toml");
62676
62685
  const command = hookCommand(contextPath);
62677
62686
  await mkdir(dirname(configPath), {
@@ -62688,7 +62697,7 @@ async function ensureHookDeclaration(workspace, contextPath) {
62688
62697
  }
62689
62698
  const hooks = isRecord$1(config.hooks) ? config.hooks : {};
62690
62699
  const sessionStart = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
62691
- let changed = ensureCompanionMcp(config, workspace);
62700
+ let changed = ensureCompanionMcp(config, workspace, partnerConfigPath);
62692
62701
  let own = false;
62693
62702
  const normalizedGroups = sessionStart.map((group) => {
62694
62703
  if (!isRecord$1(group) || !Array.isArray(group.hooks)) return group;
@@ -64892,140 +64901,8 @@ function normalizeVoiceExpression(value) {
64892
64901
  if (!isVoiceExpression(value)) return void 0;
64893
64902
  return value.trim().toLowerCase();
64894
64903
  }
64895
- //#endregion
64896
- //#region apps/partner/runtime/speech.ts
64897
- const MAX_AUDIO_BYTES = 10485760;
64898
- const MAX_RESPONSE_BYTES = Math.ceil(MAX_AUDIO_BYTES / 3) * 4 + 65536;
64899
- const pending = /* @__PURE__ */ new Map();
64900
- const BTD_ENDPOINT = "https://openspeech.bytedance.com/api/v3/tts/unidirectional/sse";
64901
- function decode(value) {
64902
- const source = value.replace(/^data:[^;,]+;base64,/u, "");
64903
- if (!/^[A-Za-z0-9+/]*={0,2}$/u.test(source) || source.length % 4 === 1) return;
64904
- return new Uint8Array(Buffer.from(source, "base64"));
64905
- }
64906
- async function bounded$1(response, limit) {
64907
- if (!response.body) throw new Error("Missing provider response");
64908
- const parts = [];
64909
- let total = 0;
64910
- for await (const part of response.body) {
64911
- total += part.byteLength;
64912
- if (total > limit) throw new Error("Provider response too large");
64913
- parts.push(part);
64914
- }
64915
- return Buffer.concat(parts);
64916
- }
64917
- async function provider(options) {
64918
- const init = options.provider === "alibaba" ? {
64919
- method: "POST",
64920
- signal: options.signal,
64921
- headers: {
64922
- authorization: `Bearer ${options.credential}`,
64923
- "content-type": "application/json"
64924
- },
64925
- body: JSON.stringify({
64926
- model: options.model,
64927
- input: {
64928
- text: options.text,
64929
- voice: options.voice,
64930
- language_type: "Chinese"
64931
- },
64932
- parameters: { format: "mp3" },
64933
- stream: false
64934
- })
64935
- } : {
64936
- method: "POST",
64937
- signal: options.signal,
64938
- headers: {
64939
- accept: "text/event-stream",
64940
- "content-type": "application/json",
64941
- "X-Api-Key": options.credential,
64942
- "X-Api-Resource-Id": options.model
64943
- },
64944
- body: JSON.stringify({
64945
- user: { uid: "dsh-speech" },
64946
- req_params: {
64947
- text: options.text,
64948
- speaker: options.voice,
64949
- audio_params: {
64950
- format: "mp3",
64951
- sample_rate: 24e3
64952
- }
64953
- }
64954
- })
64955
- };
64956
- const response = await options.fetchImpl(options.provider === "bytedance" ? BTD_ENDPOINT : options.endpoint, init);
64957
- if (!response.ok) throw new Error("Speech provider rejected the request");
64958
- const raw = await bounded$1(response, MAX_RESPONSE_BYTES);
64959
- if (options.provider === "alibaba") {
64960
- const audio = JSON.parse(new TextDecoder().decode(raw))?.output?.audio;
64961
- let bytes = audio?.data ? decode(audio.data) : void 0;
64962
- if (!bytes && audio?.url) {
64963
- const r = await options.fetchImpl(audio.url, { signal: options.signal });
64964
- if (!r.ok) throw new Error("Invalid provider audio");
64965
- bytes = await bounded$1(r, MAX_AUDIO_BYTES);
64966
- }
64967
- if (!bytes?.byteLength || bytes.byteLength > MAX_AUDIO_BYTES) throw new Error("Invalid provider audio");
64968
- return bytes;
64969
- }
64970
- const chunks = [];
64971
- let total = 0;
64972
- for (const line of new TextDecoder().decode(raw).split(/\r?\n/u)) {
64973
- if (!line.trim() || line.startsWith("event:")) continue;
64974
- if (!line.startsWith("data:")) throw new Error("Invalid provider response");
64975
- const frame = JSON.parse(line.slice(5));
64976
- if (frame.code === 0 && frame.data) {
64977
- const b = decode(frame.data);
64978
- if (!b || (total += b.byteLength) > MAX_AUDIO_BYTES) throw new Error("Invalid provider audio");
64979
- chunks.push(b);
64980
- } else if (frame.code !== 0 && frame.code !== 2e7) throw new Error("Speech provider rejected the request");
64981
- }
64982
- const bytes = Buffer.concat(chunks);
64983
- if (!bytes.byteLength) throw new Error("Invalid provider audio");
64984
- return bytes;
64985
- }
64986
- async function synthesizeSpeech(options) {
64987
- const text = options.text.replace(/[\s\u00a0]+/gu, " ").trim();
64988
- if (!text || Array.from(text).length > 240) throw new Error("Invalid speech passage");
64989
- const key = createHash("sha256").update(JSON.stringify([
64990
- 2,
64991
- options.provider,
64992
- options.model,
64993
- options.voice,
64994
- text
64995
- ])).digest("hex");
64996
- const path = join(options.audioDir, `${key}.mp3`);
64997
- try {
64998
- if ((await readFile(path)).byteLength) return key;
64999
- } catch {}
65000
- if (pending.has(path)) return pending.get(path);
65001
- const task = (async () => {
65002
- const signal = AbortSignal.any([options.signal ?? new AbortController().signal, AbortSignal.timeout(6e4)]);
65003
- const bytes = await provider({
65004
- ...options,
65005
- text,
65006
- signal,
65007
- fetchImpl: options.fetchImpl ?? fetch
65008
- });
65009
- await mkdir(options.audioDir, { recursive: true });
65010
- const temp = join(options.audioDir, `.${randomUUID()}.tmp`);
65011
- try {
65012
- await writeFile(temp, bytes, {
65013
- mode: 384,
65014
- flag: "wx"
65015
- });
65016
- await rename(temp, path);
65017
- } finally {
65018
- await unlink(temp).catch(() => void 0);
65019
- }
65020
- return key;
65021
- })();
65022
- pending.set(path, task);
65023
- try {
65024
- return await task;
65025
- } finally {
65026
- if (pending.get(path) === task) pending.delete(path);
65027
- }
65028
- }
64904
+ Math.ceil(10485760 / 3) * 4 + 65536;
64905
+ promisify(execFile);
65029
64906
  /** Qwen short-audio STT protocol adapted from dsh-speech; no synthesis path. */
65030
64907
  async function transcribeAudio(endpoint, credential, data, mediaType, signal, fetchImpl = fetch) {
65031
64908
  const normalized = normalizeVoiceMediaType(mediaType);
@@ -65169,7 +65046,7 @@ function visibleHistoryPage(messages, results, before) {
65169
65046
  for (; index >= 0; index -= 1) {
65170
65047
  const message = candidates[index];
65171
65048
  const result = resultByOwner.get(message.id);
65172
- const units = 1 + (result ? result.answers.length + result.generatedIds.length + (result.error || [
65049
+ const units = 1 + (result ? result.answers.length + result.generatedIds.length + result.voiceIds.length + (result.error || [
65173
65050
  "failed",
65174
65051
  "interrupted",
65175
65052
  "cancelled"
@@ -65256,6 +65133,22 @@ function answersFromTurn(turn) {
65256
65133
  if (!turn?.items) return [];
65257
65134
  return turn.items.filter((item) => item.type === "agentMessage" && (item.phase === void 0 || item.phase === null || item.phase === "final_answer" || item.phase === "finalAnswer")).map((item) => item.text).filter((value) => typeof value === "string" && value.trim().length > 0);
65258
65135
  }
65136
+ function voiceIdsFromTurn(turn) {
65137
+ const ids = /* @__PURE__ */ new Set();
65138
+ const visit = (value) => {
65139
+ if (typeof value === "string") try {
65140
+ visit(JSON.parse(value));
65141
+ } catch {}
65142
+ else if (Array.isArray(value)) value.forEach(visit);
65143
+ else if (value && typeof value === "object") {
65144
+ const entry = value;
65145
+ if (entry.kind === "voice" && typeof entry.audioId === "string" && /^[a-f0-9]{64}$/u.test(entry.audioId)) ids.add(entry.audioId);
65146
+ Object.values(entry).forEach(visit);
65147
+ }
65148
+ };
65149
+ for (const item of turn?.items ?? []) if (item.type === "mcpToolCall" && item.status === "completed" && item.server === "companion" && item.tool === "send_voice") visit(item.result);
65150
+ return [...ids];
65151
+ }
65259
65152
  function errorFromTurn(turn) {
65260
65153
  if (!turn?.error) return null;
65261
65154
  const error = record(turn.error);
@@ -65382,7 +65275,8 @@ function versionFromUserAgent(value) {
65382
65275
  * ordinary resume safe without a second model journal.
65383
65276
  */
65384
65277
  async function createPartner(config, credentials, dependencies = {}) {
65385
- if (config.speech && !credentials.speech) throw new Error("Configured speech requires a CLI-managed speech credential");
65278
+ if (config.speech?.tts?.provider === "alibaba" && !credentials.speech) throw new Error("Alibaba Voice requires a CLI-managed speech credential");
65279
+ if (config.speech?.tts?.provider === "bytedance" && !credentials.tts) throw new Error("ByteDance Voice requires a CLI-managed tts credential");
65386
65280
  const persona = await readFile(config.persona, "utf8");
65387
65281
  if (!persona.trim()) throw new Error("Persona must not be empty");
65388
65282
  const paths = partnerPaths(config.workspace);
@@ -65752,6 +65646,7 @@ async function createPartner(config, credentials, dependencies = {}) {
65752
65646
  const status = turnStatus(turn);
65753
65647
  if (status === "completed") completedOperationId = sourceIds.at(-1) ?? completedOperationId;
65754
65648
  const generatedIds = [];
65649
+ const voiceIds = voiceIdsFromTurn(turn);
65755
65650
  let resultError = errorFromTurn(turn);
65756
65651
  let resultStatus = status;
65757
65652
  for (const item of turn.items ?? []) {
@@ -65769,7 +65664,8 @@ async function createPartner(config, credentials, dependencies = {}) {
65769
65664
  answers: answersFromTurn(turn),
65770
65665
  error: resultError,
65771
65666
  status: resultStatus,
65772
- generatedIds
65667
+ generatedIds,
65668
+ voiceIds
65773
65669
  };
65774
65670
  const fingerprint = JSON.stringify(resultBody);
65775
65671
  const previous = presentationFingerprints.get(turn.id);
@@ -66097,7 +65993,9 @@ async function createPartner(config, credentials, dependencies = {}) {
66097
65993
  if (turnId && [
66098
65994
  "userMessage",
66099
65995
  "agentMessage",
66100
- "imageGeneration"
65996
+ "imageGeneration",
65997
+ "mcpToolCall",
65998
+ "mcpToolResult"
66101
65999
  ].includes(typeof item.type === "string" ? item.type : "")) {
66102
66000
  const turn = await reconcileTurn(turnId, void 0, typeof item.id !== "string" || typeof item.type !== "string");
66103
66001
  if (turn && userItems(turn).length) await projectTurn(turn);
@@ -66183,7 +66081,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66183
66081
  ...config.codex.home ? { CODEX_HOME: config.codex.home } : {},
66184
66082
  ...injected.env ?? {}
66185
66083
  };
66186
- const hook = await ensureHookDeclaration(paths.workspaceRoot, contextFile);
66084
+ const hook = await ensureHookDeclaration(paths.workspaceRoot, contextFile, config.configPath);
66187
66085
  let lastReportedSdkError;
66188
66086
  const reportAppServerError = (error) => {
66189
66087
  if (closing) return;
@@ -66331,18 +66229,6 @@ async function createPartner(config, credentials, dependencies = {}) {
66331
66229
  throw error;
66332
66230
  }
66333
66231
  },
66334
- async synthesize(text, signal) {
66335
- const tts = config.speech?.tts;
66336
- const credential = tts?.provider === "alibaba" ? credentials.speech : credentials.tts;
66337
- if (!tts || !credential) throw new Error("Speech synthesis is unavailable");
66338
- return synthesizeSpeech({
66339
- ...tts,
66340
- credential,
66341
- text,
66342
- audioDir: paths.audio,
66343
- signal
66344
- });
66345
- },
66346
66232
  async audio(id) {
66347
66233
  try {
66348
66234
  return await readFile(join(paths.audio, `${id}.mp3`));
@@ -66408,6 +66294,10 @@ async function createPartner(config, credentials, dependencies = {}) {
66408
66294
  id,
66409
66295
  name,
66410
66296
  url: `/api/images/${id}`
66297
+ })),
66298
+ voices: result.voiceIds.map((id) => ({
66299
+ id,
66300
+ url: `/api/audio/${id}.mp3`
66411
66301
  }))
66412
66302
  }));
66413
66303
  const unresolved = await unresolvedMessages();
@@ -67241,7 +67131,7 @@ function json(response, value, status = 200) {
67241
67131
  });
67242
67132
  response.end(JSON.stringify(value));
67243
67133
  }
67244
- function createWebServer(partner, assets) {
67134
+ function createWebServer(partner, assets, options = {}) {
67245
67135
  const serve = build_default(assets, {
67246
67136
  single: true,
67247
67137
  setHeaders(response, pathname) {
@@ -67323,12 +67213,6 @@ function createWebServer(partner, assets) {
67323
67213
  response.end(image.data);
67324
67214
  return;
67325
67215
  }
67326
- if (path === "/api/voice/synthesize" && request.method === "POST") {
67327
- const parsed = object({ text: string().trim().min(1).max(240) }).strict().parse(await body(request));
67328
- const abort = new AbortController();
67329
- response.once("close", () => abort.abort());
67330
- return json(response, { url: `/api/audio/${await partner.synthesize(parsed.text, abort.signal)}.mp3` });
67331
- }
67332
67216
  if (path.startsWith("/api/audio/") && request.method === "GET") {
67333
67217
  const id = string().regex(/^[a-f0-9]{64}\.mp3$/u).parse(path.slice(11));
67334
67218
  const audio = await partner.audio(id.slice(0, -4));
@@ -67386,7 +67270,7 @@ function createWebServer(partner, assets) {
67386
67270
  if (!response.write("data: changed\n\n")) response.end();
67387
67271
  };
67388
67272
  const off = partner.subscribe(invalidate);
67389
- const heartbeat = setInterval(() => response.write(": keepalive\n\n"), 15e3);
67273
+ const heartbeat = setInterval(() => response.write("event: heartbeat\ndata: live\n\n"), options.heartbeatMs ?? 15e3);
67390
67274
  invalidate();
67391
67275
  response.once("close", () => {
67392
67276
  off();