@livestore/cli 0.4.0-dev.9 → 0.5.0-dev.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 (136) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/__tests__/fixtures/mock-config.d.ts +63 -0
  3. package/dist/__tests__/fixtures/mock-config.d.ts.map +1 -0
  4. package/dist/__tests__/fixtures/mock-config.js +94 -0
  5. package/dist/__tests__/fixtures/mock-config.js.map +1 -0
  6. package/dist/__tests__/sync-operations.test.d.ts +2 -0
  7. package/dist/__tests__/sync-operations.test.d.ts.map +1 -0
  8. package/dist/__tests__/sync-operations.test.js +167 -0
  9. package/dist/__tests__/sync-operations.test.js.map +1 -0
  10. package/dist/bin.js +6 -5
  11. package/dist/bin.js.map +1 -1
  12. package/dist/cli.d.ts +2 -9
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +4 -5
  15. package/dist/cli.js.map +1 -1
  16. package/dist/commands/__fixtures__/mcp-protocol-server.d.ts +2 -0
  17. package/dist/commands/__fixtures__/mcp-protocol-server.d.ts.map +1 -0
  18. package/dist/commands/__fixtures__/mcp-protocol-server.js +10 -0
  19. package/dist/commands/__fixtures__/mcp-protocol-server.js.map +1 -0
  20. package/dist/commands/import-export.d.ts +20 -0
  21. package/dist/commands/import-export.d.ts.map +1 -0
  22. package/dist/commands/import-export.js +135 -0
  23. package/dist/commands/import-export.js.map +1 -0
  24. package/dist/commands/mcp-coach.d.ts +21 -12
  25. package/dist/commands/mcp-coach.d.ts.map +1 -1
  26. package/dist/commands/mcp-coach.js +49 -53
  27. package/dist/commands/mcp-coach.js.map +1 -1
  28. package/dist/commands/mcp-protocols.d.ts +7 -0
  29. package/dist/commands/mcp-protocols.d.ts.map +1 -0
  30. package/dist/commands/mcp-protocols.js +12 -0
  31. package/dist/commands/mcp-protocols.js.map +1 -0
  32. package/dist/commands/mcp-protocols.test.d.ts +2 -0
  33. package/dist/commands/mcp-protocols.test.d.ts.map +1 -0
  34. package/dist/commands/mcp-protocols.test.js +60 -0
  35. package/dist/commands/mcp-protocols.test.js.map +1 -0
  36. package/dist/commands/mcp-stdio.test.d.ts +2 -0
  37. package/dist/commands/mcp-stdio.test.d.ts.map +1 -0
  38. package/dist/commands/mcp-stdio.test.js +79 -0
  39. package/dist/commands/mcp-stdio.test.js.map +1 -0
  40. package/dist/commands/mcp-tool-handlers.d.ts +6 -0
  41. package/dist/commands/mcp-tool-handlers.d.ts.map +1 -0
  42. package/dist/commands/{mcp-tools.js → mcp-tool-handlers.js} +80 -45
  43. package/dist/commands/mcp-tool-handlers.js.map +1 -0
  44. package/dist/commands/mcp-tools-defs.d.ts +134 -0
  45. package/dist/commands/mcp-tools-defs.d.ts.map +1 -0
  46. package/dist/commands/mcp-tools-defs.js +285 -0
  47. package/dist/commands/mcp-tools-defs.js.map +1 -0
  48. package/dist/commands/mcp-tools-defs.test.d.ts +2 -0
  49. package/dist/commands/mcp-tools-defs.test.d.ts.map +1 -0
  50. package/dist/commands/mcp-tools-defs.test.js +11 -0
  51. package/dist/commands/mcp-tools-defs.test.js.map +1 -0
  52. package/dist/commands/mcp.d.ts +1 -3
  53. package/dist/commands/mcp.d.ts.map +1 -1
  54. package/dist/commands/mcp.js +7 -5
  55. package/dist/commands/mcp.js.map +1 -1
  56. package/dist/commands/new-project.d.ts +22 -23
  57. package/dist/commands/new-project.d.ts.map +1 -1
  58. package/dist/commands/new-project.js +85 -44
  59. package/dist/commands/new-project.js.map +1 -1
  60. package/dist/mcp-content/architecture.d.ts +1 -1
  61. package/dist/mcp-content/architecture.d.ts.map +1 -1
  62. package/dist/mcp-content/architecture.js +3 -3
  63. package/dist/mcp-content/features.d.ts +1 -1
  64. package/dist/mcp-content/features.d.ts.map +1 -1
  65. package/dist/mcp-content/features.js +1 -1
  66. package/dist/mcp-content/getting-started.d.ts +1 -1
  67. package/dist/mcp-content/getting-started.d.ts.map +1 -1
  68. package/dist/mcp-content/getting-started.js +37 -37
  69. package/dist/mcp-content/overview.d.ts +1 -1
  70. package/dist/mcp-content/overview.d.ts.map +1 -1
  71. package/dist/mcp-content/overview.js +6 -6
  72. package/dist/mcp-content/schemas/blog.d.ts +1 -1
  73. package/dist/mcp-content/schemas/blog.d.ts.map +1 -1
  74. package/dist/mcp-content/schemas/blog.js +25 -25
  75. package/dist/mcp-content/schemas/ecommerce.d.ts +1 -1
  76. package/dist/mcp-content/schemas/ecommerce.d.ts.map +1 -1
  77. package/dist/mcp-content/schemas/ecommerce.js +67 -67
  78. package/dist/mcp-content/schemas/social.d.ts +1 -1
  79. package/dist/mcp-content/schemas/social.d.ts.map +1 -1
  80. package/dist/mcp-content/schemas/social.js +48 -48
  81. package/dist/mcp-content/schemas/todo.d.ts +1 -1
  82. package/dist/mcp-content/schemas/todo.d.ts.map +1 -1
  83. package/dist/mcp-content/schemas/todo.js +35 -35
  84. package/dist/mcp-runtime/runtime.d.ts +48 -0
  85. package/dist/mcp-runtime/runtime.d.ts.map +1 -0
  86. package/dist/mcp-runtime/runtime.js +107 -0
  87. package/dist/mcp-runtime/runtime.js.map +1 -0
  88. package/dist/module-loader.d.ts +22 -0
  89. package/dist/module-loader.d.ts.map +1 -0
  90. package/dist/module-loader.js +75 -0
  91. package/dist/module-loader.js.map +1 -0
  92. package/dist/package-manager.d.ts +38 -0
  93. package/dist/package-manager.d.ts.map +1 -0
  94. package/dist/package-manager.js +33 -0
  95. package/dist/package-manager.js.map +1 -0
  96. package/dist/package-manager.test.d.ts +2 -0
  97. package/dist/package-manager.test.d.ts.map +1 -0
  98. package/dist/package-manager.test.js +57 -0
  99. package/dist/package-manager.test.js.map +1 -0
  100. package/dist/sync-operations.d.ts +115 -0
  101. package/dist/sync-operations.d.ts.map +1 -0
  102. package/dist/sync-operations.js +178 -0
  103. package/dist/sync-operations.js.map +1 -0
  104. package/package.json +57 -15
  105. package/src/__tests__/fixtures/mock-config.ts +112 -0
  106. package/src/__tests__/sync-operations.test.ts +218 -0
  107. package/src/bin.ts +8 -6
  108. package/src/cli.ts +5 -6
  109. package/src/commands/__fixtures__/mcp-protocol-server.ts +12 -0
  110. package/src/commands/import-export.ts +283 -0
  111. package/src/commands/mcp-coach.ts +76 -75
  112. package/src/commands/mcp-protocols.test.ts +76 -0
  113. package/src/commands/mcp-protocols.ts +12 -0
  114. package/src/commands/mcp-stdio.test.ts +93 -0
  115. package/src/commands/{mcp-tools.ts → mcp-tool-handlers.ts} +100 -61
  116. package/src/commands/mcp-tools-defs.test.ts +13 -0
  117. package/src/commands/mcp-tools-defs.ts +320 -0
  118. package/src/commands/mcp.ts +10 -6
  119. package/src/commands/new-project.ts +124 -57
  120. package/src/mcp-content/architecture.ts +3 -3
  121. package/src/mcp-content/features.ts +1 -1
  122. package/src/mcp-content/getting-started.ts +37 -37
  123. package/src/mcp-content/overview.ts +6 -6
  124. package/src/mcp-content/schemas/blog.ts +25 -25
  125. package/src/mcp-content/schemas/ecommerce.ts +67 -67
  126. package/src/mcp-content/schemas/social.ts +48 -48
  127. package/src/mcp-content/schemas/todo.ts +35 -35
  128. package/src/mcp-runtime/runtime.ts +147 -0
  129. package/src/module-loader.ts +94 -0
  130. package/src/package-manager.test.ts +66 -0
  131. package/src/package-manager.ts +44 -0
  132. package/src/sync-operations.ts +360 -0
  133. package/dist/commands/mcp-tools.d.ts +0 -58
  134. package/dist/commands/mcp-tools.d.ts.map +0 -1
  135. package/dist/commands/mcp-tools.js.map +0 -1
  136. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -19,7 +19,7 @@ export const tables = {
19
19
  allowsFollowers: State.SQLite.boolean({ default: true }),
20
20
  },
21
21
  }),
22
-
22
+
23
23
  posts: State.SQLite.table({
24
24
  name: 'posts',
25
25
  columns: {
@@ -34,7 +34,7 @@ export const tables = {
34
34
  deletedAt: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
35
35
  },
36
36
  }),
37
-
37
+
38
38
  follows: State.SQLite.table({
39
39
  name: 'follows',
40
40
  columns: {
@@ -45,7 +45,7 @@ export const tables = {
45
45
  approvedAt: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
46
46
  },
47
47
  }),
48
-
48
+
49
49
  likes: State.SQLite.table({
50
50
  name: 'likes',
51
51
  columns: {
@@ -54,7 +54,7 @@ export const tables = {
54
54
  createdAt: State.SQLite.integer({ schema: Schema.DateFromNumber }),
55
55
  },
56
56
  }),
57
-
57
+
58
58
  // Aggregate tables for performance (eventually consistent)
59
59
  postStats: State.SQLite.table({
60
60
  name: 'post_stats',
@@ -66,9 +66,9 @@ export const tables = {
66
66
  lastUpdated: State.SQLite.integer({ schema: Schema.DateFromNumber }),
67
67
  },
68
68
  }),
69
-
69
+
70
70
  userStats: State.SQLite.table({
71
- name: 'user_stats',
71
+ name: 'user_stats',
72
72
  columns: {
73
73
  userId: State.SQLite.text({ primaryKey: true }),
74
74
  followerCount: State.SQLite.integer({ default: 0 }),
@@ -77,7 +77,7 @@ export const tables = {
77
77
  lastUpdated: State.SQLite.integer({ schema: Schema.DateFromNumber }),
78
78
  },
79
79
  }),
80
-
80
+
81
81
  // Client-side state for UI
82
82
  feedState: State.SQLite.clientDocument({
83
83
  name: 'feedState',
@@ -86,8 +86,8 @@ export const tables = {
86
86
  lastRefresh: Schema.Date,
87
87
  scrollPosition: Schema.Number,
88
88
  }),
89
- default: {
90
- id: SessionIdSymbol,
89
+ default: {
90
+ id: SessionIdSymbol,
91
91
  value: { currentFeed: 'home', lastRefresh: new Date(), scrollPosition: 0 }
92
92
  },
93
93
  }),
@@ -105,7 +105,7 @@ export const events = {
105
105
  createdAt: Schema.Date,
106
106
  }),
107
107
  }),
108
-
108
+
109
109
  userProfileUpdated: Events.synced({
110
110
  name: 'v1.UserProfileUpdated',
111
111
  schema: Schema.Struct({
@@ -115,7 +115,7 @@ export const events = {
115
115
  avatarUrl: Schema.NullOr(Schema.String),
116
116
  }),
117
117
  }),
118
-
118
+
119
119
  userPrivacySettingsChanged: Events.synced({
120
120
  name: 'v1.UserPrivacySettingsChanged',
121
121
  schema: Schema.Struct({
@@ -124,7 +124,7 @@ export const events = {
124
124
  allowsFollowers: Schema.Boolean,
125
125
  }),
126
126
  }),
127
-
127
+
128
128
  userLastActiveUpdated: Events.synced({
129
129
  name: 'v1.UserLastActiveUpdated',
130
130
  schema: Schema.Struct({
@@ -132,7 +132,7 @@ export const events = {
132
132
  lastActiveAt: Schema.Date,
133
133
  }),
134
134
  }),
135
-
135
+
136
136
  // Post events
137
137
  postCreated: Events.synced({
138
138
  name: 'v1.PostCreated',
@@ -146,7 +146,7 @@ export const events = {
146
146
  createdAt: Schema.Date,
147
147
  }),
148
148
  }),
149
-
149
+
150
150
  postEdited: Events.synced({
151
151
  name: 'v1.PostEdited',
152
152
  schema: Schema.Struct({
@@ -155,7 +155,7 @@ export const events = {
155
155
  editedAt: Schema.Date,
156
156
  }),
157
157
  }),
158
-
158
+
159
159
  postDeleted: Events.synced({
160
160
  name: 'v1.PostDeleted',
161
161
  schema: Schema.Struct({
@@ -163,7 +163,7 @@ export const events = {
163
163
  deletedAt: Schema.Date,
164
164
  }),
165
165
  }),
166
-
166
+
167
167
  // Social interaction events
168
168
  followRequested: Events.synced({
169
169
  name: 'v1.FollowRequested',
@@ -173,7 +173,7 @@ export const events = {
173
173
  createdAt: Schema.Date,
174
174
  }),
175
175
  }),
176
-
176
+
177
177
  followApproved: Events.synced({
178
178
  name: 'v1.FollowApproved',
179
179
  schema: Schema.Struct({
@@ -182,7 +182,7 @@ export const events = {
182
182
  approvedAt: Schema.Date,
183
183
  }),
184
184
  }),
185
-
185
+
186
186
  unfollowed: Events.synced({
187
187
  name: 'v1.Unfollowed',
188
188
  schema: Schema.Struct({
@@ -190,7 +190,7 @@ export const events = {
190
190
  followingId: Schema.String,
191
191
  }),
192
192
  }),
193
-
193
+
194
194
  postLiked: Events.synced({
195
195
  name: 'v1.PostLiked',
196
196
  schema: Schema.Struct({
@@ -199,7 +199,7 @@ export const events = {
199
199
  createdAt: Schema.Date,
200
200
  }),
201
201
  }),
202
-
202
+
203
203
  postUnliked: Events.synced({
204
204
  name: 'v1.PostUnliked',
205
205
  schema: Schema.Struct({
@@ -207,7 +207,7 @@ export const events = {
207
207
  postId: Schema.String,
208
208
  }),
209
209
  }),
210
-
210
+
211
211
  // Local UI state
212
212
  feedStateUpdated: tables.feedState.set,
213
213
  }
@@ -219,85 +219,85 @@ const materializers = State.SQLite.materializers(events, {
219
219
  tables.users.insert({ id, username, email, displayName, createdAt }),
220
220
  tables.userStats.insert({ userId: id, lastUpdated: createdAt }),
221
221
  ],
222
-
222
+
223
223
  'v1.UserProfileUpdated': ({ id, displayName, bio, avatarUrl }) =>
224
224
  tables.users.update({ displayName, bio, avatarUrl }).where({ id }),
225
-
225
+
226
226
  'v1.UserPrivacySettingsChanged': ({ id, isPrivate, allowsFollowers }) =>
227
227
  tables.users.update({ isPrivate, allowsFollowers }).where({ id }),
228
-
228
+
229
229
  'v1.UserLastActiveUpdated': ({ id, lastActiveAt }) =>
230
230
  tables.users.update({ lastActiveAt }).where({ id }),
231
-
231
+
232
232
  // Post materializers
233
233
  'v1.PostCreated': ({ id, authorId, content, mediaUrls, replyToId, visibility, createdAt }) => [
234
- tables.posts.insert({
235
- id,
236
- authorId,
237
- content,
234
+ tables.posts.insert({
235
+ id,
236
+ authorId,
237
+ content,
238
238
  mediaUrls: mediaUrls ? JSON.stringify(mediaUrls) : null,
239
- replyToId,
240
- visibility,
241
- createdAt
239
+ replyToId,
240
+ visibility,
241
+ createdAt
242
242
  }),
243
243
  tables.postStats.insert({ postId: id, lastUpdated: createdAt }),
244
244
  // Update user post count
245
- tables.userStats.update({
245
+ tables.userStats.update({
246
246
  postCount: tables.userStats.select('postCount').where({ userId: authorId }).scalar() + 1,
247
247
  lastUpdated: createdAt
248
248
  }).where({ userId: authorId }),
249
249
  ],
250
-
250
+
251
251
  'v1.PostEdited': ({ id, content, editedAt }) =>
252
252
  tables.posts.update({ content, editedAt }).where({ id }),
253
-
253
+
254
254
  'v1.PostDeleted': ({ id, deletedAt }) =>
255
255
  tables.posts.update({ deletedAt }).where({ id }),
256
-
256
+
257
257
  // Follow materializers
258
258
  'v1.FollowRequested': ({ followerId, followingId, createdAt }) =>
259
259
  tables.follows.insert({ followerId, followingId, status: 'pending', createdAt }),
260
-
260
+
261
261
  'v1.FollowApproved': ({ followerId, followingId, approvedAt }) => [
262
262
  tables.follows.update({ status: 'active', approvedAt }).where({ followerId, followingId }),
263
263
  // Update follower counts
264
- tables.userStats.update({
264
+ tables.userStats.update({
265
265
  followerCount: tables.userStats.select('followerCount').where({ userId: followingId }).scalar() + 1,
266
266
  lastUpdated: approvedAt
267
267
  }).where({ userId: followingId }),
268
- tables.userStats.update({
268
+ tables.userStats.update({
269
269
  followingCount: tables.userStats.select('followingCount').where({ userId: followerId }).scalar() + 1,
270
270
  lastUpdated: approvedAt
271
271
  }).where({ userId: followerId }),
272
272
  ],
273
-
273
+
274
274
  'v1.Unfollowed': ({ followerId, followingId }) => [
275
275
  tables.follows.delete().where({ followerId, followingId }),
276
276
  // Update follower counts (eventual consistency)
277
- tables.userStats.update({
277
+ tables.userStats.update({
278
278
  followerCount: Math.max(0, tables.userStats.select('followerCount').where({ userId: followingId }).scalar() - 1),
279
279
  lastUpdated: new Date()
280
280
  }).where({ userId: followingId }),
281
- tables.userStats.update({
281
+ tables.userStats.update({
282
282
  followingCount: Math.max(0, tables.userStats.select('followingCount').where({ userId: followerId }).scalar() - 1),
283
283
  lastUpdated: new Date()
284
284
  }).where({ userId: followerId }),
285
285
  ],
286
-
286
+
287
287
  // Like materializers (idempotent)
288
288
  'v1.PostLiked': ({ userId, postId, createdAt }) => [
289
289
  tables.likes.insert({ userId, postId, createdAt }),
290
290
  // Update like count
291
- tables.postStats.update({
291
+ tables.postStats.update({
292
292
  likeCount: tables.postStats.select('likeCount').where({ postId }).scalar() + 1,
293
293
  lastUpdated: createdAt
294
294
  }).where({ postId }),
295
295
  ],
296
-
296
+
297
297
  'v1.PostUnliked': ({ userId, postId }) => [
298
298
  tables.likes.delete().where({ userId, postId }),
299
299
  // Update like count
300
- tables.postStats.update({
300
+ tables.postStats.update({
301
301
  likeCount: Math.max(0, tables.postStats.select('likeCount').where({ postId }).scalar() - 1),
302
302
  lastUpdated: new Date()
303
303
  }).where({ postId }),
@@ -317,8 +317,8 @@ export const schema = makeSchema({ events, state })
317
317
  // .join(tables.follows, 'authorId', 'followingId')
318
318
  // .join(tables.users, 'authorId', 'id')
319
319
  // .leftJoin(tables.postStats, 'id', 'postId')
320
- // .where({
321
- // followerId: userId,
320
+ // .where({
321
+ // followerId: userId,
322
322
  // 'follows.status': 'active',
323
323
  // 'posts.deletedAt': null,
324
324
  // 'posts.visibility': ['public', 'followers']
@@ -34,8 +34,8 @@ export const tables = {
34
34
  // Client-only state for UI (not synced)
35
35
  uiState: State.SQLite.clientDocument({
36
36
  name: 'uiState',
37
- schema: Schema.Struct({
38
- newTodoText: Schema.String,
37
+ schema: Schema.Struct({
38
+ newTodoText: Schema.String,
39
39
  filter: Schema.Literal('all', 'active', 'completed'),
40
40
  selectedTags: Schema.Array(Schema.String)
41
41
  }),
@@ -48,8 +48,8 @@ export const events = {
48
48
  // Todo events
49
49
  todoCreated: Events.synced({
50
50
  name: 'v1.TodoCreated',
51
- schema: Schema.Struct({
52
- id: Schema.String,
51
+ schema: Schema.Struct({
52
+ id: Schema.String,
53
53
  title: Schema.String,
54
54
  createdAt: Schema.Date,
55
55
  position: Schema.Number
@@ -60,7 +60,7 @@ export const events = {
60
60
  schema: Schema.Struct({ id: Schema.String }),
61
61
  }),
62
62
  todoUncompleted: Events.synced({
63
- name: 'v1.TodoUncompleted',
63
+ name: 'v1.TodoUncompleted',
64
64
  schema: Schema.Struct({ id: Schema.String }),
65
65
  }),
66
66
  todoTitleChanged: Events.synced({
@@ -76,16 +76,16 @@ export const events = {
76
76
  schema: Schema.Struct({ id: Schema.String, deletedAt: Schema.Date }),
77
77
  }),
78
78
  todosCleared: Events.synced({
79
- name: 'v1.TodosCleared',
79
+ name: 'v1.TodosCleared',
80
80
  schema: Schema.Struct({ deletedAt: Schema.Date }),
81
81
  }),
82
-
82
+
83
83
  // Tag events
84
84
  tagCreated: Events.synced({
85
85
  name: 'v1.TagCreated',
86
- schema: Schema.Struct({
87
- id: Schema.String,
88
- name: Schema.String,
86
+ schema: Schema.Struct({
87
+ id: Schema.String,
88
+ name: Schema.String,
89
89
  color: Schema.NullOr(Schema.String),
90
90
  createdAt: Schema.Date
91
91
  }),
@@ -94,7 +94,7 @@ export const events = {
94
94
  name: 'v1.TagDeleted',
95
95
  schema: Schema.Struct({ id: Schema.String, deletedAt: Schema.Date }),
96
96
  }),
97
-
97
+
98
98
  // Todo-Tag relationship events
99
99
  todoTagged: Events.synced({
100
100
  name: 'v1.TodoTagged',
@@ -104,7 +104,7 @@ export const events = {
104
104
  name: 'v1.TodoUntagged',
105
105
  schema: Schema.Struct({ todoId: Schema.String, tagId: Schema.String }),
106
106
  }),
107
-
107
+
108
108
  // UI state events (local only)
109
109
  uiStateSet: tables.uiState.set,
110
110
  }
@@ -112,39 +112,39 @@ export const events = {
112
112
  // Materializers - Map events to state changes with conflict-free semantics
113
113
  const materializers = State.SQLite.materializers(events, {
114
114
  // Todo materializers
115
- 'v1.TodoCreated': ({ id, title, createdAt, position }) =>
115
+ 'v1.TodoCreated': ({ id, title, createdAt, position }) =>
116
116
  tables.todos.insert({ id, title, completed: false, createdAt, position }),
117
-
118
- 'v1.TodoCompleted': ({ id }) =>
117
+
118
+ 'v1.TodoCompleted': ({ id }) =>
119
119
  tables.todos.update({ completed: true }).where({ id }),
120
-
121
- 'v1.TodoUncompleted': ({ id }) =>
120
+
121
+ 'v1.TodoUncompleted': ({ id }) =>
122
122
  tables.todos.update({ completed: false }).where({ id }),
123
-
124
- 'v1.TodoTitleChanged': ({ id, title }) =>
123
+
124
+ 'v1.TodoTitleChanged': ({ id, title }) =>
125
125
  tables.todos.update({ title }).where({ id }),
126
-
127
- 'v1.TodoReordered': ({ id, position }) =>
126
+
127
+ 'v1.TodoReordered': ({ id, position }) =>
128
128
  tables.todos.update({ position }).where({ id }),
129
-
130
- 'v1.TodoDeleted': ({ id, deletedAt }) =>
129
+
130
+ 'v1.TodoDeleted': ({ id, deletedAt }) =>
131
131
  tables.todos.update({ deletedAt }).where({ id }),
132
-
133
- 'v1.TodosCleared': ({ deletedAt }) =>
132
+
133
+ 'v1.TodosCleared': ({ deletedAt }) =>
134
134
  tables.todos.update({ deletedAt }).where({ completed: true, deletedAt: null }),
135
-
136
- // Tag materializers
137
- 'v1.TagCreated': ({ id, name, color, createdAt }) =>
135
+
136
+ // Tag materializers
137
+ 'v1.TagCreated': ({ id, name, color, createdAt }) =>
138
138
  tables.tags.insert({ id, name, color, createdAt }),
139
-
140
- 'v1.TagDeleted': ({ id, deletedAt }) =>
139
+
140
+ 'v1.TagDeleted': ({ id, deletedAt }) =>
141
141
  tables.tags.update({ deletedAt }).where({ id }),
142
-
142
+
143
143
  // Todo-Tag relationship materializers
144
- 'v1.TodoTagged': ({ todoId, tagId, createdAt }) =>
144
+ 'v1.TodoTagged': ({ todoId, tagId, createdAt }) =>
145
145
  tables.todoTags.insert({ todoId, tagId, createdAt }),
146
-
147
- 'v1.TodoUntagged': ({ todoId, tagId }) =>
146
+
147
+ 'v1.TodoUntagged': ({ todoId, tagId }) =>
148
148
  tables.todoTags.delete().where({ todoId, tagId }),
149
149
  })
150
150
 
@@ -153,7 +153,7 @@ const state = State.SQLite.makeState({ tables, materializers })
153
153
  export const schema = makeSchema({ events, state })
154
154
 
155
155
  // Example usage:
156
- //
156
+ //
157
157
  // import { queryDb, dispatchEvent } from '@livestore/livestore'
158
158
  // import { schema, events, tables } from './schema.js'
159
159
  //
@@ -0,0 +1,147 @@
1
+ import { makeAdapter as makeNodeAdapter } from '@livestore/adapter-node'
2
+ import { UnknownError } from '@livestore/common'
3
+ import { LiveStoreEvent, SystemTables } from '@livestore/common/schema'
4
+ import type { Store } from '@livestore/livestore'
5
+ import { createStorePromise } from '@livestore/livestore'
6
+ import { Effect, FetchHttpClient, Layer, Option, Schema } from '@livestore/utils/effect'
7
+ import { PlatformNode } from '@livestore/utils/node'
8
+
9
+ import { loadModuleConfig } from '../module-loader.ts'
10
+
11
+ /** Currently connected store */
12
+ let store: Store<any> | undefined
13
+
14
+ /** Layer providing FileSystem and HttpClient for module loading */
15
+ const ModuleLoaderLayer = Layer.mergeAll(PlatformNode.NodeFileSystem.layer, FetchHttpClient.layer)
16
+
17
+ /**
18
+ * Dynamically imports a module that exports a `makeStore({ storeId }): Promise<Store>` function,
19
+ * calls it with the provided storeId, and caches the Store instance for subsequent tool calls.
20
+ */
21
+ export const init = ({
22
+ configPath,
23
+ storeId,
24
+ clientId,
25
+ sessionId,
26
+ }: {
27
+ configPath: string
28
+ storeId: string
29
+ clientId?: string
30
+ sessionId?: string
31
+ }): Effect.Effect<Store<any>, UnknownError> =>
32
+ Effect.gen(function* () {
33
+ if (storeId === '' || typeof storeId !== 'string') {
34
+ return yield* UnknownError.make({ cause: new Error('Invalid storeId: expected a non-empty string') })
35
+ }
36
+
37
+ const { schema, syncBackendConstructor, syncPayloadSchema, syncPayload } = yield* loadModuleConfig({ configPath })
38
+
39
+ // Build Node adapter internally
40
+ const adapter = makeNodeAdapter({
41
+ storage: { type: 'in-memory' },
42
+ ...(clientId !== undefined && clientId !== '' ? { clientId } : {}),
43
+ ...(sessionId !== undefined && sessionId !== '' ? { sessionId } : {}),
44
+ sync: {
45
+ backend: syncBackendConstructor,
46
+ initialSyncOptions: { _tag: 'Blocking', timeout: 5000 },
47
+ onSyncError: 'shutdown',
48
+ },
49
+ })
50
+
51
+ // Create the store
52
+ const s = yield* Effect.promise(() =>
53
+ createStorePromise({
54
+ schema,
55
+ storeId,
56
+ adapter,
57
+ disableDevtools: true,
58
+ ...(syncPayload !== undefined ? { syncPayload } : {}),
59
+ syncPayloadSchema,
60
+ }),
61
+ )
62
+
63
+ // Replace existing store if any
64
+ if (store !== undefined) {
65
+ yield* Effect.promise(async () => {
66
+ try {
67
+ await store!.shutdownPromise()
68
+ } catch {}
69
+ })
70
+ }
71
+
72
+ store = s
73
+ return store
74
+ }).pipe(Effect.provide(ModuleLoaderLayer), Effect.withSpan('mcp-runtime:init'))
75
+
76
+ export const getStore = Effect.sync(() => Option.fromNullishOr(store))
77
+
78
+ export const status = Effect.gen(function* () {
79
+ const opt = yield* getStore
80
+ if (opt._tag === 'None') {
81
+ return {
82
+ _tag: 'disconnected' as const,
83
+ }
84
+ }
85
+ const s = opt.value
86
+ const tableCounts = Array.from(s.schema.state.sqlite.tables.keys())
87
+ .filter((name) => !SystemTables.isStateSystemTable(name))
88
+ .reduce(
89
+ (acc, name) => {
90
+ acc[name] = s.query(s.schema.state.sqlite.tables.get(name)!.count())
91
+ return acc
92
+ },
93
+ {} as Record<string, number>,
94
+ )
95
+
96
+ return {
97
+ _tag: 'connected' as const,
98
+ storeId: s.storeId,
99
+ clientId: s.clientId,
100
+ sessionId: s.sessionId,
101
+ tableCounts,
102
+ }
103
+ }).pipe(Effect.withSpan('mcp-runtime:status'))
104
+
105
+ export const query = Effect.fn('mcp-runtime:query')(function* ({
106
+ sql,
107
+ bindValues,
108
+ }: {
109
+ sql: string
110
+ bindValues?: readonly any[] | Record<string, unknown>
111
+ }) {
112
+ const opt = yield* getStore
113
+ if (opt._tag === 'None') {
114
+ return yield* Effect.die(new Error('LiveStore not connected. Call livestore_instance_connect first.'))
115
+ }
116
+ const s = opt.value
117
+
118
+ const rows = s.query<Array<Record<string, unknown>>>({ query: sql, bindValues: (bindValues as any) ?? [] })
119
+ const jsonRows = rows.map((r) => Object.fromEntries(Object.entries(r).map(([k, v]) => [k, v as Schema.Json])))
120
+ return { rows: jsonRows, rowCount: jsonRows.length }
121
+ })
122
+
123
+ export const commit = Effect.fn('mcp-runtime:commit')(function* ({
124
+ events,
125
+ }: {
126
+ events: ReadonlyArray<{ name: string; args: Schema.Json }>
127
+ }) {
128
+ const opt = yield* getStore
129
+ if (opt._tag === 'None') {
130
+ return yield* Effect.die(new Error('LiveStore not connected. Call livestore_instance_connect first.'))
131
+ }
132
+ const s = opt.value
133
+ const InputEventSchema = LiveStoreEvent.Input.makeSchema(s.schema) as Schema.Codec<any>
134
+ const decoded = events.map((e) => Schema.decodeSync(InputEventSchema)(e))
135
+ s.commit(...decoded)
136
+ return { committed: decoded.length }
137
+ })
138
+
139
+ export const disconnect = Effect.promise(async () => {
140
+ if (store !== undefined) {
141
+ try {
142
+ await store.shutdownPromise()
143
+ } catch {}
144
+ store = undefined
145
+ }
146
+ return { _tag: 'disconnected' as const }
147
+ }).pipe(Effect.withSpan('mcp-runtime:disconnect'))
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Shared module loading utility for CLI and MCP.
3
+ * Loads and validates user config modules that export schema, syncBackend, and optional syncPayload.
4
+ */
5
+ import path from 'node:path'
6
+ import { pathToFileURL } from 'node:url'
7
+
8
+ import type { SyncBackend } from '@livestore/common'
9
+ import { UnknownError } from '@livestore/common'
10
+ import { isLiveStoreSchema, type LiveStoreSchema } from '@livestore/common/schema'
11
+ import { shouldNeverHappen } from '@livestore/utils'
12
+ import { Effect, FileSystem, Schema } from '@livestore/utils/effect'
13
+
14
+ export interface ModuleConfig {
15
+ schema: LiveStoreSchema
16
+ syncBackendConstructor: SyncBackend.SyncBackendConstructor
17
+ syncPayloadSchema: Schema.Codec<Schema.Json, Schema.Json>
18
+ syncPayload: Schema.Json | undefined
19
+ }
20
+
21
+ /**
22
+ * Loads and validates a user config module.
23
+ * The module must export:
24
+ * - `schema`: A valid LiveStore schema
25
+ * - `syncBackend`: A sync backend constructor function
26
+ * - `syncPayloadSchema` (optional): Schema for validating syncPayload
27
+ * - `syncPayload` (optional): Payload data for the sync backend
28
+ */
29
+ export const loadModuleConfig = ({
30
+ configPath,
31
+ }: {
32
+ configPath: string
33
+ }): Effect.Effect<ModuleConfig, UnknownError, FileSystem.FileSystem> =>
34
+ Effect.gen(function* () {
35
+ const abs = path.isAbsolute(configPath) === true ? configPath : path.resolve(process.cwd(), configPath)
36
+
37
+ const fs = yield* FileSystem.FileSystem
38
+ const exists = yield* fs.exists(abs).pipe(UnknownError.mapToUnknownError)
39
+ if (exists === false) {
40
+ return yield* UnknownError.make({
41
+ cause: `Store module not found at ${abs}`,
42
+ note: 'Make sure the path points to a valid LiveStore module',
43
+ })
44
+ }
45
+
46
+ const mod = yield* Effect.tryPromise({
47
+ try: () => import(pathToFileURL(abs).href),
48
+ catch: (cause) =>
49
+ UnknownError.make({
50
+ cause,
51
+ note: `Failed to import module at ${abs}`,
52
+ }),
53
+ })
54
+
55
+ const schema = mod?.schema
56
+ if (isLiveStoreSchema(schema) === false) {
57
+ return yield* UnknownError.make({
58
+ cause: `Module at ${abs} must export a valid LiveStore 'schema'`,
59
+ note: `Ex: export { schema } from './src/livestore/schema.ts'`,
60
+ })
61
+ }
62
+
63
+ const syncBackendConstructor = mod?.syncBackend
64
+ if (typeof syncBackendConstructor !== 'function') {
65
+ return yield* UnknownError.make({
66
+ cause: `Module at ${abs} must export a 'syncBackend' constructor`,
67
+ note: `Ex: export const syncBackend = makeWsSync({ url })`,
68
+ })
69
+ }
70
+
71
+ const syncPayloadSchemaExport = mod?.syncPayloadSchema
72
+ const syncPayloadSchema =
73
+ syncPayloadSchemaExport === undefined
74
+ ? Schema.Json
75
+ : Schema.isSchema(syncPayloadSchemaExport) === true
76
+ ? (syncPayloadSchemaExport as Schema.Codec<Schema.Json, Schema.Json>)
77
+ : shouldNeverHappen(
78
+ `Exported 'syncPayloadSchema' from ${abs} must be an Effect Schema (received ${typeof syncPayloadSchemaExport}).`,
79
+ )
80
+
81
+ const syncPayloadExport = mod?.syncPayload
82
+ const syncPayload = yield* (
83
+ syncPayloadExport === undefined
84
+ ? Effect.succeed<Schema.Json | undefined>(undefined)
85
+ : Schema.decodeUnknownEffect(syncPayloadSchema)(syncPayloadExport)
86
+ ).pipe(UnknownError.mapToUnknownError)
87
+
88
+ return {
89
+ schema,
90
+ syncBackendConstructor,
91
+ syncPayloadSchema,
92
+ syncPayload,
93
+ }
94
+ }).pipe(Effect.withSpan('module-loader:loadModuleConfig'))