privateboard 0.1.16 → 0.1.18

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.
Files changed (45) hide show
  1. package/dist/boot.d.ts +63 -0
  2. package/dist/boot.js +23542 -0
  3. package/dist/boot.js.map +1 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +295 -70
  6. package/dist/cli.js.map +1 -1
  7. package/dist/server.d.ts +15 -0
  8. package/dist/server.js +22652 -0
  9. package/dist/server.js.map +1 -0
  10. package/dist/version.d.ts +17 -0
  11. package/dist/version.js +8 -0
  12. package/dist/version.js.map +1 -0
  13. package/electron-entry.cjs +80 -0
  14. package/package.json +55 -6
  15. package/public/agent-build-bgm.js +259 -194
  16. package/public/agent-overlay.js +49 -1
  17. package/public/agent-profile.css +59 -10
  18. package/public/agent-profile.js +175 -16
  19. package/public/app.js +1139 -261
  20. package/public/avatar-skill.js +520 -681
  21. package/public/avatars/chair.svg +1 -1
  22. package/public/avatars/first-principles.svg +1 -1
  23. package/public/avatars/historian.svg +1 -1
  24. package/public/avatars/long-horizon.svg +1 -1
  25. package/public/avatars/phenomenologist.svg +1 -1
  26. package/public/avatars/socrates.svg +1 -1
  27. package/public/avatars/user-empathy.svg +1 -1
  28. package/public/avatars/value-investor.svg +1 -1
  29. package/public/home.html +44 -6
  30. package/public/i18n.js +110 -16
  31. package/public/icons/logo.png +0 -0
  32. package/public/icons/logo.svg +10 -0
  33. package/public/icons.css +25 -4
  34. package/public/index.html +1220 -333
  35. package/public/onboarding.css +2 -3
  36. package/public/onboarding.js +2 -2
  37. package/public/ppt.html +99 -4
  38. package/public/quote-cta.css +2 -2
  39. package/public/room-settings.css +159 -11
  40. package/public/themes.css +93 -268
  41. package/public/user-settings.css +30 -69
  42. package/public/user-settings.js +92 -88
  43. package/public/voice-replay.js +37 -0
  44. package/public/icons/logo2.png +0 -0
  45. package/public/icons/private-board-vi.html +0 -1716
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/cli.js CHANGED
@@ -60,7 +60,7 @@ var init_paths = __esm({
60
60
  var init_default;
61
61
  var init_init = __esm({
62
62
  "src/storage/migrations/001_init.sql"() {
63
- init_default = "-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- Boardroom \xB7 initial schema\n-- All tables for the v1 MVP. memory_* and knowledge_* are deferred\n-- (post-MVP) and will land in a later migration.\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n-- User preferences \xB7 single-row table.\nCREATE TABLE prefs (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n name TEXT NOT NULL DEFAULT 'You',\n intro TEXT NOT NULL DEFAULT '',\n avatar_seed TEXT,\n theme TEXT NOT NULL DEFAULT 'regent',\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\nINSERT INTO prefs (id, name, intro, theme, created_at, updated_at)\nVALUES (1, 'You', '', 'regent',\n CAST(strftime('%s','now') AS INTEGER) * 1000,\n CAST(strftime('%s','now') AS INTEGER) * 1000);\n\n-- LLM provider API keys \xB7 stored AES-GCM encrypted.\nCREATE TABLE provider_keys (\n provider TEXT PRIMARY KEY,\n key_blob BLOB NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\n-- Directors / Agents.\nCREATE TABLE agents (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n handle TEXT UNIQUE NOT NULL,\n role_tag TEXT NOT NULL DEFAULT '', -- e.g. 'skeptic', 'physicist'\n bio TEXT NOT NULL DEFAULT '',\n cover_quote TEXT,\n instruction TEXT NOT NULL,\n model_v TEXT NOT NULL, -- 'sonnet-4-6' | 'gpt-5' | ...\n avatar_path TEXT NOT NULL,\n is_pinned INTEGER NOT NULL DEFAULT 0,\n is_seed INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\n-- Rooms (boardroom sessions).\nCREATE TABLE rooms (\n id TEXT PRIMARY KEY,\n number INTEGER NOT NULL UNIQUE,\n name TEXT NOT NULL,\n subject TEXT NOT NULL,\n mode TEXT NOT NULL DEFAULT 'discovery', -- discovery|constructive|adversarial\n status TEXT NOT NULL DEFAULT 'live', -- live|adjourned\n brief_style TEXT,\n created_at INTEGER NOT NULL,\n adjourned_at INTEGER\n);\n\n-- Room \u2194 Agent membership (M:N).\nCREATE TABLE room_members (\n room_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n position INTEGER NOT NULL, -- speaking order in round-robin\n joined_at INTEGER NOT NULL,\n PRIMARY KEY (room_id, agent_id),\n FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE,\n FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE\n);\n\nCREATE INDEX idx_room_members_room ON room_members(room_id, position);\n\n-- Messages (user, agents, system).\nCREATE TABLE messages (\n id TEXT PRIMARY KEY,\n room_id TEXT NOT NULL,\n author_kind TEXT NOT NULL, -- 'agent' | 'user' | 'system'\n author_id TEXT, -- agent.id or NULL\n reply_to_id TEXT,\n body TEXT NOT NULL,\n meta_json TEXT, -- JSON: mentions[], speakerStatus, ...\n round_num INTEGER NOT NULL DEFAULT 1,\n created_at INTEGER NOT NULL,\n FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE,\n FOREIGN KEY (reply_to_id) REFERENCES messages(id) ON DELETE SET NULL\n);\n\nCREATE INDEX idx_messages_room ON messages(room_id, created_at);\n\n-- Configuration / lifecycle events (room-opened, room-adjourned, member-add, ...).\nCREATE TABLE config_events (\n id TEXT PRIMARY KEY,\n room_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n payload TEXT, -- JSON\n actor_kind TEXT NOT NULL, -- 'user' | 'system'\n created_at INTEGER NOT NULL,\n FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE\n);\n\n-- Briefs \xB7 adjourn product.\nCREATE TABLE briefs (\n id TEXT PRIMARY KEY,\n room_id TEXT NOT NULL UNIQUE,\n style TEXT NOT NULL,\n title TEXT NOT NULL,\n body_md TEXT NOT NULL,\n body_json TEXT,\n created_at INTEGER NOT NULL,\n FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE\n);\n";
63
+ init_default = "-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- Boardroom \xB7 initial schema\n-- All tables for the v1 MVP. memory_* and knowledge_* are deferred\n-- (post-MVP) and will land in a later migration.\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n-- User preferences \xB7 single-row table.\n-- Note: the `theme` column is historical \xB7 appearance moved to\n-- localStorage. The column stays defined here so existing rows\n-- keep validating (NOT NULL DEFAULT), but the server code no\n-- longer reads or writes it. Will be retired in a future cleanup\n-- migration bundled with other DB-shape changes.\nCREATE TABLE prefs (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n name TEXT NOT NULL DEFAULT 'You',\n intro TEXT NOT NULL DEFAULT '',\n avatar_seed TEXT,\n theme TEXT NOT NULL DEFAULT 'regent',\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\nINSERT INTO prefs (id, name, intro, theme, created_at, updated_at)\nVALUES (1, 'You', '', 'regent',\n CAST(strftime('%s','now') AS INTEGER) * 1000,\n CAST(strftime('%s','now') AS INTEGER) * 1000);\n\n-- LLM provider API keys \xB7 stored AES-GCM encrypted.\nCREATE TABLE provider_keys (\n provider TEXT PRIMARY KEY,\n key_blob BLOB NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\n-- Directors / Agents.\nCREATE TABLE agents (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n handle TEXT UNIQUE NOT NULL,\n role_tag TEXT NOT NULL DEFAULT '', -- e.g. 'skeptic', 'physicist'\n bio TEXT NOT NULL DEFAULT '',\n cover_quote TEXT,\n instruction TEXT NOT NULL,\n model_v TEXT NOT NULL, -- 'sonnet-4-6' | 'gpt-5' | ...\n avatar_path TEXT NOT NULL,\n is_pinned INTEGER NOT NULL DEFAULT 0,\n is_seed INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\n-- Rooms (boardroom sessions).\nCREATE TABLE rooms (\n id TEXT PRIMARY KEY,\n number INTEGER NOT NULL UNIQUE,\n name TEXT NOT NULL,\n subject TEXT NOT NULL,\n mode TEXT NOT NULL DEFAULT 'discovery', -- discovery|constructive|adversarial\n status TEXT NOT NULL DEFAULT 'live', -- live|adjourned\n brief_style TEXT,\n created_at INTEGER NOT NULL,\n adjourned_at INTEGER\n);\n\n-- Room \u2194 Agent membership (M:N).\nCREATE TABLE room_members (\n room_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n position INTEGER NOT NULL, -- speaking order in round-robin\n joined_at INTEGER NOT NULL,\n PRIMARY KEY (room_id, agent_id),\n FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE,\n FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE\n);\n\nCREATE INDEX idx_room_members_room ON room_members(room_id, position);\n\n-- Messages (user, agents, system).\nCREATE TABLE messages (\n id TEXT PRIMARY KEY,\n room_id TEXT NOT NULL,\n author_kind TEXT NOT NULL, -- 'agent' | 'user' | 'system'\n author_id TEXT, -- agent.id or NULL\n reply_to_id TEXT,\n body TEXT NOT NULL,\n meta_json TEXT, -- JSON: mentions[], speakerStatus, ...\n round_num INTEGER NOT NULL DEFAULT 1,\n created_at INTEGER NOT NULL,\n FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE,\n FOREIGN KEY (reply_to_id) REFERENCES messages(id) ON DELETE SET NULL\n);\n\nCREATE INDEX idx_messages_room ON messages(room_id, created_at);\n\n-- Configuration / lifecycle events (room-opened, room-adjourned, member-add, ...).\nCREATE TABLE config_events (\n id TEXT PRIMARY KEY,\n room_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n payload TEXT, -- JSON\n actor_kind TEXT NOT NULL, -- 'user' | 'system'\n created_at INTEGER NOT NULL,\n FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE\n);\n\n-- Briefs \xB7 adjourn product.\nCREATE TABLE briefs (\n id TEXT PRIMARY KEY,\n room_id TEXT NOT NULL UNIQUE,\n style TEXT NOT NULL,\n title TEXT NOT NULL,\n body_md TEXT NOT NULL,\n body_json TEXT,\n created_at INTEGER NOT NULL,\n FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE\n);\n";
64
64
  }
65
65
  });
66
66
 
@@ -998,6 +998,18 @@ function parseVoice(raw) {
998
998
  if (typeof obj.speed === "number" && Number.isFinite(obj.speed)) out.speed = obj.speed;
999
999
  if (typeof obj.pitch === "number" && Number.isFinite(obj.pitch)) out.pitch = obj.pitch;
1000
1000
  if (typeof obj.volume === "number" && Number.isFinite(obj.volume)) out.volume = obj.volume;
1001
+ if (typeof obj.emotion === "string" && obj.emotion.trim()) {
1002
+ out.emotion = obj.emotion.trim();
1003
+ }
1004
+ if (typeof obj.modifyPitch === "number" && Number.isFinite(obj.modifyPitch)) {
1005
+ out.modifyPitch = obj.modifyPitch;
1006
+ }
1007
+ if (typeof obj.modifyIntensity === "number" && Number.isFinite(obj.modifyIntensity)) {
1008
+ out.modifyIntensity = obj.modifyIntensity;
1009
+ }
1010
+ if (typeof obj.modifyTimbre === "number" && Number.isFinite(obj.modifyTimbre)) {
1011
+ out.modifyTimbre = obj.modifyTimbre;
1012
+ }
1001
1013
  if (typeof obj.instructions === "string" && obj.instructions.trim()) {
1002
1014
  out.instructions = obj.instructions.trim().slice(0, 500);
1003
1015
  }
@@ -1182,6 +1194,13 @@ function serializeVoice(v) {
1182
1194
  ...typeof v.speed === "number" && Number.isFinite(v.speed) ? { speed: Math.max(0.5, Math.min(2, v.speed)) } : {},
1183
1195
  ...typeof v.pitch === "number" && Number.isFinite(v.pitch) ? { pitch: Math.max(-12, Math.min(12, v.pitch)) } : {},
1184
1196
  ...typeof v.volume === "number" && Number.isFinite(v.volume) ? { volume: Math.max(0, Math.min(2, v.volume)) } : {},
1197
+ // Emotion + voice_modify fine-tuning fields. Without these the
1198
+ // route layer accepts the patch but the storage layer silently
1199
+ // drops them, so settings disappear on the next page load.
1200
+ ...typeof v.emotion === "string" && v.emotion.trim() ? { emotion: v.emotion.trim() } : {},
1201
+ ...typeof v.modifyPitch === "number" && Number.isFinite(v.modifyPitch) ? { modifyPitch: Math.max(-100, Math.min(100, v.modifyPitch)) } : {},
1202
+ ...typeof v.modifyIntensity === "number" && Number.isFinite(v.modifyIntensity) ? { modifyIntensity: Math.max(-100, Math.min(100, v.modifyIntensity)) } : {},
1203
+ ...typeof v.modifyTimbre === "number" && Number.isFinite(v.modifyTimbre) ? { modifyTimbre: Math.max(-100, Math.min(100, v.modifyTimbre)) } : {},
1185
1204
  ...v.instructions && v.instructions.trim() ? { instructions: v.instructions.trim().slice(0, 500) } : {}
1186
1205
  });
1187
1206
  }
@@ -3290,7 +3309,6 @@ function mapRow3(row) {
3290
3309
  name: row.name,
3291
3310
  intro: row.intro,
3292
3311
  avatarSeed: row.avatar_seed,
3293
- theme: row.theme,
3294
3312
  defaultModelV: row.default_model_v,
3295
3313
  webSearchProvider: normalizeWebSearchProviderPref(row.web_search_provider),
3296
3314
  minimaxRegion: normalizeMinimaxRegion(row.minimax_region),
@@ -3300,7 +3318,7 @@ function mapRow3(row) {
3300
3318
  }
3301
3319
  function getPrefs() {
3302
3320
  const row = getDb().prepare(
3303
- `SELECT name, intro, avatar_seed, theme, default_model_v,
3321
+ `SELECT name, intro, avatar_seed, default_model_v,
3304
3322
  COALESCE(web_search_provider, 'brave') AS web_search_provider,
3305
3323
  COALESCE(minimax_region, 'cn') AS minimax_region,
3306
3324
  created_at, updated_at FROM prefs WHERE id = 1`
@@ -3325,10 +3343,6 @@ function updatePrefs(patch) {
3325
3343
  fields.push("avatar_seed = ?");
3326
3344
  values.push(patch.avatarSeed);
3327
3345
  }
3328
- if (patch.theme !== void 0) {
3329
- fields.push("theme = ?");
3330
- values.push(patch.theme);
3331
- }
3332
3346
  if (patch.defaultModelV !== void 0) {
3333
3347
  fields.push("default_model_v = ?");
3334
3348
  values.push(patch.defaultModelV);
@@ -7261,7 +7275,7 @@ function agentsRouter() {
7261
7275
  }
7262
7276
  const b = body ?? {};
7263
7277
  const description = typeof b.description === "string" ? b.description.trim() : "";
7264
- if (description.length < 4) {
7278
+ if (description.length < 2) {
7265
7279
  return c.json({ error: "describe the director in at least a few words" }, 400);
7266
7280
  }
7267
7281
  if (description.length > 1200) {
@@ -7353,7 +7367,7 @@ function agentsRouter() {
7353
7367
  }
7354
7368
  const b = body ?? {};
7355
7369
  const description = typeof b.description === "string" ? b.description.trim() : "";
7356
- if (description.length < 4) {
7370
+ if (description.length < 2) {
7357
7371
  return c.json({ error: "describe the director in at least a few words" }, 400);
7358
7372
  }
7359
7373
  if (description.length > 1200) {
@@ -7653,6 +7667,15 @@ function agentsRouter() {
7653
7667
  ...typeof v.speed === "number" ? { speed: v.speed } : {},
7654
7668
  ...typeof v.pitch === "number" ? { pitch: v.pitch } : {},
7655
7669
  ...typeof v.volume === "number" ? { volume: v.volume } : {},
7670
+ // Emotion + voice_modify fine-tuning fields. Previously these
7671
+ // were dropped at the route layer, so the agent-profile UI
7672
+ // could call PATCH /api/agents/:id with `voice.emotion` set
7673
+ // and the server would persist the voice WITHOUT the emotion,
7674
+ // making it look like the setting failed to save.
7675
+ ...typeof v.emotion === "string" ? { emotion: v.emotion } : {},
7676
+ ...typeof v.modifyPitch === "number" ? { modifyPitch: v.modifyPitch } : {},
7677
+ ...typeof v.modifyIntensity === "number" ? { modifyIntensity: v.modifyIntensity } : {},
7678
+ ...typeof v.modifyTimbre === "number" ? { modifyTimbre: v.modifyTimbre } : {},
7656
7679
  ...typeof v.instructions === "string" ? { instructions: v.instructions } : {}
7657
7680
  };
7658
7681
  }
@@ -15037,33 +15060,112 @@ async function listAvailableVoices() {
15037
15060
  }
15038
15061
  const elKey = getKey("elevenlabs");
15039
15062
  if (elKey) {
15040
- try {
15041
- const res = await fetch("https://api.elevenlabs.io/v1/voices", {
15042
- headers: { "xi-api-key": elKey }
15043
- });
15044
- if (res.ok) {
15045
- const json = await res.json();
15046
- const rows = elevenLabsVoiceRows(json.voices);
15047
- if (rows.length > 0) {
15048
- const nonEl = voices.filter((v) => v.provider !== "elevenlabs");
15049
- voices = [
15050
- ...nonEl,
15051
- ...rows.map((r) => ({
15052
- provider: "elevenlabs",
15053
- model: "eleven_multilingual_v2",
15054
- voiceId: r.voiceId,
15055
- label: r.label,
15056
- language: r.category,
15057
- configured: true
15058
- }))
15059
- ];
15063
+ const personal = [];
15064
+ const shared = [];
15065
+ await Promise.all([
15066
+ (async () => {
15067
+ try {
15068
+ const res = await fetch(
15069
+ "https://api.elevenlabs.io/v1/voices?show_legacy=true&include_total_count=true",
15070
+ { headers: { "xi-api-key": elKey } }
15071
+ );
15072
+ if (!res.ok) {
15073
+ const errText = await res.text();
15074
+ process.stderr.write(
15075
+ `[voice-registry] elevenlabs /v1/voices HTTP ${res.status}: ${errText.slice(0, 300)}
15076
+ `
15077
+ );
15078
+ return;
15079
+ }
15080
+ const json = await res.json();
15081
+ const rows = elevenLabsVoiceRows(json.voices);
15082
+ process.stderr.write(`[voice-registry] elevenlabs /v1/voices \xB7 ${rows.length} voices in personal library
15083
+ `);
15084
+ personal.push(...rows);
15085
+ } catch (e) {
15086
+ const cause = e instanceof Error ? e.cause : null;
15087
+ const detail = cause?.message ? `: ${cause.message}` : "";
15088
+ process.stderr.write(
15089
+ `[voice-registry] elevenlabs /v1/voices fetch failed${detail} \xB7 ${e instanceof Error ? e.message : String(e)}
15090
+ `
15091
+ );
15060
15092
  }
15061
- }
15062
- } catch {
15093
+ })(),
15094
+ (async () => {
15095
+ try {
15096
+ const res = await fetch(
15097
+ "https://api.elevenlabs.io/v1/shared-voices?page_size=100",
15098
+ { headers: { "xi-api-key": elKey } }
15099
+ );
15100
+ if (!res.ok) {
15101
+ const errText = await res.text();
15102
+ process.stderr.write(
15103
+ `[voice-registry] elevenlabs /v1/shared-voices HTTP ${res.status}: ${errText.slice(0, 300)}
15104
+ `
15105
+ );
15106
+ return;
15107
+ }
15108
+ const json = await res.json();
15109
+ const rows = elevenLabsSharedVoiceRows(json.voices);
15110
+ process.stderr.write(`[voice-registry] elevenlabs /v1/shared-voices \xB7 ${rows.length} voices from public library
15111
+ `);
15112
+ shared.push(...rows);
15113
+ } catch (e) {
15114
+ const cause = e instanceof Error ? e.cause : null;
15115
+ const detail = cause?.message ? `: ${cause.message}` : "";
15116
+ process.stderr.write(
15117
+ `[voice-registry] elevenlabs /v1/shared-voices fetch failed${detail} \xB7 ${e instanceof Error ? e.message : String(e)}
15118
+ `
15119
+ );
15120
+ }
15121
+ })()
15122
+ ]);
15123
+ if (personal.length > 0 || shared.length > 0) {
15124
+ const nonEl = voices.filter((v) => v.provider !== "elevenlabs");
15125
+ const personalIds = new Set(personal.map((r) => r.voiceId));
15126
+ const sharedDeduped = shared.filter((r) => !personalIds.has(r.voiceId));
15127
+ const personalMapped = personal.map((r) => ({
15128
+ provider: "elevenlabs",
15129
+ model: "eleven_multilingual_v2",
15130
+ voiceId: r.voiceId,
15131
+ label: r.label,
15132
+ // Personal-library rows keep their actual category
15133
+ // ("premade", "cloned", "professional", "generated").
15134
+ language: r.category,
15135
+ configured: true
15136
+ }));
15137
+ const sharedMapped = sharedDeduped.map((r) => ({
15138
+ provider: "elevenlabs",
15139
+ model: "eleven_multilingual_v2",
15140
+ voiceId: r.voiceId,
15141
+ // Prefix shared-library voices so users can tell at a glance
15142
+ // which set they're picking from. The dropdown's group header
15143
+ // already says "elevenlabs", so the per-row prefix is the
15144
+ // tightest signal we have for personal-vs-shared.
15145
+ label: `${r.label} \xB7 shared`,
15146
+ language: r.language || r.category,
15147
+ configured: true
15148
+ }));
15149
+ voices = [...nonEl, ...personalMapped, ...sharedMapped];
15063
15150
  }
15064
15151
  }
15065
15152
  return voices;
15066
15153
  }
15154
+ function elevenLabsSharedVoiceRows(raw) {
15155
+ if (!Array.isArray(raw)) return [];
15156
+ const out = [];
15157
+ for (const item of raw) {
15158
+ if (!item || typeof item !== "object") continue;
15159
+ const obj = item;
15160
+ const voiceId = typeof obj.voice_id === "string" ? obj.voice_id : "";
15161
+ if (!voiceId) continue;
15162
+ const label = typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : voiceId;
15163
+ const category = typeof obj.category === "string" && obj.category.trim() ? obj.category.trim() : "shared";
15164
+ const language = typeof obj.language === "string" && obj.language.trim() ? obj.language.trim() : void 0;
15165
+ out.push({ voiceId, label, category, language });
15166
+ }
15167
+ return out;
15168
+ }
15067
15169
  function elevenLabsVoiceRows(raw) {
15068
15170
  if (!Array.isArray(raw)) return [];
15069
15171
  const out = [];
@@ -16220,7 +16322,6 @@ function prefsRouter() {
16220
16322
  const b = body;
16221
16323
  if (typeof b.name === "string") patch.name = b.name.trim().slice(0, 64);
16222
16324
  if (typeof b.intro === "string") patch.intro = b.intro.slice(0, 320);
16223
- if (typeof b.theme === "string") patch.theme = b.theme.trim().slice(0, 32);
16224
16325
  if (b.avatarSeed === null || typeof b.avatarSeed === "string") {
16225
16326
  patch.avatarSeed = b.avatarSeed;
16226
16327
  }
@@ -18861,6 +18962,7 @@ function minimaxBaseUrl2() {
18861
18962
  return region === "intl" ? "https://api.minimax.io" : "https://api.minimaxi.com";
18862
18963
  }
18863
18964
  var ELEVENLABS_API = "https://api.elevenlabs.io/v1";
18965
+ var OPENAI_API = "https://api.openai.com/v1";
18864
18966
  function cleanForSpeech(md) {
18865
18967
  if (!md) return "";
18866
18968
  let out = md;
@@ -18902,6 +19004,7 @@ function voiceProfileForAgent(agent) {
18902
19004
  async function synthesizeSpeech(text, profile, signal) {
18903
19005
  if (profile.provider === "minimax") return synthesizeMiniMax(text, profile, signal);
18904
19006
  if (profile.provider === "elevenlabs") return synthesizeElevenLabs(text, profile, signal);
19007
+ if (profile.provider === "openai") return synthesizeOpenAI(text, profile, signal);
18905
19008
  return {
18906
19009
  provider: profile.provider,
18907
19010
  model: profile.model,
@@ -19067,10 +19170,38 @@ async function synthesizeMiniMax(text, profile, signal) {
19067
19170
  }
19068
19171
  })
19069
19172
  });
19070
- if (!res.ok) throw new Error(`MiniMax TTS HTTP ${res.status}: ${await res.text()}`);
19173
+ if (!res.ok) {
19174
+ const errText = await res.text();
19175
+ if (res.status === 402 || /insufficient[ _-]?(?:balance|quota|credit|fund)|余额不足|余额[^a-zA-Z]?(?:不足|不够)/i.test(errText)) {
19176
+ const err2 = new Error(
19177
+ "Your MiniMax account balance is insufficient for TTS. Top up your account in the MiniMax console and try again."
19178
+ );
19179
+ err2.code = "paid-plan-required";
19180
+ err2.provider = "minimax";
19181
+ err2.upgradeUrl = getPrefs().minimaxRegion === "intl" ? "https://platform.minimax.io/user-center/billing/overview" : "https://platform.minimaxi.com/user-center/payment";
19182
+ throw err2;
19183
+ }
19184
+ throw new Error(`MiniMax TTS HTTP ${res.status}: ${errText}`);
19185
+ }
19071
19186
  const json = await res.json();
19187
+ const status = json.base_resp?.status_code ?? 0;
19188
+ const statusMsg = json.base_resp?.status_msg || "";
19189
+ if (status !== 0 && (status === 1008 || /insufficient[ _-]?(?:balance|quota|credit|fund)|余额不足|余额[^a-zA-Z]?(?:不足|不够)/i.test(statusMsg))) {
19190
+ const err2 = new Error(
19191
+ "Your MiniMax account balance is insufficient for TTS. Top up your account in the MiniMax console and try again."
19192
+ );
19193
+ err2.code = "paid-plan-required";
19194
+ err2.provider = "minimax";
19195
+ err2.upgradeUrl = getPrefs().minimaxRegion === "intl" ? "https://platform.minimax.io/user-center/billing/overview" : "https://platform.minimaxi.com/user-center/payment";
19196
+ throw err2;
19197
+ }
19072
19198
  const hex = json.data?.audio ?? json.audio ?? "";
19073
- if (!hex) throw new Error("MiniMax TTS returned no audio");
19199
+ if (!hex) {
19200
+ if (status !== 0 && statusMsg) {
19201
+ throw new Error(`MiniMax TTS failed (${status}): ${statusMsg}`);
19202
+ }
19203
+ throw new Error("MiniMax TTS returned no audio");
19204
+ }
19074
19205
  return {
19075
19206
  provider: "minimax",
19076
19207
  model,
@@ -19080,6 +19211,47 @@ async function synthesizeMiniMax(text, profile, signal) {
19080
19211
  audioBase64: Buffer.from(hex, "hex").toString("base64")
19081
19212
  };
19082
19213
  }
19214
+ async function synthesizeOpenAI(text, profile, signal) {
19215
+ const key = getKey("openai");
19216
+ if (!key) {
19217
+ return { provider: "browser", model: "speechSynthesis", voiceId: "system-default", text };
19218
+ }
19219
+ const model = profile.model?.trim() || "gpt-4o-mini-tts";
19220
+ const voice = profile.voiceId?.trim() || "marin";
19221
+ const speed = Math.min(4, Math.max(0.25, profile.speed ?? 1));
19222
+ const res = await fetch(`${OPENAI_API}/audio/speech`, {
19223
+ method: "POST",
19224
+ signal,
19225
+ headers: {
19226
+ "authorization": `Bearer ${key}`,
19227
+ "content-type": "application/json",
19228
+ "accept": "audio/mpeg"
19229
+ },
19230
+ body: JSON.stringify({
19231
+ model,
19232
+ input: text,
19233
+ voice,
19234
+ response_format: "mp3",
19235
+ speed
19236
+ })
19237
+ });
19238
+ if (!res.ok) {
19239
+ const errText = await res.text();
19240
+ throw new Error(`OpenAI TTS HTTP ${res.status}: ${errText.slice(0, 400)}`);
19241
+ }
19242
+ const buf = Buffer.from(await res.arrayBuffer());
19243
+ if (buf.length === 0) {
19244
+ throw new Error("OpenAI TTS returned empty body");
19245
+ }
19246
+ return {
19247
+ provider: "openai",
19248
+ model,
19249
+ voiceId: voice,
19250
+ text,
19251
+ mimeType: "audio/mpeg",
19252
+ audioBase64: buf.toString("base64")
19253
+ };
19254
+ }
19083
19255
  async function synthesizeElevenLabs(text, profile, signal) {
19084
19256
  const key = getKey("elevenlabs");
19085
19257
  if (!key) {
@@ -19103,6 +19275,15 @@ async function synthesizeElevenLabs(text, profile, signal) {
19103
19275
  });
19104
19276
  if (!res.ok) {
19105
19277
  const errText = await res.text();
19278
+ if (res.status === 402 && /paid_plan_required|library voices/i.test(errText)) {
19279
+ const err2 = new Error(
19280
+ "ElevenLabs library voices (Rachel, George, etc.) require a paid plan to use via the API. Either upgrade your ElevenLabs subscription, or clone your own voice in the ElevenLabs dashboard and pick it here."
19281
+ );
19282
+ err2.code = "paid-plan-required";
19283
+ err2.provider = "elevenlabs";
19284
+ err2.upgradeUrl = "https://elevenlabs.io/pricing";
19285
+ throw err2;
19286
+ }
19106
19287
  throw new Error(`ElevenLabs TTS HTTP ${res.status}: ${errText.slice(0, 400)}`);
19107
19288
  }
19108
19289
  const buf = Buffer.from(await res.arrayBuffer());
@@ -19139,6 +19320,15 @@ async function* synthesizeElevenLabsStream(text, profile, signal) {
19139
19320
  });
19140
19321
  if (!res.ok) {
19141
19322
  const errText = await res.text();
19323
+ if (res.status === 402 && /paid_plan_required|library voices/i.test(errText)) {
19324
+ const err2 = new Error(
19325
+ "ElevenLabs library voices (Rachel, George, etc.) require a paid plan to use via the API. Either upgrade your ElevenLabs subscription, or clone your own voice in the ElevenLabs dashboard and pick it here."
19326
+ );
19327
+ err2.code = "paid-plan-required";
19328
+ err2.provider = "elevenlabs";
19329
+ err2.upgradeUrl = "https://elevenlabs.io/pricing";
19330
+ throw err2;
19331
+ }
19142
19332
  throw new Error(`ElevenLabs TTS stream HTTP ${res.status}: ${errText.slice(0, 400)}`);
19143
19333
  }
19144
19334
  const body = res.body;
@@ -23008,6 +23198,21 @@ function usageRouter() {
23008
23198
 
23009
23199
  // src/routes/voices.ts
23010
23200
  import { Hono as Hono12 } from "hono";
23201
+ function ttsErrorMessage(e, providerLabel) {
23202
+ if (!(e instanceof Error)) return String(e);
23203
+ const cause = e.cause;
23204
+ if (cause && cause instanceof Error) {
23205
+ const code = cause.code;
23206
+ const tail = code ? ` (${String(code)})` : "";
23207
+ const full = `${e.message}: ${cause.message}${tail}`;
23208
+ process.stderr.write(`[tts-preview] ${providerLabel} fetch error \xB7 ${full}
23209
+ `);
23210
+ return full;
23211
+ }
23212
+ process.stderr.write(`[tts-preview] ${providerLabel} error \xB7 ${e.message}
23213
+ `);
23214
+ return e.message;
23215
+ }
23011
23216
  var TTS_CACHE_MAX = 50;
23012
23217
  var ttsCache = /* @__PURE__ */ new Map();
23013
23218
  function ttsCacheKey(messageId, profile) {
@@ -23067,7 +23272,12 @@ function voicesRouter() {
23067
23272
  }
23068
23273
  return c.json({ audioBase64: chunk.audioBase64, mimeType: chunk.mimeType });
23069
23274
  } catch (e) {
23070
- return c.json({ error: e instanceof Error ? e.message : String(e) }, 502);
23275
+ const tagged = e ?? {};
23276
+ const payload = { error: ttsErrorMessage(e, profile.provider) };
23277
+ if (typeof tagged.code === "string") payload.code = tagged.code;
23278
+ if (typeof tagged.provider === "string") payload.provider = tagged.provider;
23279
+ if (typeof tagged.upgradeUrl === "string") payload.upgradeUrl = tagged.upgradeUrl;
23280
+ return c.json(payload, 502);
23071
23281
  }
23072
23282
  });
23073
23283
  r.post("/by-message/:id", async (c) => {
@@ -23117,7 +23327,7 @@ function voicesRouter() {
23117
23327
  ttsCacheSet(key, out);
23118
23328
  return c.json(out);
23119
23329
  } catch (e) {
23120
- const msg = e instanceof Error ? e.message : String(e);
23330
+ const msg = ttsErrorMessage(e, profile.provider);
23121
23331
  const isNoKey = /401|403|api[\s-]?key|unauthor/i.test(msg);
23122
23332
  return c.json({
23123
23333
  error: msg,
@@ -23133,7 +23343,7 @@ function voicesRouter() {
23133
23343
  init_paths();
23134
23344
 
23135
23345
  // src/version.ts
23136
- var VERSION = "0.1.16";
23346
+ var VERSION = "0.1.18";
23137
23347
 
23138
23348
  // src/server.ts
23139
23349
  function createApp() {
@@ -23209,7 +23419,7 @@ async function startServer(opts) {
23209
23419
  };
23210
23420
  }
23211
23421
 
23212
- // src/cli.ts
23422
+ // src/boot.ts
23213
23423
  init_db();
23214
23424
  init_persona_jobs();
23215
23425
  init_paths();
@@ -23231,11 +23441,8 @@ function isPortFree(port) {
23231
23441
  });
23232
23442
  }
23233
23443
 
23234
- // src/cli.ts
23235
- async function main() {
23236
- const program = new Command().name("privateboard").description("PrivateBoard \xB7 your private board meeting, on call. Local-first, multi-agent thinking.").version(VERSION).option("-p, --port <n>", "port to listen on (default: auto-detect from 3030)").option("--host <h>", "host to bind", "127.0.0.1").option("--no-open", "don't open the browser automatically");
23237
- program.parse();
23238
- const opts = program.opts();
23444
+ // src/boot.ts
23445
+ async function bootApp(opts = {}) {
23239
23446
  const dirs2 = ensureBoardroomDir();
23240
23447
  const { applied } = runMigrations();
23241
23448
  const seed = runSeed();
@@ -23244,7 +23451,7 @@ async function main() {
23244
23451
  const r = reconcileAgentModels();
23245
23452
  reconcile = { switched: r.switched.length, cleared: r.cleared.length };
23246
23453
  } catch (e) {
23247
- process.stderr.write(`[boot] reconcile failed: ${e instanceof Error ? e.message : String(e)}
23454
+ process.stderr.write(`[boot] reconcile failed: ${errMsg(e)}
23248
23455
  `);
23249
23456
  }
23250
23457
  try {
@@ -23256,7 +23463,7 @@ async function main() {
23256
23463
  );
23257
23464
  }
23258
23465
  } catch (e) {
23259
- process.stderr.write(`[boot] orphan cleanup failed: ${e instanceof Error ? e.message : String(e)}
23466
+ process.stderr.write(`[boot] orphan cleanup failed: ${errMsg(e)}
23260
23467
  `);
23261
23468
  }
23262
23469
  try {
@@ -23266,7 +23473,7 @@ async function main() {
23266
23473
  `);
23267
23474
  }
23268
23475
  } catch (e) {
23269
- process.stderr.write(`[boot] clarify recovery failed: ${e instanceof Error ? e.message : String(e)}
23476
+ process.stderr.write(`[boot] clarify recovery failed: ${errMsg(e)}
23270
23477
  `);
23271
23478
  }
23272
23479
  try {
@@ -23276,7 +23483,7 @@ async function main() {
23276
23483
  `);
23277
23484
  }
23278
23485
  } catch (e) {
23279
- process.stderr.write(`[boot] persona-job recovery failed: ${e instanceof Error ? e.message : String(e)}
23486
+ process.stderr.write(`[boot] persona-job recovery failed: ${errMsg(e)}
23280
23487
  `);
23281
23488
  }
23282
23489
  try {
@@ -23286,7 +23493,7 @@ async function main() {
23286
23493
  `);
23287
23494
  }
23288
23495
  } catch (e) {
23289
- process.stderr.write(`[boot] topic-rec recovery failed: ${e instanceof Error ? e.message : String(e)}
23496
+ process.stderr.write(`[boot] topic-rec recovery failed: ${errMsg(e)}
23290
23497
  `);
23291
23498
  }
23292
23499
  void (async () => {
@@ -23304,18 +23511,49 @@ async function main() {
23304
23511
  `);
23305
23512
  }
23306
23513
  } catch (e) {
23307
- process.stderr.write(`[boot] dream sweep failed: ${e instanceof Error ? e.message : String(e)}
23514
+ process.stderr.write(`[boot] dream sweep failed: ${errMsg(e)}
23308
23515
  `);
23309
23516
  }
23310
23517
  })();
23518
+ const port = opts.port ?? await findFreePort(3030);
23519
+ const host = opts.host ?? "127.0.0.1";
23520
+ const server = await startServer({ port, host });
23521
+ return { server, dirs: dirs2, port, host, applied, seed, reconcile };
23522
+ }
23523
+ var shuttingDown = false;
23524
+ async function shutdownApp(server) {
23525
+ if (shuttingDown) return;
23526
+ shuttingDown = true;
23527
+ try {
23528
+ await server?.close();
23529
+ } catch (e) {
23530
+ console.error(" ! error closing server", e);
23531
+ }
23532
+ try {
23533
+ closeDb();
23534
+ } catch (e) {
23535
+ console.error(" ! error closing db", e);
23536
+ }
23537
+ }
23538
+ function errMsg(e) {
23539
+ return e instanceof Error ? e.message : String(e);
23540
+ }
23541
+
23542
+ // src/cli.ts
23543
+ init_db();
23544
+ async function main() {
23545
+ const program = new Command().name("privateboard").description("PrivateBoard \xB7 your private board meeting, on call. Local-first, multi-agent thinking.").version(VERSION).option("-p, --port <n>", "port to listen on (default: auto-detect from 3030)").option("--host <h>", "host to bind", "127.0.0.1").option("--no-open", "don't open the browser automatically");
23546
+ program.parse();
23547
+ const opts = program.opts();
23311
23548
  const portArg = opts.port ? Number.parseInt(opts.port, 10) : void 0;
23312
23549
  if (portArg !== void 0 && (Number.isNaN(portArg) || portArg < 1 || portArg > 65535)) {
23313
23550
  console.error(`Invalid --port: ${opts.port}`);
23314
23551
  process.exit(1);
23315
23552
  }
23316
- const port = portArg ?? await findFreePort(3030);
23317
- const host = opts.host ?? "127.0.0.1";
23318
- const server = await startServer({ port, host });
23553
+ const { server, dirs: dirs2, applied, seed, reconcile } = await bootApp({
23554
+ port: portArg,
23555
+ host: opts.host
23556
+ });
23319
23557
  const bannerLines = [
23320
23558
  "",
23321
23559
  " \u25B8 privateboard v" + VERSION,
@@ -23340,28 +23578,15 @@ async function main() {
23340
23578
  open(server.url).catch(() => {
23341
23579
  });
23342
23580
  }
23343
- let shuttingDown = false;
23344
- const shutdown = async (signal) => {
23345
- if (shuttingDown) return;
23346
- shuttingDown = true;
23581
+ const onSignal = (signal) => {
23347
23582
  process.stdout.write(`
23348
23583
  \u25B8 ${signal} received \xB7 shutting down
23349
23584
  `);
23350
- try {
23351
- await server.close();
23352
- } catch (e) {
23353
- console.error(" ! error closing server", e);
23354
- }
23355
- try {
23356
- closeDb();
23357
- } catch (e) {
23358
- console.error(" ! error closing db", e);
23359
- }
23360
- process.exit(0);
23585
+ void shutdownApp(server).finally(() => process.exit(0));
23361
23586
  };
23362
- process.on("SIGINT", () => shutdown("SIGINT"));
23363
- process.on("SIGTERM", () => shutdown("SIGTERM"));
23364
- process.on("SIGHUP", () => shutdown("SIGHUP"));
23587
+ process.on("SIGINT", () => onSignal("SIGINT"));
23588
+ process.on("SIGTERM", () => onSignal("SIGTERM"));
23589
+ process.on("SIGHUP", () => onSignal("SIGHUP"));
23365
23590
  process.on("exit", () => {
23366
23591
  try {
23367
23592
  closeDb();