astrocode-workflow 0.1.57 → 0.1.59

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 (49) hide show
  1. package/README.md +243 -11
  2. package/dist/agents/prompts.d.ts +1 -0
  3. package/dist/agents/prompts.js +159 -0
  4. package/dist/agents/registry.js +11 -1
  5. package/dist/config/loader.js +34 -0
  6. package/dist/config/schema.d.ts +12 -1
  7. package/dist/config/schema.js +14 -0
  8. package/dist/hooks/continuation-enforcer.d.ts +9 -1
  9. package/dist/hooks/continuation-enforcer.js +2 -1
  10. package/dist/hooks/inject-provider.d.ts +9 -1
  11. package/dist/hooks/inject-provider.js +11 -3
  12. package/dist/hooks/tool-output-truncator.d.ts +9 -1
  13. package/dist/hooks/tool-output-truncator.js +2 -1
  14. package/dist/index.js +228 -45
  15. package/dist/state/adapters/index.d.ts +4 -2
  16. package/dist/state/adapters/index.js +23 -27
  17. package/dist/state/db.d.ts +6 -8
  18. package/dist/state/db.js +106 -45
  19. package/dist/tools/index.d.ts +13 -3
  20. package/dist/tools/index.js +14 -31
  21. package/dist/tools/init.d.ts +10 -1
  22. package/dist/tools/init.js +73 -18
  23. package/dist/tools/spec.d.ts +0 -1
  24. package/dist/tools/spec.js +4 -1
  25. package/dist/tools/status.d.ts +1 -1
  26. package/dist/tools/status.js +70 -52
  27. package/dist/tools/workflow.js +6 -3
  28. package/dist/workflow/directives.d.ts +2 -0
  29. package/dist/workflow/directives.js +34 -19
  30. package/dist/workflow/state-machine.d.ts +27 -0
  31. package/dist/workflow/state-machine.js +167 -86
  32. package/package.json +1 -1
  33. package/src/agents/prompts.ts +160 -0
  34. package/src/agents/registry.ts +16 -1
  35. package/src/config/loader.ts +39 -4
  36. package/src/config/schema.ts +16 -0
  37. package/src/hooks/continuation-enforcer.ts +9 -2
  38. package/src/hooks/inject-provider.ts +18 -4
  39. package/src/hooks/tool-output-truncator.ts +9 -2
  40. package/src/index.ts +260 -56
  41. package/src/state/adapters/index.ts +21 -26
  42. package/src/state/db.ts +114 -58
  43. package/src/tools/index.ts +29 -31
  44. package/src/tools/init.ts +91 -22
  45. package/src/tools/spec.ts +6 -2
  46. package/src/tools/status.ts +71 -55
  47. package/src/tools/workflow.ts +7 -4
  48. package/src/workflow/directives.ts +103 -75
  49. package/src/workflow/state-machine.ts +229 -105
@@ -2,7 +2,8 @@ import { selectEligibleInjects } from "../tools/injects";
2
2
  import { injectChatPrompt } from "../ui/inject";
3
3
  import { nowISO } from "../shared/time";
4
4
  export function createInjectProvider(opts) {
5
- const { ctx, config, db } = opts;
5
+ const { ctx, config, runtime } = opts;
6
+ const { db } = runtime;
6
7
  // Cache to avoid re-injecting the same injects repeatedly
7
8
  const injectedCache = new Map();
8
9
  function shouldSkipInject(injectId, nowMs) {
@@ -65,6 +66,7 @@ export function createInjectProvider(opts) {
65
66
  // Get allowlists from config or defaults
66
67
  const scopeAllowlist = config.inject?.scope_allowlist ?? ["repo", "global"];
67
68
  const typeAllowlist = config.inject?.type_allowlist ?? ["note", "policy"];
69
+ const EMIT_TELEMETRY = config.debug?.telemetry?.enabled ?? false;
68
70
  // Get diagnostic data
69
71
  const diagnostics = getInjectionDiagnostics(now, scopeAllowlist, typeAllowlist);
70
72
  const eligibleInjects = selectEligibleInjects(db, {
@@ -77,7 +79,10 @@ export function createInjectProvider(opts) {
77
79
  let skippedDeduped = 0;
78
80
  if (eligibleInjects.length === 0) {
79
81
  // Log when no injects are eligible
80
- console.log(`[Astrocode:inject] ${now} selected=${diagnostics.selected_eligible} injected=0 skipped={expired:${diagnostics.skipped.expired} scope:${diagnostics.skipped.scope} type:${diagnostics.skipped.type} deduped:0}`);
82
+ if (EMIT_TELEMETRY) {
83
+ // eslint-disable-next-line no-console
84
+ console.log(`[Astrocode:inject] ${now} selected=${diagnostics.selected_eligible} injected=0 skipped={expired:${diagnostics.skipped.expired} scope:${diagnostics.skipped.scope} type:${diagnostics.skipped.type} deduped:0}`);
85
+ }
81
86
  return;
82
87
  }
83
88
  // Inject each eligible inject, skipping recently injected ones
@@ -98,7 +103,10 @@ export function createInjectProvider(opts) {
98
103
  markInjected(inject.inject_id, nowMs);
99
104
  }
100
105
  // Log diagnostic summary
101
- console.log(`[Astrocode:inject] ${now} selected=${diagnostics.selected_eligible} injected=${injected} skipped={expired:${diagnostics.skipped.expired} scope:${diagnostics.skipped.scope} type:${diagnostics.skipped.type} deduped:${skippedDeduped}}`);
106
+ if (EMIT_TELEMETRY) {
107
+ // eslint-disable-next-line no-console
108
+ console.log(`[Astrocode:inject] ${now} selected=${diagnostics.selected_eligible} injected=${injected} skipped={expired:${diagnostics.skipped.expired} scope:${diagnostics.skipped.scope} type:${diagnostics.skipped.type} deduped:${skippedDeduped}}`);
109
+ }
102
110
  }
103
111
  // Public hook handlers
104
112
  return {
@@ -9,9 +9,17 @@ type ToolExecuteAfterOutput = {
9
9
  output?: string;
10
10
  metadata?: Record<string, any>;
11
11
  };
12
+ type RuntimeState = {
13
+ db: SqliteDb | null;
14
+ limitedMode: boolean;
15
+ limitedModeReason: null | {
16
+ code: "db_init_failed" | "schema_too_old" | "schema_downgrade" | "schema_migration_failed";
17
+ details: any;
18
+ };
19
+ };
12
20
  export declare function createToolOutputTruncatorHook(opts: {
13
21
  ctx: any;
14
22
  config: AstrocodeConfig;
15
- db: SqliteDb;
23
+ runtime: RuntimeState;
16
24
  }): (input: ToolExecuteAfterInput, output: ToolExecuteAfterOutput) => Promise<void>;
17
25
  export {};
@@ -13,7 +13,8 @@ function pickMaxChars(toolName, cfg) {
13
13
  return cfg.truncation.max_chars_default;
14
14
  }
15
15
  export function createToolOutputTruncatorHook(opts) {
16
- const { ctx, config, db } = opts;
16
+ const { ctx, config, runtime } = opts;
17
+ const { db } = runtime;
17
18
  return async function toolExecuteAfter(input, output) {
18
19
  if (!config.truncation.enabled)
19
20
  return;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { loadAstrocodeConfig } from "./config/loader";
2
2
  import { createConfigHandler } from "./config/config-handler";
3
- import { openSqlite, configurePragmas, ensureSchema } from "./state/db";
3
+ import { openSqlite, configurePragmas, ensureSchema, getSchemaVersion } from "./state/db";
4
4
  import { getAstroPaths, ensureAstroDirs } from "./shared/paths";
5
5
  import { createAstroTools } from "./tools";
6
6
  import { createContinuationEnforcer } from "./hooks/continuation-enforcer";
@@ -8,106 +8,289 @@ import { createToolOutputTruncatorHook } from "./hooks/tool-output-truncator";
8
8
  import { createInjectProvider } from "./hooks/inject-provider";
9
9
  import { createToastManager } from "./ui/toasts";
10
10
  import { createAstroAgents } from "./agents/registry";
11
+ import { info, warn } from "./shared/log";
12
+ // Safe config cloning with structuredClone preference (fallback for older Node versions)
13
+ // CONTRACT: Config is guaranteed JSON-serializable (enforced by loadAstrocodeConfig validation)
14
+ const cloneConfig = (v) => {
15
+ // Prefer structuredClone when available (keeps more types)
16
+ if (typeof globalThis.structuredClone === "function") {
17
+ return globalThis.structuredClone(v);
18
+ }
19
+ // Fallback: JSON clone (safe since config is validated as JSON-serializable)
20
+ return JSON.parse(JSON.stringify(v));
21
+ };
22
+ // Minimal config defaults for limited-mode fallback
23
+ const applyMinimalConfigDefaults = (cfg) => {
24
+ if (!cfg || typeof cfg !== "object")
25
+ return;
26
+ if (!Array.isArray(cfg.disabled_hooks))
27
+ cfg.disabled_hooks = [];
28
+ if (!cfg.ui || typeof cfg.ui !== "object")
29
+ cfg.ui = {};
30
+ if (!cfg.ui.toasts || typeof cfg.ui.toasts !== "object")
31
+ cfg.ui.toasts = { enabled: false };
32
+ if (typeof cfg.ui.toasts.enabled !== "boolean")
33
+ cfg.ui.toasts.enabled = false;
34
+ };
11
35
  const Astrocode = async (ctx) => {
36
+ if (!ctx.directory || typeof ctx.directory !== "string") {
37
+ throw new Error("Astrocode requires ctx.directory to be a string repo root.");
38
+ }
12
39
  const repoRoot = ctx.directory;
13
40
  // Always load config first - this provides defaults even in limited mode
14
- let pluginConfig = loadAstrocodeConfig(repoRoot);
15
- // Create agents for registration
16
- const agents = createAstroAgents({ pluginConfig });
41
+ let pluginConfig;
42
+ try {
43
+ pluginConfig = loadAstrocodeConfig(repoRoot);
44
+ }
45
+ catch (e) {
46
+ warn(`Config loading failed, using minimal defaults.`, { err: e });
47
+ // Fallback to minimal safe defaults when config loading fails
48
+ pluginConfig = {
49
+ disabled_hooks: [],
50
+ disabled_agents: [],
51
+ determinism: { mode: "on", strict_stage_order: true },
52
+ db: {
53
+ path: ".astro/astro.db",
54
+ busy_timeout_ms: 5000,
55
+ pragmas: {},
56
+ schema_version_required: 2,
57
+ allow_auto_migrate: true,
58
+ fail_on_downgrade: true,
59
+ },
60
+ ui: {
61
+ toasts: { enabled: true, throttle_ms: 1500, show_run_started: true, show_stage_started: true, show_stage_completed: true, show_stage_failed: true, show_run_completed: true, show_auto_continue: true },
62
+ },
63
+ agents: {
64
+ orchestrator_name: "Astro",
65
+ stage_agent_names: { frame: "Frame", plan: "Plan", spec: "Spec", implement: "Implement", review: "Review", verify: "Verify", close: "Close" },
66
+ librarian_name: "Librarian",
67
+ explore_name: "Explore",
68
+ qa_name: "QA",
69
+ agent_variant_overrides: {},
70
+ },
71
+ workflow: {
72
+ default_mode: "step",
73
+ default_max_steps: 10,
74
+ loop_max_steps_hard_cap: 100,
75
+ evidence_required: { verify: false },
76
+ },
77
+ permissions: { enforce_task_tool_restrictions: false, deny_delegate_task_in_subagents: false },
78
+ };
79
+ }
17
80
  // Always ensure .astro directories exist, even in limited mode
18
81
  const paths = getAstroPaths(repoRoot, pluginConfig.db.path);
19
82
  ensureAstroDirs(paths);
20
83
  let db = null;
21
- let tools = null;
22
- let configHandler = null;
84
+ let agents = undefined;
85
+ let configHandler = async () => {
86
+ throw new Error("ConfigHandler called before initialization");
87
+ };
23
88
  let continuation = null;
24
89
  let truncatorHook = null;
25
90
  let injectProvider = null;
26
- let toasts = null;
91
+ let toastManager = null;
92
+ let limitedModeReason = null;
93
+ let limitedMode = false;
94
+ // Phase 1: Database initialization (only DB-related operations)
27
95
  try {
28
96
  db = openSqlite(paths.dbPath, { busyTimeoutMs: pluginConfig.db.busy_timeout_ms });
29
97
  configurePragmas(db, pluginConfig.db.pragmas);
30
- ensureSchema(db, { allowAutoMigrate: pluginConfig.db.allow_auto_migrate, failOnDowngrade: pluginConfig.db.fail_on_downgrade });
31
- // Database initialized successfully
32
- configHandler = createConfigHandler({ pluginConfig });
33
- tools = createAstroTools({ ctx, config: pluginConfig, db, agents });
34
- continuation = createContinuationEnforcer({ ctx, config: pluginConfig, db });
35
- truncatorHook = createToolOutputTruncatorHook({ ctx, config: pluginConfig, db });
36
- injectProvider = createInjectProvider({ ctx, config: pluginConfig, db });
37
- toasts = createToastManager({ ctx, throttleMs: pluginConfig.ui.toasts.throttle_ms });
98
+ // Policy enforced in harness; db layer is mechanics only (migration + structural schema creation)
99
+ ensureSchema(db, { allowAutoMigrate: pluginConfig.db.allow_auto_migrate });
100
+ // Harness-exclusive: enforce schema version requirement
101
+ // Explicitly enforce schema version requirement (harness-level policy)
102
+ const required = pluginConfig.db.schema_version_required;
103
+ let current = getSchemaVersion(db);
104
+ if (current < required) {
105
+ if (!pluginConfig.db.allow_auto_migrate) {
106
+ // Migration disabled - enter limited mode
107
+ limitedModeReason = { code: "schema_too_old", details: { current, required } };
108
+ warn("Entering limited mode (schema too old, migration disabled).", {
109
+ dbPath: paths.dbPath,
110
+ schemaCurrent: current,
111
+ schemaRequired: required,
112
+ failOnDowngrade: pluginConfig.db.fail_on_downgrade,
113
+ allowAutoMigrate: pluginConfig.db.allow_auto_migrate,
114
+ });
115
+ db.close?.();
116
+ db = null;
117
+ }
118
+ else {
119
+ // Migration allowed - re-run ensureSchema (idempotent) in case first call failed
120
+ ensureSchema(db, { allowAutoMigrate: true });
121
+ current = getSchemaVersion(db);
122
+ if (current < required) {
123
+ // Migration failed - enter limited mode
124
+ limitedModeReason = { code: "schema_migration_failed", details: { current, required } };
125
+ warn("Entering limited mode (schema migration failed).", {
126
+ dbPath: paths.dbPath,
127
+ schemaCurrent: current,
128
+ schemaRequired: required,
129
+ failOnDowngrade: pluginConfig.db.fail_on_downgrade,
130
+ allowAutoMigrate: pluginConfig.db.allow_auto_migrate,
131
+ });
132
+ db.close?.();
133
+ db = null;
134
+ }
135
+ }
136
+ }
137
+ else if (current > required && pluginConfig.db.fail_on_downgrade) {
138
+ limitedModeReason = { code: "schema_downgrade", details: { current, required } };
139
+ warn("Entering limited mode (schema downgrade).", {
140
+ dbPath: paths.dbPath,
141
+ schemaCurrent: current,
142
+ schemaRequired: required,
143
+ failOnDowngrade: pluginConfig.db.fail_on_downgrade,
144
+ allowAutoMigrate: pluginConfig.db.allow_auto_migrate,
145
+ });
146
+ db.close?.();
147
+ db = null;
148
+ }
38
149
  }
39
150
  catch (e) {
40
- // Database initialization failed - setup limited mode
41
- // Reload config to ensure all defaults are present
42
- pluginConfig = loadAstrocodeConfig(repoRoot);
43
- // Modify config for limited mode
44
- pluginConfig.disabled_hooks = [...(pluginConfig.disabled_hooks || []), "continuation-enforcer", "tool-output-truncator"];
45
- pluginConfig.ui.toasts.enabled = false;
46
- // Create limited functionality
151
+ // True database initialization failure - enter limited mode
152
+ warn(`Database initialization failed. Entering limited mode with reduced functionality.`, { err: e });
153
+ // Clean up partially-opened database connection
154
+ if (db && typeof db.close === "function") {
155
+ try {
156
+ db.close();
157
+ }
158
+ catch (closeErr) {
159
+ // Ignore close errors during cleanup
160
+ }
161
+ }
47
162
  db = null;
48
- configHandler = createConfigHandler({ pluginConfig });
49
- tools = createAstroTools({ ctx, config: pluginConfig, db, agents });
163
+ limitedModeReason = { code: "db_init_failed", details: e };
164
+ }
165
+ // Derive limited mode flag once for single source of truth
166
+ limitedMode = db === null;
167
+ // Single runtime state object for downstream components
168
+ const runtime = { db, limitedMode, limitedModeReason };
169
+ // Phase 2: Component initialization (based on DB availability)
170
+ if (!limitedMode) {
171
+ // Normal mode: DB available, create all components
172
+ try {
173
+ agents = createAstroAgents({ pluginConfig });
174
+ // Database initialized successfully
175
+ info("Database initialized successfully. Plugin ready.");
176
+ configHandler = createConfigHandler({ pluginConfig });
177
+ continuation = createContinuationEnforcer({ ctx, config: pluginConfig, runtime });
178
+ truncatorHook = createToolOutputTruncatorHook({ ctx, config: pluginConfig, runtime });
179
+ injectProvider = createInjectProvider({ ctx, config: pluginConfig, runtime });
180
+ toastManager = createToastManager({ ctx, throttleMs: pluginConfig.ui.toasts.throttle_ms });
181
+ }
182
+ catch (e) {
183
+ // Component failure with working DB → fail plugin boot
184
+ const err = e instanceof Error ? e : new Error(String(e));
185
+ throw new Error("Plugin initialization failed after successful database setup", { cause: err });
186
+ }
187
+ }
188
+ else {
189
+ // Limited mode: DB unavailable, create minimal components
190
+ // Clone the already-loaded config to avoid mutating shared state, then apply limited mode overrides
191
+ pluginConfig = cloneConfig(pluginConfig);
192
+ // Apply limited-mode config overrides BEFORE creating any components
193
+ pluginConfig.disabled_hooks = Array.from(new Set([...(pluginConfig.disabled_hooks || []), "continuation-enforcer", "tool-output-truncator", "inject-provider"]));
194
+ pluginConfig.ui.toasts.enabled = false;
195
+ // Show one-shot limited mode notification via logging (keeps toast policy consistent)
196
+ const reasonMessage = limitedModeReason ?
197
+ (limitedModeReason.code === "db_init_failed" ? "Database initialization failed. Check native deps or run astro_init." :
198
+ limitedModeReason.code === "schema_too_old" ? "Database schema too old and migration disabled." :
199
+ limitedModeReason.code === "schema_downgrade" ? "Database schema newer than plugin (downgrade detected)." :
200
+ limitedModeReason.code === "schema_migration_failed" ? "Database schema migration failed." :
201
+ "Database unavailable.") :
202
+ "Database unavailable.";
203
+ warn(`Astrocode: Limited Mode - ${reasonMessage}`, { dbPath: paths.dbPath });
204
+ // Create limited functionality (no global toast manager since toasts are disabled)
205
+ try {
206
+ configHandler = createConfigHandler({ pluginConfig });
207
+ }
208
+ catch (e) {
209
+ warn("Limited Mode: config handler unavailable", { err: e });
210
+ configHandler = async (config) => {
211
+ applyMinimalConfigDefaults(config);
212
+ };
213
+ }
50
214
  continuation = null;
51
215
  truncatorHook = null;
52
216
  injectProvider = null;
53
- toasts = null;
217
+ toastManager = null; // No global toast manager in limited mode
54
218
  }
219
+ // Helper for hook enablement checks (config is now finalized)
220
+ const hookEnabled = (key) => !pluginConfig.disabled_hooks?.includes(key);
221
+ // Create tools once with final state (limited mode gets DB-safe tool implementations)
222
+ const tools = createAstroTools({ ctx, config: pluginConfig, agents, runtime });
55
223
  return {
56
- name: "Astrocode",
224
+ name: "astrocode-harness",
57
225
  // Register agents
58
226
  agents,
59
227
  // Merge agents + slash commands into system config
60
228
  config: configHandler,
61
229
  // Register tools
62
- tool: createAstroTools({ ctx, config: pluginConfig, db, agents }),
230
+ tool: tools,
63
231
  // Limit created subagents from spawning more subagents (OMO-style).
64
232
  "tool.execute.before": async (input, output) => {
65
233
  if (!pluginConfig.permissions.enforce_task_tool_restrictions)
66
234
  return;
67
- if (input.tool !== "task")
235
+ if (input?.tool !== "task")
68
236
  return;
69
- output.args = output.args ?? {};
70
- const toolsMap = { ...(output.args.tools ?? {}) };
237
+ const baseArgs = (output && typeof output === "object" ? output.args : undefined) ?? {};
238
+ const baseTools = (baseArgs && typeof baseArgs === "object" && baseArgs.tools && typeof baseArgs.tools === "object")
239
+ ? baseArgs.tools
240
+ : {};
241
+ const nextTools = { ...baseTools };
71
242
  if (pluginConfig.permissions.deny_delegate_task_in_subagents) {
72
- toolsMap.delegate_task = false;
243
+ nextTools.delegate_task = false;
244
+ }
245
+ const nextArgs = { ...baseArgs, tools: nextTools };
246
+ // Back-compat: mutate if possible
247
+ if (output && typeof output === "object" && !Object.isFrozen(output)) {
248
+ output.args = nextArgs;
73
249
  }
74
- output.args.tools = toolsMap;
250
+ // Forward-compat: return conservative merge
251
+ if (output && typeof output === "object") {
252
+ return { ...output, args: nextArgs };
253
+ }
254
+ return { args: nextArgs };
75
255
  },
76
256
  "tool.execute.after": async (input, output) => {
77
257
  // Truncate huge tool outputs to artifacts
78
- if (truncatorHook && !pluginConfig.disabled_hooks.includes("tool-output-truncator")) {
79
- await truncatorHook(input, output);
258
+ if (truncatorHook && hookEnabled("tool-output-truncator")) {
259
+ await truncatorHook(input, output ?? null);
80
260
  }
81
261
  // Schedule continuation (do not immediately spam)
82
- if (continuation && !pluginConfig.disabled_hooks.includes("continuation-enforcer")) {
262
+ if (continuation && hookEnabled("continuation-enforcer")) {
83
263
  await continuation.onToolAfter(input);
84
264
  }
265
+ return output;
85
266
  },
86
267
  "chat.message": async (input, output) => {
87
- if (injectProvider && !pluginConfig.disabled_hooks.includes("inject-provider")) {
268
+ if (injectProvider && hookEnabled("inject-provider")) {
88
269
  await injectProvider.onChatMessage(input);
89
270
  }
90
- if (continuation && !pluginConfig.disabled_hooks.includes("continuation-enforcer")) {
271
+ if (continuation && hookEnabled("continuation-enforcer")) {
91
272
  await continuation.onChatMessage(input);
92
273
  }
93
274
  return output;
94
275
  },
95
276
  event: async (input) => {
96
- if (continuation && !pluginConfig.disabled_hooks.includes("continuation-enforcer")) {
277
+ if (continuation && hookEnabled("continuation-enforcer")) {
97
278
  await continuation.onEvent(input);
98
279
  }
99
280
  },
100
281
  // Best-effort cleanup
101
282
  close: async () => {
102
- try {
103
- db.close();
104
- }
105
- catch {
106
- // ignore
283
+ if (db && typeof db.close === "function") {
284
+ try {
285
+ db.close();
286
+ }
287
+ catch {
288
+ // ignore
289
+ }
107
290
  }
108
- if (toasts && pluginConfig.ui.toasts.enabled) {
291
+ if (toastManager && pluginConfig.ui.toasts.enabled) {
109
292
  try {
110
- await toasts.show({ title: "Astrocode", message: "Plugin closed", variant: "info" });
293
+ await toastManager.show({ title: "Astrocode", message: "Plugin closed", variant: "info" });
111
294
  }
112
295
  catch {
113
296
  // ignore
@@ -18,9 +18,11 @@ export interface DatabaseAdapter {
18
18
  busyTimeoutMs?: number;
19
19
  }): DatabaseConnection;
20
20
  }
21
- export declare class MockDatabaseAdapter implements DatabaseAdapter {
21
+ export declare class NullAdapter implements DatabaseAdapter {
22
22
  isAvailable(): boolean;
23
- open(): DatabaseConnection;
23
+ open(path: string, opts?: {
24
+ busyTimeoutMs?: number;
25
+ }): DatabaseConnection;
24
26
  }
25
27
  export declare class BunSqliteAdapter implements DatabaseAdapter {
26
28
  isAvailable(): boolean;
@@ -1,21 +1,10 @@
1
- import { warn } from "../../shared/log";
2
- // Mock adapter for when no database is available
3
- export class MockDatabaseAdapter {
1
+ // Null adapter for when no database driver is available
2
+ export class NullAdapter {
4
3
  isAvailable() {
5
- return true; // Mock is always available
4
+ return false;
6
5
  }
7
- open() {
8
- warn("Using mock database - no persistence available");
9
- return {
10
- pragma: () => { },
11
- exec: () => { },
12
- prepare: () => ({
13
- run: () => ({ changes: 0, lastInsertRowid: null }),
14
- get: () => null,
15
- all: () => []
16
- }),
17
- close: () => { }
18
- };
6
+ open(path, opts) {
7
+ throw new Error("No SQLite driver available");
19
8
  }
20
9
  }
21
10
  // Bun SQLite adapter - singleton pattern to avoid multiple initializations
@@ -92,7 +81,7 @@ export class BetterSqliteAdapter {
92
81
  db.pragma(`busy_timeout = ${opts.busyTimeoutMs}`);
93
82
  }
94
83
  return {
95
- pragma: (sql) => db.pragma(sql.replace('PRAGMA ', '')),
84
+ pragma: (sql) => db.pragma(sql),
96
85
  exec: (sql) => db.exec(sql),
97
86
  prepare: (sql) => db.prepare(sql),
98
87
  close: () => db.close()
@@ -103,17 +92,24 @@ export class BetterSqliteAdapter {
103
92
  export function createDatabaseAdapter() {
104
93
  const isBun = typeof globalThis.Bun !== 'undefined';
105
94
  if (isBun) {
106
- const adapter = new BunSqliteAdapter();
107
- if (adapter.isAvailable()) {
108
- return adapter;
109
- }
110
- throw new Error("Bun runtime detected but bun:sqlite not available");
95
+ // Prefer bun:sqlite in Bun runtime
96
+ const bunAdapter = new BunSqliteAdapter();
97
+ if (bunAdapter.isAvailable())
98
+ return bunAdapter;
99
+ // Fallback to better-sqlite3
100
+ const betterAdapter = new BetterSqliteAdapter();
101
+ if (betterAdapter.isAvailable())
102
+ return betterAdapter;
111
103
  }
112
104
  else {
113
- const adapter = new BetterSqliteAdapter();
114
- if (adapter.isAvailable()) {
115
- return adapter;
116
- }
117
- throw new Error("Node.js runtime detected but better-sqlite3 not available");
105
+ // Prefer better-sqlite3 in Node.js
106
+ const betterAdapter = new BetterSqliteAdapter();
107
+ if (betterAdapter.isAvailable())
108
+ return betterAdapter;
109
+ // Fallback to bun:sqlite (rare but possible)
110
+ const bunAdapter = new BunSqliteAdapter();
111
+ if (bunAdapter.isAvailable())
112
+ return bunAdapter;
118
113
  }
114
+ return new NullAdapter();
119
115
  }
@@ -3,15 +3,13 @@ export type SqliteDb = DatabaseConnection;
3
3
  export declare function openSqlite(dbPath: string, opts?: {
4
4
  busyTimeoutMs?: number;
5
5
  }): SqliteDb;
6
- export declare function configurePragmas(db: SqliteDb, pragmas: {
7
- journal_mode?: "WAL" | "DELETE";
8
- synchronous?: "NORMAL" | "FULL" | "OFF";
9
- foreign_keys?: boolean;
10
- temp_store?: "DEFAULT" | "MEMORY" | "FILE";
11
- }): void;
6
+ export declare function configurePragmas(db: SqliteDb, pragmas: Record<string, any>): void;
12
7
  /** BEGIN IMMEDIATE transaction helper. */
13
- export declare function withTx<T>(db: SqliteDb, fn: () => T): T;
8
+ export declare function withTx<T>(db: SqliteDb, fn: () => T, opts?: {
9
+ require?: boolean;
10
+ }): T;
11
+ export declare function getSchemaVersion(db: SqliteDb): number;
14
12
  export declare function ensureSchema(db: SqliteDb, opts?: {
15
13
  allowAutoMigrate?: boolean;
16
- failOnDowngrade?: boolean;
14
+ silent?: boolean;
17
15
  }): void;