@cortexkit/opencode-magic-context 0.21.8 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +124 -323
  2. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  3. package/dist/agents/permissions.d.ts +29 -14
  4. package/dist/agents/permissions.d.ts.map +1 -1
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/migrate-experimental.d.ts +29 -0
  7. package/dist/config/migrate-experimental.d.ts.map +1 -0
  8. package/dist/config/schema/agent-overrides.d.ts.map +1 -1
  9. package/dist/config/schema/magic-context.d.ts +95 -104
  10. package/dist/config/schema/magic-context.d.ts.map +1 -1
  11. package/dist/features/builtin-commands/commands.d.ts.map +1 -1
  12. package/dist/features/magic-context/compartment-embedding.d.ts +34 -0
  13. package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -0
  14. package/dist/features/magic-context/compartment-events.d.ts +50 -0
  15. package/dist/features/magic-context/compartment-events.d.ts.map +1 -0
  16. package/dist/features/magic-context/compartment-storage.d.ts +22 -0
  17. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/queue.d.ts +13 -2
  20. package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
  21. package/dist/features/magic-context/dreamer/runner.d.ts +11 -0
  22. package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
  23. package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -1
  24. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  25. package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
  26. package/dist/features/magic-context/key-files/identify-key-files.d.ts +1 -1
  27. package/dist/features/magic-context/key-files/identify-key-files.d.ts.map +1 -1
  28. package/dist/features/magic-context/key-files/project-key-files.d.ts.map +1 -1
  29. package/dist/features/magic-context/key-files/read-stats.d.ts +1 -1
  30. package/dist/features/magic-context/key-files/read-stats.d.ts.map +1 -1
  31. package/dist/features/magic-context/memory/constants.d.ts +4 -0
  32. package/dist/features/magic-context/memory/constants.d.ts.map +1 -1
  33. package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
  34. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  35. package/dist/features/magic-context/memory/embedding-openai.d.ts +6 -0
  36. package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
  37. package/dist/features/magic-context/memory/embedding-probe.d.ts +5 -0
  38. package/dist/features/magic-context/memory/embedding-probe.d.ts.map +1 -1
  39. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  40. package/dist/features/magic-context/memory/index.d.ts +1 -1
  41. package/dist/features/magic-context/memory/index.d.ts.map +1 -1
  42. package/dist/features/magic-context/memory/memory-migration.d.ts +133 -0
  43. package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -0
  44. package/dist/features/magic-context/memory/project-identity.d.ts +38 -7
  45. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  46. package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
  47. package/dist/features/magic-context/memory/storage-memory.d.ts +15 -1
  48. package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
  49. package/dist/features/magic-context/memory/types.d.ts +3 -1
  50. package/dist/features/magic-context/memory/types.d.ts.map +1 -1
  51. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  52. package/dist/features/magic-context/migrations.d.ts +7 -0
  53. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  54. package/dist/features/magic-context/project-docs-hash.d.ts +6 -0
  55. package/dist/features/magic-context/project-docs-hash.d.ts.map +1 -0
  56. package/dist/features/magic-context/project-identity.d.ts +2 -0
  57. package/dist/features/magic-context/project-identity.d.ts.map +1 -0
  58. package/dist/features/magic-context/storage-db.d.ts +51 -7
  59. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  60. package/dist/features/magic-context/storage-historian-runs.d.ts +73 -0
  61. package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -0
  62. package/dist/features/magic-context/storage-identity-rekey-map.d.ts +11 -0
  63. package/dist/features/magic-context/storage-identity-rekey-map.d.ts.map +1 -0
  64. package/dist/features/magic-context/storage-m0-mutation-log.d.ts +22 -0
  65. package/dist/features/magic-context/storage-m0-mutation-log.d.ts.map +1 -0
  66. package/dist/features/magic-context/storage-memory-mutation-log.d.ts +25 -0
  67. package/dist/features/magic-context/storage-memory-mutation-log.d.ts.map +1 -0
  68. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  69. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  70. package/dist/features/magic-context/storage-meta-shared.d.ts +44 -0
  71. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  72. package/dist/features/magic-context/storage-meta.d.ts +1 -0
  73. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  74. package/dist/features/magic-context/storage-project-state.d.ts +19 -0
  75. package/dist/features/magic-context/storage-project-state.d.ts.map +1 -0
  76. package/dist/features/magic-context/storage-subagent-invocations.d.ts +9 -0
  77. package/dist/features/magic-context/storage-subagent-invocations.d.ts.map +1 -1
  78. package/dist/features/magic-context/storage-tags.d.ts +21 -1
  79. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  80. package/dist/features/magic-context/storage-v22-backfill-failures.d.ts +24 -0
  81. package/dist/features/magic-context/storage-v22-backfill-failures.d.ts.map +1 -0
  82. package/dist/features/magic-context/storage.d.ts +12 -3
  83. package/dist/features/magic-context/storage.d.ts.map +1 -1
  84. package/dist/features/magic-context/subagent-token-capture.d.ts +1 -1
  85. package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -1
  86. package/dist/features/magic-context/tagger.d.ts +15 -1
  87. package/dist/features/magic-context/tagger.d.ts.map +1 -1
  88. package/dist/features/magic-context/types.d.ts +21 -0
  89. package/dist/features/magic-context/types.d.ts.map +1 -1
  90. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  91. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
  92. package/dist/features/magic-context/v22-deferred-backfill.d.ts +46 -0
  93. package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -0
  94. package/dist/features/magic-context/work-metrics.d.ts +66 -0
  95. package/dist/features/magic-context/work-metrics.d.ts.map +1 -1
  96. package/dist/hooks/auto-update-checker/cache.d.ts.map +1 -1
  97. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  98. package/dist/hooks/magic-context/cache-busting-signals.d.ts +9 -0
  99. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
  100. package/dist/hooks/magic-context/command-handler.d.ts +13 -1
  101. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/compartment-parser.d.ts +25 -0
  103. package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
  104. package/dist/hooks/magic-context/compartment-prompt.d.ts +27 -16
  105. package/dist/hooks/magic-context/compartment-prompt.d.ts.map +1 -1
  106. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts +6 -2
  108. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
  109. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  110. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts +9 -1
  111. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/compartment-runner-types.d.ts +67 -4
  113. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  115. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  116. package/dist/hooks/magic-context/decay-curve.d.ts +78 -0
  117. package/dist/hooks/magic-context/decay-curve.d.ts.map +1 -0
  118. package/dist/hooks/magic-context/decay-render.d.ts +67 -0
  119. package/dist/hooks/magic-context/decay-render.d.ts.map +1 -0
  120. package/dist/hooks/magic-context/event-handler.d.ts +1 -1
  121. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  122. package/dist/hooks/magic-context/event-resolvers.d.ts +17 -0
  123. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  124. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  125. package/dist/hooks/magic-context/historian-prompt.generated.d.ts +2 -0
  126. package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -0
  127. package/dist/hooks/magic-context/hook-handlers.d.ts +3 -0
  128. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  129. package/dist/hooks/magic-context/hook.d.ts +9 -21
  130. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  131. package/dist/hooks/magic-context/inject-compartments.d.ts +126 -0
  132. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  133. package/dist/hooks/magic-context/key-files-block.d.ts.map +1 -1
  134. package/dist/hooks/magic-context/live-session-state.d.ts +9 -0
  135. package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
  136. package/dist/hooks/magic-context/m0-token-breakdown.d.ts +35 -0
  137. package/dist/hooks/magic-context/m0-token-breakdown.d.ts.map +1 -0
  138. package/dist/hooks/magic-context/read-session-chunk.d.ts +9 -0
  139. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  140. package/dist/hooks/magic-context/read-session-db.d.ts +7 -0
  141. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  142. package/dist/hooks/magic-context/recomp-orchestrator.d.ts +104 -0
  143. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -0
  144. package/dist/hooks/magic-context/reference-retrieval.d.ts +61 -0
  145. package/dist/hooks/magic-context/reference-retrieval.d.ts.map +1 -0
  146. package/dist/hooks/magic-context/reference-seeds.generated.d.ts +8 -0
  147. package/dist/hooks/magic-context/reference-seeds.generated.d.ts.map +1 -0
  148. package/dist/hooks/magic-context/send-session-notification.d.ts +1 -1
  149. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  150. package/dist/hooks/magic-context/system-prompt-hash.d.ts +5 -6
  151. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  152. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  153. package/dist/hooks/magic-context/tokenizer-calibration.d.ts +6 -0
  154. package/dist/hooks/magic-context/tokenizer-calibration.d.ts.map +1 -1
  155. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +0 -7
  156. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  157. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +18 -0
  158. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  159. package/dist/hooks/magic-context/transform.d.ts +9 -7
  160. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  161. package/dist/hooks/magic-context/upgrade-reminder.d.ts +73 -0
  162. package/dist/hooks/magic-context/upgrade-reminder.d.ts.map +1 -0
  163. package/dist/index.d.ts.map +1 -1
  164. package/dist/index.js +9435 -4001
  165. package/dist/plugin/conflict-warning-hook.d.ts +13 -0
  166. package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
  167. package/dist/plugin/dream-timer.d.ts.map +1 -1
  168. package/dist/plugin/event.d.ts +10 -0
  169. package/dist/plugin/event.d.ts.map +1 -1
  170. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  171. package/dist/plugin/messages-transform.d.ts.map +1 -1
  172. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  173. package/dist/plugin/tool-registry.d.ts.map +1 -1
  174. package/dist/shared/announcement.d.ts +17 -1
  175. package/dist/shared/announcement.d.ts.map +1 -1
  176. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  177. package/dist/shared/rpc-client.d.ts +1 -0
  178. package/dist/shared/rpc-client.d.ts.map +1 -1
  179. package/dist/shared/rpc-notifications.d.ts +27 -5
  180. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  181. package/dist/shared/rpc-server.d.ts +1 -0
  182. package/dist/shared/rpc-server.d.ts.map +1 -1
  183. package/dist/shared/rpc-types.d.ts +30 -2
  184. package/dist/shared/rpc-types.d.ts.map +1 -1
  185. package/dist/shared/rpc-utils.d.ts +9 -0
  186. package/dist/shared/rpc-utils.d.ts.map +1 -1
  187. package/dist/shared/sqlite-helpers.d.ts +7 -7
  188. package/dist/shared/sqlite.d.ts +23 -14
  189. package/dist/shared/sqlite.d.ts.map +1 -1
  190. package/dist/shared/tag-transcript.d.ts +10 -1
  191. package/dist/shared/tag-transcript.d.ts.map +1 -1
  192. package/dist/tools/ctx-expand/tools.d.ts +5 -1
  193. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  194. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  195. package/dist/tui/data/context-db.d.ts +16 -1
  196. package/dist/tui/data/context-db.d.ts.map +1 -1
  197. package/package.json +5 -7
  198. package/src/shared/announcement.test.ts +23 -7
  199. package/src/shared/announcement.ts +30 -8
  200. package/src/shared/conflict-detector.test.ts +15 -2
  201. package/src/shared/conflict-fixer.test.ts +5 -1
  202. package/src/shared/models-dev-cache.test.ts +72 -4
  203. package/src/shared/models-dev-cache.ts +47 -8
  204. package/src/shared/opencode-compaction-detector.test.ts +10 -2
  205. package/src/shared/rpc-client.test.ts +54 -3
  206. package/src/shared/rpc-client.ts +19 -9
  207. package/src/shared/rpc-notifications.test.ts +54 -1
  208. package/src/shared/rpc-notifications.ts +82 -13
  209. package/src/shared/rpc-server.ts +33 -4
  210. package/src/shared/rpc-types.ts +30 -2
  211. package/src/shared/rpc-utils.ts +10 -0
  212. package/src/shared/sqlite-helpers.ts +9 -9
  213. package/src/shared/sqlite.ts +99 -80
  214. package/src/shared/tag-transcript.test.ts +280 -0
  215. package/src/shared/tag-transcript.ts +162 -33
  216. package/src/tui/data/context-db.ts +75 -11
  217. package/src/tui/index.tsx +223 -32
  218. package/src/tui/slots/sidebar-content.tsx +366 -34
  219. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts +0 -87
  220. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +0 -1
  221. package/dist/shared/native-binding.d.ts +0 -87
  222. package/dist/shared/native-binding.d.ts.map +0 -1
  223. package/src/shared/native-binding.ts +0 -311
@@ -1,3 +1,4 @@
1
+ import { randomBytes } from "node:crypto";
1
2
  import {
2
3
  mkdirSync,
3
4
  readdirSync,
@@ -20,6 +21,11 @@ export class MagicContextRpcServer {
20
21
  private portFilePath: string;
21
22
  private portDir: string;
22
23
  private startedAt = Date.now();
24
+ // Unguessable per-process bearer token, published in the (user-private) port
25
+ // file and required on every non-health RPC call. Defends side-effecting
26
+ // endpoints (recomp/upgrade/dismiss) against any local process or
27
+ // browser-origin script that merely discovers/guesses the port.
28
+ private readonly token = randomBytes(32).toString("hex");
23
29
 
24
30
  constructor(storageDir: string, directory: string) {
25
31
  this.portFilePath = rpcPortFilePath(storageDir, directory);
@@ -57,7 +63,15 @@ export class MagicContextRpcServer {
57
63
  try {
58
64
  this.warnIfOtherLiveInstance();
59
65
  const dir = dirname(this.portFilePath);
60
- mkdirSync(dir, { recursive: true });
66
+ // The port file holds the per-process bearer token that
67
+ // gates side-effecting RPC endpoints (recomp/upgrade/
68
+ // dismiss). Under the default umask 0o022 a plain write
69
+ // lands at 0o644 in a 0o755 dir — world-readable, so any
70
+ // local UID could read the token and drive those endpoints,
71
+ // defeating the auth defense. Restrict both: dir 0o700,
72
+ // file 0o600. renameSync preserves the tmp file's mode, so
73
+ // the 0o600 on the write covers the final file.
74
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
61
75
  const tmpPath = `${this.portFilePath}.tmp`;
62
76
  writeFileSync(
63
77
  tmpPath,
@@ -65,8 +79,9 @@ export class MagicContextRpcServer {
65
79
  port: this.port,
66
80
  pid: process.pid,
67
81
  started_at: this.startedAt,
82
+ token: this.token,
68
83
  }),
69
- "utf-8",
84
+ { encoding: "utf-8", mode: 0o600 },
70
85
  );
71
86
  renameSync(tmpPath, this.portFilePath);
72
87
  log(`[rpc] server listening on 127.0.0.1:${this.port}`);
@@ -114,8 +129,10 @@ export class MagicContextRpcServer {
114
129
  private dispatch(req: IncomingMessage, res: ServerResponse): void {
115
130
  const url = req.url ?? "";
116
131
 
117
- // CORS headers for same-origin fetch
118
- res.setHeader("Access-Control-Allow-Origin", "*");
132
+ // No wildcard CORS: the only legitimate client is the in-process TUI
133
+ // client, which is not a browser origin. Omitting
134
+ // Access-Control-Allow-Origin makes browsers refuse to read responses,
135
+ // closing the CSRF-style read path a malicious local page could use.
119
136
 
120
137
  if (req.method === "GET" && url === "/health") {
121
138
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -129,6 +146,18 @@ export class MagicContextRpcServer {
129
146
  return;
130
147
  }
131
148
 
149
+ // Require the per-process bearer token on every side-effecting call.
150
+ // The legitimate TUI client reads it from the same port file it used to
151
+ // discover the port; a process that only guessed the port cannot.
152
+ const auth = req.headers.authorization;
153
+ const presented = typeof auth === "string" ? auth.replace(/^Bearer\s+/i, "") : "";
154
+ if (presented !== this.token) {
155
+ res.writeHead(401, { "Content-Type": "application/json" });
156
+ res.end(JSON.stringify({ error: "Unauthorized" }));
157
+ req.resume();
158
+ return;
159
+ }
160
+
132
161
  const method = url.slice(5); // strip "/rpc/"
133
162
  const handler = this.handlers.get(method);
134
163
  if (!handler) {
@@ -24,10 +24,23 @@ export interface SidebarSnapshot {
24
24
  compartmentTokens: number;
25
25
  factTokens: number;
26
26
  memoryTokens: number;
27
+ /**
28
+ * Token estimate of the injected <project-docs> block (root ARCHITECTURE.md
29
+ * + STRUCTURE.md) that lives in m[0] in v2. Part of the message stream, not
30
+ * conversation. Display layer shows this as "Docs".
31
+ */
32
+ docsTokens: number;
33
+ /**
34
+ * Token estimate of the injected <user-profile> block (promoted user
35
+ * memories) that lives in m[0] in v2. Part of the message stream, not
36
+ * conversation. Display layer shows this as "Profile".
37
+ */
38
+ profileTokens: number;
27
39
  /**
28
40
  * Token estimate of real user/assistant discussion (text + reasoning +
29
- * image parts) inside messages, excluding injected <session-history>
30
- * blocks. Display layer shows this as "Conversation".
41
+ * image parts) inside messages, excluding injected <session-history>,
42
+ * <project-docs>, and <user-profile> blocks. Display layer shows this as
43
+ * "Conversation".
31
44
  */
32
45
  conversationTokens: number;
33
46
  /**
@@ -57,6 +70,21 @@ export interface SidebarSnapshot {
57
70
  executeThreshold: number;
58
71
  newWorkTokens?: number | null;
59
72
  totalInputTokens?: number | null;
73
+ /**
74
+ * Live recomp / session-upgrade progress for this session, or null when no
75
+ * recomp is running (and no recent terminal state is being shown). Drives the
76
+ * sidebar "Recomp"/"Upgrade" progress bar and the /ctx-status dialog. Mirrors
77
+ * the runtime `RecompProgress` shape from compartment-runner-types.ts.
78
+ */
79
+ recompProgress?: {
80
+ phase: "recomp" | "migration" | "done" | "failed";
81
+ processedMessages: number;
82
+ totalMessages: number;
83
+ passCount: number;
84
+ compartmentsCreated: number;
85
+ message?: string;
86
+ note?: string;
87
+ } | null;
60
88
  }
61
89
 
62
90
  export interface StatusDetail extends SidebarSnapshot {
@@ -5,6 +5,15 @@ export interface RpcPortFileRecord {
5
5
  port: number;
6
6
  pid: number;
7
7
  started_at: number;
8
+ /**
9
+ * Per-process bearer token. The server requires it on all non-health RPC
10
+ * calls so a random local process or browser-origin script that merely
11
+ * discovers/guesses the port cannot drive side-effecting endpoints
12
+ * (recomp/upgrade/dismiss). Optional in the type for forward/backward
13
+ * compatibility with port files written by older builds (treated as "no
14
+ * auth required" only when the server itself didn't set one).
15
+ */
16
+ token?: string;
8
17
  }
9
18
 
10
19
  /**
@@ -56,6 +65,7 @@ export function parseRpcPortFile(content: string, fallbackPid = 0): RpcPortFileR
56
65
  port,
57
66
  pid,
58
67
  started_at: Number.isFinite(startedAt) ? startedAt : 0,
68
+ token: typeof parsed.token === "string" ? parsed.token : undefined,
59
69
  };
60
70
  } catch {
61
71
  return null;
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Cross-runtime helpers that smooth over the small bun:sqlite ↔ better-sqlite3
3
- * API differences without leaking either library into call sites.
2
+ * Cross-runtime helpers that smooth over the small bun:sqlite ↔ node:sqlite
3
+ * API differences without leaking either backend into call sites.
4
4
  */
5
5
 
6
6
  import type { Database } from "./sqlite";
@@ -8,17 +8,17 @@ import type { Database } from "./sqlite";
8
8
  /**
9
9
  * Close a database, ignoring errors.
10
10
  *
11
- * bun:sqlite supports `db.close(throwOnError = false)`. better-sqlite3 has
12
- * only `db.close()` and throws on already-closed databases. This helper
13
- * mirrors the bun "swallow errors" semantics for both runtimes — useful in
14
- * test teardown and `finally` blocks where the caller doesn't care whether
15
- * the close succeeded.
11
+ * bun:sqlite supports `db.close(throwOnError = false)`. node:sqlite has only
12
+ * `db.close()` and throws ("database is not open") on an already-closed
13
+ * handle. This helper mirrors the bun "swallow errors" semantics for both
14
+ * runtimes — useful in test teardown and `finally` blocks where the caller
15
+ * doesn't care whether the close succeeded.
16
16
  */
17
17
  export function closeQuietly(db: Database | null | undefined): void {
18
18
  if (!db) return;
19
19
  // Just attempt close and swallow errors. bun:sqlite has no `open` property,
20
- // and better-sqlite3 throws TypeError on already-closed databases — both
21
- // are handled by the bare try/catch.
20
+ // and node:sqlite throws on an already-closed handle — both are handled by
21
+ // the bare try/catch.
22
22
  try {
23
23
  db.close();
24
24
  } catch {
@@ -3,33 +3,42 @@
3
3
  *
4
4
  * The same shipped plugin artifact must run under two different runtimes:
5
5
  * - Bun (current OpenCode releases) → uses `bun:sqlite` (built-in, fast)
6
- * - Node (OpenCode beta + future Pi plugin) → uses `better-sqlite3`
6
+ * - Node / Electron (Pi plugin, OpenCode Desktop) → uses `node:sqlite`
7
+ * (`DatabaseSync`, built into Node 22.5+ / Electron 41+, stable-enough and
8
+ * flag-free since Node 22.13/23.4).
7
9
  *
8
- * Bun cannot load `better-sqlite3` (oven-sh/bun#4290), and Node has no
9
- * `bun:sqlite` module. Static imports of either would crash at parse time
10
- * in the wrong runtime, so we use dynamic imports gated by runtime detection.
10
+ * Bun has no `node:sqlite`, and Node/Electron have no `bun:sqlite`. Static
11
+ * imports of either would crash at parse time in the wrong runtime, so we use
12
+ * dynamic imports gated by runtime detection.
11
13
  *
12
- * The Function-constructor wrapper around `import()` defeats bundler static
13
- * analysis without it, esbuild/bun build would try to resolve both modules
14
- * during the bundle step, including the one that doesn't exist in the build
15
- * runtime.
14
+ * Why `node:sqlite` instead of `better-sqlite3`: better-sqlite3 is a native
15
+ * module requiring per-ABI prebuilds, and Electron's ABI never matches the npm
16
+ * Node prebuild which forced a runtime download of an Electron-matched
17
+ * `.node` binary (a supply-chain + maintenance liability). `node:sqlite` is
18
+ * built into the runtime, so there is NOTHING to download or rebuild. Both Pi
19
+ * (plain Node 24) and OpenCode Desktop (Electron 41 → Node 24.14.1) ship it.
16
20
  *
17
- * Both libraries expose ~95% API parity:
18
- * - new Database(path, { readonly?: boolean })
21
+ * API surface we use (common across both backends, modulo the shims below):
22
+ * - new Database(path, { readonly?: boolean }) ← we map readonly→readOnly
19
23
  * - db.prepare(sql).run/get/all
20
24
  * - db.exec(multistatement)
21
- * - db.transaction(fn) → wrapped function
25
+ * - db.transaction(fn) → wrapped function ← shimmed for node:sqlite
22
26
  * - db.close()
23
27
  *
24
- * The 5% that differs (db.query, db.run, db.close(boolean), Database.open)
25
- * is either rewritten to common-subset patterns or hidden behind the helpers
26
- * in `./sqlite-helpers.ts`.
28
+ * The two backend differences we bridge for node:sqlite:
29
+ * 1. node:sqlite has no `db.transaction(fn)` helper we add a savepoint-aware
30
+ * shim (below) that matches better-sqlite3/bun semantics.
31
+ * 2. node:sqlite's constructor option is `readOnly` (camel-case), not
32
+ * better-sqlite3/bun's `readonly` — we translate it so call sites are
33
+ * unchanged.
34
+ * Everything else (named params with bare keys, ATTACH under defensive mode,
35
+ * `run()` → {changes,lastInsertRowid}) is identical and was verified directly.
27
36
  */
28
37
 
29
- // Type import only — better-sqlite3's runtime is loaded dynamically below.
30
- // @types/better-sqlite3 has richer definitions than @types/bun's bun:sqlite
31
- // types, and bun:sqlite is a structural superset for the API surface we use,
32
- // so calls typed against BetterSqlite3 work under both runtimes at runtime.
38
+ // Type import only — runtime is loaded dynamically below. @types/better-sqlite3
39
+ // has the richest definitions and is a structural superset of the API surface
40
+ // we use, so calls typed against BetterSqlite3 work under bun:sqlite and
41
+ // node:sqlite at runtime (both expose prepare/run/get/all/exec/close).
33
42
  import type BetterSqlite3 from "better-sqlite3";
34
43
 
35
44
  // Detect Bun via process.versions.bun. Both globalThis.Bun and
@@ -41,80 +50,90 @@ const isBun = typeof process !== "undefined" && typeof process.versions?.bun ===
41
50
 
42
51
  // IMPORTANT: bundler-evading dynamic imports.
43
52
  //
44
- // We can't write `await import("better-sqlite3")` directly because esbuild/bun
53
+ // We can't write `await import("node:sqlite")` directly because esbuild/bun
45
54
  // would try to resolve both modules at build time, and one of them won't exist
46
- // in the build runtime (bun:sqlite is missing in Node, better-sqlite3 isn't
47
- // shipped in Bun-only environments). Earlier versions used
48
- // `new Function("p", "return import(p)")("modname")` to defeat static
49
- // analysis, but that breaks Pi's vm-based extension loader: a Function
50
- // constructed at runtime has no module record, so `import()` inside it has
51
- // no referrer module and Node throws "A dynamic import callback was not
55
+ // in the build runtime (bun:sqlite is missing in Node, node:sqlite is missing
56
+ // in Bun). Earlier versions used `new Function("p", "return import(p)")(...)`
57
+ // to defeat static analysis, but that breaks Pi's vm-based extension loader: a
58
+ // Function constructed at runtime has no module record, so `import()` inside it
59
+ // has no referrer module and Node throws "A dynamic import callback was not
52
60
  // specified".
53
61
  //
54
62
  // The /* @vite-ignore */ + variable indirection pattern hides the specifier
55
63
  // from static analyzers while keeping a real referrer module for the
56
64
  // dynamic import — Pi's loader, esbuild, and bun build all accept it.
57
65
  const bunSpec = "bun:" + "sqlite";
58
- const betterSpec = "better-" + "sqlite3";
59
-
60
- // Under Electron, the npm-installed better-sqlite3 binary has the wrong ABI
61
- // (it's a Node prebuild but Electron embeds a different NODE_MODULE_VERSION).
62
- // resolveBetterSqliteNativeBinding() detects this and downloads + caches the
63
- // matching Electron prebuild, then returns its absolute path so we can pass
64
- // it to better-sqlite3 via the `nativeBinding` constructor option (a
65
- // documented public API). Returns null outside Electron OR when the on-disk
66
- // binary already matches the runtime ABI — in those cases the default
67
- // bindings() lookup just works.
68
- const electronNativeBinding = isBun
69
- ? null
70
- : await (async () => {
71
- const mod = await import("./native-binding");
72
- return mod.resolveBetterSqliteNativeBinding();
73
- })();
66
+ const nodeSpec = "node:" + "sqlite";
74
67
 
75
68
  const sqliteModule = isBun
76
69
  ? await import(/* @vite-ignore */ bunSpec)
77
- : await import(/* @vite-ignore */ betterSpec);
70
+ : await import(/* @vite-ignore */ nodeSpec);
78
71
 
79
- // Different export shapes between the two libraries:
80
- // - bun:sqlite → named export `Database`
81
- // - better-sqlite3 default export
82
- const RawDatabaseImpl = isBun ? sqliteModule.Database : sqliteModule.default;
72
+ // Different export shapes between the two backends:
73
+ // - bun:sqlite → named export `Database` (has its own .transaction, accepts
74
+ // `{ readonly }`) usable as-is.
75
+ // - node:sqlite named export `DatabaseSync` (no .transaction, option is
76
+ // `readOnly`) — wrapped below.
77
+ const DatabaseImpl: typeof BetterSqlite3 = isBun
78
+ ? (sqliteModule.Database as typeof BetterSqlite3)
79
+ : buildNodeSqliteDatabaseClass(sqliteModule.DatabaseSync);
83
80
 
84
- // When we resolved a non-default Electron-compatible native binding above,
85
- // transparently inject it into every `new Database(...)` call. This is the
86
- // public `nativeBinding` constructor option that better-sqlite3 ships
87
- // specifically for cross-runtime extension scenarios it makes
88
- // better-sqlite3 `require()` the binary at the supplied path directly,
89
- // bypassing the default bindings() resolver.
90
- //
91
- // Subclassing keeps the call sites untouched: existing
92
- // `new Database(filename, { readonly: true })` invocations work as-is.
93
- // Callers can still override `nativeBinding` explicitly if they need to.
94
- //
95
- // The TypeScript type intentionally references @types/better-sqlite3 because
96
- // its definitions are richer than @types/bun's bun:sqlite types and bun:sqlite
97
- // is a structural superset for the API surface we use. Calls written against
98
- // this type work correctly under both runtimes at runtime.
99
- //
100
- // @types/better-sqlite3 uses `export = Database` (CommonJS interop), which
101
- // surfaces in TypeScript as `import Database = require("better-sqlite3")`.
102
- // We capture the DatabaseConstructor type from the namespace re-export.
103
- const DatabaseImpl: typeof BetterSqlite3 =
104
- electronNativeBinding == null
105
- ? (RawDatabaseImpl as typeof BetterSqlite3)
106
- : (class DatabaseWithElectronBinding extends (RawDatabaseImpl as typeof BetterSqlite3) {
107
- constructor(filename?: string | Buffer, options?: BetterSqlite3.Options) {
108
- // Type narrowing: the surrounding ternary already proved
109
- // electronNativeBinding is non-null in this branch, but
110
- // TypeScript can't follow that across the class boundary.
111
- const fallback = electronNativeBinding as string;
112
- super(filename, {
113
- ...options,
114
- nativeBinding: options?.nativeBinding ?? fallback,
115
- });
116
- }
117
- } as typeof BetterSqlite3);
81
+ /**
82
+ * Wrap node:sqlite's `DatabaseSync` so it presents the better-sqlite3/bun
83
+ * surface the rest of the codebase calls:
84
+ * - translate the `{ readonly }` constructor option → node:sqlite's `readOnly`
85
+ * - add a `transaction(fn)` helper that matches better-sqlite3 semantics,
86
+ * using `db.isTransaction` to pick BEGIN (top-level) vs SAVEPOINT (nested),
87
+ * so it composes correctly with manual `BEGIN IMMEDIATE` blocks too.
88
+ */
89
+ // biome-ignore lint/suspicious/noExplicitAny: node:sqlite has no shipped types here; the public export is cast to the better-sqlite3 shape.
90
+ function buildNodeSqliteDatabaseClass(DatabaseSync: any): typeof BetterSqlite3 {
91
+ // Single constant savepoint name is correct for arbitrary nesting depth:
92
+ // SQLite savepoints with the same name stack LIFO — RELEASE / ROLLBACK TO
93
+ // always target the most recent. node:sqlite is synchronous + single-process
94
+ // per connection, so there is no concurrent-savepoint hazard.
95
+ const SAVEPOINT = "mc_tx_sp";
96
+
97
+ class NodeSqliteDatabase extends DatabaseSync {
98
+ constructor(filename?: string | Buffer, options?: BetterSqlite3.Options) {
99
+ const translated: Record<string, unknown> = { ...options };
100
+ if (options && "readonly" in options) {
101
+ translated.readOnly = (options as { readonly?: boolean }).readonly;
102
+ delete translated.readonly;
103
+ }
104
+ super(typeof filename === "string" ? filename : ":memory:", translated);
105
+ }
106
+
107
+ // biome-ignore lint/suspicious/noExplicitAny: mirrors better-sqlite3's generic transaction(fn) signature.
108
+ transaction<F extends (...args: any[]) => any>(fn: F): F {
109
+ // biome-ignore lint/suspicious/noExplicitAny: faithful pass-through of this/args to fn.
110
+ const self = this as any;
111
+ const wrapped = function (this: unknown, ...args: unknown[]): unknown {
112
+ const nested = self.isTransaction === true;
113
+ self.exec(nested ? `SAVEPOINT ${SAVEPOINT}` : "BEGIN");
114
+ try {
115
+ const result = fn.apply(this, args);
116
+ self.exec(nested ? `RELEASE ${SAVEPOINT}` : "COMMIT");
117
+ return result;
118
+ } catch (error) {
119
+ if (nested) {
120
+ // ROLLBACK TO unwinds the savepoint's changes but leaves
121
+ // it on the stack; RELEASE then pops it (better-sqlite3
122
+ // does both).
123
+ self.exec(`ROLLBACK TO ${SAVEPOINT}`);
124
+ self.exec(`RELEASE ${SAVEPOINT}`);
125
+ } else {
126
+ self.exec("ROLLBACK");
127
+ }
128
+ throw error;
129
+ }
130
+ };
131
+ return wrapped as unknown as F;
132
+ }
133
+ }
134
+
135
+ return NodeSqliteDatabase as unknown as typeof BetterSqlite3;
136
+ }
118
137
 
119
138
  export const Database: typeof BetterSqlite3 = DatabaseImpl;
120
139