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