@livestore/cli 0.4.0-dev.8 → 0.4.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 (77) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/__tests__/fixtures/mock-config.d.ts +65 -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.map +1 -1
  11. package/dist/cli.d.ts +16 -2
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +3 -4
  14. package/dist/cli.js.map +1 -1
  15. package/dist/commands/import-export.d.ts +34 -0
  16. package/dist/commands/import-export.d.ts.map +1 -0
  17. package/dist/commands/import-export.js +135 -0
  18. package/dist/commands/import-export.js.map +1 -0
  19. package/dist/commands/mcp-coach.d.ts +13 -5
  20. package/dist/commands/mcp-coach.d.ts.map +1 -1
  21. package/dist/commands/mcp-coach.js +41 -45
  22. package/dist/commands/mcp-coach.js.map +1 -1
  23. package/dist/commands/mcp-tool-handlers.d.ts +6 -0
  24. package/dist/commands/mcp-tool-handlers.d.ts.map +1 -0
  25. package/dist/commands/{mcp-tools.js → mcp-tool-handlers.js} +79 -45
  26. package/dist/commands/mcp-tool-handlers.js.map +1 -0
  27. package/dist/commands/mcp-tools-defs.d.ts +133 -0
  28. package/dist/commands/mcp-tools-defs.d.ts.map +1 -0
  29. package/dist/commands/mcp-tools-defs.js +278 -0
  30. package/dist/commands/mcp-tools-defs.js.map +1 -0
  31. package/dist/commands/mcp.d.ts.map +1 -1
  32. package/dist/commands/mcp.js +2 -1
  33. package/dist/commands/mcp.js.map +1 -1
  34. package/dist/commands/new-project.d.ts +9 -2
  35. package/dist/commands/new-project.d.ts.map +1 -1
  36. package/dist/commands/new-project.js +72 -32
  37. package/dist/commands/new-project.js.map +1 -1
  38. package/dist/mcp-runtime/runtime.d.ts +48 -0
  39. package/dist/mcp-runtime/runtime.d.ts.map +1 -0
  40. package/dist/mcp-runtime/runtime.js +107 -0
  41. package/dist/mcp-runtime/runtime.js.map +1 -0
  42. package/dist/module-loader.d.ts +22 -0
  43. package/dist/module-loader.d.ts.map +1 -0
  44. package/dist/module-loader.js +75 -0
  45. package/dist/module-loader.js.map +1 -0
  46. package/dist/package-manager.d.ts +38 -0
  47. package/dist/package-manager.d.ts.map +1 -0
  48. package/dist/package-manager.js +33 -0
  49. package/dist/package-manager.js.map +1 -0
  50. package/dist/package-manager.test.d.ts +2 -0
  51. package/dist/package-manager.test.d.ts.map +1 -0
  52. package/dist/package-manager.test.js +57 -0
  53. package/dist/package-manager.test.js.map +1 -0
  54. package/dist/sync-operations.d.ts +121 -0
  55. package/dist/sync-operations.d.ts.map +1 -0
  56. package/dist/sync-operations.js +180 -0
  57. package/dist/sync-operations.js.map +1 -0
  58. package/package.json +71 -15
  59. package/src/__tests__/fixtures/mock-config.ts +112 -0
  60. package/src/__tests__/sync-operations.test.ts +232 -0
  61. package/src/bin.ts +1 -0
  62. package/src/cli.ts +4 -5
  63. package/src/commands/import-export.ts +283 -0
  64. package/src/commands/mcp-coach.ts +67 -67
  65. package/src/commands/{mcp-tools.ts → mcp-tool-handlers.ts} +93 -57
  66. package/src/commands/mcp-tools-defs.ts +318 -0
  67. package/src/commands/mcp.ts +3 -2
  68. package/src/commands/new-project.ts +90 -36
  69. package/src/mcp-runtime/runtime.ts +147 -0
  70. package/src/module-loader.ts +94 -0
  71. package/src/package-manager.test.ts +66 -0
  72. package/src/package-manager.ts +44 -0
  73. package/src/sync-operations.ts +360 -0
  74. package/dist/commands/mcp-tools.d.ts +0 -58
  75. package/dist/commands/mcp-tools.d.ts.map +0 -1
  76. package/dist/commands/mcp-tools.js.map +0 -1
  77. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,283 @@
1
+ import path from 'node:path'
2
+
3
+ import type { UnknownError } from '@livestore/common'
4
+ import { Console, Effect, FileSystem, type HttpClient, Schema, type Scope } from '@livestore/utils/effect'
5
+ import { Cli } from '@livestore/utils/node'
6
+
7
+ import * as SyncOps from '../sync-operations.ts'
8
+
9
+ const jsonStringifyPretty = Schema.encodeSync(Schema.parseJson({ space: 2 }))
10
+ const jsonParse = Schema.decodeUnknownSync(Schema.parseJson())
11
+
12
+ const LARGE_EVENT_WARNING_THRESHOLD = 100_000
13
+
14
+ /**
15
+ * Export events from the sync backend to a JSON file.
16
+ */
17
+ const exportEvents = ({
18
+ configPath,
19
+ storeId,
20
+ clientId,
21
+ outputPath,
22
+ }: {
23
+ configPath: string
24
+ storeId: string
25
+ clientId: string
26
+ outputPath: string
27
+ }): Effect.Effect<
28
+ void,
29
+ SyncOps.ExportError | SyncOps.ConnectionError | UnknownError,
30
+ FileSystem.FileSystem | HttpClient.HttpClient | Scope.Scope
31
+ > =>
32
+ Effect.gen(function* () {
33
+ yield* Console.log(`Connecting to sync backend...`)
34
+
35
+ const result = yield* SyncOps.pullEventsFromSyncBackend({ configPath, storeId, clientId })
36
+
37
+ yield* Console.log(`✓ Connected to sync backend: ${result.backendName}`)
38
+ yield* Console.log(`Pulled ${result.eventCount} events`)
39
+ if (result.eventCount > LARGE_EVENT_WARNING_THRESHOLD) {
40
+ yield* Console.log(
41
+ `Warning: exporting ${result.eventCount} events may consume significant memory. Consider exporting on a machine with enough RAM.`,
42
+ )
43
+ }
44
+
45
+ const fs = yield* FileSystem.FileSystem
46
+ const absOutputPath = path.isAbsolute(outputPath) === true ? outputPath : path.resolve(process.cwd(), outputPath)
47
+
48
+ yield* fs.writeFileString(absOutputPath, jsonStringifyPretty(result.data)).pipe(
49
+ Effect.mapError(
50
+ (cause) =>
51
+ new SyncOps.ExportError({
52
+ cause,
53
+ note: `Failed to write export file: ${String(cause)}`,
54
+ }),
55
+ ),
56
+ )
57
+
58
+ yield* Console.log(`Exported ${result.eventCount} events to ${absOutputPath}`)
59
+ }).pipe(Effect.withSpan('cli:export'))
60
+
61
+ /**
62
+ * Import events from a JSON file to the sync backend.
63
+ */
64
+ const importEvents = ({
65
+ configPath,
66
+ storeId,
67
+ clientId,
68
+ inputPath,
69
+ force,
70
+ dryRun,
71
+ }: {
72
+ configPath: string
73
+ storeId: string
74
+ clientId: string
75
+ inputPath: string
76
+ force: boolean
77
+ dryRun: boolean
78
+ }): Effect.Effect<
79
+ void,
80
+ SyncOps.ImportError | SyncOps.ConnectionError | UnknownError,
81
+ FileSystem.FileSystem | HttpClient.HttpClient | Scope.Scope
82
+ > =>
83
+ Effect.gen(function* () {
84
+ const fs = yield* FileSystem.FileSystem
85
+ const absInputPath = path.isAbsolute(inputPath) === true ? inputPath : path.resolve(process.cwd(), inputPath)
86
+
87
+ const exists = yield* fs.exists(absInputPath).pipe(
88
+ Effect.mapError(
89
+ (cause) =>
90
+ new SyncOps.ImportError({
91
+ cause,
92
+ note: `Failed to check file existence: ${String(cause)}`,
93
+ }),
94
+ ),
95
+ )
96
+ if (exists === false) {
97
+ return yield* new SyncOps.ImportError({
98
+ cause: new Error(`File not found: ${absInputPath}`),
99
+ note: `Import file does not exist at ${absInputPath}`,
100
+ })
101
+ }
102
+
103
+ yield* Console.log(`Reading import file...`)
104
+
105
+ const fileContent = yield* fs.readFileString(absInputPath).pipe(
106
+ Effect.mapError(
107
+ (cause) =>
108
+ new SyncOps.ImportError({
109
+ cause,
110
+ note: `Failed to read import file: ${String(cause)}`,
111
+ }),
112
+ ),
113
+ )
114
+
115
+ const parsedContent = yield* Effect.try({
116
+ try: () => jsonParse(fileContent),
117
+ catch: (error) =>
118
+ new SyncOps.ImportError({
119
+ cause: new Error(`Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`),
120
+ note: `Invalid JSON in import file: ${error instanceof Error ? error.message : String(error)}`,
121
+ }),
122
+ })
123
+
124
+ /** Validate export file format before proceeding */
125
+ const validation = yield* SyncOps.validateExportData({ data: parsedContent, targetStoreId: storeId })
126
+
127
+ if (validation.storeIdMismatch === true) {
128
+ if (force !== true) {
129
+ return yield* new SyncOps.ImportError({
130
+ cause: new Error(`Store ID mismatch: file has '${validation.sourceStoreId}', expected '${storeId}'`),
131
+ note: `The export file was created for a different store. Use --force to import anyway.`,
132
+ })
133
+ }
134
+ yield* Console.log(
135
+ `Store ID mismatch: file has '${validation.sourceStoreId}', importing to '${storeId}' (--force)`,
136
+ )
137
+ }
138
+
139
+ yield* Console.log(`Found ${validation.eventCount} events in export file`)
140
+ if (validation.eventCount > LARGE_EVENT_WARNING_THRESHOLD) {
141
+ yield* Console.log(
142
+ `Warning: importing ${validation.eventCount} events may consume significant memory. Ensure the machine has enough RAM.`,
143
+ )
144
+ }
145
+
146
+ if (dryRun === true) {
147
+ yield* Console.log(`Dry run - validating import file...`)
148
+ yield* Console.log(`Dry run complete. ${validation.eventCount} events would be imported.`)
149
+ return
150
+ }
151
+
152
+ yield* Console.log(`Checking for existing events...`)
153
+
154
+ yield* Console.log(`Connecting to sync backend...`)
155
+ yield* Console.log(`Pushing events to sync backend...`)
156
+
157
+ const result = yield* SyncOps.pushEventsToSyncBackend({
158
+ configPath,
159
+ storeId,
160
+ clientId,
161
+ data: parsedContent,
162
+ force,
163
+ dryRun: false,
164
+ onProgress: (pushed, total) => Console.log(` Pushed ${pushed}/${total} events`),
165
+ })
166
+
167
+ yield* Console.log(`✓ Connected to sync backend: ${result.backendName ?? 'unknown'}`)
168
+ yield* Console.log(`Successfully imported ${result.eventCount} events`)
169
+ }).pipe(Effect.withSpan('cli:import'))
170
+
171
+ export const exportCommand = Cli.Command.make(
172
+ 'export',
173
+ {
174
+ config: Cli.Options.text('config').pipe(
175
+ Cli.Options.withAlias('c'),
176
+ Cli.Options.withDescription('Path to the config module that exports schema and syncBackend'),
177
+ ),
178
+ storeId: Cli.Options.text('store-id').pipe(
179
+ Cli.Options.withAlias('i'),
180
+ Cli.Options.withDescription('Store identifier'),
181
+ ),
182
+ clientId: Cli.Options.text('client-id').pipe(
183
+ Cli.Options.withDefault('cli-export'),
184
+ Cli.Options.withDescription('Client identifier for the sync connection'),
185
+ ),
186
+ output: Cli.Args.text({ name: 'file' }).pipe(Cli.Args.withDescription('Output JSON file path')),
187
+ },
188
+ Effect.fn(function* ({
189
+ config,
190
+ storeId,
191
+ clientId,
192
+ output,
193
+ }: {
194
+ config: string
195
+ storeId: string
196
+ clientId: string
197
+ output: string
198
+ }) {
199
+ yield* Console.log(`Exporting events from LiveStore...`)
200
+ yield* Console.log(` Config: ${config}`)
201
+ yield* Console.log(` Store ID: ${storeId}`)
202
+ yield* Console.log(` Output: ${output}`)
203
+ yield* Console.log('')
204
+
205
+ yield* exportEvents({
206
+ configPath: config,
207
+ storeId,
208
+ clientId,
209
+ outputPath: output,
210
+ }).pipe(Effect.scoped)
211
+ }),
212
+ ).pipe(
213
+ Cli.Command.withDescription(
214
+ 'Export all events from the sync backend to a JSON file. Useful for backup and migration.',
215
+ ),
216
+ )
217
+
218
+ export const importCommand = Cli.Command.make(
219
+ 'import',
220
+ {
221
+ config: Cli.Options.text('config').pipe(
222
+ Cli.Options.withAlias('c'),
223
+ Cli.Options.withDescription('Path to the config module that exports schema and syncBackend'),
224
+ ),
225
+ storeId: Cli.Options.text('store-id').pipe(
226
+ Cli.Options.withAlias('i'),
227
+ Cli.Options.withDescription('Store identifier'),
228
+ ),
229
+ clientId: Cli.Options.text('client-id').pipe(
230
+ Cli.Options.withDefault('cli-import'),
231
+ Cli.Options.withDescription('Client identifier for the sync connection'),
232
+ ),
233
+ force: Cli.Options.boolean('force').pipe(
234
+ Cli.Options.withAlias('f'),
235
+ Cli.Options.withDefault(false),
236
+ Cli.Options.withDescription('Force import even if store ID does not match'),
237
+ ),
238
+ dryRun: Cli.Options.boolean('dry-run').pipe(
239
+ Cli.Options.withDefault(false),
240
+ Cli.Options.withDescription('Validate the import file without actually importing'),
241
+ ),
242
+ input: Cli.Args.text({ name: 'file' }).pipe(Cli.Args.withDescription('Input JSON file to import')),
243
+ },
244
+ Effect.fn(function* ({
245
+ config,
246
+ storeId,
247
+ clientId,
248
+ force,
249
+ dryRun,
250
+ input,
251
+ }: {
252
+ config: string
253
+ storeId: string
254
+ clientId: string
255
+ force: boolean
256
+ dryRun: boolean
257
+ input: string
258
+ }) {
259
+ yield* Console.log(`Importing events to LiveStore...`)
260
+ yield* Console.log(` Config: ${config}`)
261
+ yield* Console.log(` Store ID: ${storeId}`)
262
+ yield* Console.log(` Input: ${input}`)
263
+ if (force === true) yield* Console.log(` Force: enabled`)
264
+ if (dryRun === true) yield* Console.log(` Dry run: enabled`)
265
+ yield* Console.log('')
266
+
267
+ yield* importEvents({
268
+ configPath: config,
269
+ storeId,
270
+ clientId,
271
+ inputPath: input,
272
+ force,
273
+ dryRun,
274
+ }).pipe(Effect.scoped)
275
+ }),
276
+ ).pipe(
277
+ Cli.Command.withDescription('Import events from a JSON file to the sync backend. The sync backend must be empty.'),
278
+ )
279
+
280
+ export const syncCommand = Cli.Command.make('sync').pipe(
281
+ Cli.Command.withSubcommands([exportCommand, importCommand]),
282
+ Cli.Command.withDescription('Import and export events from the sync backend'),
283
+ )
@@ -1,15 +1,7 @@
1
+ import { LanguageModel } from '@effect/ai'
1
2
  import { OpenAiClient, OpenAiLanguageModel } from '@effect/ai-openai'
2
- import {
3
- AiError,
4
- Config,
5
- Effect,
6
- FetchHttpClient,
7
- LanguageModel,
8
- Layer,
9
- Prompt,
10
- Schema,
11
- Tool,
12
- } from '@livestore/utils/effect'
3
+
4
+ import { AiError, Config, Effect, FetchHttpClient, Layer, Prompt, Schema, Tool } from '@livestore/utils/effect'
13
5
 
14
6
  // Define the coach tool that analyzes LiveStore usage
15
7
  export const coachTool = Tool.make('livestore_coach', {
@@ -45,24 +37,33 @@ export const coachTool = Tool.make('livestore_coach', {
45
37
  failure: AiError.AiError,
46
38
  })
47
39
 
48
- const OpenAiClientLayer = OpenAiClient.layerConfig({
49
- apiKey: Config.redacted('OPENAI_API_KEY'),
50
- })
40
+ /** Composed layer: HttpClient → OpenAiClient → LanguageModel */
41
+ const LanguageModelLayer = OpenAiLanguageModel.layer({ model: 'gpt-4o-mini' }).pipe(
42
+ Layer.provide(OpenAiClient.layerConfig({ apiKey: Config.redacted('OPENAI_API_KEY') })),
43
+ Layer.provide(FetchHttpClient.layer),
44
+ )
51
45
 
52
- const OpenAiModel = OpenAiLanguageModel.layer({
53
- model: 'gpt-5-nano',
54
- })
46
+ type CoachToolHandlerInput = {
47
+ readonly code: string
48
+ readonly codeType?: string | undefined
49
+ }
55
50
 
56
- // Coach tool handler that uses OpenAI for analysis
57
- export const coachToolHandler = Effect.fnUntraced(
58
- function* ({ code, codeType }) {
59
- // Build the analysis prompt
60
- const codeTypeContext = codeType ? `This is ${codeType} code using LiveStore. ` : 'This is LiveStore code. '
51
+ type CoachToolResult = {
52
+ readonly feedback: string
53
+ readonly score: number | undefined
54
+ readonly suggestions: string[]
55
+ }
61
56
 
62
- const prompt = Prompt.makeMessage('user', {
63
- content: [
64
- Prompt.makePart('text', {
65
- text: `${codeTypeContext}Please review the following code and provide helpful feedback focusing on:
57
+ /** Coach tool handler that uses OpenAI for analysis */
58
+ export const coachToolHandler: (input: CoachToolHandlerInput) => Effect.Effect<CoachToolResult, AiError.AiError> =
59
+ Effect.fn('mcp-coach-handler')(({ code, codeType }) => {
60
+ const effect = Effect.gen(function* () {
61
+ const codeTypeContext = codeType !== undefined ? `This is ${codeType} code using LiveStore. ` : 'This is LiveStore code. '
62
+
63
+ const prompt = Prompt.makeMessage('user', {
64
+ content: [
65
+ Prompt.makePart('text', {
66
+ text: `${codeTypeContext}Please review the following code and provide helpful feedback focusing on:
66
67
 
67
68
  1. LiveStore best practices and conventions
68
69
  2. Schema design and relationships (if applicable)
@@ -84,47 +85,46 @@ Please provide:
84
85
  4. Any potential issues or concerns
85
86
 
86
87
  Format your response as constructive feedback that helps developers improve their LiveStore usage.`,
87
- }),
88
- ],
89
- })
88
+ }),
89
+ ],
90
+ })
91
+
92
+ const systemPrompt = Prompt.makeMessage('system', {
93
+ content: `You are an expert LiveStore developer and code reviewer. Provide constructive, specific, and actionable feedback on LiveStore code. Focus on best practices, performance, and maintainability.`,
94
+ })
90
95
 
91
- const systemPrompt = Prompt.makeMessage('system', {
92
- content: `You are an expert LiveStore developer and code reviewer. Provide constructive, specific, and actionable feedback on LiveStore code. Focus on best practices, performance, and maintainability.`,
96
+ const llm = yield* LanguageModel.LanguageModel
97
+ const completion = yield* llm.generateText({ prompt: Prompt.fromMessages([systemPrompt, prompt]) })
98
+
99
+ const feedback = completion.text ?? 'Unable to generate feedback'
100
+
101
+ const suggestions = feedback
102
+ .split('\n')
103
+ .filter(
104
+ (line: string) =>
105
+ line.trim().startsWith('-') ||
106
+ line.trim().startsWith('*') ||
107
+ line.trim().startsWith('•') ||
108
+ line.toLowerCase().includes('recommend') ||
109
+ line.toLowerCase().includes('consider') ||
110
+ line.toLowerCase().includes('improve'),
111
+ )
112
+ .map((line: string) => line.replace(/^[-*•]\s*/, '').trim())
113
+ .filter((suggestion: string) => suggestion.length > 10)
114
+ .slice(0, 5)
115
+
116
+ const scoreMatch = feedback.match(/(?:score|rating|quality).*?(\d+(?:\.\d+)?)/i)
117
+ const score = scoreMatch !== null ? Number.parseFloat(scoreMatch[1] ?? '0') : undefined
118
+
119
+ return {
120
+ feedback,
121
+ score: score !== undefined && score >= 1 && score <= 10 ? score : undefined,
122
+ suggestions,
123
+ }
93
124
  })
94
125
 
95
- // Get OpenAI client and call the API
96
- const llm = yield* LanguageModel.LanguageModel
97
- const completion = yield* llm.generateText({ prompt: Prompt.fromMessages([systemPrompt, prompt]) })
98
-
99
- const feedback = completion.text ?? 'Unable to generate feedback'
100
-
101
- // Extract suggestions from the feedback (simple approach)
102
- const suggestions: string[] = feedback
103
- .split('\n')
104
- .filter(
105
- (line: string) =>
106
- line.trim().startsWith('-') ||
107
- line.trim().startsWith('*') ||
108
- line.trim().startsWith('•') ||
109
- line.toLowerCase().includes('recommend') ||
110
- line.toLowerCase().includes('consider') ||
111
- line.toLowerCase().includes('improve'),
112
- )
113
- .map((line: string) => line.replace(/^[-*•]\s*/, '').trim())
114
- .filter((suggestion: string) => suggestion.length > 10)
115
- .slice(0, 5) // Limit to 5 suggestions
116
-
117
- // Try to extract a score (simple regex approach)
118
- const scoreMatch = feedback.match(/(?:score|rating|quality).*?(\d+(?:\.\d+)?)/i)
119
- const score = scoreMatch ? Number.parseFloat(scoreMatch[1] ?? '0') : undefined
120
-
121
- return {
122
- feedback,
123
- score: score && score >= 1 && score <= 10 ? score : undefined,
124
- suggestions,
125
- }
126
- },
127
- Effect.provide(Layer.provideMerge(OpenAiModel, OpenAiClientLayer)),
128
- Effect.provide(FetchHttpClient.layer),
129
- Effect.catchTag('ConfigError', (e) => Effect.die(e)),
130
- )
126
+ return effect.pipe(
127
+ Effect.provide(LanguageModelLayer),
128
+ Effect.catchTag('ConfigError', (e) => Effect.die(e)),
129
+ )
130
+ })
@@ -1,61 +1,21 @@
1
- import { Effect, Schema, Tool, Toolkit } from '@livestore/utils/effect'
1
+ import { Effect, FetchHttpClient, Layer, type Toolkit } from '@livestore/utils/effect'
2
+ import { PlatformNode } from '@livestore/utils/node'
3
+
2
4
  import { blogSchemaContent } from '../mcp-content/schemas/blog.ts'
3
5
  import { ecommerceSchemaContent } from '../mcp-content/schemas/ecommerce.ts'
4
6
  import { socialSchemaContent } from '../mcp-content/schemas/social.ts'
5
7
  import { todoSchemaContent } from '../mcp-content/schemas/todo.ts'
6
- import { coachTool, coachToolHandler } from './mcp-coach.ts'
7
-
8
- // Create toolkit with tools and handlers following Tim Smart's pattern
9
- export const livestoreToolkit = Toolkit.make(
10
- coachTool,
11
-
12
- Tool.make('livestore_generate_schema', {
13
- description:
14
- 'Generate a LiveStore schema for a specific use case. Choose from predefined types (todo, blog, social, ecommerce) or request a custom schema by providing a description.',
15
- parameters: {
16
- schemaType: Schema.String.annotations({
17
- description: "Schema type: 'todo', 'blog', 'social', 'ecommerce', or 'custom'",
18
- }),
19
- customDescription: Schema.optional(
20
- Schema.String.annotations({
21
- description:
22
- "For custom schemas: describe your data model needs (e.g., 'user management system with roles and permissions')",
23
- }),
24
- ),
25
- },
26
- success: Schema.Struct({
27
- schemaCode: Schema.String.annotations({
28
- description: 'The generated LiveStore schema TypeScript code',
29
- }),
30
- explanation: Schema.String.annotations({
31
- description: 'Brief explanation of the schema structure',
32
- }),
33
- }),
34
- }),
8
+ import * as Runtime from '../mcp-runtime/runtime.ts'
9
+ import * as SyncOps from '../sync-operations.ts'
10
+ import { coachToolHandler } from './mcp-coach.ts'
11
+ import { livestoreToolkit } from './mcp-tools-defs.ts'
12
+
13
+ /** Layer providing FileSystem and HttpClient for sync operations */
14
+ const SyncOpsLayer = Layer.mergeAll(PlatformNode.NodeFileSystem.layer, FetchHttpClient.layer)
35
15
 
36
- Tool.make('livestore_get_example_schema', {
37
- description:
38
- 'Get a complete example LiveStore schema with TypeScript code. Returns ready-to-use schema definitions for common application types.',
39
- parameters: {
40
- type: Schema.String.annotations({
41
- description: "Example type: 'todo', 'blog', 'social', or 'ecommerce'",
42
- }),
43
- },
44
- success: Schema.Struct({
45
- schemaCode: Schema.String.annotations({
46
- description: 'The complete LiveStore schema code',
47
- }),
48
- description: Schema.String.annotations({
49
- description: 'Description of what this schema models',
50
- }),
51
- }),
52
- })
53
- .annotate(Tool.Readonly, true)
54
- .annotate(Tool.Destructive, false),
55
- )
56
-
57
- // Tool handlers using Tim Smart's pattern
58
- export const toolHandlers = livestoreToolkit.of({
16
+ type LivestoreToolHandlers = Toolkit.HandlersFrom<Toolkit.Tools<typeof livestoreToolkit>>
17
+
18
+ export const toolHandlers: LivestoreToolHandlers = livestoreToolkit.of({
59
19
  livestore_coach: coachToolHandler,
60
20
 
61
21
  livestore_generate_schema: Effect.fnUntraced(function* ({ schemaType, customDescription }) {
@@ -80,7 +40,7 @@ export const toolHandlers = livestoreToolkit.of({
80
40
  explanation = 'E-commerce schema with products, categories, orders, and inventory'
81
41
  break
82
42
  case 'custom': {
83
- if (!customDescription) {
43
+ if (customDescription == null) {
84
44
  schemaCode = `// Custom schema requested but no description provided
85
45
  import { Schema } from '@livestore/livestore'
86
46
 
@@ -99,11 +59,11 @@ export const schema = Schema.create({
99
59
  }
100
60
 
101
61
  // Generate a basic custom schema based on description
102
- const tableName = customDescription.toLowerCase().includes('user')
62
+ const tableName = customDescription.toLowerCase().includes('user') === true
103
63
  ? 'users'
104
- : customDescription.toLowerCase().includes('product')
64
+ : customDescription.toLowerCase().includes('product') === true
105
65
  ? 'products'
106
- : customDescription.toLowerCase().includes('post')
66
+ : customDescription.toLowerCase().includes('post') === true
107
67
  ? 'posts'
108
68
  : 'items'
109
69
 
@@ -166,4 +126,80 @@ export const schema = Schema.create({
166
126
 
167
127
  return { schemaCode, description }
168
128
  }),
129
+
130
+ // Connect the single in-process LiveStore instance from user module
131
+ livestore_instance_connect: Effect.fnUntraced(function* ({ configPath, storeId, clientId, sessionId }) {
132
+ const store = yield* Runtime.init({
133
+ configPath,
134
+ storeId,
135
+ ...(clientId !== undefined ? { clientId } : {}),
136
+ ...(sessionId !== undefined ? { sessionId } : {}),
137
+ }).pipe(Effect.orDie)
138
+ const eventNames = Array.from(store.schema.eventsDefsMap.keys())
139
+ const tableNames = Array.from(store.schema.state.sqlite.tables.keys())
140
+
141
+ return {
142
+ storeId: store.storeId,
143
+ clientId: store.clientId,
144
+ sessionId: store.sessionId,
145
+ schemaInfo: {
146
+ tableNames,
147
+ eventNames,
148
+ },
149
+ }
150
+ }),
151
+
152
+ // Execute a raw SQL query against the local client DB
153
+ livestore_instance_query: Effect.fnUntraced(function* ({ sql, bindValues }) {
154
+ return yield* Runtime.query({ sql, bindValues })
155
+ }),
156
+
157
+ // Validate and commit events
158
+ livestore_instance_commit_events: Effect.fnUntraced(function* ({ events }) {
159
+ return yield* Runtime.commit({ events })
160
+ }),
161
+
162
+ // Status
163
+ livestore_instance_status: Effect.fnUntraced(function* () {
164
+ return yield* Runtime.status
165
+ }),
166
+
167
+ // Disconnect
168
+ livestore_instance_disconnect: Effect.fnUntraced(function* () {
169
+ return yield* Runtime.disconnect
170
+ }),
171
+
172
+ // Sync export - pull all events from sync backend
173
+ livestore_sync_export: Effect.fnUntraced(function* ({ configPath, storeId, clientId }) {
174
+ const result = yield* SyncOps.pullEventsFromSyncBackend({
175
+ configPath,
176
+ storeId,
177
+ clientId: clientId ?? 'mcp-export',
178
+ }).pipe(Effect.scoped, Effect.provide(SyncOpsLayer), Effect.orDie)
179
+
180
+ return {
181
+ storeId: result.storeId,
182
+ eventCount: result.eventCount,
183
+ exportedAt: result.exportedAt,
184
+ data: result.data,
185
+ }
186
+ }),
187
+
188
+ // Sync import - push events to sync backend
189
+ livestore_sync_import: Effect.fnUntraced(function* ({ configPath, storeId, clientId, data, force, dryRun }) {
190
+ const result = yield* SyncOps.pushEventsToSyncBackend({
191
+ configPath,
192
+ storeId,
193
+ clientId: clientId ?? 'mcp-import',
194
+ data,
195
+ force: force ?? false,
196
+ dryRun: dryRun ?? false,
197
+ }).pipe(Effect.scoped, Effect.provide(SyncOpsLayer), Effect.orDie)
198
+
199
+ return {
200
+ storeId: result.storeId,
201
+ eventCount: result.eventCount,
202
+ dryRun: result.dryRun,
203
+ }
204
+ }),
169
205
  })