@membranehq/cli 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/.turbo/turbo-build.log +9 -0
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +54 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +335 -0
  6. package/package.json +46 -0
  7. package/scripts/add-shebang.sh +6 -0
  8. package/scripts/prepare-package-json.ts +29 -0
  9. package/src/agent.tsx +50 -0
  10. package/src/cli.ts +72 -0
  11. package/src/commands/open.command.ts +51 -0
  12. package/src/commands/pull.command.ts +75 -0
  13. package/src/commands/push.command.ts +79 -0
  14. package/src/commands/test.command.ts +99 -0
  15. package/src/components/AddMcpServerScreen.tsx +215 -0
  16. package/src/components/AgentStatus.tsx +15 -0
  17. package/src/components/Main.tsx +64 -0
  18. package/src/components/OverviewSection.tsx +24 -0
  19. package/src/components/PersonalAccessTokenInput.tsx +56 -0
  20. package/src/components/RecentChanges.tsx +65 -0
  21. package/src/components/SelectWorkspace.tsx +112 -0
  22. package/src/components/Setup.tsx +121 -0
  23. package/src/components/WorkspaceStatus.tsx +61 -0
  24. package/src/contexts/FileWatcherContext.tsx +81 -0
  25. package/src/index.ts +27 -0
  26. package/src/legacy/commands/pullWorkspace.ts +70 -0
  27. package/src/legacy/commands/pushWorkspace.ts +246 -0
  28. package/src/legacy/integrationElements.ts +78 -0
  29. package/src/legacy/push/types.ts +17 -0
  30. package/src/legacy/reader/index.ts +113 -0
  31. package/src/legacy/types.ts +17 -0
  32. package/src/legacy/util.ts +149 -0
  33. package/src/legacy/workspace-elements/connectors.ts +397 -0
  34. package/src/legacy/workspace-elements/index.ts +27 -0
  35. package/src/legacy/workspace-tools/commands/pullWorkspace.ts +70 -0
  36. package/src/legacy/workspace-tools/integrationElements.ts +78 -0
  37. package/src/legacy/workspace-tools/util.ts +149 -0
  38. package/src/mcp/server-status.ts +27 -0
  39. package/src/mcp/server.ts +36 -0
  40. package/src/mcp/tools/getTestAccessToken.ts +32 -0
  41. package/src/modules/api/account-api-client.ts +89 -0
  42. package/src/modules/api/index.ts +3 -0
  43. package/src/modules/api/membrane-api-client.ts +116 -0
  44. package/src/modules/api/workspace-api-client.ts +11 -0
  45. package/src/modules/config/cwd-context.tsx +11 -0
  46. package/src/modules/config/project/getAgentVersion.ts +16 -0
  47. package/src/modules/config/project/index.ts +8 -0
  48. package/src/modules/config/project/paths.ts +25 -0
  49. package/src/modules/config/project/readProjectConfig.ts +27 -0
  50. package/src/modules/config/project/useProjectConfig.tsx +103 -0
  51. package/src/modules/config/system/index.ts +35 -0
  52. package/src/modules/file-watcher/index.ts +166 -0
  53. package/src/modules/file-watcher/types.ts +14 -0
  54. package/src/modules/setup/steps.ts +9 -0
  55. package/src/modules/setup/useSetup.ts +16 -0
  56. package/src/modules/status/useStatus.ts +16 -0
  57. package/src/modules/workspace-element-service/constants.ts +121 -0
  58. package/src/modules/workspace-element-service/getTypeAndKeyFromPath.ts +69 -0
  59. package/src/modules/workspace-element-service/index.ts +304 -0
  60. package/src/testing/environment.ts +172 -0
  61. package/src/testing/runners/base.runner.ts +27 -0
  62. package/src/testing/runners/test.runner.ts +123 -0
  63. package/src/testing/scripts/generate-test-report.ts +757 -0
  64. package/src/testing/test-suites/base.ts +92 -0
  65. package/src/testing/test-suites/data-collection.ts +128 -0
  66. package/src/testing/testers/base.ts +115 -0
  67. package/src/testing/testers/create.ts +273 -0
  68. package/src/testing/testers/delete.ts +155 -0
  69. package/src/testing/testers/find-by-id.ts +135 -0
  70. package/src/testing/testers/list.ts +110 -0
  71. package/src/testing/testers/match.ts +149 -0
  72. package/src/testing/testers/search.ts +148 -0
  73. package/src/testing/testers/spec.ts +30 -0
  74. package/src/testing/testers/update.ts +284 -0
  75. package/src/utils/auth.ts +19 -0
  76. package/src/utils/constants.ts +27 -0
  77. package/src/utils/fields.ts +83 -0
  78. package/src/utils/logger.ts +106 -0
  79. package/src/utils/templating.ts +50 -0
  80. package/tsconfig.json +21 -0
@@ -0,0 +1,92 @@
1
+ import { extractIntegrationAppErrorData, setValueAtLocator } from '@integration-app/sdk'
2
+ import chalk from 'chalk'
3
+ import merge from 'lodash/merge.js'
4
+
5
+ import { handleTemplate } from '../../utils/templating'
6
+ import { TestEnvironment } from '../environment'
7
+ import { BaseTester } from '../testers/base'
8
+
9
+ export class BaseTestSuite {
10
+ private result: any
11
+ protected environment: TestEnvironment
12
+
13
+ constructor({ environment }: { environment: TestEnvironment }) {
14
+ this.environment = environment
15
+ this.result = {}
16
+ }
17
+
18
+ async run() {}
19
+
20
+ getResult() {
21
+ return this.result
22
+ }
23
+
24
+ protected async runTest(tester: BaseTester) {
25
+ console.debug(`${chalk.bold.cyan('[start]')} ${chalk.yellow(tester.path)}`)
26
+
27
+ let done = false
28
+ let triedToFix = false
29
+
30
+ let result = {}
31
+ let testCase = {}
32
+ let processedTestCase
33
+
34
+ while (!done) {
35
+ try {
36
+ testCase = await tester.readTestCase()
37
+
38
+ // Create test file if it doesn't exist
39
+ if (!testCase) {
40
+ testCase = await tester.generateConfig()
41
+ await this.environment.writeYaml(tester.getTestCasePath(), testCase)
42
+ console.debug(`${chalk.bold.yellow('[initialized]')} ${chalk.yellow(tester.path)}`)
43
+ }
44
+
45
+ processedTestCase = handleTemplate(testCase, this.environment.state)
46
+
47
+ await tester.run(processedTestCase)
48
+
49
+ console.debug(`${chalk.bold.green('[success]')} ${chalk.yellow(tester.path)}`)
50
+ done = true
51
+ result = tester.getResult()
52
+
53
+ setValueAtLocator(this.result, tester.path, result)
54
+ } catch (e) {
55
+ console.error(`${chalk.bold.red('[error]')} ${chalk.yellow(tester.path)}: ${e}`)
56
+
57
+ if (this.environment.options.fix && !triedToFix) {
58
+ triedToFix = true
59
+
60
+ try {
61
+ await tester.fix(e as Error)
62
+ continue
63
+ } catch (e) {
64
+ console.error(`${chalk.bold.red('[fix fail]')} ${chalk.yellow(tester.path)}: ${e}`)
65
+ }
66
+ }
67
+
68
+ setValueAtLocator(this.result, tester.path, {
69
+ error: extractIntegrationAppErrorData(e),
70
+ })
71
+
72
+ done = true
73
+ result = {
74
+ error: extractIntegrationAppErrorData(e),
75
+ }
76
+ }
77
+ }
78
+
79
+ const updatedTestCase = {
80
+ ...processedTestCase,
81
+ result,
82
+ }
83
+
84
+ await this.environment.writeYaml(tester.getTestCasePath(), updatedTestCase)
85
+ }
86
+
87
+ protected async runTestSuite(suite: BaseTestSuite) {
88
+ await suite.run()
89
+
90
+ merge(this.result, suite.getResult())
91
+ }
92
+ }
@@ -0,0 +1,128 @@
1
+ import { DataCollectionSpec } from '@integration-app/sdk'
2
+
3
+ import { TestEnvironment } from '../environment'
4
+ import { BaseTestSuite } from './base'
5
+ import { Logger } from '../../utils/logger'
6
+ import { DataCollectionCreateTester } from '../testers/create'
7
+ import { DataCollectionDeleteTester } from '../testers/delete'
8
+ import { DataCollectionFindByIdTester } from '../testers/find-by-id'
9
+ import { DataCollectionListTester } from '../testers/list'
10
+ import { DataCollectionMatchTester } from '../testers/match'
11
+ import { DataCollectionSearchTester } from '../testers/search'
12
+ import { DataCollectionSpecTester } from '../testers/spec'
13
+ import { DataCollectionUpdateTester } from '../testers/update'
14
+
15
+ interface TesterConfig {
16
+ testerClass: any
17
+ operationKey?: string
18
+ }
19
+
20
+ const DATA_COLLECTION_TESTERS = {
21
+ spec: { testerClass: DataCollectionSpecTester },
22
+ create: { testerClass: DataCollectionCreateTester, operationKey: 'create' },
23
+ 'find-by-id': { testerClass: DataCollectionFindByIdTester, operationKey: 'findById' },
24
+ list: { testerClass: DataCollectionListTester, operationKey: 'list' },
25
+ match: { testerClass: DataCollectionMatchTester, operationKey: 'match' },
26
+ search: { testerClass: DataCollectionSearchTester, operationKey: 'search' },
27
+ update: { testerClass: DataCollectionUpdateTester, operationKey: 'update' },
28
+ delete: { testerClass: DataCollectionDeleteTester, operationKey: 'delete' },
29
+ } as const
30
+
31
+ type TestMethod = keyof typeof DATA_COLLECTION_TESTERS
32
+
33
+ export class DataCollectionTestSuite extends BaseTestSuite {
34
+ private readonly dataCollectionKey: string
35
+ private readonly testMethod?: string
36
+
37
+ constructor({
38
+ environment,
39
+ dataCollectionKey,
40
+ testMethod,
41
+ }: {
42
+ environment: TestEnvironment
43
+ dataCollectionKey: string
44
+ testMethod?: string
45
+ }) {
46
+ super({ environment })
47
+ this.dataCollectionKey = dataCollectionKey
48
+ this.testMethod = testMethod
49
+ }
50
+
51
+ async run(): Promise<void> {
52
+ const dataCollection = await this.fetchDataCollection()
53
+ const testersToRun = this.testMethod
54
+ ? [this.createTester(this.testMethod as TestMethod, dataCollection)]
55
+ : this.createAllAvailableTesters(dataCollection)
56
+
57
+ Logger.info('🚀 Running tests...')
58
+
59
+ for (const tester of testersToRun) {
60
+ await this.runTest(tester)
61
+ }
62
+
63
+ this.displayTestSummary(testersToRun.length)
64
+ }
65
+
66
+ private async fetchDataCollection() {
67
+ return this.environment.client
68
+ .connection(this.environment.connectionId)
69
+ .dataCollection(this.dataCollectionKey)
70
+ .get()
71
+ }
72
+
73
+ private isOperationAvailable(dataCollection: DataCollectionSpec, operationKey?: string): boolean {
74
+ // If no operation is required (e.g., spec validation), tester is always available
75
+ if (!operationKey) return true
76
+
77
+ return !!dataCollection[operationKey]
78
+ }
79
+
80
+ private createTester(testMethod: TestMethod, dataCollection: any) {
81
+ const config = DATA_COLLECTION_TESTERS[testMethod]
82
+
83
+ if (!config) {
84
+ throw new Error(
85
+ `Unknown test method "${testMethod}". Available: ${this.getAvailableTestMethods(dataCollection).join(', ')}`,
86
+ )
87
+ }
88
+
89
+ const operationKey = 'operationKey' in config ? config.operationKey : undefined
90
+ if (!this.isOperationAvailable(dataCollection, operationKey)) {
91
+ throw new Error(`${operationKey} operation not available for data collection "${this.dataCollectionKey}"`)
92
+ }
93
+
94
+ return this.instantiateTester(config, dataCollection)
95
+ }
96
+
97
+ private createAllAvailableTesters(dataCollection: any) {
98
+ return Object.entries(DATA_COLLECTION_TESTERS)
99
+ .filter(([, config]) => {
100
+ const operationKey = 'operationKey' in config ? config.operationKey : undefined
101
+ return this.isOperationAvailable(dataCollection, operationKey)
102
+ })
103
+ .map(([, config]) => this.instantiateTester(config, dataCollection))
104
+ }
105
+
106
+ private instantiateTester(config: TesterConfig, dataCollection: any) {
107
+ const testerConfig = {
108
+ environment: this.environment,
109
+ dataCollectionKey: this.dataCollectionKey,
110
+ dataCollection,
111
+ }
112
+
113
+ return new config.testerClass(testerConfig)
114
+ }
115
+
116
+ private getAvailableTestMethods(dataCollection: any): string[] {
117
+ return Object.entries(DATA_COLLECTION_TESTERS)
118
+ .filter(([, config]) => {
119
+ const operationKey = 'operationKey' in config ? config.operationKey : undefined
120
+ return this.isOperationAvailable(dataCollection, operationKey)
121
+ })
122
+ .map(([method]) => method)
123
+ }
124
+
125
+ private displayTestSummary(testCount: number): void {
126
+ Logger.info(`📊 ${testCount} test${testCount === 1 ? '' : 's'} executed`)
127
+ }
128
+ }
@@ -0,0 +1,115 @@
1
+ import { extractIntegrationAppErrorData } from '@integration-app/sdk'
2
+ import chalk from 'chalk'
3
+
4
+ import { TestEnvironment } from '../environment'
5
+
6
+ export type TestRunnerOptions = {
7
+ environment: TestEnvironment
8
+ level: number
9
+ }
10
+
11
+ export type TestRunnerFixConfigParams<ConfigClass> = {
12
+ config: ConfigClass
13
+ error: Error
14
+ }
15
+
16
+ export type TestRunAssertion = {
17
+ message: string
18
+ result: boolean
19
+ details?: any
20
+ }
21
+
22
+ export type TestRunnerResult = {
23
+ logs: any[]
24
+ assertions: TestRunAssertion[]
25
+ }
26
+
27
+ export abstract class BaseTester<ConfigClass = any> {
28
+ protected environment: TestEnvironment
29
+ protected level: number
30
+ protected logs: any[]
31
+ protected assertions: TestRunAssertion[]
32
+ protected resultsLocator: string
33
+ public path: string
34
+
35
+ constructor({ environment, path }: { environment: TestEnvironment; path: string }) {
36
+ this.environment = environment
37
+ this.logs = []
38
+ this.assertions = []
39
+ this.path = path
40
+ this.level = 0
41
+ }
42
+
43
+ abstract run(config: ConfigClass): Promise<void>
44
+
45
+ async fix(error: Error) {
46
+ const testCase = await this.readTestCase()
47
+
48
+ if (!testCase) {
49
+ throw new Error(`No config found for test ${this.path}`)
50
+ }
51
+
52
+ const fixedTestCase = await this.fixTestCase({
53
+ config: testCase,
54
+ error,
55
+ })
56
+
57
+ console.debug(chalk.bold.yellow('[auto-fix]'), this.path)
58
+ await this.environment.writeYaml(this.getTestCasePath(), fixedTestCase)
59
+ }
60
+
61
+ async readTestCase(): Promise<ConfigClass | undefined> {
62
+ const configPath = this.getTestCasePath()
63
+ return this.environment.readYaml<ConfigClass>(configPath)
64
+ }
65
+
66
+ async generateConfig(): Promise<ConfigClass> {
67
+ return {} as ConfigClass
68
+ }
69
+
70
+ protected async fixTestCase(_params: TestRunnerFixConfigParams<ConfigClass>): Promise<ConfigClass> {
71
+ throw new Error(`Auto-fix is not implemented for test ${this.path}`)
72
+ }
73
+
74
+ getResult(): TestRunnerResult {
75
+ return {
76
+ logs: this.logs,
77
+ assertions: this.assertions,
78
+ }
79
+ }
80
+
81
+ protected async assert(condition: () => Promise<boolean> | boolean, message: string, details?: any) {
82
+ try {
83
+ const result = await condition()
84
+ if (result) {
85
+ this.logMsg(`✅ ${message}`)
86
+ this.assertions.push({
87
+ message,
88
+ result,
89
+ })
90
+ } else {
91
+ this.logMsg(`❌ ${message}`)
92
+ this.assertions.push({
93
+ message,
94
+ result: false,
95
+ details,
96
+ })
97
+ }
98
+ } catch (e) {
99
+ this.assertions.push({
100
+ message,
101
+ result: false,
102
+ details: extractIntegrationAppErrorData(e),
103
+ })
104
+ this.logMsg(`❌ ${message}: ${(e as Error).message}`)
105
+ }
106
+ }
107
+
108
+ protected logMsg(message: string) {
109
+ console.debug(`${' '.repeat(this.level * 2)}${message}`)
110
+ }
111
+
112
+ public getTestCasePath() {
113
+ return `${this.path}.test.yml`
114
+ }
115
+ }
@@ -0,0 +1,273 @@
1
+ import {
2
+ DataCollectionSpec,
3
+ excludeWriteOnlyFieldsFromSchema,
4
+ getDataCollectionCreateFields,
5
+ valueToSchema,
6
+ extractIntegrationAppErrorData,
7
+ makeDataLocationPath,
8
+ getRequiredFieldsFromSchema,
9
+ getValueAtLocator,
10
+ setValueAtLocator,
11
+ walkSchema,
12
+ } from '@integration-app/sdk'
13
+ import chalk from 'chalk'
14
+
15
+ import { TestEnvironment } from '../environment'
16
+ import { BaseTester } from './base'
17
+ import { getNotMatchingSubFields } from '../../utils/fields'
18
+
19
+ interface DataCollectionCreateTestConfig {
20
+ input: {
21
+ fields: Record<string, any>
22
+ }
23
+ }
24
+
25
+ interface ReferenceCollection {
26
+ key: string
27
+ parameters?: Record<string, any>
28
+ }
29
+
30
+ interface ExampleRecord {
31
+ id: string
32
+ fields: Record<string, any>
33
+ }
34
+
35
+ export class DataCollectionCreateTester extends BaseTester<DataCollectionCreateTestConfig> {
36
+ private dataCollectionKey: string
37
+ private dataCollection: DataCollectionSpec
38
+
39
+ constructor({
40
+ environment,
41
+ dataCollectionKey,
42
+ dataCollection,
43
+ }: {
44
+ environment: TestEnvironment
45
+ dataCollectionKey: string
46
+ dataCollection: DataCollectionSpec
47
+ }) {
48
+ super({
49
+ environment,
50
+ path: `data/${dataCollectionKey}/create`,
51
+ })
52
+
53
+ this.dataCollectionKey = dataCollectionKey
54
+ this.dataCollection = dataCollection
55
+ }
56
+
57
+ async run(config: DataCollectionCreateTestConfig): Promise<void> {
58
+ const createResponse = await this.environment.client
59
+ .connection(this.environment.connectionId)
60
+ .dataCollection(this.dataCollectionKey)
61
+ .create(config.input)
62
+
63
+ await this.assert(() => !!createResponse.id, 'Returned ID of a created record')
64
+
65
+ if (createResponse.id) {
66
+ this.environment.state[this.dataCollectionKey] = {
67
+ createdRecordId: createResponse.id,
68
+ }
69
+ }
70
+
71
+ const createFieldsSchema = getDataCollectionCreateFields(this.dataCollection)
72
+
73
+ if (this.dataCollection.findById) {
74
+ const findByIdResponse = await this.environment.client
75
+ .connection(this.environment.connectionId)
76
+ .dataCollection(this.dataCollectionKey)
77
+ .findById({
78
+ id: createResponse.id,
79
+ })
80
+
81
+ await this.assert(() => !!findByIdResponse.record, 'Record is returned from findById')
82
+
83
+ if (findByIdResponse.record) {
84
+ this.environment.state[this.dataCollectionKey] = {
85
+ createdRecordId: createResponse.id,
86
+ createdRecord: findByIdResponse.record.fields,
87
+ }
88
+ }
89
+
90
+ const schemaToCompare = excludeWriteOnlyFieldsFromSchema(createFieldsSchema)
91
+ const sentFieldsToCheck = valueToSchema(config.input.fields, schemaToCompare, {
92
+ skipUnknownFields: true,
93
+ })
94
+ const receivedFieldsToCheck = valueToSchema(findByIdResponse.record.fields, schemaToCompare)
95
+
96
+ const diff = getNotMatchingSubFields(receivedFieldsToCheck, sentFieldsToCheck)
97
+
98
+ await this.assert(() => !diff, 'Returned fields match created fields', {
99
+ difference: diff,
100
+ sentFields: sentFieldsToCheck,
101
+ receivedFields: receivedFieldsToCheck,
102
+ })
103
+ }
104
+
105
+ const requiredFields = getRequiredFieldsFromSchema(createFieldsSchema)
106
+
107
+ if (requiredFields.length > 0) {
108
+ const requiredPayload: Record<string, any> = {}
109
+
110
+ requiredFields.forEach((path) => {
111
+ const value = getValueAtLocator(config.input.fields, path)
112
+ if (typeof value !== 'undefined') {
113
+ setValueAtLocator(requiredPayload, path, value)
114
+ }
115
+ })
116
+
117
+ const result = await this.environment.client
118
+ .connection(this.environment.connectionId)
119
+ .dataCollection(this.dataCollectionKey)
120
+ .create({ fields: requiredPayload })
121
+
122
+ await this.assert(() => !!result.id, 'Record can be created with schema-defined required fields only')
123
+ }
124
+ }
125
+
126
+ async generateConfig(): Promise<DataCollectionCreateTestConfig> {
127
+ const createFieldsSchema = getDataCollectionCreateFields(this.dataCollection)
128
+
129
+ if (!createFieldsSchema?.properties) {
130
+ throw new Error('No fields schema found for data collection')
131
+ }
132
+
133
+ const fields = await this.generateFieldsWithLLM(createFieldsSchema)
134
+
135
+ return {
136
+ input: {
137
+ fields,
138
+ },
139
+ }
140
+ }
141
+
142
+ protected async fixTestCase({
143
+ config,
144
+ error,
145
+ }: {
146
+ config: DataCollectionCreateTestConfig
147
+ error: any
148
+ }): Promise<DataCollectionCreateTestConfig> {
149
+ const errorData = extractIntegrationAppErrorData(error)
150
+
151
+ const createFieldsSchema = getDataCollectionCreateFields(this.dataCollection)
152
+
153
+ const examples = await this.getExampleRecordsForSchema(createFieldsSchema)
154
+
155
+ const prompt = `I'm trying to create a data record in a data collection with the following fields schema:
156
+
157
+ ${JSON.stringify(createFieldsSchema, null, 2)}
158
+
159
+ I tried to create a record with these fields:
160
+
161
+ ${JSON.stringify(config.input?.fields, null, 2)}
162
+
163
+ But got this error:
164
+
165
+ ${JSON.stringify(errorData)}
166
+
167
+ ${this.getExampleRecordsPrompt(examples)}
168
+
169
+ Please analyze the error and provide:
170
+ 1. A fixed fields object that would work (as a JSON object)
171
+ 2. A short explanation of what was wrong with the original fields
172
+
173
+ Format your response as a JSON object with two fields:
174
+ {
175
+ "explanation": "short explanation of what was wrong and how you fixed it",
176
+ "fields": { ... fixed fields ... }
177
+ }.
178
+
179
+ Only return the JSON object, no other text or wrapping (like \`\`\`json or \`\`\`).`
180
+
181
+ const response = await this.environment.llm.complete({
182
+ prompt,
183
+ maxTokens: 10000,
184
+ })
185
+
186
+ const result = JSON.parse(response.trim())
187
+ console.warn(chalk.bold.yellow('[auto-fix]'), `${this.path}:`, result.explanation)
188
+
189
+ return {
190
+ input: {
191
+ fields: result.fields,
192
+ },
193
+ }
194
+ }
195
+
196
+ private async generateFieldsWithLLM(schema: any): Promise<Record<string, any>> {
197
+ const examples = await this.getExampleRecordsForSchema(schema)
198
+
199
+ const prompt = `Generate a valid JSON object that matches this JSONSchema. Return only the JSON object, no other text.
200
+
201
+ ${this.getExampleRecordsPrompt(examples)}
202
+
203
+ JSONSchema:
204
+ ${JSON.stringify(schema, null, 2)}.
205
+
206
+ Do not populate fields with null or empty strings, unless the schema explicitly requires it.
207
+
208
+ Only return the JSON object, no other text or wrapping (like \`\`\`json or \`\`\`).`
209
+
210
+ const response = await this.environment.llm.complete({
211
+ prompt,
212
+ maxTokens: 10000,
213
+ })
214
+
215
+ const fields = JSON.parse(response.trim())
216
+ return fields
217
+ }
218
+
219
+ private getExampleRecordsPrompt(examples: Record<string, ExampleRecord[]>): string {
220
+ const examplesText = Object.entries(examples)
221
+ .map(([key, records]) => {
222
+ return `Example records from collection "${key}":\n${JSON.stringify(records, null, 2)}`
223
+ })
224
+ .join('\n\n')
225
+
226
+ return `When a field has a "referenceCollection" property, its value should be an ID of a record from that collection. Here are some example records you can use as references:
227
+
228
+ ${examplesText}.
229
+
230
+ If you don't have an example for a given collection, leave the field empty instead of coming up with a fake record id.`
231
+ }
232
+
233
+ private async findReferenceCollections(schema: any): Promise<ReferenceCollection[]> {
234
+ const references = new Set<string>()
235
+ const result: ReferenceCollection[] = []
236
+
237
+ walkSchema(schema, (obj) => {
238
+ if (obj.referenceCollection) {
239
+ const key = obj.referenceCollection.key
240
+ const parameters = obj.referenceCollection.parameters
241
+ const refKey = `${key}:${JSON.stringify(parameters || {})}`
242
+
243
+ if (!references.has(refKey)) {
244
+ references.add(refKey)
245
+ result.push({ key, parameters })
246
+ }
247
+ }
248
+ return obj
249
+ })
250
+
251
+ return result
252
+ }
253
+
254
+ private async fetchExampleRecords(collection: ReferenceCollection): Promise<ExampleRecord[]> {
255
+ const response = await this.environment.client
256
+ .connection(this.environment.connectionId)
257
+ .dataCollection(collection.key)
258
+ .list({
259
+ parameters: collection.parameters,
260
+ })
261
+ return response.records.map((r) => ({ id: r.id, fields: r.fields }))
262
+ }
263
+
264
+ private async getExampleRecordsForSchema(schema: any): Promise<Record<string, ExampleRecord[]>> {
265
+ const collections = await this.findReferenceCollections(schema)
266
+ const result: Record<string, ExampleRecord[]> = {}
267
+ for (const collection of collections) {
268
+ const locationPath = makeDataLocationPath(collection)
269
+ result[locationPath] = await this.fetchExampleRecords(collection)
270
+ }
271
+ return result
272
+ }
273
+ }