@jini-ai/server 0.0.0-stage → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { buildDaemonDbOperations, classifyRunFailureForRetry, createLocalNodeDaemon, projectDetectedAgent, resolveBoundPort, resolveReportHost, } from './create-local-node-daemon.js';
2
+ export { composeJiniKernel, defaultServerMessages } from './compose-jini-kernel.js';
3
+ export { CAPABILITY_IDS, CORE_CAPABILITIES, defineJiniFeature, isCapabilityId, JINI_PROFILES } from './feature.js';
4
+ export { resolveFeatureActivation } from './feature-activation.js';
5
+ export { ANONYMOUS_DELEGATED_PRINCIPAL, createBuiltInFeatures, LOCAL_DAEMON_PRINCIPAL, } from './builtin-features.js';
6
+ export { createJiniKernelBase } from './kernel-base.js';
7
+ // Frontend control belongs to the daemon HTTP surface: hosts that build their own Express app
8
+ // can compose it without the server kernel. This server entry keeps its existing public export.
9
+ export { createFrontendControl } from '@jini-ai/daemon/http';
10
+ export { DEFAULT_DAEMON_BIND_HOST, closeHttpServer, installGracefulShutdown, normalizeDaemonBindHost, } from './host-bootstrap.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @module kernel-base
3
+ *
4
+ * The infrastructure every composition owns regardless of which features it activates: the durable
5
+ * (or in-memory) event log, the run lifecycle, the byte journal, the agent executor, the one shared
6
+ * `ToolRegistry`/`ToolExecutor` pair, and — for sqlite storage — the raw database connection.
7
+ *
8
+ * **The database connection belongs here, not to a feature.** It is borrowed by the readiness probe
9
+ * (`health`), by the raw db-operations tools (`daemonDb`), and by the durable tool catalog
10
+ * (`toolCatalog`) — three independently-selectable features. Had any one of them owned it, disabling
11
+ * that feature would have silently broken the others: turning off `daemonDb` would leave `health`'s
12
+ * readiness probe with nothing to check, which is a much worse failure than the one it was meant to
13
+ * simplify, because a readiness probe that stops probing still answers `200`. Ownership by the base,
14
+ * borrowing by features, is what makes the feature set genuinely independent.
15
+ */
16
+ import { type SqliteDb, type SqliteSyncOpener } from '@jini-ai/db/sqlite';
17
+ import { type ToolRegistry } from '@jini-ai/core';
18
+ import { createAgentExecutor, type AgentExecutor, type EventLog, type RunLifecycle, type RunRetrySideEffectState, type ToolExecutor } from '@jini-ai/daemon';
19
+ /** Where a composition's durable state lives. */
20
+ export type JiniKernelStorage =
21
+ /** No files touched. Runs and their events vanish with the process — right for tests and for an
22
+ * embedded host that treats runs as ephemeral. Features needing a real database file
23
+ * (`daemonDb`, `toolCatalog`, `xai`) cannot be activated against it. */
24
+ {
25
+ readonly kind: 'memory';
26
+ }
27
+ /** `<dataDir>/events.db` + `<dataDir>/journal.db`. The directory must already exist. */
28
+ | {
29
+ readonly kind: 'sqlite';
30
+ readonly dataDir: string;
31
+ readonly open: SqliteSyncOpener;
32
+ };
33
+ /** The raw sqlite handle and its path, borrowed by `health`/`daemonDb`/`toolCatalog`. */
34
+ export interface KernelSqliteAccess {
35
+ readonly connection: SqliteDb;
36
+ readonly eventsDbPath: string;
37
+ readonly dataDir: string;
38
+ }
39
+ /**
40
+ * An `EventLog` that may or may not hold an OS resource. `@jini-ai/daemon/store/event-log/sqlite`'s `SqliteEventLog` adds
41
+ * `close({})`; `@jini-ai/daemon`'s in-memory reference adapter has nothing to release and so declares
42
+ * none. Modelling `close` as optional lets the base treat both storage kinds uniformly instead of
43
+ * branching on which one it happens to hold.
44
+ */
45
+ export type ClosableEventLog = EventLog & {
46
+ close?: (requiredArgs: Record<string, never>) => Promise<void>;
47
+ };
48
+ export interface JiniKernelBase {
49
+ readonly eventLog: ClosableEventLog;
50
+ readonly journalEventLog: ClosableEventLog;
51
+ readonly lifecycle: RunLifecycle;
52
+ readonly agentExecutor: AgentExecutor;
53
+ /** The single registry every active feature's tools land in, and the only one `toolExecutor` reads. */
54
+ readonly registry: ToolRegistry;
55
+ readonly toolExecutor: ToolExecutor;
56
+ /** `null` under memory storage. Owned here; features borrow and must never close it. */
57
+ readonly sqlite: KernelSqliteAccess | null;
58
+ /** Closes every handle this base opened, in the same order the standalone daemon always has. */
59
+ close(): Promise<void>;
60
+ }
61
+ export interface CreateJiniKernelBaseOptions {
62
+ readonly storage: JiniKernelStorage;
63
+ /** Extra `createAgentExecutor` options (e.g. `mcpJsonInjection`). `lifecycle`/`journal`/`classifyFailure` are kernel-owned. */
64
+ readonly agentExecutor?: Omit<NonNullable<Parameters<typeof createAgentExecutor>[1]>, 'journal' | 'classifyFailure'>;
65
+ }
66
+ /**
67
+ * The zero-config retry classifier every composition gets. Delegates entirely to `@jini-ai/daemon`'s
68
+ * `resumableFromProcessExit` — see that function's own doc for the classification policy.
69
+ */
70
+ export declare function classifyRunFailureForRetry(context: {
71
+ code: number | null;
72
+ signal: string | null;
73
+ sideEffects?: Pick<RunRetrySideEffectState, 'userVisibleOutputSeen' | 'toolCallSeen'>;
74
+ }): boolean;
75
+ /**
76
+ * Opens every kernel-owned resource and rehydrates durable run history.
77
+ *
78
+ * @throws If rehydration fails against a corrupt or unreadable durable history. Every handle this
79
+ * call had already opened is closed before it rethrows, so a failed boot never leaks a sqlite file
80
+ * handle — the invariant the standalone daemon has always maintained.
81
+ */
82
+ export declare function createJiniKernelBase(options: CreateJiniKernelBaseOptions): Promise<JiniKernelBase>;
83
+ //# sourceMappingURL=kernel-base.d.ts.map
@@ -0,0 +1,125 @@
1
+ /**
2
+ * @module kernel-base
3
+ *
4
+ * The infrastructure every composition owns regardless of which features it activates: the durable
5
+ * (or in-memory) event log, the run lifecycle, the byte journal, the agent executor, the one shared
6
+ * `ToolRegistry`/`ToolExecutor` pair, and — for sqlite storage — the raw database connection.
7
+ *
8
+ * **The database connection belongs here, not to a feature.** It is borrowed by the readiness probe
9
+ * (`health`), by the raw db-operations tools (`daemonDb`), and by the durable tool catalog
10
+ * (`toolCatalog`) — three independently-selectable features. Had any one of them owned it, disabling
11
+ * that feature would have silently broken the others: turning off `daemonDb` would leave `health`'s
12
+ * readiness probe with nothing to check, which is a much worse failure than the one it was meant to
13
+ * simplify, because a readiness probe that stops probing still answers `200`. Ownership by the base,
14
+ * borrowing by features, is what makes the feature set genuinely independent.
15
+ */
16
+ import { openSqliteConnection } from '@jini-ai/db/sqlite';
17
+ import { join } from 'node:path';
18
+ import { createToolRegistry } from '@jini-ai/core';
19
+ import { createAgentExecutor, createInMemoryEventLog, createRunByteJournal, createRunLifecycle, createToolExecutor, resumableFromProcessExit, } from '@jini-ai/daemon';
20
+ import { openSqliteEventLog } from '@jini-ai/daemon/store/event-log/sqlite';
21
+ /**
22
+ * The zero-config retry classifier every composition gets. Delegates entirely to `@jini-ai/daemon`'s
23
+ * `resumableFromProcessExit` — see that function's own doc for the classification policy.
24
+ */
25
+ export function classifyRunFailureForRetry(context) {
26
+ return resumableFromProcessExit({ code: context.code, signal: context.signal }, context.sideEffects === undefined ? {} : { sideEffects: context.sideEffects });
27
+ }
28
+ /**
29
+ * Opens every kernel-owned resource and rehydrates durable run history.
30
+ *
31
+ * @throws If rehydration fails against a corrupt or unreadable durable history. Every handle this
32
+ * call had already opened is closed before it rethrows, so a failed boot never leaks a sqlite file
33
+ * handle — the invariant the standalone daemon has always maintained.
34
+ */
35
+ export async function createJiniKernelBase(options) {
36
+ const sqliteStorage = options.storage.kind === 'sqlite' ? options.storage : null;
37
+ const dataDir = sqliteStorage?.dataDir ?? null;
38
+ const eventsDbPath = dataDir === null ? null : join(dataDir, 'events.db');
39
+ // One acquisition block, one cleanup path — and **every** acquisition is inside it. Opening even
40
+ // the first event log outside the block would reintroduce exactly the leak this shape exists to
41
+ // prevent: `events.db` opens, `journal.db` fails (corrupt file, or a directory sitting where a
42
+ // database was expected), and the first handle stays open for the lifetime of a process that has
43
+ // no kernel. The second sqlite connection and the rehydration are grouped here for the same
44
+ // reason: all four can fail after handles are already open, and two try/catch blocks with
45
+ // near-identical cleanup is how one of them eventually drifts and starts leaking.
46
+ const opened = [];
47
+ const openEventLog = (path) => {
48
+ let log;
49
+ if (path === null)
50
+ log = createInMemoryEventLog({});
51
+ else {
52
+ // A non-null path is derived only from sqliteStorage above.
53
+ log = openSqliteEventLog({ file: path, open: sqliteStorage.open });
54
+ }
55
+ opened.push(log);
56
+ return log;
57
+ };
58
+ let sqlite = null;
59
+ let featureConnection;
60
+ let eventLog;
61
+ let journalEventLog;
62
+ let lifecycle;
63
+ let journal;
64
+ try {
65
+ eventLog = openEventLog(eventsDbPath);
66
+ // Gap 1's byte journal gets its own store, deliberately separate from `eventLog`: that log
67
+ // replays every entry it holds to SSE subscribers as a `RunProtocolEvent`, and a journal entry
68
+ // has no corresponding protocol-event kind.
69
+ journalEventLog = openEventLog(dataDir === null ? null : join(dataDir, 'journal.db'));
70
+ if (sqliteStorage && eventsDbPath !== null && dataDir !== null) {
71
+ // A *second* connection to the same `events.db` the log above owns — safe: both run in WAL
72
+ // mode, which permits multiple concurrently open handles on one file within a single process.
73
+ featureConnection = openSqliteConnection({ filePath: eventsDbPath, open(file, settings) {
74
+ featureConnection = sqliteStorage.open(file, settings);
75
+ return featureConnection;
76
+ } });
77
+ sqlite = { connection: featureConnection, eventsDbPath, dataDir };
78
+ }
79
+ journal = createRunByteJournal({ eventLog: journalEventLog });
80
+ lifecycle = createRunLifecycle({ eventLog });
81
+ await lifecycle.rehydrate({});
82
+ }
83
+ catch (error) {
84
+ // Every acquired handle is attempted even if another closer fails. Preserve the boot error.
85
+ try {
86
+ featureConnection?.close();
87
+ }
88
+ catch { /* Keep the boot failure. */ }
89
+ await Promise.allSettled(opened.map((log) => log.close?.({})));
90
+ throw error;
91
+ }
92
+ const agentExecutor = createAgentExecutor({ lifecycle }, {
93
+ ...options.agentExecutor,
94
+ journal,
95
+ classifyFailure: classifyRunFailureForRetry,
96
+ });
97
+ const registry = createToolRegistry({});
98
+ // Created eagerly against the still-empty registry, and that is correct: `ToolExecutor` resolves
99
+ // a registration at execute time, not at construction, so features composed later can contribute
100
+ // tools this executor will find. It is what lets a feature's routes take the executor as a
101
+ // dependency while its own tools register in the same pass.
102
+ const toolExecutor = createToolExecutor({ registry });
103
+ let closed = null;
104
+ return {
105
+ eventLog,
106
+ journalEventLog,
107
+ lifecycle,
108
+ agentExecutor,
109
+ registry,
110
+ toolExecutor,
111
+ sqlite,
112
+ close() {
113
+ closed ??= (async () => {
114
+ try {
115
+ sqlite?.connection.close();
116
+ }
117
+ finally {
118
+ await Promise.all([eventLog.close?.({}), journalEventLog.close?.({})]);
119
+ }
120
+ })();
121
+ return closed;
122
+ },
123
+ };
124
+ }
125
+ //# sourceMappingURL=kernel-base.js.map
@@ -0,0 +1,61 @@
1
+ /** Two-stage boot orchestration. Modules clean up their own partially failed stage. */
2
+ export type ModuleLifecycleStatus = {
3
+ status: 'ready';
4
+ } | {
5
+ status: 'disabled';
6
+ reasonCode: string;
7
+ remediationHint: string;
8
+ } | {
9
+ status: 'failed';
10
+ reasonCode: string;
11
+ remediationHint: string;
12
+ };
13
+ export type BootCriticality = 'critical' | 'optional';
14
+ export interface BootModule {
15
+ name: string;
16
+ owner: string;
17
+ criticality: BootCriticality;
18
+ /** Acquire reversible resources; do not start external work yet. */
19
+ prepare(): Promise<void>;
20
+ start(): Promise<void>;
21
+ /** Idempotent; never called on the module whose current stage failed. */
22
+ stop(): Promise<void>;
23
+ }
24
+ export interface BootModuleResult {
25
+ name: string;
26
+ owner: string;
27
+ criticality: BootCriticality;
28
+ lifecycle: ModuleLifecycleStatus;
29
+ }
30
+ export interface BootResult {
31
+ ok: boolean;
32
+ modules: BootModuleResult[];
33
+ }
34
+ export interface ReadinessStore {
35
+ set(required: {
36
+ snapshot: BootResult;
37
+ }): void;
38
+ get(): BootResult;
39
+ }
40
+ export interface LifecycleReporter {
41
+ report(required: {
42
+ result: BootResult;
43
+ }): void;
44
+ }
45
+ /**
46
+ * Preserve source rollback semantics: prepare failure unwinds completed preparations;
47
+ * start failure unwinds completed starts only. Failing modules own their partial cleanup.
48
+ * Prepared modules not yet started on a start-stage abort remain host-owned.
49
+
50
+ * Contract rationale (including readiness ownership):
51
+ * Ports: `BootModule`, `ReadinessStore`, optional `LifecycleReporter`. `runBootLifecycle({ modules }, { readiness, reporter })` runs all preparations before starts. Optional failures do not set `ok=false`; critical failures stop the stage and unwind completed modules in reverse order. The failing module self-cleans. The existing start-stage rollback stops only modules that completed start, leaving other prepared resources host-owned; this extraction deliberately preserves that source behavior. A ready record for an unwound module retains the original source status.
52
+ *
53
+ * `createReadinessStore({}, { initial })` owns defensive snapshot copies per instance. No module-global state or synthetic daemon name. Keep daemon health latching in the host and compose it with this store. Duplicate module names now reject before acquisition; reporter failure is best effort. Lifecycle calls can be concurrent only if callers provide separate modules/stores or coordinate their own boot.
54
+ */
55
+ export declare function runBootLifecycle(required: {
56
+ modules: readonly BootModule[];
57
+ }, optional?: {
58
+ readiness?: ReadinessStore;
59
+ reporter?: LifecycleReporter;
60
+ }): Promise<BootResult>;
61
+ //# sourceMappingURL=boot-lifecycle.d.ts.map
@@ -0,0 +1,74 @@
1
+ async function stopInReverse(modules) {
2
+ for (const module of [...modules].reverse()) {
3
+ try {
4
+ await module.stop();
5
+ }
6
+ catch { /* Preserve the original failure and continue cleanup. */ }
7
+ }
8
+ }
9
+ /**
10
+ * Preserve source rollback semantics: prepare failure unwinds completed preparations;
11
+ * start failure unwinds completed starts only. Failing modules own their partial cleanup.
12
+ * Prepared modules not yet started on a start-stage abort remain host-owned.
13
+
14
+ * Contract rationale (including readiness ownership):
15
+ * Ports: `BootModule`, `ReadinessStore`, optional `LifecycleReporter`. `runBootLifecycle({ modules }, { readiness, reporter })` runs all preparations before starts. Optional failures do not set `ok=false`; critical failures stop the stage and unwind completed modules in reverse order. The failing module self-cleans. The existing start-stage rollback stops only modules that completed start, leaving other prepared resources host-owned; this extraction deliberately preserves that source behavior. A ready record for an unwound module retains the original source status.
16
+ *
17
+ * `createReadinessStore({}, { initial })` owns defensive snapshot copies per instance. No module-global state or synthetic daemon name. Keep daemon health latching in the host and compose it with this store. Duplicate module names now reject before acquisition; reporter failure is best effort. Lifecycle calls can be concurrent only if callers provide separate modules/stores or coordinate their own boot.
18
+ */
19
+ export async function runBootLifecycle(required, optional = {}) {
20
+ const modules = [...required.modules];
21
+ if (new Set(modules.map(module => module.name)).size !== modules.length) {
22
+ throw new Error('boot module names must be unique');
23
+ }
24
+ const results = new Map();
25
+ const prepared = [];
26
+ const ready = [];
27
+ let aborted = false;
28
+ for (const stage of ['prepare', 'start']) {
29
+ const completed = stage === 'prepare' ? prepared : ready;
30
+ for (const module of stage === 'prepare' ? modules : prepared) {
31
+ try {
32
+ await module[stage]();
33
+ completed.push(module);
34
+ if (stage === 'start')
35
+ results.set(module.name, {
36
+ name: module.name, owner: module.owner, criticality: module.criticality,
37
+ lifecycle: { status: 'ready' },
38
+ });
39
+ }
40
+ catch (error) {
41
+ results.set(module.name, {
42
+ name: module.name, owner: module.owner, criticality: module.criticality,
43
+ lifecycle: {
44
+ status: 'failed', reasonCode: error instanceof Error ? error.message : String(error),
45
+ remediationHint: 'check the boot log for the underlying error and retry after fixing it',
46
+ },
47
+ });
48
+ if (module.criticality === 'critical') {
49
+ await stopInReverse(completed);
50
+ aborted = true;
51
+ break;
52
+ }
53
+ }
54
+ }
55
+ if (aborted)
56
+ break;
57
+ }
58
+ const ordered = modules.flatMap(module => {
59
+ const result = results.get(module.name);
60
+ return result === undefined ? [] : [result];
61
+ });
62
+ const result = {
63
+ ok: ordered.every(module => module.criticality !== 'critical' || module.lifecycle.status === 'ready'),
64
+ modules: ordered,
65
+ };
66
+ optional.readiness?.set({ snapshot: result });
67
+ // A reporter must never turn a completed boot into an apparent failure.
68
+ try {
69
+ optional.reporter?.report({ result });
70
+ }
71
+ catch { /* Best-effort reporting. */ }
72
+ return result;
73
+ }
74
+ //# sourceMappingURL=boot-lifecycle.js.map
@@ -0,0 +1,3 @@
1
+ export * from './boot-lifecycle.js';
2
+ export * from './readiness-state.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export * from './boot-lifecycle.js';
2
+ export * from './readiness-state.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,6 @@
1
+ import type { BootResult, ReadinessStore } from './boot-lifecycle.js';
2
+ /** Instance-owned snapshots. Host-specific health records are supplied by the caller. */
3
+ export declare function createReadinessStore(required: Record<string, never>, optional?: {
4
+ initial?: BootResult;
5
+ }): ReadinessStore;
6
+ //# sourceMappingURL=readiness-state.d.ts.map
@@ -0,0 +1,15 @@
1
+ function copy(snapshot) {
2
+ return { ok: snapshot.ok, modules: snapshot.modules.map(module => ({
3
+ ...module, lifecycle: { ...module.lifecycle },
4
+ })) };
5
+ }
6
+ /** Instance-owned snapshots. Host-specific health records are supplied by the caller. */
7
+ export function createReadinessStore(required, optional = {}) {
8
+ void required;
9
+ let snapshot = copy(optional.initial ?? { ok: true, modules: [] });
10
+ return {
11
+ set({ snapshot: next }) { snapshot = copy(next); },
12
+ get() { return copy(snapshot); },
13
+ };
14
+ }
15
+ //# sourceMappingURL=readiness-state.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @module backend-config
3
+ *
4
+ * Resolves which SQL backend a host daemon should use from environment
5
+ * variables. v1 ships local SQLite via an injected `better-sqlite3` opener (the event-log adapter
6
+ * now belongs to the daemon's storage subpath);
7
+ * a `postgres` selection is recorded so a future adapter can read it, but
8
+ * `resolveSqliteBackendConfig` never opens a connection itself — it only
9
+ * pins the parameter surface (host/port/database/user/sslMode) so a
10
+ * follow-up package can land the adapter without re-litigating the env-var
11
+ * contract.
12
+ */
13
+ export type SqliteBackendKind = 'sqlite' | 'postgres';
14
+ export interface SqliteBackendConfig {
15
+ kind: SqliteBackendKind;
16
+ /** Resolution metadata a future Postgres adapter will read. */
17
+ postgres?: {
18
+ host: string;
19
+ port: number;
20
+ database: string;
21
+ user: string;
22
+ /**
23
+ * Password / connection string are looked up at runtime from the
24
+ * caller's own secret manager; this layer never reads them through env.
25
+ */
26
+ sslMode?: 'disable' | 'require' | 'verify-full';
27
+ };
28
+ }
29
+ export declare class SqliteBackendConfigError extends Error {
30
+ constructor({ message }: {
31
+ message: string;
32
+ });
33
+ }
34
+ /**
35
+ * Reads `JINI_SQLITE_BACKEND` (`'sqlite'` default, or `'postgres'`) and, for
36
+ * `postgres`, `JINI_PG_HOST`/`JINI_PG_PORT`/`JINI_PG_DATABASE`/`JINI_PG_USER`/
37
+ * `JINI_PG_SSL_MODE` from the given env map (defaults to `process.env`).
38
+ *
39
+ * @param required - No required configuration values; pass an empty argument object.
40
+ * @param optional.env - Environment map to read from. Defaults to `process.env`.
41
+ * @returns The resolved backend config.
42
+ * @throws {SqliteBackendConfigError} When `kind` is unrecognized, or `postgres`
43
+ * is selected without the required host/database/user.
44
+ */
45
+ export declare function resolveSqliteBackendConfig(required: Record<string, never>, optional?: {
46
+ env?: Record<string, string | undefined>;
47
+ }): SqliteBackendConfig;
48
+ //# sourceMappingURL=backend-config.d.ts.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @module backend-config
3
+ *
4
+ * Resolves which SQL backend a host daemon should use from environment
5
+ * variables. v1 ships local SQLite via an injected `better-sqlite3` opener (the event-log adapter
6
+ * now belongs to the daemon's storage subpath);
7
+ * a `postgres` selection is recorded so a future adapter can read it, but
8
+ * `resolveSqliteBackendConfig` never opens a connection itself — it only
9
+ * pins the parameter surface (host/port/database/user/sslMode) so a
10
+ * follow-up package can land the adapter without re-litigating the env-var
11
+ * contract.
12
+ */
13
+ export class SqliteBackendConfigError extends Error {
14
+ constructor({ message }) {
15
+ super(message);
16
+ this.name = 'SqliteBackendConfigError';
17
+ }
18
+ }
19
+ /**
20
+ * Reads `JINI_SQLITE_BACKEND` (`'sqlite'` default, or `'postgres'`) and, for
21
+ * `postgres`, `JINI_PG_HOST`/`JINI_PG_PORT`/`JINI_PG_DATABASE`/`JINI_PG_USER`/
22
+ * `JINI_PG_SSL_MODE` from the given env map (defaults to `process.env`).
23
+ *
24
+ * @param required - No required configuration values; pass an empty argument object.
25
+ * @param optional.env - Environment map to read from. Defaults to `process.env`.
26
+ * @returns The resolved backend config.
27
+ * @throws {SqliteBackendConfigError} When `kind` is unrecognized, or `postgres`
28
+ * is selected without the required host/database/user.
29
+ */
30
+ export function resolveSqliteBackendConfig(required, optional = {}) {
31
+ void required;
32
+ const e = optional.env ?? process.env;
33
+ const kind = (e.JINI_SQLITE_BACKEND ?? 'sqlite').trim().toLowerCase();
34
+ if (kind === 'postgres') {
35
+ const host = e.JINI_PG_HOST ?? '';
36
+ const portStr = e.JINI_PG_PORT ?? '5432';
37
+ const database = e.JINI_PG_DATABASE ?? '';
38
+ const user = e.JINI_PG_USER ?? '';
39
+ const sslMode = e.JINI_PG_SSL_MODE === 'disable' || e.JINI_PG_SSL_MODE === 'verify-full' ? e.JINI_PG_SSL_MODE : 'require';
40
+ if (!host || !database || !user) {
41
+ throw new SqliteBackendConfigError({ message: 'JINI_SQLITE_BACKEND=postgres requires JINI_PG_HOST, JINI_PG_DATABASE, JINI_PG_USER. ' +
42
+ 'JINI_PG_PORT defaults to 5432; JINI_PG_SSL_MODE defaults to "require".',
43
+ });
44
+ }
45
+ return {
46
+ kind: 'postgres',
47
+ postgres: {
48
+ host,
49
+ port: Number.parseInt(portStr, 10) || 5432,
50
+ database,
51
+ user,
52
+ sslMode,
53
+ },
54
+ };
55
+ }
56
+ if (kind !== 'sqlite' && kind !== '') {
57
+ throw new SqliteBackendConfigError({ message: `unknown JINI_SQLITE_BACKEND value '${kind}'. Accepted: 'sqlite' (default), 'postgres'.` });
58
+ }
59
+ return { kind: 'sqlite' };
60
+ }
61
+ //# sourceMappingURL=backend-config.js.map
@@ -0,0 +1,3 @@
1
+ /** Host backend selection only; never opens a database or imports a driver. */
2
+ export * from './backend-config.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ /** Host backend selection only; never opens a database or imports a driver. */
2
+ export * from './backend-config.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,26 @@
1
+ import fs from 'node:fs';
2
+ import { type SqliteDb, type SqliteSyncOpener } from '@jini-ai/db/sqlite';
3
+ /**
4
+ * Explicit idempotent bootstrap of unchanged legacy tables; no versioned migration ledger.
5
+ * @param db Borrowed host handle; this operation neither opens nor closes it.
6
+ * @complexity O(1) schema statements.
7
+ */
8
+ export declare function migrate({ db }: {
9
+ db: SqliteDb;
10
+ }): void;
11
+ /**
12
+ * Opens/caches <dataDir>/app.sqlite using the injected opener, WAL and foreign keys.
13
+ * A new path closes the previous singleton; setup failure closes only the newly acquired handle.
14
+ * @throws The original host acquisition/schema failure, or a missing opener.
15
+ * @complexity O(1) acquisition/bootstrap calls, independent of stored row count.
16
+ */
17
+ export declare function openDatabase({ projectRoot, open }: {
18
+ projectRoot: string;
19
+ open: SqliteSyncOpener;
20
+ }, options?: {
21
+ dataDir?: string;
22
+ filesystem?: Pick<typeof fs, 'mkdirSync'>;
23
+ }): SqliteDb;
24
+ /** Close the active owned singleton once; clearing state first permits a clean retry after failure. */
25
+ export declare function closeDatabase(): void;
26
+ //# sourceMappingURL=sqlite.d.ts.map
@@ -0,0 +1,63 @@
1
+ /** Local app.sqlite singleton policy; the host explicitly supplies its SQLite driver. */
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import { openSqliteConnection } from '@jini-ai/db/sqlite';
5
+ import { LEGACY_CHAT_DDL } from '@jini-ai/chat/store/legacy/sqlite';
6
+ import { LEGACY_AGENT_SESSIONS_DDL } from '@jini-ai/daemon/store/agent-sessions/sqlite';
7
+ import { LEGACY_PROJECTS_DDL } from '../../store/projects/sqlite.js';
8
+ let dbInstance = null;
9
+ let dbFile = null;
10
+ /**
11
+ * Explicit idempotent bootstrap of unchanged legacy tables; no versioned migration ledger.
12
+ * @param db Borrowed host handle; this operation neither opens nor closes it.
13
+ * @complexity O(1) schema statements.
14
+ */
15
+ // Only generic engine tables belong in this bootstrap; consumer feature tables and migrations
16
+ // stay in host adapters. Every legacy column is declared in the base DDL, so this is not an
17
+ // ALTER TABLE migration path for a consumer's pre-existing product schema.
18
+ // Catalog tables are created only by hosts that actually seed and query them. A parallel execution
19
+ // table would also weaken the existing ToolExecutionAuditRecord (principal, run and phase data);
20
+ // durable audit storage should preserve that record rather than introduce a second audit noun.
21
+ export function migrate({ db }) {
22
+ db.exec(LEGACY_PROJECTS_DDL + LEGACY_CHAT_DDL + LEGACY_AGENT_SESSIONS_DDL);
23
+ }
24
+ /**
25
+ * Opens/caches <dataDir>/app.sqlite using the injected opener, WAL and foreign keys.
26
+ * A new path closes the previous singleton; setup failure closes only the newly acquired handle.
27
+ * @throws The original host acquisition/schema failure, or a missing opener.
28
+ * @complexity O(1) acquisition/bootstrap calls, independent of stored row count.
29
+ */
30
+ export function openDatabase({ projectRoot, open }, options = {}) {
31
+ // Keep the historical refusal text for JavaScript callers that omit the required port.
32
+ if (!open)
33
+ throw new Error('openDatabase: inject options.open');
34
+ const dir = options.dataDir ? path.resolve(options.dataDir) : path.join(projectRoot, '.jini');
35
+ const file = path.join(dir, 'app.sqlite');
36
+ if (dbInstance && dbFile === file)
37
+ return dbInstance;
38
+ closeDatabase();
39
+ (options.filesystem ?? fs).mkdirSync(dir, { recursive: true });
40
+ let db;
41
+ try {
42
+ db = openSqliteConnection({ filePath: file, open(file, settings) { db = open(file, settings); return db; }, pragmas: ['journal_mode = WAL', 'foreign_keys = ON'] });
43
+ migrate({ db });
44
+ dbInstance = db;
45
+ dbFile = file;
46
+ return db;
47
+ }
48
+ catch (error) {
49
+ try {
50
+ db?.close();
51
+ }
52
+ catch { /* Keep the original bootstrap failure. */ }
53
+ throw error;
54
+ }
55
+ }
56
+ /** Close the active owned singleton once; clearing state first permits a clean retry after failure. */
57
+ export function closeDatabase() {
58
+ const db = dbInstance;
59
+ dbInstance = null;
60
+ dbFile = null;
61
+ db?.close();
62
+ }
63
+ //# sourceMappingURL=sqlite.js.map