@livestore/cli 0.4.0-dev.9 → 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.
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/fixtures/mock-config.d.ts +65 -0
- package/dist/__tests__/fixtures/mock-config.d.ts.map +1 -0
- package/dist/__tests__/fixtures/mock-config.js +94 -0
- package/dist/__tests__/fixtures/mock-config.js.map +1 -0
- package/dist/__tests__/sync-operations.test.d.ts +2 -0
- package/dist/__tests__/sync-operations.test.d.ts.map +1 -0
- package/dist/__tests__/sync-operations.test.js +167 -0
- package/dist/__tests__/sync-operations.test.js.map +1 -0
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +16 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -4
- package/dist/cli.js.map +1 -1
- package/dist/commands/import-export.d.ts +34 -0
- package/dist/commands/import-export.d.ts.map +1 -0
- package/dist/commands/import-export.js +135 -0
- package/dist/commands/import-export.js.map +1 -0
- package/dist/commands/mcp-coach.d.ts +13 -5
- package/dist/commands/mcp-coach.d.ts.map +1 -1
- package/dist/commands/mcp-coach.js +41 -45
- package/dist/commands/mcp-coach.js.map +1 -1
- package/dist/commands/mcp-tool-handlers.d.ts +6 -0
- package/dist/commands/mcp-tool-handlers.d.ts.map +1 -0
- package/dist/commands/{mcp-tools.js → mcp-tool-handlers.js} +79 -45
- package/dist/commands/mcp-tool-handlers.js.map +1 -0
- package/dist/commands/mcp-tools-defs.d.ts +133 -0
- package/dist/commands/mcp-tools-defs.d.ts.map +1 -0
- package/dist/commands/mcp-tools-defs.js +278 -0
- package/dist/commands/mcp-tools-defs.js.map +1 -0
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +2 -1
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/new-project.d.ts +9 -2
- package/dist/commands/new-project.d.ts.map +1 -1
- package/dist/commands/new-project.js +72 -32
- package/dist/commands/new-project.js.map +1 -1
- package/dist/mcp-runtime/runtime.d.ts +48 -0
- package/dist/mcp-runtime/runtime.d.ts.map +1 -0
- package/dist/mcp-runtime/runtime.js +107 -0
- package/dist/mcp-runtime/runtime.js.map +1 -0
- package/dist/module-loader.d.ts +22 -0
- package/dist/module-loader.d.ts.map +1 -0
- package/dist/module-loader.js +75 -0
- package/dist/module-loader.js.map +1 -0
- package/dist/package-manager.d.ts +38 -0
- package/dist/package-manager.d.ts.map +1 -0
- package/dist/package-manager.js +33 -0
- package/dist/package-manager.js.map +1 -0
- package/dist/package-manager.test.d.ts +2 -0
- package/dist/package-manager.test.d.ts.map +1 -0
- package/dist/package-manager.test.js +57 -0
- package/dist/package-manager.test.js.map +1 -0
- package/dist/sync-operations.d.ts +121 -0
- package/dist/sync-operations.d.ts.map +1 -0
- package/dist/sync-operations.js +180 -0
- package/dist/sync-operations.js.map +1 -0
- package/package.json +71 -15
- package/src/__tests__/fixtures/mock-config.ts +112 -0
- package/src/__tests__/sync-operations.test.ts +232 -0
- package/src/bin.ts +1 -0
- package/src/cli.ts +4 -5
- package/src/commands/import-export.ts +283 -0
- package/src/commands/mcp-coach.ts +67 -67
- package/src/commands/{mcp-tools.ts → mcp-tool-handlers.ts} +93 -57
- package/src/commands/mcp-tools-defs.ts +318 -0
- package/src/commands/mcp.ts +3 -2
- package/src/commands/new-project.ts +90 -36
- package/src/mcp-runtime/runtime.ts +147 -0
- package/src/module-loader.ts +94 -0
- package/src/package-manager.test.ts +66 -0
- package/src/package-manager.ts +44 -0
- package/src/sync-operations.ts +360 -0
- package/dist/commands/mcp-tools.d.ts +0 -58
- package/dist/commands/mcp-tools.d.ts.map +0 -1
- package/dist/commands/mcp-tools.js.map +0 -1
- 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
|
-
|
|
3
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
type CoachToolHandlerInput = {
|
|
47
|
+
readonly code: string
|
|
48
|
+
readonly codeType?: string | undefined
|
|
49
|
+
}
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
type CoachToolResult = {
|
|
52
|
+
readonly feedback: string
|
|
53
|
+
readonly score: number | undefined
|
|
54
|
+
readonly suggestions: string[]
|
|
55
|
+
}
|
|
61
56
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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,
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 (
|
|
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
|
})
|