@openspecui/server 3.7.0 → 3.7.1

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 (2) hide show
  1. package/dist/index.mjs +330 -26
  2. package/package.json +7 -3
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, DocumentTranslationConfigSchema, GitConfigSchema, HOSTED_SHELL_PROTOCOL_VERSION, MarkdownParser, NotificationPublishInputSchema, NotificationSettingsSchema, OPENSPECUI_HOOKS_VERSION, OpenSpecAdapter, OpenSpecWatcher, OpsxConfigSchema, OpsxKernel, PtyClientMessageSchema, ReactiveContext, TerminalConfigSchema, TerminalControlParser, TerminalRendererEngineSchema, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getDetectedProjectTools, getToolInitStates, getWatcherRuntimeStatus, initWatcherPool, isWatcherPoolInitialized, parseOpsxEntityMetadata, parseOpsxSchemaDetail, resolveTerminalShellDefaults, sniffGlobalCli, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput } from "@openspecui/core";
1
+ import { CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, DocumentTranslationConfigSchema, GitConfigSchema, GlobalSettingsManager, MarkdownParser, NotificationPublishInputSchema, NotificationSettingsSchema, OPENSPECUI_HOOKS_VERSION, OpenSpecAdapter, OpenSpecUIGlobalSettingsSchema, OpenSpecWatcher, OpsxConfigSchema, OpsxKernel, PtyClientMessageSchema, ReactiveContext, TerminalConfigSchema, TerminalControlParser, TerminalRendererEngineSchema, TranslationCacheReadInputSchema, TranslationCacheSettingsSchema, TranslationCacheWriteInputSchema, buildBackendHealthPayload, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getDetectedProjectTools, getToolInitStates, getWatcherRuntimeStatus, initWatcherPool, isWatcherPoolInitialized, parseOpsxEntityMetadata, parseOpsxSchemaDetail, resolveTerminalShellDefaults, sniffGlobalCli, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput } from "@openspecui/core";
2
2
  import { basename, dirname, join, matchesGlob, relative, resolve, sep } from "node:path";
3
3
  import { access, mkdir, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -12,7 +12,7 @@ import { readFileSync } from "node:fs";
12
12
  import { WebSocketServer } from "ws";
13
13
  import { CustomSoundHashSchema as CustomSoundHashSchema$1, CustomSoundIdSchema, CustomSoundMetadataFileSchema, customHashFromSoundId, soundIdFromCustomHash } from "@openspecui/core/sounds";
14
14
  import { createHash } from "node:crypto";
15
- import { homedir } from "node:os";
15
+ import { homedir, platform } from "node:os";
16
16
  import { EventEmitter } from "node:events";
17
17
  import { execFile } from "node:child_process";
18
18
  import { promisify } from "node:util";
@@ -1728,8 +1728,8 @@ function detectPtyPlatform() {
1728
1728
  if (process.platform === "darwin") return "macos";
1729
1729
  return "common";
1730
1730
  }
1731
- function resolveDefaultShell(platform, env) {
1732
- if (platform === "windows") return env.ComSpec?.trim() || "cmd.exe";
1731
+ function resolveDefaultShell(platform$1, env) {
1732
+ if (platform$1 === "windows") return env.ComSpec?.trim() || "cmd.exe";
1733
1733
  return env.SHELL?.trim() || "/bin/sh";
1734
1734
  }
1735
1735
  function resolvePtyShellDefaults(opts) {
@@ -2833,6 +2833,20 @@ async function buildGitWorktreeOverview(options) {
2833
2833
  };
2834
2834
  });
2835
2835
  }
2836
+ async function resolveGitWorktreeSwitchTarget(options) {
2837
+ const resolvedProjectDir = resolve(options.projectDir);
2838
+ const resolvedInputPath = resolve(options.targetPath);
2839
+ const worktrees = await listGitWorktrees(resolvedProjectDir, options.runGit ?? defaultRunGit);
2840
+ for (const worktree of worktrees) {
2841
+ const worktreePath = resolve(worktree.path);
2842
+ if (!await sameGitPath(worktreePath, resolvedInputPath)) continue;
2843
+ return {
2844
+ path: worktreePath,
2845
+ pathAvailable: await pathExists(worktreePath)
2846
+ };
2847
+ }
2848
+ return null;
2849
+ }
2836
2850
  async function listCurrentWorktreeGitEntries(options) {
2837
2851
  const resolvedProjectDir = resolve(options.projectDir);
2838
2852
  const limit = clampEntryLimit(options.limit);
@@ -3067,6 +3081,35 @@ const soundsRouter = router({
3067
3081
  return { success: true };
3068
3082
  })
3069
3083
  });
3084
+ const globalSettingsRouter = router({
3085
+ get: publicProcedure.query(({ ctx }) => {
3086
+ return ctx.globalSettingsManager.readSettings();
3087
+ }),
3088
+ update: publicProcedure.input(OpenSpecUIGlobalSettingsSchema.partial().extend({ translationCache: TranslationCacheSettingsSchema.partial().optional() })).mutation(async ({ ctx, input }) => {
3089
+ await ctx.globalSettingsManager.writeSettings(input);
3090
+ return { success: true };
3091
+ }),
3092
+ subscribe: publicProcedure.subscription(({ ctx }) => {
3093
+ return createReactiveSubscription(() => ctx.globalSettingsManager.readSettings());
3094
+ })
3095
+ });
3096
+ const translationCacheRouter = router({
3097
+ stats: publicProcedure.query(({ ctx }) => {
3098
+ return ctx.translationCacheService.getStats();
3099
+ }),
3100
+ read: publicProcedure.input(TranslationCacheReadInputSchema).query(({ ctx, input }) => {
3101
+ return ctx.translationCacheService.read(input.keyHash);
3102
+ }),
3103
+ write: publicProcedure.input(TranslationCacheWriteInputSchema).mutation(({ ctx, input }) => {
3104
+ return ctx.translationCacheService.write(input);
3105
+ }),
3106
+ clean: publicProcedure.mutation(({ ctx }) => {
3107
+ return ctx.translationCacheService.clean();
3108
+ }),
3109
+ clear: publicProcedure.mutation(({ ctx }) => {
3110
+ return ctx.translationCacheService.clear();
3111
+ })
3112
+ });
3070
3113
  const OPSX_CORE_PROFILE_WORKFLOWS = [
3071
3114
  "propose",
3072
3115
  "explore",
@@ -4141,14 +4184,10 @@ const gitRouter = router({
4141
4184
  }),
4142
4185
  switchWorktree: publicProcedure.input(z.object({ path: z.string().min(1) })).mutation(async ({ ctx, input }) => {
4143
4186
  if (!ctx.gitWorktreeHandoff) throw new Error("Worktree handoff is unavailable in this runtime.");
4144
- const overview = await buildGitWorktreeOverview({ projectDir: ctx.projectDir });
4145
- const resolvedInputPath = resolve(input.path);
4146
- let target = null;
4147
- if (overview.currentWorktree && await sameGitPath(overview.currentWorktree.path, resolvedInputPath)) target = overview.currentWorktree;
4148
- else for (const worktree of overview.otherWorktrees) if (await sameGitPath(worktree.path, resolvedInputPath)) {
4149
- target = worktree;
4150
- break;
4151
- }
4187
+ const target = await resolveGitWorktreeSwitchTarget({
4188
+ projectDir: ctx.projectDir,
4189
+ targetPath: input.path
4190
+ });
4152
4191
  if (!target) throw new Error("Worktree not found.");
4153
4192
  if (!target.pathAvailable) throw new Error("Worktree path is no longer available. Remove the stale worktree entry first.");
4154
4193
  return ctx.gitWorktreeHandoff.ensureWorktreeServer({ targetPath: target.path });
@@ -4166,6 +4205,8 @@ const appRouter = router({
4166
4205
  init: initRouter,
4167
4206
  realtime: realtimeRouter,
4168
4207
  config: configRouter,
4208
+ globalSettings: globalSettingsRouter,
4209
+ translationCache: translationCacheRouter,
4169
4210
  notifications: notificationsRouter,
4170
4211
  sounds: soundsRouter,
4171
4212
  cli: cliRouter,
@@ -4306,6 +4347,227 @@ var SearchService = class {
4306
4347
  }
4307
4348
  };
4308
4349
 
4350
+ //#endregion
4351
+ //#region src/translation-cache-adapter.ts
4352
+ var SqliteTranslationCacheAdapter = class {
4353
+ database = null;
4354
+ constructor(databasePath, createDatabase) {
4355
+ this.databasePath = databasePath;
4356
+ this.createDatabase = createDatabase;
4357
+ }
4358
+ async init() {
4359
+ if (this.database) return;
4360
+ await mkdir(dirname(this.databasePath), { recursive: true });
4361
+ const database = this.createDatabase(this.databasePath);
4362
+ database.exec(`
4363
+ CREATE TABLE IF NOT EXISTS translation_cache_entries (
4364
+ key_hash TEXT PRIMARY KEY,
4365
+ cache_key TEXT NOT NULL,
4366
+ source_text TEXT NOT NULL,
4367
+ translated_text TEXT NOT NULL,
4368
+ target_nodes_json TEXT,
4369
+ source_language TEXT NOT NULL,
4370
+ target_language TEXT NOT NULL,
4371
+ placeholder_topology_hash TEXT NOT NULL,
4372
+ attribute_topology_hash TEXT NOT NULL,
4373
+ display_policy_version INTEGER NOT NULL,
4374
+ created_at INTEGER NOT NULL,
4375
+ last_accessed_at INTEGER NOT NULL
4376
+ );
4377
+ CREATE INDEX IF NOT EXISTS translation_cache_entries_lru_idx
4378
+ ON translation_cache_entries(last_accessed_at ASC);
4379
+ `);
4380
+ ensureTargetNodesJsonColumn(database);
4381
+ this.database = database;
4382
+ }
4383
+ async read(keyHash, now) {
4384
+ const database = await this.requireDatabase();
4385
+ const row = database.prepare(`SELECT key_hash, cache_key, source_text, translated_text, target_nodes_json, source_language,
4386
+ target_language, placeholder_topology_hash, attribute_topology_hash,
4387
+ display_policy_version, created_at, last_accessed_at
4388
+ FROM translation_cache_entries
4389
+ WHERE key_hash = ?`).get(keyHash);
4390
+ if (!isSqliteTranslationCacheRow(row)) return null;
4391
+ database.prepare("UPDATE translation_cache_entries SET last_accessed_at = ? WHERE key_hash = ?").run(now, keyHash);
4392
+ return {
4393
+ keyHash: row.key_hash,
4394
+ key: row.cache_key,
4395
+ sourceText: row.source_text,
4396
+ translatedText: row.translated_text,
4397
+ ...row.target_nodes_json ? { targetNodesJson: row.target_nodes_json } : {},
4398
+ sourceLanguage: row.source_language,
4399
+ targetLanguage: row.target_language,
4400
+ placeholderTopologyHash: row.placeholder_topology_hash,
4401
+ attributeTopologyHash: row.attribute_topology_hash,
4402
+ displayPolicyVersion: row.display_policy_version,
4403
+ createdAt: row.created_at,
4404
+ lastAccessedAt: now
4405
+ };
4406
+ }
4407
+ async write(input, now) {
4408
+ (await this.requireDatabase()).prepare(`INSERT INTO translation_cache_entries (
4409
+ key_hash, cache_key, source_text, translated_text, target_nodes_json, source_language,
4410
+ target_language, placeholder_topology_hash, attribute_topology_hash,
4411
+ display_policy_version, created_at, last_accessed_at
4412
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4413
+ ON CONFLICT(key_hash) DO UPDATE SET
4414
+ cache_key = excluded.cache_key,
4415
+ source_text = excluded.source_text,
4416
+ translated_text = excluded.translated_text,
4417
+ target_nodes_json = excluded.target_nodes_json,
4418
+ source_language = excluded.source_language,
4419
+ target_language = excluded.target_language,
4420
+ placeholder_topology_hash = excluded.placeholder_topology_hash,
4421
+ attribute_topology_hash = excluded.attribute_topology_hash,
4422
+ display_policy_version = excluded.display_policy_version,
4423
+ last_accessed_at = excluded.last_accessed_at`).run(input.keyHash, input.key, input.sourceText, input.translatedText, input.targetNodesJson ?? null, input.sourceLanguage, input.targetLanguage, input.placeholderTopologyHash, input.attributeTopologyHash, input.displayPolicyVersion, now, now);
4424
+ }
4425
+ async count() {
4426
+ return readSqliteCount((await this.requireDatabase()).prepare("SELECT COUNT(*) AS count FROM translation_cache_entries").get());
4427
+ }
4428
+ async deleteLeastRecentlyUsed(targetEntryCount) {
4429
+ const database = await this.requireDatabase();
4430
+ const currentCount = await this.count();
4431
+ const deleteCount = Math.max(0, currentCount - targetEntryCount);
4432
+ if (deleteCount === 0) return 0;
4433
+ database.prepare(`DELETE FROM translation_cache_entries
4434
+ WHERE key_hash IN (
4435
+ SELECT key_hash FROM translation_cache_entries
4436
+ ORDER BY last_accessed_at ASC, key_hash ASC
4437
+ LIMIT ?
4438
+ )`).run(deleteCount);
4439
+ return deleteCount;
4440
+ }
4441
+ async clean(entryLimit) {
4442
+ const before = await this.count();
4443
+ const target = Math.floor(entryLimit * .6);
4444
+ const deleted = await this.deleteLeastRecentlyUsed(target);
4445
+ return {
4446
+ before,
4447
+ after: await this.count(),
4448
+ deleted
4449
+ };
4450
+ }
4451
+ async clear() {
4452
+ const database = await this.requireDatabase();
4453
+ const before = await this.count();
4454
+ database.prepare("DELETE FROM translation_cache_entries").run();
4455
+ return before;
4456
+ }
4457
+ close() {
4458
+ this.database?.close?.();
4459
+ this.database = null;
4460
+ }
4461
+ async requireDatabase() {
4462
+ await this.init();
4463
+ if (!this.database) throw new Error("Translation cache database is not initialized.");
4464
+ return this.database;
4465
+ }
4466
+ };
4467
+ async function createRuntimeSqliteTranslationCacheAdapter(databasePath) {
4468
+ return new SqliteTranslationCacheAdapter(databasePath, await resolveRuntimeSqliteDatabaseFactory());
4469
+ }
4470
+ async function resolveRuntimeSqliteDatabaseFactory() {
4471
+ if (isBunRuntime()) {
4472
+ const Database$1 = (await dynamicImport("bun:sqlite")).Database;
4473
+ return (databasePath) => new Database$1(databasePath);
4474
+ }
4475
+ const Database = (await import("better-sqlite3")).default;
4476
+ return (databasePath) => new Database(databasePath);
4477
+ }
4478
+ const dynamicImport = new Function("specifier", "return import(specifier)");
4479
+ function isBunRuntime() {
4480
+ return typeof process.versions.bun === "string";
4481
+ }
4482
+ function ensureTargetNodesJsonColumn(database) {
4483
+ if (!database.prepare("PRAGMA table_info(translation_cache_entries)").all().some((row) => {
4484
+ if (!row || typeof row !== "object") return false;
4485
+ return row.name === "target_nodes_json";
4486
+ })) database.exec("ALTER TABLE translation_cache_entries ADD COLUMN target_nodes_json TEXT");
4487
+ }
4488
+ function isSqliteTranslationCacheRow(value) {
4489
+ if (!value || typeof value !== "object") return false;
4490
+ const row = value;
4491
+ return typeof row.key_hash === "string" && typeof row.cache_key === "string" && typeof row.source_text === "string" && typeof row.translated_text === "string" && (typeof row.target_nodes_json === "string" || row.target_nodes_json === null) && typeof row.source_language === "string" && typeof row.target_language === "string" && typeof row.placeholder_topology_hash === "string" && typeof row.attribute_topology_hash === "string" && typeof row.display_policy_version === "number" && typeof row.created_at === "number" && typeof row.last_accessed_at === "number";
4492
+ }
4493
+ function readSqliteCount(value) {
4494
+ if (!value || typeof value !== "object") return 0;
4495
+ const count = value.count;
4496
+ return typeof count === "number" ? count : 0;
4497
+ }
4498
+
4499
+ //#endregion
4500
+ //#region src/translation-cache-path.ts
4501
+ function getDefaultTranslationCacheDatabasePath() {
4502
+ return join(getOpenSpecUICacheDir(), "translation-cache.sqlite");
4503
+ }
4504
+ function getOpenSpecUICacheDir() {
4505
+ const currentPlatform = platform();
4506
+ if (currentPlatform === "darwin") return join(homedir(), "Library", "Caches", "openspecui");
4507
+ if (currentPlatform === "win32") return join(process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "OpenSpecUI", "Cache");
4508
+ return join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "openspecui");
4509
+ }
4510
+
4511
+ //#endregion
4512
+ //#region src/translation-cache-service.ts
4513
+ var TranslationCacheService = class {
4514
+ configManager;
4515
+ globalSettingsManager;
4516
+ adapter;
4517
+ now;
4518
+ onWriteError;
4519
+ constructor(options) {
4520
+ this.configManager = options.configManager;
4521
+ this.globalSettingsManager = options.globalSettingsManager;
4522
+ this.adapter = options.adapter;
4523
+ this.now = options.now ?? Date.now;
4524
+ this.onWriteError = options.onWriteError ?? (() => void 0);
4525
+ }
4526
+ async getStats() {
4527
+ const [{ translation }, globalSettings] = await Promise.all([this.configManager.readConfig(), this.globalSettingsManager.readSettings()]);
4528
+ const entryLimit = globalSettings.translationCache.entryLimit;
4529
+ const enabled = translation.cacheEnabled;
4530
+ return {
4531
+ enabled,
4532
+ entryLimit,
4533
+ entries: enabled ? await this.adapter.count() : 0,
4534
+ ...this.adapter.databasePath ? { databasePath: this.adapter.databasePath } : {}
4535
+ };
4536
+ }
4537
+ async read(keyHash) {
4538
+ if (!(await this.configManager.readConfig()).translation.cacheEnabled) return null;
4539
+ try {
4540
+ return await this.adapter.read(keyHash, this.now());
4541
+ } catch {
4542
+ return null;
4543
+ }
4544
+ }
4545
+ async write(input) {
4546
+ const [{ translation }, globalSettings] = await Promise.all([this.configManager.readConfig(), this.globalSettingsManager.readSettings()]);
4547
+ if (!translation.cacheEnabled) return { accepted: false };
4548
+ this.writeAndClean(input, globalSettings.translationCache.entryLimit);
4549
+ return { accepted: true };
4550
+ }
4551
+ async clean() {
4552
+ const globalSettings = await this.globalSettingsManager.readSettings();
4553
+ return this.adapter.clean(globalSettings.translationCache.entryLimit);
4554
+ }
4555
+ async clear() {
4556
+ return { deleted: await this.adapter.clear() };
4557
+ }
4558
+ close() {
4559
+ this.adapter.close?.();
4560
+ }
4561
+ async writeAndClean(input, entryLimit) {
4562
+ try {
4563
+ await this.adapter.write(input, this.now());
4564
+ if (await this.adapter.count() >= Math.floor(entryLimit * .9)) await this.adapter.clean(entryLimit);
4565
+ } catch (error) {
4566
+ this.onWriteError(error);
4567
+ }
4568
+ }
4569
+ };
4570
+
4309
4571
  //#endregion
4310
4572
  //#region src/workflow-invocation-service.ts
4311
4573
  const COMMAND_CAPABLE_ACTIONS = new Set([
@@ -4546,12 +4808,21 @@ const SERVER_PACKAGE_VERSION = getServerPackageVersion();
4546
4808
  function buildEmbeddedUiUrlForPort(port) {
4547
4809
  return `http://localhost:${port}`;
4548
4810
  }
4811
+ function initializeWatcherPoolInBackground(projectDir) {
4812
+ initWatcherPool(projectDir).catch((err) => {
4813
+ console.error("Watcher pool initialization failed:", err);
4814
+ });
4815
+ }
4816
+ function deferBackgroundTask(task) {
4817
+ setTimeout(task, 0);
4818
+ }
4549
4819
  /**
4550
4820
  * Create an OpenSpecUI HTTP server with optional WebSocket support
4551
4821
  */
4552
4822
  function createServer(config) {
4553
4823
  const adapter = new OpenSpecAdapter(config.projectDir);
4554
4824
  const configManager = new ConfigManager(config.projectDir);
4825
+ const globalSettingsManager = new GlobalSettingsManager();
4555
4826
  const cliExecutor = new CliExecutor(configManager, config.projectDir);
4556
4827
  const kernel = config.kernel;
4557
4828
  const hookRuntime = createHookRuntime(config.projectDir);
@@ -4563,6 +4834,31 @@ function createServer(config) {
4563
4834
  });
4564
4835
  const notificationService = new NotificationService();
4565
4836
  const customSoundService = new CustomSoundService();
4837
+ let translationCacheAdapterPromise = null;
4838
+ const getTranslationCacheAdapter = () => {
4839
+ translationCacheAdapterPromise ??= createRuntimeSqliteTranslationCacheAdapter(getDefaultTranslationCacheDatabasePath());
4840
+ return translationCacheAdapterPromise;
4841
+ };
4842
+ const translationCacheService = new TranslationCacheService({
4843
+ configManager,
4844
+ globalSettingsManager,
4845
+ adapter: {
4846
+ databasePath: getDefaultTranslationCacheDatabasePath(),
4847
+ init: async () => (await getTranslationCacheAdapter()).init(),
4848
+ read: async (keyHash, now) => (await getTranslationCacheAdapter()).read(keyHash, now),
4849
+ write: async (input, now) => (await getTranslationCacheAdapter()).write(input, now),
4850
+ count: async () => (await getTranslationCacheAdapter()).count(),
4851
+ deleteLeastRecentlyUsed: async (targetEntryCount) => (await getTranslationCacheAdapter()).deleteLeastRecentlyUsed(targetEntryCount),
4852
+ clean: async (entryLimit) => (await getTranslationCacheAdapter()).clean(entryLimit),
4853
+ clear: async () => (await getTranslationCacheAdapter()).clear(),
4854
+ close: () => {
4855
+ translationCacheAdapterPromise?.then((cacheAdapter) => cacheAdapter.close()).catch(() => {});
4856
+ }
4857
+ },
4858
+ onWriteError(error) {
4859
+ console.warn("Translation cache write failed:", error);
4860
+ }
4861
+ });
4566
4862
  const watcher = config.enableWatcher !== false ? new OpenSpecWatcher(config.projectDir) : void 0;
4567
4863
  const entityReadOptionsContext = {
4568
4864
  adapter,
@@ -4585,15 +4881,13 @@ function createServer(config) {
4585
4881
  credentials: true
4586
4882
  }));
4587
4883
  app.get("/api/health", (c) => {
4588
- return c.json({
4589
- status: "ok",
4884
+ return c.json(buildBackendHealthPayload({
4590
4885
  projectDir: config.projectDir,
4591
4886
  projectName: basename(config.projectDir) || config.projectDir,
4592
4887
  watcherEnabled: !!watcher,
4593
4888
  openspecuiVersion: SERVER_PACKAGE_VERSION,
4594
- hostedShellProtocolVersion: HOSTED_SHELL_PROTOCOL_VERSION,
4595
4889
  embeddedUiUrl: buildEmbeddedUiUrlForPort(config.port ?? 3100)
4596
- });
4890
+ }));
4597
4891
  });
4598
4892
  app.post("/api/notifications", async (c) => {
4599
4893
  const body = await c.req.json().catch(() => null);
@@ -4644,6 +4938,8 @@ function createServer(config) {
4644
4938
  projectRecoveryService,
4645
4939
  notificationService,
4646
4940
  customSoundService,
4941
+ globalSettingsManager,
4942
+ translationCacheService,
4647
4943
  gitWorktreeHandoff: config.gitWorktreeHandoff,
4648
4944
  watcher,
4649
4945
  projectDir: config.projectDir
@@ -4662,6 +4958,8 @@ function createServer(config) {
4662
4958
  projectRecoveryService,
4663
4959
  notificationService,
4664
4960
  customSoundService,
4961
+ globalSettingsManager,
4962
+ translationCacheService,
4665
4963
  gitWorktreeHandoff: config.gitWorktreeHandoff,
4666
4964
  watcher,
4667
4965
  projectDir: config.projectDir
@@ -4679,6 +4977,8 @@ function createServer(config) {
4679
4977
  projectRecoveryService,
4680
4978
  notificationService,
4681
4979
  customSoundService,
4980
+ globalSettingsManager,
4981
+ translationCacheService,
4682
4982
  hookRuntime,
4683
4983
  watcher,
4684
4984
  createContext,
@@ -4689,7 +4989,7 @@ function createServer(config) {
4689
4989
  * Create WebSocket server for tRPC subscriptions and PTY terminals
4690
4990
  */
4691
4991
  async function createWebSocketServer(server, httpServer, config) {
4692
- if (!isWatcherPoolInitialized()) await initWatcherPool(config.projectDir);
4992
+ if (!isWatcherPoolInitialized()) deferBackgroundTask(() => initializeWatcherPoolInBackground(config.projectDir));
4693
4993
  const wss = new WebSocketServer({ noServer: true });
4694
4994
  const handler = applyWSSHandler({
4695
4995
  wss,
@@ -4729,6 +5029,7 @@ async function createWebSocketServer(server, httpServer, config) {
4729
5029
  server.searchService.dispose().catch(() => {});
4730
5030
  server.dashboardOverviewService.dispose();
4731
5031
  server.projectRecoveryService.dispose();
5032
+ server.translationCacheService.close();
4732
5033
  }
4733
5034
  };
4734
5035
  }
@@ -4745,7 +5046,7 @@ async function startServer(config, setupApp) {
4745
5046
  const port = await findAvailablePort(preferredPort);
4746
5047
  const cliExecutor = new CliExecutor(new ConfigManager(config.projectDir), config.projectDir);
4747
5048
  const kernel = new OpsxKernel(config.projectDir, cliExecutor);
4748
- await initWatcherPool(config.projectDir);
5049
+ deferBackgroundTask(() => initializeWatcherPoolInBackground(config.projectDir));
4749
5050
  const server = createServer({
4750
5051
  ...config,
4751
5052
  port,
@@ -4758,14 +5059,16 @@ async function startServer(config, setupApp) {
4758
5059
  });
4759
5060
  const wsServer = await createWebSocketServer(server, httpServer, { projectDir: config.projectDir });
4760
5061
  const url = `http://localhost:${port}`;
4761
- kernel.warmup().catch((err) => {
4762
- console.error("Kernel warmup failed:", err);
4763
- });
4764
- server.searchService.init().catch((err) => {
4765
- console.error("Search service warmup failed:", err);
4766
- });
4767
- server.dashboardOverviewService.init().catch((err) => {
4768
- console.error("Dashboard overview warmup failed:", err);
5062
+ deferBackgroundTask(() => {
5063
+ kernel.warmup().catch((err) => {
5064
+ console.error("Kernel warmup failed:", err);
5065
+ });
5066
+ server.searchService.init().catch((err) => {
5067
+ console.error("Search service warmup failed:", err);
5068
+ });
5069
+ server.dashboardOverviewService.init().catch((err) => {
5070
+ console.error("Dashboard overview warmup failed:", err);
5071
+ });
4769
5072
  });
4770
5073
  return {
4771
5074
  url,
@@ -4774,6 +5077,7 @@ async function startServer(config, setupApp) {
4774
5077
  close: async () => {
4775
5078
  kernel.dispose();
4776
5079
  await server.hookRuntime.dispose();
5080
+ server.translationCacheService.close();
4777
5081
  wsServer.close();
4778
5082
  httpServer.close();
4779
5083
  }
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@openspecui/server",
3
- "version": "3.7.0",
3
+ "version": "3.7.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.mjs",
6
6
  "exports": {
7
7
  ".": {
8
- "import": "./dist/index.mjs"
8
+ "development": "./src/index.ts",
9
+ "import": "./dist/index.mjs",
10
+ "default": "./dist/index.mjs"
9
11
  }
10
12
  },
11
13
  "files": [
@@ -14,7 +16,7 @@
14
16
  "scripts": {
15
17
  "build": "tsdown src/index.ts --format esm --no-dts",
16
18
  "typecheck": "tsc -p tsconfig.check.json --noEmit",
17
- "dev": "tsx watch --include '../core/dist/**' --include '../search/dist/**' src/standalone.ts",
19
+ "dev": "NODE_OPTIONS=\"${NODE_OPTIONS:+$NODE_OPTIONS }--conditions=development\" tsx watch --include '../core/dist/**' --include '../search/dist/**' src/standalone.ts",
18
20
  "test": "vitest run",
19
21
  "test:watch": "vitest"
20
22
  },
@@ -24,6 +26,8 @@
24
26
  "@openspecui/core": "workspace:*",
25
27
  "@openspecui/search": "workspace:*",
26
28
  "@trpc/server": "^11.0.0",
29
+ "@types/better-sqlite3": "^7.6.13",
30
+ "better-sqlite3": "^12.5.0",
27
31
  "hono": "^4.7.3",
28
32
  "tsx": "^4.19.2",
29
33
  "ws": "^8.18.0",