@openspecui/server 3.6.1 → 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 +501 -83
  2. package/package.json +7 -3
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, 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, resolveTerminalShellDefaults, sniffGlobalCli, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput } from "@openspecui/core";
2
- import { basename, dirname, join, relative, resolve, sep } from "node:path";
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
+ 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";
5
5
  import { createServer as createServer$1 } from "node:net";
@@ -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";
@@ -35,6 +35,9 @@ function toErrorDiagnostic$1(error) {
35
35
  function isNotNull(value) {
36
36
  return value !== null;
37
37
  }
38
+ function normalizeChangeFilePath(path) {
39
+ return path.replace(/\\/g, "/").replace(/^\/+/, "");
40
+ }
38
41
  var DocumentService = class {
39
42
  parser = new MarkdownParser();
40
43
  constructor(projectDir, adapter, hookRuntime) {
@@ -176,37 +179,125 @@ var DocumentService = class {
176
179
  return null;
177
180
  }
178
181
  }
182
+ async readEntityDetail(stage, changeId, consumer = "view", mode = "processed", options = {}) {
183
+ const detail = await this.adapter.readEntityDetail(stage, changeId, options);
184
+ if (!detail) return null;
185
+ const root = stage === "change" ? `openspec/changes/${changeId}` : `openspec/changes/archive/${changeId}`;
186
+ const processedByPath = /* @__PURE__ */ new Map();
187
+ const processArtifactFile = async (artifact, file) => {
188
+ const artifactFile = {
189
+ ...await this.processEntityFile({
190
+ stage,
191
+ changeId,
192
+ root,
193
+ file,
194
+ consumer,
195
+ mode,
196
+ schemaName: detail.schemaName,
197
+ artifactId: artifact.id,
198
+ artifactOutputPath: artifact.outputPath
199
+ }),
200
+ type: "file"
201
+ };
202
+ processedByPath.set(file.path, artifactFile);
203
+ return artifactFile;
204
+ };
205
+ const artifacts = await Promise.all(detail.artifacts.map(async (artifact) => ({
206
+ ...artifact,
207
+ files: await Promise.all(artifact.files.map((file) => processArtifactFile(artifact, file)))
208
+ })));
209
+ const files = await Promise.all(detail.files.map(async (file) => {
210
+ const processed = processedByPath.get(file.path);
211
+ if (processed) return processed;
212
+ return this.processEntityFile({
213
+ stage,
214
+ changeId,
215
+ root,
216
+ file,
217
+ consumer,
218
+ mode,
219
+ schemaName: detail.schemaName
220
+ });
221
+ }));
222
+ const filesByPath = new Map(files.map((file) => [file.path, file]));
223
+ const ungroupedFiles = detail.ungroupedFiles.map((file) => filesByPath.get(file.path) ?? file);
224
+ return {
225
+ ...detail,
226
+ files,
227
+ artifacts,
228
+ ungroupedFiles
229
+ };
230
+ }
179
231
  async readChangeFiles(changeId, consumer = "view", mode = "processed") {
180
232
  const files = await this.adapter.readChangeFiles(changeId);
181
233
  return this.processChangeFiles("change", changeId, files, consumer, mode);
182
234
  }
235
+ async readChangeArtifactOutput(changeId, outputPath, consumer = "view", mode = "processed") {
236
+ const normalizedPath = normalizeChangeFilePath(outputPath);
237
+ return (await this.readChangeArtifactFiles(changeId, normalizedPath, consumer, mode)).find((file) => file.path === normalizedPath)?.content ?? null;
238
+ }
239
+ async readChangeGlobArtifactFiles(changeId, outputPath, consumer = "view", mode = "processed") {
240
+ const normalizedPattern = normalizeChangeFilePath(outputPath);
241
+ return this.readChangeArtifactFiles(changeId, normalizedPattern, consumer, mode);
242
+ }
183
243
  async readArchivedChangeFiles(changeId, consumer = "view", mode = "processed") {
184
244
  const files = await this.adapter.readArchivedChangeFiles(changeId);
185
245
  return this.processChangeFiles("archive", changeId, files, consumer, mode);
186
246
  }
187
247
  async processChangeFiles(stage, changeId, files, consumer, mode) {
188
248
  const root = stage === "change" ? `openspec/changes/${changeId}` : `openspec/changes/archive/${changeId}`;
189
- return (await Promise.all(files.map(async (file) => {
190
- if (file.type !== "file" || file.content === void 0 || !file.path.endsWith(".md")) return file;
191
- const kind = this.inferChangeFileKind(file.path);
192
- if (!kind) return file;
193
- const result = await this.processDocument({
194
- consumer,
195
- mode,
196
- document: {
197
- stage,
198
- kind,
199
- changeId,
200
- relativePath: `${root}/${file.path}`,
201
- absolutePath: join(this.projectDir, root, file.path)
202
- },
203
- source: file.content
204
- });
205
- return {
206
- ...file,
207
- content: result.markdown
208
- };
209
- }))).filter(isNotNull);
249
+ return (await Promise.all(files.map((file) => this.processChangeFile(stage, changeId, root, file, consumer, mode)))).filter(isNotNull);
250
+ }
251
+ async readChangeArtifactFiles(changeId, outputPath, consumer, mode) {
252
+ const matchingFiles = (await this.adapter.readChangeFiles(changeId)).filter((file) => {
253
+ if (file.type !== "file" || file.content === void 0) return false;
254
+ return matchesGlob(file.path, outputPath) || file.path === outputPath;
255
+ });
256
+ const root = `openspec/changes/${changeId}`;
257
+ return (await Promise.all(matchingFiles.map((file) => this.processChangeFile("change", changeId, root, file, consumer, mode)))).filter(isNotNull).filter((file) => file.type === "file" && file.content !== void 0);
258
+ }
259
+ async processChangeFile(stage, changeId, root, file, consumer, mode) {
260
+ if (file.type !== "file" || file.content === void 0 || !file.path.endsWith(".md")) return file;
261
+ const kind = this.inferChangeFileKind(file.path);
262
+ if (!kind) return file;
263
+ const result = await this.processDocument({
264
+ consumer,
265
+ mode,
266
+ document: {
267
+ stage,
268
+ kind,
269
+ changeId,
270
+ relativePath: `${root}/${file.path}`,
271
+ absolutePath: join(this.projectDir, root, file.path)
272
+ },
273
+ source: file.content
274
+ });
275
+ return {
276
+ ...file,
277
+ content: result.markdown
278
+ };
279
+ }
280
+ async processEntityFile(input) {
281
+ if (input.file.type !== "file" || input.file.content === void 0 || !input.file.path.endsWith(".md")) return input.file;
282
+ const result = await this.processDocument({
283
+ consumer: input.consumer,
284
+ mode: input.mode,
285
+ document: {
286
+ stage: input.stage,
287
+ kind: "artifact",
288
+ changeId: input.changeId,
289
+ schemaName: input.schemaName,
290
+ artifactId: input.artifactId,
291
+ artifactOutputPath: input.artifactOutputPath,
292
+ relativePath: `${input.root}/${input.file.path}`,
293
+ absolutePath: join(this.projectDir, input.root, input.file.path)
294
+ },
295
+ source: input.file.content
296
+ });
297
+ return {
298
+ ...input.file,
299
+ content: result.markdown
300
+ };
210
301
  }
211
302
  inferChangeFileKind(path) {
212
303
  if (path === "proposal.md") return "proposal";
@@ -1194,16 +1285,11 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
1194
1285
  updatedAt: changeMeta.updatedAt
1195
1286
  }));
1196
1287
  const activeChanges = selectRecentDashboardItems(allActiveChanges);
1197
- const archivedChanges = (await Promise.all(archiveMetas.map(async (meta) => {
1198
- const change = await ctx.adapter.readArchivedChange(meta.id);
1199
- if (!change) return null;
1200
- return {
1201
- id: meta.id,
1202
- createdAt: meta.createdAt,
1203
- updatedAt: meta.updatedAt,
1204
- tasksCompleted: change.tasks.filter((task) => task.completed).length
1205
- };
1206
- }))).filter((item) => item !== null);
1288
+ const archivedChanges = archiveMetas.map((meta) => ({
1289
+ id: meta.id,
1290
+ createdAt: meta.createdAt,
1291
+ updatedAt: meta.updatedAt
1292
+ }));
1207
1293
  const allSpecifications = (await Promise.all(specMetas.map(async (meta) => {
1208
1294
  const spec = await ctx.adapter.readSpec(meta.id);
1209
1295
  if (!spec) return null;
@@ -1218,7 +1304,7 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
1218
1304
  const requirements = allSpecifications.reduce((sum, spec) => sum + spec.requirements, 0);
1219
1305
  const tasksTotal = allActiveChanges.reduce((sum, change) => sum + change.progress.total, 0);
1220
1306
  const tasksCompleted = allActiveChanges.reduce((sum, change) => sum + change.progress.completed, 0);
1221
- const archivedTasksCompleted = archivedChanges.reduce((sum, change) => sum + change.tasksCompleted, 0);
1307
+ const archivedTasksCompleted = 0;
1222
1308
  const taskCompletionPercent = tasksTotal > 0 ? Math.round(tasksCompleted / tasksTotal * 100) : null;
1223
1309
  const inProgressChanges = allActiveChanges.filter((change) => change.progress.total > 0 && change.progress.completed < change.progress.total).length;
1224
1310
  const specificationTrendEvents = specMetas.flatMap((spec) => {
@@ -1232,7 +1318,7 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
1232
1318
  const ts = parseDatedIdTimestamp(archive.id) ?? resolveTrendTimestamp(archive.updatedAt, archive.createdAt);
1233
1319
  return ts === null ? [] : [{
1234
1320
  ts,
1235
- value: archive.tasksCompleted
1321
+ value: 1
1236
1322
  }];
1237
1323
  });
1238
1324
  const specMetaById = new Map(specMetas.map((meta) => [meta.id, meta]));
@@ -1339,6 +1425,29 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
1339
1425
  };
1340
1426
  }
1341
1427
 
1428
+ //#endregion
1429
+ //#region src/entity-read-options.ts
1430
+ async function readEntityMetadata(ctx, stage, id) {
1431
+ return (stage === "change" ? await ctx.adapter.readChangeFiles(id) : await ctx.adapter.readArchivedChangeFiles(id)).find((file) => file.type === "file" && file.path === ".openspec.yaml")?.content ?? null;
1432
+ }
1433
+ async function buildEntityReadOptions(ctx, stage, id) {
1434
+ const schemaName = parseOpsxEntityMetadata(await readEntityMetadata(ctx, stage, id)).schemaName;
1435
+ if (!schemaName) return {};
1436
+ try {
1437
+ await ctx.kernel.waitForWarmup();
1438
+ await ctx.kernel.ensureSchemaDetail(schemaName);
1439
+ await ctx.kernel.ensureSchemaYaml(schemaName);
1440
+ const schemaYaml = ctx.kernel.getSchemaYaml(schemaName);
1441
+ const diagnostics = schemaYaml ? parseOpsxSchemaDetail(schemaYaml, schemaName, { path: `schema:${schemaName}` }).diagnostics : [];
1442
+ return {
1443
+ schemas: { [schemaName]: ctx.kernel.getSchemaDetail(schemaName) },
1444
+ schemaDiagnostics: diagnostics.length > 0 ? { [schemaName]: diagnostics } : void 0
1445
+ };
1446
+ } catch {
1447
+ return { schemas: {} };
1448
+ }
1449
+ }
1450
+
1342
1451
  //#endregion
1343
1452
  //#region src/notification-service.ts
1344
1453
  var NotificationService = class {
@@ -1619,8 +1728,8 @@ function detectPtyPlatform() {
1619
1728
  if (process.platform === "darwin") return "macos";
1620
1729
  return "common";
1621
1730
  }
1622
- function resolveDefaultShell(platform, env) {
1623
- 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";
1624
1733
  return env.SHELL?.trim() || "/bin/sh";
1625
1734
  }
1626
1735
  function resolvePtyShellDefaults(opts) {
@@ -2724,6 +2833,20 @@ async function buildGitWorktreeOverview(options) {
2724
2833
  };
2725
2834
  });
2726
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
+ }
2727
2850
  async function listCurrentWorktreeGitEntries(options) {
2728
2851
  const resolvedProjectDir = resolve(options.projectDir);
2729
2852
  const limit = clampEntryLimit(options.limit);
@@ -2958,6 +3081,35 @@ const soundsRouter = router({
2958
3081
  return { success: true };
2959
3082
  })
2960
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
+ });
2961
3113
  const OPSX_CORE_PROFILE_WORKFLOWS = [
2962
3114
  "propose",
2963
3115
  "explore",
@@ -3283,19 +3435,21 @@ const archiveRouter = router({
3283
3435
  return ctx.adapter.listArchivedChangesWithMeta();
3284
3436
  }),
3285
3437
  get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
3286
- return ctx.documentService.readArchivedChange(input.id);
3438
+ return ctx.documentService.readEntityDetail("archive", input.id, "view", "processed", await buildEntityReadOptions(ctx, "archive", input.id));
3287
3439
  }),
3288
3440
  getRaw: publicProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
3289
- return ctx.adapter.readArchivedChangeRaw(input.id);
3441
+ return ctx.documentService.readEntityDetail("archive", input.id, "view", "source", await buildEntityReadOptions(ctx, "archive", input.id));
3290
3442
  }),
3291
3443
  subscribe: publicProcedure.subscription(({ ctx }) => {
3292
3444
  return createReactiveSubscription(() => ctx.adapter.listArchivedChangesWithMeta());
3293
3445
  }),
3294
3446
  subscribeOne: publicProcedure.input(z.object({ id: z.string() })).subscription(({ ctx, input }) => {
3295
- return createReactiveSubscriptionWithInput((id) => ctx.documentService.readArchivedChange(id))(input.id);
3447
+ return createReactiveSubscriptionWithInput(async (id) => ctx.documentService.readEntityDetail("archive", id, "view", "processed", await buildEntityReadOptions(ctx, "archive", id)))(input.id);
3296
3448
  }),
3297
3449
  subscribeFiles: publicProcedure.input(z.object({ id: z.string() })).subscription(({ ctx, input }) => {
3298
- return createReactiveSubscriptionWithInput((id) => ctx.adapter.readArchivedChangeFiles(id))(input.id);
3450
+ return createReactiveSubscriptionWithInput(async (id) => {
3451
+ return (await ctx.documentService.readEntityDetail("archive", id, "view", "processed", await buildEntityReadOptions(ctx, "archive", id)))?.files ?? [];
3452
+ })(input.id);
3299
3453
  })
3300
3454
  });
3301
3455
  z.object({
@@ -3397,13 +3551,14 @@ const configRouter = router({
3397
3551
  terminal: TerminalConfigSchema.omit({ rendererEngine: true }).partial().extend({ rendererEngine: TerminalRendererEngineSchema.optional() }).optional(),
3398
3552
  dashboard: DashboardConfigSchema.partial().optional(),
3399
3553
  git: GitConfigSchema.partial().optional(),
3400
- notifications: NotificationSettingsSchema.partial().optional()
3554
+ notifications: NotificationSettingsSchema.partial().optional(),
3555
+ translation: DocumentTranslationConfigSchema.partial().optional()
3401
3556
  })).mutation(async ({ ctx, input }) => {
3402
3557
  const hasCliCommand = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "command");
3403
3558
  const hasCliArgs = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "args");
3404
3559
  if (hasCliCommand && !hasCliArgs) {
3405
3560
  await ctx.configManager.setCliCommand(input.cli?.command ?? "");
3406
- if (input.theme !== void 0 || input.codeEditor !== void 0 || input.appBaseUrl !== void 0 || input.opsx !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0 || input.git !== void 0 || input.notifications !== void 0) await ctx.configManager.writeConfig({
3561
+ if (input.theme !== void 0 || input.codeEditor !== void 0 || input.appBaseUrl !== void 0 || input.opsx !== void 0 || input.terminal !== void 0 || input.dashboard !== void 0 || input.git !== void 0 || input.notifications !== void 0 || input.translation !== void 0) await ctx.configManager.writeConfig({
3407
3562
  theme: input.theme,
3408
3563
  codeEditor: input.codeEditor,
3409
3564
  appBaseUrl: input.appBaseUrl,
@@ -3411,7 +3566,8 @@ const configRouter = router({
3411
3566
  terminal: input.terminal,
3412
3567
  dashboard: input.dashboard,
3413
3568
  git: input.git,
3414
- notifications: input.notifications
3569
+ notifications: input.notifications,
3570
+ translation: input.translation
3415
3571
  });
3416
3572
  return { success: true };
3417
3573
  }
@@ -3820,7 +3976,7 @@ const opsxRouter = router({
3820
3976
  })).query(async ({ ctx, input }) => {
3821
3977
  await ctx.kernel.waitForWarmup();
3822
3978
  await ctx.kernel.ensureArtifactOutput(input.changeId, input.outputPath);
3823
- return ctx.kernel.getArtifactOutput(input.changeId, input.outputPath);
3979
+ return ctx.documentService.readChangeArtifactOutput(input.changeId, input.outputPath, "view", "processed");
3824
3980
  }),
3825
3981
  subscribeArtifactOutput: publicProcedure.input(z.object({
3826
3982
  changeId: z.string(),
@@ -3829,7 +3985,7 @@ const opsxRouter = router({
3829
3985
  return createReactiveSubscription(async () => {
3830
3986
  await ctx.kernel.waitForWarmup();
3831
3987
  await ctx.kernel.ensureArtifactOutput(input.changeId, input.outputPath);
3832
- return ctx.kernel.getArtifactOutput(input.changeId, input.outputPath);
3988
+ return ctx.documentService.readChangeArtifactOutput(input.changeId, input.outputPath, "view", "processed");
3833
3989
  });
3834
3990
  }),
3835
3991
  readGlobArtifactFiles: publicProcedure.input(z.object({
@@ -3838,7 +3994,7 @@ const opsxRouter = router({
3838
3994
  })).query(async ({ ctx, input }) => {
3839
3995
  await ctx.kernel.waitForWarmup();
3840
3996
  await ctx.kernel.ensureGlobArtifactFiles(input.changeId, input.outputPath);
3841
- return ctx.kernel.getGlobArtifactFiles(input.changeId, input.outputPath);
3997
+ return ctx.documentService.readChangeGlobArtifactFiles(input.changeId, input.outputPath, "view", "processed");
3842
3998
  }),
3843
3999
  subscribeGlobArtifactFiles: publicProcedure.input(z.object({
3844
4000
  changeId: z.string(),
@@ -3847,7 +4003,7 @@ const opsxRouter = router({
3847
4003
  return createReactiveSubscription(async () => {
3848
4004
  await ctx.kernel.waitForWarmup();
3849
4005
  await ctx.kernel.ensureGlobArtifactFiles(input.changeId, input.outputPath);
3850
- return ctx.kernel.getGlobArtifactFiles(input.changeId, input.outputPath);
4006
+ return ctx.documentService.readChangeGlobArtifactFiles(input.changeId, input.outputPath, "view", "processed");
3851
4007
  });
3852
4008
  }),
3853
4009
  writeArtifactOutput: publicProcedure.input(z.object({
@@ -4028,14 +4184,10 @@ const gitRouter = router({
4028
4184
  }),
4029
4185
  switchWorktree: publicProcedure.input(z.object({ path: z.string().min(1) })).mutation(async ({ ctx, input }) => {
4030
4186
  if (!ctx.gitWorktreeHandoff) throw new Error("Worktree handoff is unavailable in this runtime.");
4031
- const overview = await buildGitWorktreeOverview({ projectDir: ctx.projectDir });
4032
- const resolvedInputPath = resolve(input.path);
4033
- let target = null;
4034
- if (overview.currentWorktree && await sameGitPath(overview.currentWorktree.path, resolvedInputPath)) target = overview.currentWorktree;
4035
- else for (const worktree of overview.otherWorktrees) if (await sameGitPath(worktree.path, resolvedInputPath)) {
4036
- target = worktree;
4037
- break;
4038
- }
4187
+ const target = await resolveGitWorktreeSwitchTarget({
4188
+ projectDir: ctx.projectDir,
4189
+ targetPath: input.path
4190
+ });
4039
4191
  if (!target) throw new Error("Worktree not found.");
4040
4192
  if (!target.pathAvailable) throw new Error("Worktree path is no longer available. Remove the stale worktree entry first.");
4041
4193
  return ctx.gitWorktreeHandoff.ensureWorktreeServer({ targetPath: target.path });
@@ -4053,6 +4205,8 @@ const appRouter = router({
4053
4205
  init: initRouter,
4054
4206
  realtime: realtimeRouter,
4055
4207
  config: configRouter,
4208
+ globalSettings: globalSettingsRouter,
4209
+ translationCache: translationCacheRouter,
4056
4210
  notifications: notificationsRouter,
4057
4211
  sounds: soundsRouter,
4058
4212
  cli: cliRouter,
@@ -4067,7 +4221,7 @@ const appRouter = router({
4067
4221
  function joinParts(parts) {
4068
4222
  return parts.map((part) => part?.trim() ?? "").filter((part) => part.length > 0).join("\n\n");
4069
4223
  }
4070
- async function collectSearchDocuments(adapter, documentService) {
4224
+ async function collectSearchDocuments(adapter, documentService, resolveEntityReadOptions) {
4071
4225
  const docs = [];
4072
4226
  const specs = await adapter.listSpecsWithMeta();
4073
4227
  for (const spec of specs) {
@@ -4104,20 +4258,16 @@ async function collectSearchDocuments(adapter, documentService) {
4104
4258
  }
4105
4259
  const archives = await adapter.listArchivedChangesWithMeta();
4106
4260
  for (const archive of archives) {
4107
- const raw = documentService ? await documentService.readArchivedChangeRaw(archive.id, "search", "processed") : await adapter.readArchivedChangeRaw(archive.id);
4108
- if (!raw) continue;
4261
+ const entityOptions = resolveEntityReadOptions ? await resolveEntityReadOptions("archive", archive.id) : void 0;
4262
+ const entity = documentService ? await documentService.readEntityDetail("archive", archive.id, "search", "processed", entityOptions) : await adapter.readEntityDetail("archive", archive.id);
4263
+ if (!entity) continue;
4109
4264
  docs.push({
4110
4265
  id: `archive:${archive.id}`,
4111
4266
  kind: "archive",
4112
4267
  title: archive.name,
4113
4268
  href: `/archive/${encodeURIComponent(archive.id)}`,
4114
4269
  path: `openspec/changes/archive/${archive.id}`,
4115
- content: joinParts([
4116
- typeof raw.proposal === "string" ? raw.proposal : raw.proposal.markdown,
4117
- typeof raw.tasks === "string" ? raw.tasks : raw.tasks.markdown,
4118
- typeof raw.design === "string" ? raw.design : raw.design?.markdown,
4119
- ...raw.deltaSpecs.map((deltaSpec) => deltaSpec.content)
4120
- ]),
4270
+ content: joinParts(entity.files.filter((file) => file.type === "file").map((file) => file.content)),
4121
4271
  updatedAt: archive.updatedAt
4122
4272
  });
4123
4273
  }
@@ -4133,9 +4283,10 @@ var SearchService = class {
4133
4283
  initPromise = null;
4134
4284
  rebuildPromise = null;
4135
4285
  rebuildTimer = null;
4136
- constructor(adapter, watcher, provider = new NodeWorkerSearchProvider(), documentService) {
4286
+ constructor(adapter, watcher, provider = new NodeWorkerSearchProvider(), documentService, resolveEntityReadOptions) {
4137
4287
  this.adapter = adapter;
4138
4288
  this.documentService = documentService;
4289
+ this.resolveEntityReadOptions = resolveEntityReadOptions;
4139
4290
  this.provider = provider;
4140
4291
  watcher?.on("change", () => {
4141
4292
  this.scheduleRebuild();
@@ -4181,7 +4332,7 @@ var SearchService = class {
4181
4332
  if (!forceInit && !this.initialized) return;
4182
4333
  if (this.rebuildPromise) return this.rebuildPromise;
4183
4334
  this.rebuildPromise = (async () => {
4184
- const docs = await collectSearchDocuments(this.adapter, this.documentService);
4335
+ const docs = await collectSearchDocuments(this.adapter, this.documentService, this.resolveEntityReadOptions);
4185
4336
  if (this.initialized) await this.provider.replaceAll(docs);
4186
4337
  else {
4187
4338
  await this.provider.init(docs);
@@ -4196,6 +4347,227 @@ var SearchService = class {
4196
4347
  }
4197
4348
  };
4198
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
+
4199
4571
  //#endregion
4200
4572
  //#region src/workflow-invocation-service.ts
4201
4573
  const COMMAND_CAPABLE_ACTIONS = new Set([
@@ -4436,12 +4808,21 @@ const SERVER_PACKAGE_VERSION = getServerPackageVersion();
4436
4808
  function buildEmbeddedUiUrlForPort(port) {
4437
4809
  return `http://localhost:${port}`;
4438
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
+ }
4439
4819
  /**
4440
4820
  * Create an OpenSpecUI HTTP server with optional WebSocket support
4441
4821
  */
4442
4822
  function createServer(config) {
4443
4823
  const adapter = new OpenSpecAdapter(config.projectDir);
4444
4824
  const configManager = new ConfigManager(config.projectDir);
4825
+ const globalSettingsManager = new GlobalSettingsManager();
4445
4826
  const cliExecutor = new CliExecutor(configManager, config.projectDir);
4446
4827
  const kernel = config.kernel;
4447
4828
  const hookRuntime = createHookRuntime(config.projectDir);
@@ -4453,8 +4834,37 @@ function createServer(config) {
4453
4834
  });
4454
4835
  const notificationService = new NotificationService();
4455
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
+ });
4456
4862
  const watcher = config.enableWatcher !== false ? new OpenSpecWatcher(config.projectDir) : void 0;
4457
- const searchService = new SearchService(adapter, watcher, void 0, documentService);
4863
+ const entityReadOptionsContext = {
4864
+ adapter,
4865
+ kernel
4866
+ };
4867
+ const searchService = new SearchService(adapter, watcher, void 0, documentService, (stage, id) => buildEntityReadOptions(entityReadOptionsContext, stage, id));
4458
4868
  const dashboardOverviewService = new DashboardOverviewService((reason) => loadDashboardOverview({
4459
4869
  adapter,
4460
4870
  configManager,
@@ -4471,15 +4881,13 @@ function createServer(config) {
4471
4881
  credentials: true
4472
4882
  }));
4473
4883
  app.get("/api/health", (c) => {
4474
- return c.json({
4475
- status: "ok",
4884
+ return c.json(buildBackendHealthPayload({
4476
4885
  projectDir: config.projectDir,
4477
4886
  projectName: basename(config.projectDir) || config.projectDir,
4478
4887
  watcherEnabled: !!watcher,
4479
4888
  openspecuiVersion: SERVER_PACKAGE_VERSION,
4480
- hostedShellProtocolVersion: HOSTED_SHELL_PROTOCOL_VERSION,
4481
4889
  embeddedUiUrl: buildEmbeddedUiUrlForPort(config.port ?? 3100)
4482
- });
4890
+ }));
4483
4891
  });
4484
4892
  app.post("/api/notifications", async (c) => {
4485
4893
  const body = await c.req.json().catch(() => null);
@@ -4530,6 +4938,8 @@ function createServer(config) {
4530
4938
  projectRecoveryService,
4531
4939
  notificationService,
4532
4940
  customSoundService,
4941
+ globalSettingsManager,
4942
+ translationCacheService,
4533
4943
  gitWorktreeHandoff: config.gitWorktreeHandoff,
4534
4944
  watcher,
4535
4945
  projectDir: config.projectDir
@@ -4548,6 +4958,8 @@ function createServer(config) {
4548
4958
  projectRecoveryService,
4549
4959
  notificationService,
4550
4960
  customSoundService,
4961
+ globalSettingsManager,
4962
+ translationCacheService,
4551
4963
  gitWorktreeHandoff: config.gitWorktreeHandoff,
4552
4964
  watcher,
4553
4965
  projectDir: config.projectDir
@@ -4565,6 +4977,8 @@ function createServer(config) {
4565
4977
  projectRecoveryService,
4566
4978
  notificationService,
4567
4979
  customSoundService,
4980
+ globalSettingsManager,
4981
+ translationCacheService,
4568
4982
  hookRuntime,
4569
4983
  watcher,
4570
4984
  createContext,
@@ -4575,7 +4989,7 @@ function createServer(config) {
4575
4989
  * Create WebSocket server for tRPC subscriptions and PTY terminals
4576
4990
  */
4577
4991
  async function createWebSocketServer(server, httpServer, config) {
4578
- if (!isWatcherPoolInitialized()) await initWatcherPool(config.projectDir);
4992
+ if (!isWatcherPoolInitialized()) deferBackgroundTask(() => initializeWatcherPoolInBackground(config.projectDir));
4579
4993
  const wss = new WebSocketServer({ noServer: true });
4580
4994
  const handler = applyWSSHandler({
4581
4995
  wss,
@@ -4615,6 +5029,7 @@ async function createWebSocketServer(server, httpServer, config) {
4615
5029
  server.searchService.dispose().catch(() => {});
4616
5030
  server.dashboardOverviewService.dispose();
4617
5031
  server.projectRecoveryService.dispose();
5032
+ server.translationCacheService.close();
4618
5033
  }
4619
5034
  };
4620
5035
  }
@@ -4631,7 +5046,7 @@ async function startServer(config, setupApp) {
4631
5046
  const port = await findAvailablePort(preferredPort);
4632
5047
  const cliExecutor = new CliExecutor(new ConfigManager(config.projectDir), config.projectDir);
4633
5048
  const kernel = new OpsxKernel(config.projectDir, cliExecutor);
4634
- await initWatcherPool(config.projectDir);
5049
+ deferBackgroundTask(() => initializeWatcherPoolInBackground(config.projectDir));
4635
5050
  const server = createServer({
4636
5051
  ...config,
4637
5052
  port,
@@ -4644,14 +5059,16 @@ async function startServer(config, setupApp) {
4644
5059
  });
4645
5060
  const wsServer = await createWebSocketServer(server, httpServer, { projectDir: config.projectDir });
4646
5061
  const url = `http://localhost:${port}`;
4647
- kernel.warmup().catch((err) => {
4648
- console.error("Kernel warmup failed:", err);
4649
- });
4650
- server.searchService.init().catch((err) => {
4651
- console.error("Search service warmup failed:", err);
4652
- });
4653
- server.dashboardOverviewService.init().catch((err) => {
4654
- 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
+ });
4655
5072
  });
4656
5073
  return {
4657
5074
  url,
@@ -4660,6 +5077,7 @@ async function startServer(config, setupApp) {
4660
5077
  close: async () => {
4661
5078
  kernel.dispose();
4662
5079
  await server.hookRuntime.dispose();
5080
+ server.translationCacheService.close();
4663
5081
  wsServer.close();
4664
5082
  httpServer.close();
4665
5083
  }
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@openspecui/server",
3
- "version": "3.6.1",
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",