@livestore/cli 0.0.0-snapshot-d8085caf44a1e13adef4e8c199343a8cbca2e45c → 0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livestore/cli",
3
- "version": "0.0.0-snapshot-d8085caf44a1e13adef4e8c199343a8cbca2e45c",
3
+ "version": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -11,17 +11,17 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@effect/ai-openai": "0.32.0",
14
- "@livestore/adapter-node": "0.0.0-snapshot-d8085caf44a1e13adef4e8c199343a8cbca2e45c",
15
- "@livestore/livestore": "0.0.0-snapshot-d8085caf44a1e13adef4e8c199343a8cbca2e45c",
16
- "@livestore/common": "0.0.0-snapshot-d8085caf44a1e13adef4e8c199343a8cbca2e45c",
17
- "@livestore/peer-deps": "0.0.0-snapshot-d8085caf44a1e13adef4e8c199343a8cbca2e45c",
18
- "@livestore/sync-cf": "0.0.0-snapshot-d8085caf44a1e13adef4e8c199343a8cbca2e45c",
19
- "@livestore/utils": "0.0.0-snapshot-d8085caf44a1e13adef4e8c199343a8cbca2e45c"
14
+ "@livestore/adapter-node": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b",
15
+ "@livestore/common": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b",
16
+ "@livestore/peer-deps": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b",
17
+ "@livestore/livestore": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b",
18
+ "@livestore/sync-cf": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b",
19
+ "@livestore/utils": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "24.10.1",
23
23
  "typescript": "5.9.2",
24
- "@livestore/utils-dev": "0.0.0-snapshot-d8085caf44a1e13adef4e8c199343a8cbca2e45c"
24
+ "@livestore/utils-dev": "0.0.0-snapshot-75de2b24a1fb1df981b56b5d9ec7381ea351e95b"
25
25
  },
26
26
  "files": [
27
27
  "package.json",
@@ -1,200 +1,8 @@
1
1
  import path from 'node:path'
2
- import { pathToFileURL } from 'node:url'
3
- import type { SyncBackend } from '@livestore/common'
4
- import { UnknownError } from '@livestore/common'
5
- import { isLiveStoreSchema, LiveStoreEvent } from '@livestore/common/schema'
6
- import { shouldNeverHappen } from '@livestore/utils'
7
- import {
8
- Cause,
9
- Console,
10
- Effect,
11
- FileSystem,
12
- type HttpClient,
13
- KeyValueStore,
14
- Layer,
15
- Option,
16
- Schema,
17
- type Scope,
18
- Stream,
19
- } from '@livestore/utils/effect'
2
+ import type { UnknownError } from '@livestore/common'
3
+ import { Console, Effect, FileSystem, type HttpClient, type Scope } from '@livestore/utils/effect'
20
4
  import { Cli } from '@livestore/utils/node'
21
-
22
- /** Connection timeout for sync backend ping (5 seconds) */
23
- const CONNECTION_TIMEOUT_MS = 5000
24
-
25
- /**
26
- * Schema for the export file format.
27
- * Contains metadata about the export and an array of events in global encoded format.
28
- */
29
- const ExportFileSchema = Schema.Struct({
30
- /** Format version for future compatibility */
31
- version: Schema.Literal(1),
32
- /** Store identifier */
33
- storeId: Schema.String,
34
- /** ISO timestamp of when the export was created */
35
- exportedAt: Schema.String,
36
- /** Total number of events in the export */
37
- eventCount: Schema.Number,
38
- /** Array of events in global encoded format */
39
- events: Schema.Array(LiveStoreEvent.Global.Encoded),
40
- })
41
-
42
- type ExportFile = typeof ExportFileSchema.Type
43
-
44
- class ConnectionError extends Schema.TaggedError<ConnectionError>()('ConnectionError', {
45
- cause: Schema.Defect,
46
- note: Schema.String,
47
- }) {}
48
-
49
- class ExportError extends Schema.TaggedError<ExportError>()('ExportError', {
50
- cause: Schema.Defect,
51
- note: Schema.String,
52
- }) {}
53
-
54
- class ImportError extends Schema.TaggedError<ImportError>()('ImportError', {
55
- cause: Schema.Defect,
56
- note: Schema.String,
57
- }) {}
58
-
59
- /**
60
- * Creates a sync backend connection from a user module and verifies connectivity.
61
- * This is a simplified version of the MCP runtime that only creates the sync backend.
62
- */
63
- const makeSyncBackend = ({
64
- storePath,
65
- storeId,
66
- clientId,
67
- }: {
68
- storePath: string
69
- storeId: string
70
- clientId: string
71
- }): Effect.Effect<
72
- SyncBackend.SyncBackend,
73
- UnknownError | ConnectionError,
74
- FileSystem.FileSystem | HttpClient.HttpClient | Scope.Scope
75
- > =>
76
- Effect.gen(function* () {
77
- const abs = path.isAbsolute(storePath) ? storePath : path.resolve(process.cwd(), storePath)
78
-
79
- const fs = yield* FileSystem.FileSystem
80
- const exists = yield* fs.exists(abs).pipe(UnknownError.mapToUnknownError)
81
- if (!exists) {
82
- return yield* Effect.fail(
83
- UnknownError.make({
84
- cause: `Store module not found at ${abs}`,
85
- note: 'Make sure the path points to a valid LiveStore module',
86
- }),
87
- )
88
- }
89
-
90
- const mod = yield* Effect.tryPromise({
91
- try: () => import(pathToFileURL(abs).href),
92
- catch: (cause) =>
93
- UnknownError.make({
94
- cause,
95
- note: `Failed to import module at ${abs}`,
96
- }),
97
- })
98
-
99
- const schema = (mod as any)?.schema
100
- if (!isLiveStoreSchema(schema)) {
101
- return yield* Effect.fail(
102
- UnknownError.make({
103
- cause: `Module at ${abs} must export a valid LiveStore 'schema'`,
104
- note: `Ex: export { schema } from './src/livestore/schema.ts'`,
105
- }),
106
- )
107
- }
108
-
109
- const syncBackendConstructor = (mod as any)?.syncBackend
110
- if (typeof syncBackendConstructor !== 'function') {
111
- return yield* Effect.fail(
112
- UnknownError.make({
113
- cause: `Module at ${abs} must export a 'syncBackend' constructor`,
114
- note: `Ex: export const syncBackend = makeWsSync({ url })`,
115
- }),
116
- )
117
- }
118
-
119
- const syncPayloadSchemaExport = (mod as any)?.syncPayloadSchema
120
- const syncPayloadSchema =
121
- syncPayloadSchemaExport === undefined
122
- ? Schema.JsonValue
123
- : Schema.isSchema(syncPayloadSchemaExport)
124
- ? (syncPayloadSchemaExport as Schema.Schema<any>)
125
- : shouldNeverHappen(
126
- `Exported 'syncPayloadSchema' from ${abs} must be an Effect Schema (received ${typeof syncPayloadSchemaExport}).`,
127
- )
128
-
129
- const syncPayloadExport = (mod as any)?.syncPayload
130
- const syncPayload = yield* (
131
- syncPayloadExport === undefined
132
- ? Effect.succeed<unknown>(undefined)
133
- : Schema.decodeUnknown(syncPayloadSchema)(syncPayloadExport)
134
- ).pipe(UnknownError.mapToUnknownError)
135
-
136
- /** Simple in-memory key-value store for sync backend state */
137
- const kvStore: { backendId: string | undefined } = { backendId: undefined }
138
-
139
- const syncBackend = yield* (syncBackendConstructor as SyncBackend.SyncBackendConstructor)({
140
- storeId,
141
- clientId,
142
- payload: syncPayload,
143
- }).pipe(
144
- Effect.provide(
145
- Layer.succeed(
146
- KeyValueStore.KeyValueStore,
147
- KeyValueStore.makeStringOnly({
148
- get: (_key) => Effect.succeed(Option.fromNullable(kvStore.backendId)),
149
- set: (_key, value) =>
150
- Effect.sync(() => {
151
- kvStore.backendId = value
152
- }),
153
- clear: Effect.dieMessage('Not implemented'),
154
- remove: () => Effect.dieMessage('Not implemented'),
155
- size: Effect.dieMessage('Not implemented'),
156
- }),
157
- ),
158
- ),
159
- UnknownError.mapToUnknownError,
160
- )
161
-
162
- /** Connect to the sync backend */
163
- yield* syncBackend.connect.pipe(
164
- Effect.mapError(
165
- (cause) =>
166
- new ConnectionError({
167
- cause,
168
- note: `Failed to connect to sync backend: ${cause._tag === 'IsOfflineError' ? 'Backend is offline or unreachable' : String(cause)}`,
169
- }),
170
- ),
171
- )
172
-
173
- /** Verify connectivity with a ping (with timeout) */
174
- yield* syncBackend.ping.pipe(
175
- Effect.timeout(CONNECTION_TIMEOUT_MS),
176
- Effect.catchAll((cause) => {
177
- if (Cause.isTimeoutException(cause)) {
178
- return Effect.fail(
179
- new ConnectionError({
180
- cause,
181
- note: `Connection timeout: Sync backend did not respond within ${CONNECTION_TIMEOUT_MS}ms`,
182
- }),
183
- )
184
- }
185
- return Effect.fail(
186
- new ConnectionError({
187
- cause,
188
- note: `Failed to ping sync backend: ${cause._tag === 'IsOfflineError' ? 'Backend is offline or unreachable' : String(cause)}`,
189
- }),
190
- )
191
- }),
192
- )
193
-
194
- yield* Console.log(`✓ Connected to sync backend: ${syncBackend.metadata.name}`)
195
-
196
- return syncBackend
197
- })
5
+ import * as SyncOps from '../sync-operations.ts'
198
6
 
199
7
  /**
200
8
  * Export events from the sync backend to a JSON file.
@@ -211,61 +19,31 @@ const exportEvents = ({
211
19
  outputPath: string
212
20
  }): Effect.Effect<
213
21
  void,
214
- ExportError | UnknownError | ConnectionError,
22
+ SyncOps.ExportError | SyncOps.ConnectionError | UnknownError,
215
23
  FileSystem.FileSystem | HttpClient.HttpClient | Scope.Scope
216
24
  > =>
217
25
  Effect.gen(function* () {
218
26
  yield* Console.log(`Connecting to sync backend...`)
219
27
 
220
- const syncBackend = yield* makeSyncBackend({ storePath, storeId, clientId })
221
-
222
- yield* Console.log(`Pulling events from sync backend...`)
28
+ const result = yield* SyncOps.pullEventsFromSyncBackend({ storePath, storeId, clientId })
223
29
 
224
- const events: LiveStoreEvent.Global.Encoded[] = []
225
-
226
- yield* syncBackend.pull(Option.none(), { live: false }).pipe(
227
- Stream.tap((item) =>
228
- Effect.sync(() => {
229
- for (const { eventEncoded } of item.batch) {
230
- events.push(eventEncoded)
231
- }
232
- }),
233
- ),
234
- Stream.takeUntil((item) => item.pageInfo._tag === 'NoMore'),
235
- Stream.runDrain,
236
- Effect.mapError(
237
- (cause) =>
238
- new ExportError({
239
- cause,
240
- note: `Failed to pull events from sync backend: ${cause}`,
241
- }),
242
- ),
243
- )
244
-
245
- yield* Console.log(`Pulled ${events.length} events`)
246
-
247
- const exportData: ExportFile = {
248
- version: 1,
249
- storeId,
250
- exportedAt: new Date().toISOString(),
251
- eventCount: events.length,
252
- events,
253
- }
30
+ yield* Console.log(`✓ Connected to sync backend`)
31
+ yield* Console.log(`Pulled ${result.eventCount} events`)
254
32
 
255
33
  const fs = yield* FileSystem.FileSystem
256
34
  const absOutputPath = path.isAbsolute(outputPath) ? outputPath : path.resolve(process.cwd(), outputPath)
257
35
 
258
- yield* fs.writeFileString(absOutputPath, JSON.stringify(exportData, null, 2)).pipe(
36
+ yield* fs.writeFileString(absOutputPath, JSON.stringify(result.data, null, 2)).pipe(
259
37
  Effect.mapError(
260
38
  (cause) =>
261
- new ExportError({
39
+ new SyncOps.ExportError({
262
40
  cause,
263
41
  note: `Failed to write export file: ${cause}`,
264
42
  }),
265
43
  ),
266
44
  )
267
45
 
268
- yield* Console.log(`Exported ${events.length} events to ${absOutputPath}`)
46
+ yield* Console.log(`Exported ${result.eventCount} events to ${absOutputPath}`)
269
47
  }).pipe(Effect.withSpan('cli:export'))
270
48
 
271
49
  /**
@@ -287,21 +65,27 @@ const importEvents = ({
287
65
  dryRun: boolean
288
66
  }): Effect.Effect<
289
67
  void,
290
- ImportError | UnknownError | ConnectionError,
68
+ SyncOps.ImportError | SyncOps.ConnectionError | UnknownError,
291
69
  FileSystem.FileSystem | HttpClient.HttpClient | Scope.Scope
292
70
  > =>
293
71
  Effect.gen(function* () {
294
72
  const fs = yield* FileSystem.FileSystem
295
73
  const absInputPath = path.isAbsolute(inputPath) ? inputPath : path.resolve(process.cwd(), inputPath)
296
74
 
297
- const exists = yield* fs.exists(absInputPath).pipe(UnknownError.mapToUnknownError)
75
+ const exists = yield* fs.exists(absInputPath).pipe(
76
+ Effect.mapError(
77
+ (cause) =>
78
+ new SyncOps.ImportError({
79
+ cause,
80
+ note: `Failed to check file existence: ${cause}`,
81
+ }),
82
+ ),
83
+ )
298
84
  if (!exists) {
299
- return yield* Effect.fail(
300
- new ImportError({
301
- cause: new Error(`File not found: ${absInputPath}`),
302
- note: `Import file does not exist at ${absInputPath}`,
303
- }),
304
- )
85
+ return yield* new SyncOps.ImportError({
86
+ cause: new Error(`File not found: ${absInputPath}`),
87
+ note: `Import file does not exist at ${absInputPath}`,
88
+ })
305
89
  }
306
90
 
307
91
  yield* Console.log(`Reading import file...`)
@@ -309,7 +93,7 @@ const importEvents = ({
309
93
  const fileContent = yield* fs.readFileString(absInputPath).pipe(
310
94
  Effect.mapError(
311
95
  (cause) =>
312
- new ImportError({
96
+ new SyncOps.ImportError({
313
97
  cause,
314
98
  note: `Failed to read import file: ${cause}`,
315
99
  }),
@@ -319,100 +103,48 @@ const importEvents = ({
319
103
  const parsedContent = yield* Effect.try({
320
104
  try: () => JSON.parse(fileContent),
321
105
  catch: (error) =>
322
- new ImportError({
106
+ new SyncOps.ImportError({
323
107
  cause: new Error(`Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`),
324
108
  note: `Invalid JSON in import file: ${error instanceof Error ? error.message : String(error)}`,
325
109
  }),
326
110
  })
327
111
 
328
- const exportData = yield* Schema.decodeUnknown(ExportFileSchema)(parsedContent).pipe(
329
- Effect.mapError(
330
- (cause) =>
331
- new ImportError({
332
- cause: new Error(`Invalid export file format: ${cause}`),
333
- note: `Invalid export file format: ${cause}`,
334
- }),
335
- ),
336
- )
112
+ /** Validate export file format before proceeding */
113
+ const validation = yield* SyncOps.validateExportData({ data: parsedContent, targetStoreId: storeId })
337
114
 
338
- if (exportData.storeId !== storeId) {
115
+ if (validation.storeIdMismatch) {
339
116
  if (!force) {
340
- return yield* Effect.fail(
341
- new ImportError({
342
- cause: new Error(`Store ID mismatch: file has '${exportData.storeId}', expected '${storeId}'`),
343
- note: `The export file was created for a different store. Use --force to import anyway.`,
344
- }),
345
- )
117
+ return yield* new SyncOps.ImportError({
118
+ cause: new Error(`Store ID mismatch: file has '${validation.sourceStoreId}', expected '${storeId}'`),
119
+ note: `The export file was created for a different store. Use --force to import anyway.`,
120
+ })
346
121
  }
347
- yield* Console.log(`Store ID mismatch: file has '${exportData.storeId}', importing to '${storeId}' (--force)`)
122
+ yield* Console.log(
123
+ `Store ID mismatch: file has '${validation.sourceStoreId}', importing to '${storeId}' (--force)`,
124
+ )
348
125
  }
349
126
 
350
- yield* Console.log(`Found ${exportData.events.length} events in export file`)
127
+ yield* Console.log(`Found ${validation.eventCount} events in export file`)
351
128
 
352
129
  if (dryRun) {
353
130
  yield* Console.log(`Dry run - validating import file...`)
354
- yield* Console.log(`Dry run complete. ${exportData.events.length} events would be imported.`)
131
+ yield* Console.log(`Dry run complete. ${validation.eventCount} events would be imported.`)
355
132
  return
356
133
  }
357
134
 
358
135
  yield* Console.log(`Connecting to sync backend...`)
359
136
 
360
- const syncBackend = yield* makeSyncBackend({ storePath, storeId, clientId })
361
-
362
- /** Check if events already exist by pulling from the backend first */
363
- yield* Console.log(`Checking for existing events...`)
364
-
365
- let existingEventCount = 0
366
- yield* syncBackend.pull(Option.none(), { live: false }).pipe(
367
- Stream.tap((item) =>
368
- Effect.sync(() => {
369
- existingEventCount += item.batch.length
370
- }),
371
- ),
372
- Stream.takeUntil((item) => item.pageInfo._tag === 'NoMore'),
373
- Stream.runDrain,
374
- Effect.mapError(
375
- (cause) =>
376
- new ImportError({
377
- cause,
378
- note: `Failed to check existing events: ${cause}`,
379
- }),
380
- ),
381
- )
382
-
383
- if (existingEventCount > 0) {
384
- return yield* Effect.fail(
385
- new ImportError({
386
- cause: new Error(`Sync backend already contains ${existingEventCount} events`),
387
- note: `Cannot import into a non-empty sync backend. The sync backend must be empty.`,
388
- }),
389
- )
390
- }
391
-
392
- yield* Console.log(`Pushing ${exportData.events.length} events to sync backend...`)
393
-
394
- /** Push events in batches of 100 (sync backend constraint) */
395
- const batchSize = 100
396
- let pushed = 0
397
-
398
- for (let i = 0; i < exportData.events.length; i += batchSize) {
399
- const batch = exportData.events.slice(i, i + batchSize)
400
-
401
- yield* syncBackend.push(batch).pipe(
402
- Effect.mapError(
403
- (cause) =>
404
- new ImportError({
405
- cause,
406
- note: `Failed to push events at position ${i}: ${cause}`,
407
- }),
408
- ),
409
- )
410
-
411
- pushed += batch.length
412
- yield* Console.log(` Pushed ${pushed}/${exportData.events.length} events`)
413
- }
137
+ const result = yield* SyncOps.pushEventsToSyncBackend({
138
+ storePath,
139
+ storeId,
140
+ clientId,
141
+ data: parsedContent,
142
+ force,
143
+ dryRun: false,
144
+ })
414
145
 
415
- yield* Console.log(`Successfully imported ${exportData.events.length} events`)
146
+ yield* Console.log(`✓ Connected to sync backend`)
147
+ yield* Console.log(`Successfully imported ${result.eventCount} events`)
416
148
  }).pipe(Effect.withSpan('cli:import'))
417
149
 
418
150
  export const exportCommand = Cli.Command.make(
@@ -1,12 +1,17 @@
1
- import { Effect } from '@livestore/utils/effect'
1
+ import { Effect, FetchHttpClient, Layer } from '@livestore/utils/effect'
2
+ import { PlatformNode } from '@livestore/utils/node'
2
3
  import { blogSchemaContent } from '../mcp-content/schemas/blog.ts'
3
4
  import { ecommerceSchemaContent } from '../mcp-content/schemas/ecommerce.ts'
4
5
  import { socialSchemaContent } from '../mcp-content/schemas/social.ts'
5
6
  import { todoSchemaContent } from '../mcp-content/schemas/todo.ts'
6
7
  import * as Runtime from '../mcp-runtime/runtime.ts'
8
+ import * as SyncOps from '../sync-operations.ts'
7
9
  import { coachToolHandler } from './mcp-coach.ts'
8
10
  import { livestoreToolkit } from './mcp-tools-defs.ts'
9
11
 
12
+ /** Layer providing FileSystem and HttpClient for sync operations */
13
+ const SyncOpsLayer = Layer.mergeAll(PlatformNode.NodeFileSystem.layer, FetchHttpClient.layer)
14
+
10
15
  // Tool handlers using Tim Smart's pattern
11
16
  export const toolHandlers: any = livestoreToolkit.of({
12
17
  livestore_coach: coachToolHandler,
@@ -156,4 +161,32 @@ export const schema = Schema.create({
156
161
  livestore_instance_disconnect: Effect.fnUntraced(function* () {
157
162
  return yield* Runtime.disconnect
158
163
  }),
164
+
165
+ // Sync export - pull all events from sync backend
166
+ livestore_sync_export: Effect.fnUntraced(function* ({ storePath, storeId, clientId }) {
167
+ const result = yield* SyncOps.pullEventsFromSyncBackend({
168
+ storePath,
169
+ storeId,
170
+ clientId: clientId ?? 'mcp-export',
171
+ }).pipe(Effect.scoped, Effect.provide(SyncOpsLayer), Effect.orDie)
172
+
173
+ return {
174
+ storeId: result.storeId,
175
+ eventCount: result.eventCount,
176
+ exportedAt: result.exportedAt,
177
+ data: result.data,
178
+ }
179
+ }),
180
+
181
+ // Sync import - push events to sync backend
182
+ livestore_sync_import: Effect.fnUntraced(function* ({ storePath, storeId, clientId, data, force, dryRun }) {
183
+ return yield* SyncOps.pushEventsToSyncBackend({
184
+ storePath,
185
+ storeId,
186
+ clientId: clientId ?? 'mcp-import',
187
+ data,
188
+ force: force ?? false,
189
+ dryRun: dryRun ?? false,
190
+ }).pipe(Effect.scoped, Effect.provide(SyncOpsLayer), Effect.orDie)
191
+ }),
159
192
  })
@@ -226,4 +226,92 @@ Example success:
226
226
  parameters: {},
227
227
  success: Schema.TaggedStruct('disconnected', {}),
228
228
  }),
229
+
230
+ Tool.make('livestore_sync_export', {
231
+ description: `Export all events from a sync backend to JSON data.
232
+
233
+ This tool connects directly to the sync backend (without creating a full LiveStore instance) and pulls all events. Useful for backup, migration, and debugging.
234
+
235
+ Module contract (same as livestore_instance_connect):
236
+ \`\`\`ts
237
+ export { schema } from './src/livestore/schema.ts'
238
+ export const syncBackend = makeWsSync({ url: process.env.LIVESTORE_SYNC_URL ?? 'ws://localhost:8787' })
239
+ export const syncPayload = { authToken: process.env.LIVESTORE_SYNC_AUTH_TOKEN }
240
+ \`\`\`
241
+
242
+ Example parameters:
243
+ {
244
+ "storePath": "livestore-cli.config.ts",
245
+ "storeId": "my-store"
246
+ }
247
+
248
+ Returns on success:
249
+ {
250
+ "storeId": "my-store",
251
+ "eventCount": 127,
252
+ "exportedAt": "2024-01-15T10:30:00.000Z",
253
+ "data": { "version": 1, "storeId": "my-store", ... }
254
+ }`,
255
+ parameters: {
256
+ storePath: Schema.String.annotations({
257
+ description: 'Path to a module that exports schema and syncBackend',
258
+ }),
259
+ storeId: Schema.String.annotations({ description: 'Store identifier' }),
260
+ clientId: Schema.optional(Schema.String.annotations({ description: 'Client identifier (default: mcp-export)' })),
261
+ },
262
+ success: Schema.Struct({
263
+ storeId: Schema.String,
264
+ eventCount: Schema.Number,
265
+ exportedAt: Schema.String,
266
+ data: Schema.JsonValue.annotations({ description: 'The export file data (can be saved or passed to import)' }),
267
+ }),
268
+ }).annotate(Tool.Readonly, true),
269
+
270
+ Tool.make('livestore_sync_import', {
271
+ description: `Import events from export data to a sync backend.
272
+
273
+ This tool connects directly to the sync backend and pushes events. The sync backend must be empty.
274
+
275
+ Example parameters:
276
+ {
277
+ "storePath": "livestore-cli.config.ts",
278
+ "storeId": "my-store",
279
+ "data": { "version": 1, "storeId": "my-store", "events": [...] }
280
+ }
281
+
282
+ With options:
283
+ {
284
+ "storePath": "livestore-cli.config.ts",
285
+ "storeId": "my-store",
286
+ "data": { ... },
287
+ "force": true, // Import even if store ID doesn't match
288
+ "dryRun": true // Validate without importing
289
+ }
290
+
291
+ Returns on success:
292
+ {
293
+ "storeId": "my-store",
294
+ "eventCount": 127,
295
+ "dryRun": false
296
+ }`,
297
+ parameters: {
298
+ storePath: Schema.String.annotations({
299
+ description: 'Path to a module that exports schema and syncBackend',
300
+ }),
301
+ storeId: Schema.String.annotations({ description: 'Store identifier' }),
302
+ clientId: Schema.optional(Schema.String.annotations({ description: 'Client identifier (default: mcp-import)' })),
303
+ data: Schema.JsonValue.annotations({
304
+ description: 'The export data to import (from livestore_sync_export or a file)',
305
+ }),
306
+ force: Schema.optional(
307
+ Schema.Boolean.annotations({ description: 'Force import even if store ID does not match' }),
308
+ ),
309
+ dryRun: Schema.optional(Schema.Boolean.annotations({ description: 'Validate without actually importing' })),
310
+ },
311
+ success: Schema.Struct({
312
+ storeId: Schema.String,
313
+ eventCount: Schema.Number,
314
+ dryRun: Schema.Boolean,
315
+ }),
316
+ }).annotate(Tool.Destructive, true),
229
317
  )