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.
- package/dist/boot.d.ts +63 -0
- package/dist/boot.js +23542 -0
- package/dist/boot.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +295 -70
- package/dist/cli.js.map +1 -1
- package/dist/server.d.ts +15 -0
- package/dist/server.js +22652 -0
- package/dist/server.js.map +1 -0
- package/dist/version.d.ts +17 -0
- package/dist/version.js +8 -0
- package/dist/version.js.map +1 -0
- package/electron-entry.cjs +80 -0
- package/package.json +55 -6
- package/public/agent-build-bgm.js +259 -194
- package/public/agent-overlay.js +49 -1
- package/public/agent-profile.css +59 -10
- package/public/agent-profile.js +175 -16
- package/public/app.js +1139 -261
- package/public/avatar-skill.js +520 -681
- package/public/avatars/chair.svg +1 -1
- package/public/avatars/first-principles.svg +1 -1
- package/public/avatars/historian.svg +1 -1
- package/public/avatars/long-horizon.svg +1 -1
- package/public/avatars/phenomenologist.svg +1 -1
- package/public/avatars/socrates.svg +1 -1
- package/public/avatars/user-empathy.svg +1 -1
- package/public/avatars/value-investor.svg +1 -1
- package/public/home.html +44 -6
- package/public/i18n.js +110 -16
- package/public/icons/logo.png +0 -0
- package/public/icons/logo.svg +10 -0
- package/public/icons.css +25 -4
- package/public/index.html +1220 -333
- package/public/onboarding.css +2 -3
- package/public/onboarding.js +2 -2
- package/public/ppt.html +99 -4
- package/public/quote-cta.css +2 -2
- package/public/room-settings.css +159 -11
- package/public/themes.css +93 -268
- package/public/user-settings.css +30 -69
- package/public/user-settings.js +92 -88
- package/public/voice-replay.js +37 -0
- package/public/icons/logo2.png +0 -0
- package/public/icons/private-board-vi.html +0 -1716
package/dist/cli.d.ts
ADDED
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,
|
|
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 <
|
|
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 <
|
|
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
|
-
|
|
15041
|
-
|
|
15042
|
-
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
|
|
15052
|
-
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
15056
|
-
|
|
15057
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
|
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.
|
|
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/
|
|
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/
|
|
23235
|
-
async function
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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
|
|
23317
|
-
|
|
23318
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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", () =>
|
|
23363
|
-
process.on("SIGTERM", () =>
|
|
23364
|
-
process.on("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();
|