nolo-cli 0.1.37 → 0.1.39

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 (52) hide show
  1. package/agent-runtime/agentRecordConfig.ts +0 -14
  2. package/agent-runtime/hostAdapter.ts +0 -1
  3. package/agent-runtime/index.ts +24 -18
  4. package/agent-runtime/localDialogRead.ts +23 -0
  5. package/agent-runtime/localToolPolicy.ts +2 -7
  6. package/agent-runtime/localWorkspaceTools.ts +9 -0
  7. package/agent-runtime/noloWorkspaceTools.ts +371 -0
  8. package/agent-runtime/runtimeToolPolicy.ts +1 -3
  9. package/agent-runtime/types.ts +1 -1
  10. package/agentAliases.ts +6 -0
  11. package/agentRunCommand.ts +57 -65
  12. package/agentRuntimeLocal.ts +0 -7
  13. package/chat/messages/fetchMessages.ts +84 -0
  14. package/chat/messages/types.ts +185 -0
  15. package/cli/agentAliases.ts +6 -0
  16. package/cli/agentRunCommand.ts +57 -65
  17. package/cli/agentRuntimeLocal.ts +0 -7
  18. package/cli/cliEnvHelpers.ts +11 -7
  19. package/cli/client/agentRun.ts +123 -24
  20. package/cli/client/localRuntimeAdapter.ts +87 -59
  21. package/cli/connectorRunArtifact.ts +0 -1
  22. package/cli/dialogCommands.ts +572 -0
  23. package/cli/dialogInternalCommandEntries.ts +4 -0
  24. package/cli/machineWsRunDispatch.ts +29 -37
  25. package/cli/scriptCommandEntries.ts +0 -2
  26. package/cli/tui/readlineWorkspace.ts +69 -0
  27. package/cli/tui/session.ts +176 -1
  28. package/cliEnvHelpers.ts +11 -7
  29. package/client/agentConfigResolver.test.ts +2 -3
  30. package/client/agentRun.test.ts +185 -1
  31. package/client/agentRun.ts +123 -24
  32. package/client/localRuntimeAdapter.test.ts +90 -25
  33. package/client/localRuntimeAdapter.ts +87 -59
  34. package/connectorRunArtifact.ts +0 -1
  35. package/database/server/db.ts +99 -0
  36. package/database/server/ensureDbOpen.ts +12 -0
  37. package/database/server/memoryAuthorityStore.ts +133 -0
  38. package/database/server/serverStoreFactory.ts +118 -0
  39. package/dialogCommands.ts +572 -0
  40. package/dialogInternalCommandEntries.ts +4 -0
  41. package/machineWsRunDispatch.ts +29 -37
  42. package/package.json +10 -9
  43. package/scriptCommandEntries.ts +0 -2
  44. package/tui/readlineWorkspace.ts +69 -0
  45. package/tui/session.ts +176 -1
  46. package/agent-runtime/taskWorkspace.ts +0 -193
  47. package/agent-runtime/workspaceSession.ts +0 -76
  48. package/cli/client/taskWorktree.ts +0 -8
  49. package/cli/client/workspaceSession.ts +0 -11
  50. package/client/taskWorktree.ts +0 -8
  51. package/client/workspaceSession.test.ts +0 -57
  52. package/client/workspaceSession.ts +0 -11
@@ -5,6 +5,8 @@ import type {
5
5
  } from "../agentRuntimeLocal";
6
6
  import { request as httpRequest } from "node:http";
7
7
  import { request as httpsRequest } from "node:https";
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
8
10
  import {
9
11
  buildLocalWorkspacePolicyToolNames,
10
12
  buildLocalWorkspaceToolset,
@@ -40,19 +42,28 @@ import {
40
42
  } from "./hybridRecordStore";
41
43
  import { executeLocalToolWithPolicy } from "./localToolPolicy";
42
44
  import { taskRunFunctionSchema } from "../ai/tools/agent/taskRunTool";
43
- import { prepareTaskWorktree } from "./taskWorktree";
44
45
  import {
45
- activateWorkspaceSession,
46
- createWorkspaceSession,
47
- formatWorkspaceSessionActivation,
48
- type WorkspaceSession,
49
- } from "./workspaceSession";
46
+ NOLO_DEFAULT_AGENT_ID,
47
+ NOLO_DEFAULT_AGENT_KEY,
48
+ } from "../agentAliases";
49
+ import {
50
+ buildNoloWorkspaceCliToolExecutors,
51
+ buildNoloWorkspaceOpenAiTools,
52
+ filterNoloWorkspaceToolNames,
53
+ NOLO_WORKSPACE_TOOL_NAMES,
54
+ NOLO_WORKSPACE_TOOL_PROMPT,
55
+ parseNoloWorkspaceToolArguments,
56
+ } from "../agent-runtime/noloWorkspaceTools";
50
57
 
51
58
  type EnvLike = Record<string, string | undefined>;
52
59
  type FetchInput = Parameters<typeof fetch>[0];
53
60
  type FetchInit = Parameters<typeof fetch>[1];
54
61
  const TRANSIENT_FETCH_MAX_ATTEMPTS = 8;
55
62
  const TRANSIENT_FETCH_RETRY_BASE_DELAY_MS = 250;
63
+ export const BUILTIN_NOLO_AGENT_ID = NOLO_DEFAULT_AGENT_ID;
64
+ export const BUILTIN_NOLO_AGENT_KEY = NOLO_DEFAULT_AGENT_KEY;
65
+ const CLI_DIR = dirname(fileURLToPath(import.meta.url));
66
+ const CLI_ENTRYPOINT = join(CLI_DIR, "..", "index.ts");
56
67
 
57
68
  export type CliLocalRuntimeDb = CliKvDb;
58
69
 
@@ -64,9 +75,7 @@ export type CliLocalRuntimeAdapterDeps = {
64
75
  createId?: () => string;
65
76
  fetchImpl?: typeof fetch;
66
77
  cwd?: string;
67
- useCwdAsTaskWorkspaceBase?: boolean;
68
78
  output?: { write(chunk: string): unknown };
69
- prepareTaskWorktree?: typeof prepareTaskWorktree;
70
79
  localToolExecutors?: Record<string, (call: any) => Promise<{ content: string; metadata?: Record<string, unknown> }>>;
71
80
  sleep?: (ms: number) => Promise<void>;
72
81
  loopbackRequest?: (input: FetchInput, init?: FetchInit) => Promise<Response>;
@@ -85,7 +94,7 @@ function resolveLocalUserId(env: EnvLike) {
85
94
  }
86
95
 
87
96
  function shellModeEnabled(env: EnvLike) {
88
- return ["worktree", "dangerous", "1", "true"].includes(env.NOLO_LOCAL_SHELL_MODE || "");
97
+ return env.NOLO_LOCAL_SHELL_MODE === "worktree";
89
98
  }
90
99
 
91
100
  function parseLocalToolBudgets(env: EnvLike) {
@@ -246,6 +255,17 @@ function logLocalRuntimeDiagnostic(event: string, fields: Record<string, unknown
246
255
  console.error(`[nolo-local-runtime] ${event} ${JSON.stringify(fields)}`);
247
256
  }
248
257
 
258
+ function summarizeOpenAiToolNames(tools: Array<Record<string, unknown>>) {
259
+ return tools
260
+ .map((tool) => {
261
+ const fn = tool.function;
262
+ return fn && typeof fn === "object" && "name" in fn && typeof fn.name === "string"
263
+ ? fn.name
264
+ : null;
265
+ })
266
+ .filter((name): name is string => Boolean(name));
267
+ }
268
+
249
269
  function buildOpenAiTools(args: { toolNames?: string[]; env: EnvLike }) {
250
270
  const toolset = buildLocalWorkspaceToolsetForEnv(args);
251
271
  return [
@@ -254,6 +274,7 @@ function buildOpenAiTools(args: { toolNames?: string[]; env: EnvLike }) {
254
274
  exposeShellTools: toolset.exposeShellTools,
255
275
  }),
256
276
  ...buildServerPlatformOpenAiTools({ toolNames: args.toolNames }),
277
+ ...buildNoloWorkspaceOpenAiTools({ toolNames: args.toolNames }),
257
278
  ];
258
279
  }
259
280
 
@@ -272,6 +293,7 @@ function buildLocalPolicyToolNames(args: { toolNames?: string[]; env: EnvLike })
272
293
  exposeShellTools: shellModeEnabled(args.env),
273
294
  }),
274
295
  ...((args.toolNames ?? []).includes("taskRun") ? ["taskRun"] : []),
296
+ ...filterNoloWorkspaceToolNames(args.toolNames),
275
297
  ];
276
298
  }
277
299
 
@@ -287,13 +309,44 @@ function buildServerPlatformOpenAiTools(args: { toolNames?: string[] }) {
287
309
  ];
288
310
  }
289
311
 
290
- function parseToolArguments(raw: string) {
291
- try {
292
- const parsed = JSON.parse(raw || "{}");
293
- return parsed && typeof parsed === "object" ? parsed : {};
294
- } catch {
295
- return {};
296
- }
312
+ function buildCliWorkspaceToolExecutors(args: {
313
+ env: EnvLike;
314
+ cliEntrypoint?: string;
315
+ }) {
316
+ return buildNoloWorkspaceCliToolExecutors({
317
+ cliEntrypoint: args.cliEntrypoint ?? CLI_ENTRYPOINT,
318
+ env: args.env,
319
+ metadataKind: "cliWorkspaceTool",
320
+ });
321
+ }
322
+
323
+ export function isBuiltinNoloAgentRef(ref: unknown) {
324
+ if (typeof ref !== "string") return false;
325
+ const normalized = ref.trim();
326
+ return (
327
+ normalized === BUILTIN_NOLO_AGENT_KEY ||
328
+ normalized === BUILTIN_NOLO_AGENT_ID ||
329
+ normalized.endsWith(`-${BUILTIN_NOLO_AGENT_ID}`)
330
+ );
331
+ }
332
+
333
+ function isBuiltinNoloAgentConfig(agentConfig: AgentRuntimeAgentConfig | null | undefined) {
334
+ const key = agentConfig?.key || agentConfig?.rawRecord?.dbKey || agentConfig?.rawRecord?.agentKey;
335
+ const id = agentConfig?.rawRecord?.id;
336
+ return isBuiltinNoloAgentRef(key) || isBuiltinNoloAgentRef(id);
337
+ }
338
+
339
+ function withBuiltinNoloCliTools(agentConfig: AgentRuntimeAgentConfig | null) {
340
+ if (!agentConfig || !isBuiltinNoloAgentConfig(agentConfig)) return agentConfig;
341
+ const toolNames = [...new Set([...(agentConfig.toolNames ?? []), ...NOLO_WORKSPACE_TOOL_NAMES])];
342
+ return {
343
+ ...agentConfig,
344
+ toolNames,
345
+ prompt: [
346
+ agentConfig.prompt?.trim() ?? "",
347
+ NOLO_WORKSPACE_TOOL_PROMPT,
348
+ ].filter(Boolean).join("\n\n"),
349
+ };
297
350
  }
298
351
 
299
352
  function resolveRuntimeServerUrl(env: EnvLike) {
@@ -320,7 +373,7 @@ function buildServerPlatformToolExecutors(args: {
320
373
  "Content-Type": "application/json",
321
374
  Authorization: `Bearer ${authToken}`,
322
375
  },
323
- body: JSON.stringify(parseToolArguments(call.arguments)),
376
+ body: JSON.stringify(parseNoloWorkspaceToolArguments(call.arguments)),
324
377
  });
325
378
  const text = await response.text().catch(() => "");
326
379
  if (!response.ok) {
@@ -352,17 +405,13 @@ function buildLocalToolExecutors(args: {
352
405
  env: args.env,
353
406
  fetchImpl: args.fetchImpl,
354
407
  }),
408
+ ...buildCliWorkspaceToolExecutors({
409
+ env: args.env,
410
+ }),
355
411
  ...(args.localToolExecutors ?? {}),
356
412
  };
357
413
  }
358
414
 
359
- function readLocalWorkspaceMode(agentConfig: AgentRuntimeAgentConfig | null) {
360
- if (agentConfig?.localWorkspaceMode === "task-worktree") return "task-worktree";
361
- const runtimeBinding = agentConfig?.runtimeBinding;
362
- const value = runtimeBinding?.localWorkspaceMode ?? runtimeBinding?.workspaceMode;
363
- return value === "task-worktree" ? "task-worktree" : "current";
364
- }
365
-
366
415
  async function resolveStore(deps: CliLocalRuntimeAdapterDeps) {
367
416
  if (deps.store) return deps.store;
368
417
  return createCliHybridRecordStore({
@@ -437,51 +486,25 @@ export function createCliLocalRuntimeAdapter(
437
486
  const localToolBudgets = parseLocalToolBudgets(deps.env);
438
487
  const localToolUsage = new Map<string, number>();
439
488
  let activeAgentToolNames: string[] = [];
440
- let workspaceSession: WorkspaceSession = createWorkspaceSession(
441
- deps.useCwdAsTaskWorkspaceBase
442
- ? { defaultCwd: deps.cwd }
443
- : { cwd: deps.cwd }
444
- );
489
+ const workspaceRoot = deps.cwd ?? process.cwd();
445
490
  let runtimeToolExecutionLimits: ReturnType<typeof resolveLocalWorkspaceExecutorOptionsFromPolicy> = {};
446
491
  let localToolExecutors = buildLocalToolExecutors({
447
- workspaceRoot: workspaceSession.workspaceRoot,
492
+ workspaceRoot,
448
493
  env: deps.env,
449
494
  fetchImpl,
450
495
  localToolExecutors: deps.localToolExecutors,
451
496
  ...runtimeToolExecutionLimits,
452
497
  });
453
498
 
454
- async function activateAgentWorkspaceSession(args: {
455
- agentRef: string;
456
- agentConfig: AgentRuntimeAgentConfig | null;
457
- }) {
458
- const previousWorkspaceRoot = workspaceSession.workspaceRoot;
459
- workspaceSession = await activateWorkspaceSession(workspaceSession, {
460
- agentKey: args.agentConfig?.key ?? args.agentRef,
461
- mode: readLocalWorkspaceMode(args.agentConfig),
462
- env: deps.env,
463
- prepareTaskWorkspace: deps.prepareTaskWorktree,
464
- });
465
- if (workspaceSession.workspaceRoot === previousWorkspaceRoot) return;
466
- localToolExecutors = buildLocalToolExecutors({
467
- workspaceRoot: workspaceSession.workspaceRoot,
468
- env: deps.env,
469
- fetchImpl,
470
- localToolExecutors: deps.localToolExecutors,
471
- ...runtimeToolExecutionLimits,
472
- });
473
- deps.output?.write(formatWorkspaceSessionActivation(workspaceSession));
474
- }
475
-
476
499
  return {
477
500
  host: "cli",
478
501
  capabilities: ["leveldb-agent-config", "local-provider", "leveldb-persistence", "local-tools"],
479
502
  loadAgentConfig: async (agentRef) => {
480
- const agentConfig = await readAgentFromStore({
503
+ const agentConfig = withBuiltinNoloCliTools(await readAgentFromStore({
481
504
  agentRef,
482
505
  store: await resolveStore(deps),
483
506
  userId,
484
- });
507
+ }));
485
508
  const requestedToolNames = agentConfig
486
509
  ? resolveRequestedRuntimeToolNames({ agentConfig })
487
510
  : [];
@@ -492,9 +515,8 @@ export function createCliLocalRuntimeAdapter(
492
515
  runtimeToolExecutionLimits = resolveLocalWorkspaceExecutorOptionsFromPolicy(
493
516
  resolveCurrentRunRuntimeToolPolicy(agentConfig),
494
517
  );
495
- await activateAgentWorkspaceSession({ agentRef, agentConfig });
496
518
  localToolExecutors = buildLocalToolExecutors({
497
- workspaceRoot: workspaceSession.workspaceRoot,
519
+ workspaceRoot,
498
520
  env: deps.env,
499
521
  fetchImpl,
500
522
  localToolExecutors: deps.localToolExecutors,
@@ -512,7 +534,7 @@ export function createCliLocalRuntimeAdapter(
512
534
  userId,
513
535
  now,
514
536
  createId,
515
- cwd: workspaceSession.workspaceRoot,
537
+ cwd: workspaceRoot,
516
538
  }),
517
539
  resolveProvider: async (agentConfig) => {
518
540
  if (shouldUsePlatformChatProvider(deps.env, agentConfig)) {
@@ -558,6 +580,9 @@ export function createCliLocalRuntimeAdapter(
558
580
  model: providerConfig.model,
559
581
  messageCount: messages.length,
560
582
  toolCount: tools.length,
583
+ requestedToolNames,
584
+ openAiToolNames: summarizeOpenAiToolNames(tools),
585
+ shellMode: deps.env.NOLO_LOCAL_SHELL_MODE ?? null,
561
586
  timeoutMs: options?.timeoutMs ?? null,
562
587
  });
563
588
  const res = await fetchWithTransientRetry(fetchImpl, request.url, {
@@ -627,6 +652,9 @@ export function createCliLocalRuntimeAdapter(
627
652
  model: providerConfig.model,
628
653
  messageCount: messages.length,
629
654
  toolCount: tools.length,
655
+ requestedToolNames,
656
+ openAiToolNames: summarizeOpenAiToolNames(tools),
657
+ shellMode: deps.env.NOLO_LOCAL_SHELL_MODE ?? null,
630
658
  timeoutMs: options?.timeoutMs ?? null,
631
659
  });
632
660
  const res = await fetchWithTransientRetry(fetchImpl, request.url, {
@@ -672,8 +700,8 @@ export function createCliLocalRuntimeAdapter(
672
700
  ...result,
673
701
  metadata: {
674
702
  ...(result.metadata ?? {}),
675
- workspaceRoot: workspaceSession.workspaceRoot,
676
- workspaceKind: workspaceSession.kind,
703
+ workspaceRoot,
704
+ workspaceKind: "current",
677
705
  },
678
706
  };
679
707
  },
@@ -32,7 +32,6 @@ export function resolveConnectorRunCwd(args: {
32
32
  const explicit =
33
33
  args.env?.NOLO_CONNECTOR_WORKDIR ||
34
34
  args.env?.NOLO_AGENT_WORKDIR ||
35
- args.env?.NOLO_LOCAL_WORKTREE ||
36
35
  "";
37
36
  const candidate = explicit || args.policy.writableRoots[0] || args.fallbackCwd || process.cwd();
38
37
  const cwd = normalizePath(candidate);
@@ -0,0 +1,99 @@
1
+ // 文件路径: packages/database/server/db.ts
2
+
3
+ import { ensureDbOpen } from "./ensureDbOpen";
4
+ import { resolveServerDbPath } from "./dbPath";
5
+ import type { LegacyServerDb } from "./legacyServerDb";
6
+ import type { AuthorityStore } from "./authorityStoreTypes";
7
+ import { getOrCreateServerStoreRuntime } from "./serverStoreFactory";
8
+
9
+ const DB_PATH = resolveServerDbPath();
10
+
11
+ console.log("数据库配置:");
12
+ console.log("- 当前工作目录:", process.cwd());
13
+ console.log("- 数据库路径:", DB_PATH);
14
+
15
+ export function isServerDbLockError(error: unknown): boolean {
16
+ const queue: unknown[] = [error];
17
+ const seen = new Set<unknown>();
18
+
19
+ while (queue.length > 0) {
20
+ const current = queue.shift();
21
+ if (!current || seen.has(current)) continue;
22
+ seen.add(current);
23
+
24
+ const text = `${(current as any)?.code ?? ""} ${(current as any)?.message ?? current}`;
25
+ if (/LEVEL_LOCKED|Resource temporarily unavailable|\/LOCK|LOCK:/.test(text)) {
26
+ return true;
27
+ }
28
+
29
+ if ((current as any)?.cause) queue.push((current as any).cause);
30
+ if (Array.isArray((current as any)?.errors)) {
31
+ queue.push(...(current as any).errors);
32
+ }
33
+ }
34
+
35
+ return false;
36
+ }
37
+
38
+ const {
39
+ authorityStore,
40
+ serverDb,
41
+ }: { authorityStore: AuthorityStore; serverDb: LegacyServerDb } =
42
+ getOrCreateServerStoreRuntime(DB_PATH);
43
+
44
+ console.log("- LevelDB实际路径:", serverDb.location);
45
+
46
+ // ✅ 保留默认导出:兼容现有的 import serverDb from "./db"
47
+ export default serverDb;
48
+
49
+ // 具名导出:需要的时候可以引用
50
+ export { serverDb };
51
+ export function getServerAuthorityStore() {
52
+ return authorityStore;
53
+ }
54
+
55
+ /**
56
+ * 确保数据库处于 open 状态。
57
+ * - 已 open / opening:直接返回
58
+ * - 其他状态:尝试 open,一旦失败抛错
59
+ */
60
+ export async function ensureServerDbOpen() {
61
+ const status = authorityStore.status;
62
+ if (status === "open") return;
63
+
64
+ try {
65
+ await ensureDbOpen(authorityStore);
66
+ if (status !== "opening") {
67
+ console.log("✅ LevelDB 已打开");
68
+ }
69
+ } catch (err) {
70
+ if (isServerDbLockError(err)) {
71
+ console.error("❌ 打开 LevelDB 失败: 数据库已被其他进程占用");
72
+ } else {
73
+ console.error("❌ 打开 LevelDB 失败:", err);
74
+ }
75
+ throw err;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 兼容入口调用:启动时显式 open 一次
81
+ */
82
+ export async function openServerDb() {
83
+ return ensureServerDbOpen();
84
+ }
85
+
86
+ /**
87
+ * 优雅关机时关闭 DB
88
+ */
89
+ export async function closeServerDb() {
90
+ const status = authorityStore.status;
91
+ if (status !== "open") return;
92
+
93
+ try {
94
+ await authorityStore.close();
95
+ console.log("✅ LevelDB 已关闭");
96
+ } catch (err) {
97
+ console.error("❌ 关闭 LevelDB 失败:", err);
98
+ }
99
+ }
@@ -0,0 +1,12 @@
1
+ export type DbWithOpenStatus = {
2
+ status?: string | null;
3
+ open(): Promise<unknown>;
4
+ };
5
+
6
+ export async function ensureDbOpen(db: DbWithOpenStatus) {
7
+ if (db.status === "open") {
8
+ return;
9
+ }
10
+
11
+ await db.open();
12
+ }
@@ -0,0 +1,133 @@
1
+ import type {
2
+ AuthorityBatchOperation,
3
+ AuthorityBatchWriter,
4
+ AuthorityIteratorOptions,
5
+ AuthorityStore,
6
+ } from "./authorityStoreTypes";
7
+
8
+ type MemoryEntry = {
9
+ key: string;
10
+ value: unknown;
11
+ };
12
+
13
+ const createNotFoundError = () => {
14
+ const error = new Error("NotFound") as Error & { notFound: true };
15
+ error.notFound = true;
16
+ return error;
17
+ };
18
+
19
+ class MemoryAuthorityBacking {
20
+ readonly location: string;
21
+ status: "closed" | "open" = "closed";
22
+ private readonly data = new Map<string, unknown>();
23
+
24
+ constructor(location: string) {
25
+ this.location = location;
26
+ }
27
+
28
+ async open() {
29
+ this.status = "open";
30
+ }
31
+
32
+ async close() {
33
+ this.status = "closed";
34
+ }
35
+
36
+ async get<T = any>(key: string): Promise<T> {
37
+ if (!this.data.has(key)) {
38
+ throw createNotFoundError();
39
+ }
40
+ return this.data.get(key) as T;
41
+ }
42
+
43
+ async put(key: string, value: unknown) {
44
+ this.data.set(key, value);
45
+ }
46
+
47
+ async del(key: string) {
48
+ this.data.delete(key);
49
+ }
50
+
51
+ async batchWrite(ops: AuthorityBatchOperation[]) {
52
+ for (const op of ops) {
53
+ if (op.type === "put") {
54
+ this.data.set(op.key, op.value);
55
+ } else {
56
+ this.data.delete(op.key);
57
+ }
58
+ }
59
+ }
60
+
61
+ createBatch(): AuthorityBatchWriter {
62
+ const buffered: AuthorityBatchOperation[] = [];
63
+ return {
64
+ put(key: string, value: unknown) {
65
+ buffered.push({ type: "put", key, value });
66
+ },
67
+ del(key: string) {
68
+ buffered.push({ type: "del", key });
69
+ },
70
+ write: async () => {
71
+ await this.batchWrite(buffered);
72
+ },
73
+ };
74
+ }
75
+
76
+ async *iterator(
77
+ options: AuthorityIteratorOptions = {}
78
+ ): AsyncIterableIterator<[string, unknown]> {
79
+ const entries: MemoryEntry[] = Array.from(this.data.entries())
80
+ .sort(([left], [right]) => left.localeCompare(right))
81
+ .map(([key, value]) => ({ key, value }));
82
+
83
+ if (options.reverse) {
84
+ entries.reverse();
85
+ }
86
+
87
+ for (const entry of entries) {
88
+ if (options.gte && entry.key < options.gte) continue;
89
+ if (options.lte && entry.key > options.lte) continue;
90
+ if (options.lt && entry.key >= options.lt) continue;
91
+ yield [entry.key, entry.value];
92
+ }
93
+ }
94
+ }
95
+
96
+ export function createMemoryAuthorityStore(
97
+ location = ":memory:"
98
+ ): AuthorityStore {
99
+ const backing = new MemoryAuthorityBacking(location);
100
+
101
+ return {
102
+ get location() {
103
+ return backing.location;
104
+ },
105
+ get status() {
106
+ return backing.status;
107
+ },
108
+ async open() {
109
+ await backing.open();
110
+ },
111
+ async close() {
112
+ await backing.close();
113
+ },
114
+ async get<T = any>(key: string) {
115
+ return backing.get<T>(key);
116
+ },
117
+ async put(key: string, value: unknown) {
118
+ await backing.put(key, value);
119
+ },
120
+ async del(key: string) {
121
+ await backing.del(key);
122
+ },
123
+ async batchWrite(ops: AuthorityBatchOperation[]) {
124
+ await backing.batchWrite(ops);
125
+ },
126
+ createBatch() {
127
+ return backing.createBatch();
128
+ },
129
+ iterator(options: AuthorityIteratorOptions = {}) {
130
+ return backing.iterator(options);
131
+ },
132
+ };
133
+ }
@@ -0,0 +1,118 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { Level } from "level";
4
+
5
+ import type { AuthorityStore } from "./authorityStoreTypes";
6
+ import { createLegacyServerDb, type LegacyServerDb } from "./legacyServerDb";
7
+ import { createLevelAuthorityStore } from "./levelAuthorityStore";
8
+ import { createMemoryAuthorityStore } from "./memoryAuthorityStore";
9
+
10
+ export type ServerStoreDriver = "level" | "memory";
11
+
12
+ export type ServerStoreFactoryEnv = {
13
+ NOLO_SERVER_AUTHORITY_DRIVER?: string | undefined;
14
+ };
15
+
16
+ export const safeJsonEncoding = {
17
+ name: "safe-json",
18
+ format: "utf8" as const,
19
+ encode: (data: any) => JSON.stringify(data),
20
+ decode: (data: string) => {
21
+ try {
22
+ return JSON.parse(data);
23
+ } catch {
24
+ console.warn("[LevelDB] Corrupted value:", data?.substring?.(0, 60));
25
+ return null;
26
+ }
27
+ },
28
+ };
29
+
30
+ export type ServerStoreRuntime = {
31
+ authorityStore: AuthorityStore;
32
+ serverDb: LegacyServerDb;
33
+ };
34
+
35
+ export type ServerStoreFactoryOptions = {
36
+ driver?: ServerStoreDriver;
37
+ env?: ServerStoreFactoryEnv;
38
+ globalScope?: Record<string, unknown>;
39
+ };
40
+
41
+ export function ensureServerDbDirectory(dbPath: string) {
42
+ const dir = path.dirname(dbPath);
43
+ if (!fs.existsSync(dir)) {
44
+ fs.mkdirSync(dir, { recursive: true });
45
+ }
46
+ }
47
+
48
+ export function createLevelBackedAuthorityStore(dbPath: string): AuthorityStore {
49
+ const levelDb = new Level<string, any>(dbPath, {
50
+ valueEncoding: safeJsonEncoding,
51
+ });
52
+ return createLevelAuthorityStore(levelDb);
53
+ }
54
+
55
+ export function resolveServerStoreDriver(
56
+ env: ServerStoreFactoryEnv = process.env
57
+ ): ServerStoreDriver {
58
+ const rawDriver = env.NOLO_SERVER_AUTHORITY_DRIVER?.trim().toLowerCase();
59
+ if (!rawDriver) return "level";
60
+ if (rawDriver === "level") return "level";
61
+ if (rawDriver === "memory") return "memory";
62
+ throw new Error(`Unsupported server authority store driver: ${rawDriver}`);
63
+ }
64
+
65
+ export function createAuthorityStoreForDriver(
66
+ driver: ServerStoreDriver,
67
+ dbPath: string
68
+ ): AuthorityStore {
69
+ switch (driver) {
70
+ case "level":
71
+ return createLevelBackedAuthorityStore(dbPath);
72
+ case "memory":
73
+ return createMemoryAuthorityStore(dbPath);
74
+ }
75
+ }
76
+
77
+ export function getOrCreateServerStoreRuntime(
78
+ dbPath: string,
79
+ options: ServerStoreFactoryOptions = {}
80
+ ): ServerStoreRuntime {
81
+ const globalScope =
82
+ options.globalScope ?? (globalThis as Record<string, unknown>);
83
+ const driver = options.driver ?? resolveServerStoreDriver(options.env);
84
+ const cachedDriver = globalScope.__serverAuthorityStoreDriver;
85
+
86
+ ensureServerDbDirectory(dbPath);
87
+
88
+ if (
89
+ globalScope.__serverAuthorityStore &&
90
+ typeof cachedDriver === "string" &&
91
+ cachedDriver !== driver
92
+ ) {
93
+ throw new Error(
94
+ `Server authority store already initialized with driver ${cachedDriver}, cannot reuse it for ${driver}`
95
+ );
96
+ }
97
+
98
+ if (!globalScope.__serverAuthorityStore) {
99
+ globalScope.__serverAuthorityStore = createAuthorityStoreForDriver(
100
+ driver,
101
+ dbPath
102
+ );
103
+ globalScope.__serverAuthorityStoreDriver = driver;
104
+ } else if (!cachedDriver) {
105
+ globalScope.__serverAuthorityStoreDriver = driver;
106
+ }
107
+
108
+ const authorityStore = globalScope.__serverAuthorityStore as AuthorityStore;
109
+
110
+ if (!globalScope.__serverDb) {
111
+ globalScope.__serverDb = createLegacyServerDb(authorityStore);
112
+ }
113
+
114
+ return {
115
+ authorityStore,
116
+ serverDb: globalScope.__serverDb as LegacyServerDb,
117
+ };
118
+ }