@inkeep/create-agents 0.8.0 → 0.8.2

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,283 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { cloneTemplate, replaceContentInFiles, replaceObjectProperties, } from '../templates';
5
+ // Mock dependencies
6
+ vi.mock('fs-extra');
7
+ vi.mock('degit', () => ({
8
+ default: vi.fn(() => ({
9
+ clone: vi.fn().mockResolvedValue(undefined),
10
+ })),
11
+ }));
12
+ describe('Template Content Replacement', () => {
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ // Setup default fs-extra mocks
16
+ vi.mocked(fs.pathExists).mockResolvedValue(true);
17
+ vi.mocked(fs.readFile).mockResolvedValue('');
18
+ vi.mocked(fs.writeFile).mockResolvedValue(undefined);
19
+ vi.mocked(fs.mkdir).mockResolvedValue(undefined);
20
+ });
21
+ describe('replaceObjectProperties', () => {
22
+ it('should replace a simple object property', async () => {
23
+ const content = `export const myProject = project({
24
+ id: 'test-project',
25
+ models: {
26
+ base: {
27
+ model: 'gpt-4o-mini',
28
+ },
29
+ },
30
+ });`;
31
+ const replacement = {
32
+ base: {
33
+ model: 'anthropic/claude-3-5-haiku-20241022',
34
+ },
35
+ summarizer: {
36
+ model: 'anthropic/claude-3-5-haiku-20241022',
37
+ },
38
+ };
39
+ const result = await replaceObjectProperties(content, { models: replacement });
40
+ expect(result).toContain("'model': 'anthropic/claude-3-5-haiku-20241022'");
41
+ expect(result).toContain("'summarizer'");
42
+ expect(result).not.toContain('gpt-4o-mini');
43
+ });
44
+ it('should handle nested objects correctly', async () => {
45
+ const content = `export const config = {
46
+ models: {
47
+ base: {
48
+ model: 'old-model',
49
+ temperature: 0.5,
50
+ },
51
+ structured: {
52
+ model: 'old-structured',
53
+ }
54
+ },
55
+ other: 'value'
56
+ };`;
57
+ const replacement = {
58
+ base: {
59
+ model: 'new-model',
60
+ temperature: 0.8,
61
+ },
62
+ structured: {
63
+ model: 'new-structured',
64
+ },
65
+ };
66
+ const result = await replaceObjectProperties(content, { models: replacement });
67
+ expect(result).toContain("'model': 'new-model'");
68
+ expect(result).toContain("'model': 'new-structured'");
69
+ expect(result).toContain("'temperature': 0.8");
70
+ expect(result).toContain("other: 'value'");
71
+ expect(result).not.toContain('old-model');
72
+ });
73
+ it('should preserve code structure and formatting', async () => {
74
+ const content = `const project = {
75
+ id: 'test',
76
+ models: {
77
+ base: { model: 'old' }
78
+ },
79
+ description: 'test project'
80
+ };`;
81
+ const replacement = { base: { model: 'new' } };
82
+ const result = await replaceObjectProperties(content, { models: replacement });
83
+ expect(result).toContain("id: 'test'");
84
+ expect(result).toContain("description: 'test project'");
85
+ expect(result).toContain("'model': 'new'");
86
+ });
87
+ it('should handle multiple property replacements', async () => {
88
+ const content = `export const config = {
89
+ models: { base: { model: 'old1' } },
90
+ tools: { search: { enabled: false } },
91
+ data: 'keep this'
92
+ };`;
93
+ const replacements = {
94
+ models: { base: { model: 'new1' } },
95
+ tools: { search: { enabled: true }, newTool: { type: 'api' } },
96
+ };
97
+ const result = await replaceObjectProperties(content, replacements);
98
+ expect(result).toContain("'model': 'new1'");
99
+ expect(result).toContain("'enabled': true");
100
+ expect(result).toContain("'newTool'");
101
+ expect(result).toContain("data: 'keep this'");
102
+ });
103
+ });
104
+ describe('replaceContentInFiles', () => {
105
+ it('should process multiple files with replacements', async () => {
106
+ const replacements = [
107
+ {
108
+ filePath: 'index.ts',
109
+ replacements: {
110
+ models: { base: { model: 'new-model' } },
111
+ },
112
+ },
113
+ {
114
+ filePath: 'config.ts',
115
+ replacements: {
116
+ settings: { debug: true },
117
+ },
118
+ },
119
+ ];
120
+ const indexContent = `export const project = { models: { base: { model: 'old' } } };`;
121
+ const configContent = `export const config = { settings: { debug: false } };`;
122
+ vi.mocked(fs.readFile)
123
+ .mockResolvedValueOnce(indexContent)
124
+ .mockResolvedValueOnce(configContent);
125
+ await replaceContentInFiles('/target/path', replacements);
126
+ expect(fs.readFile).toHaveBeenCalledTimes(2);
127
+ expect(fs.readFile).toHaveBeenCalledWith(path.join('/target/path', 'index.ts'), 'utf-8');
128
+ expect(fs.readFile).toHaveBeenCalledWith(path.join('/target/path', 'config.ts'), 'utf-8');
129
+ expect(fs.writeFile).toHaveBeenCalledTimes(2);
130
+ // Verify the content contains the replacements
131
+ const writeCalls = vi.mocked(fs.writeFile).mock.calls;
132
+ expect(writeCalls[0][1]).toContain("'model': 'new-model'");
133
+ expect(writeCalls[1][1]).toContain("'debug': true");
134
+ });
135
+ it('should warn and skip non-existent files', async () => {
136
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
137
+ vi.mocked(fs.pathExists).mockResolvedValue(false);
138
+ const replacements = [
139
+ {
140
+ filePath: 'non-existent.ts',
141
+ replacements: { test: 'value' },
142
+ },
143
+ ];
144
+ await replaceContentInFiles('/target/path', replacements);
145
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('non-existent.ts'));
146
+ expect(fs.readFile).not.toHaveBeenCalled();
147
+ expect(fs.writeFile).not.toHaveBeenCalled();
148
+ consoleSpy.mockRestore();
149
+ });
150
+ });
151
+ describe('cloneTemplate with replacements', () => {
152
+ it('should clone template and apply replacements', async () => {
153
+ const degit = await import('degit');
154
+ const mockEmitter = { clone: vi.fn().mockResolvedValue(undefined) };
155
+ vi.mocked(degit.default).mockReturnValue(mockEmitter);
156
+ const replacements = [
157
+ {
158
+ filePath: 'index.ts',
159
+ replacements: {
160
+ models: { base: { model: 'new-model' } },
161
+ },
162
+ },
163
+ ];
164
+ const fileContent = `export const project = { models: { base: { model: 'old' } } };`;
165
+ vi.mocked(fs.readFile).mockResolvedValue(fileContent);
166
+ await cloneTemplate('https://github.com/test/repo', '/target/path', replacements);
167
+ expect(mockEmitter.clone).toHaveBeenCalledWith('/target/path');
168
+ expect(fs.readFile).toHaveBeenCalledWith(path.join('/target/path', 'index.ts'), 'utf-8');
169
+ expect(fs.writeFile).toHaveBeenCalledWith(path.join('/target/path', 'index.ts'), expect.stringContaining("'model': 'new-model'"), 'utf-8');
170
+ });
171
+ it('should clone template without replacements when none provided', async () => {
172
+ const degit = await import('degit');
173
+ const mockEmitter = { clone: vi.fn().mockResolvedValue(undefined) };
174
+ vi.mocked(degit.default).mockReturnValue(mockEmitter);
175
+ await cloneTemplate('https://github.com/test/repo', '/target/path');
176
+ expect(mockEmitter.clone).toHaveBeenCalledWith('/target/path');
177
+ expect(fs.readFile).not.toHaveBeenCalled();
178
+ expect(fs.writeFile).not.toHaveBeenCalled();
179
+ });
180
+ it('should handle clone errors gracefully', async () => {
181
+ const degit = await import('degit');
182
+ const mockEmitter = { clone: vi.fn().mockRejectedValue(new Error('Clone failed')) };
183
+ vi.mocked(degit.default).mockReturnValue(mockEmitter);
184
+ const processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
185
+ throw new Error('process.exit called');
186
+ });
187
+ await expect(cloneTemplate('https://github.com/test/repo', '/target/path')).rejects.toThrow('process.exit called');
188
+ expect(processExitSpy).toHaveBeenCalledWith(1);
189
+ processExitSpy.mockRestore();
190
+ });
191
+ });
192
+ describe('Integration tests', () => {
193
+ it('should handle real-world TypeScript project structure', async () => {
194
+ const projectContent = `import { project } from '@inkeep/agents-sdk';
195
+ import { weatherGraph } from './graphs/weather-graph';
196
+
197
+ export const myProject = project({
198
+ id: 'weather-project',
199
+ name: 'Weather Project',
200
+ description: 'Weather project template',
201
+ graphs: () => [weatherGraph],
202
+ models: {
203
+ base: {
204
+ model: 'gpt-4o-mini',
205
+ },
206
+ structuredOutput: {
207
+ model: 'gpt-4o-mini',
208
+ },
209
+ summarizer: {
210
+ model: 'gpt-4o-mini',
211
+ }
212
+ },
213
+ });`;
214
+ const newModels = {
215
+ base: {
216
+ model: 'anthropic/claude-3-5-haiku-20241022',
217
+ },
218
+ structuredOutput: {
219
+ model: 'anthropic/claude-3-5-haiku-20241022',
220
+ },
221
+ summarizer: {
222
+ model: 'anthropic/claude-3-5-haiku-20241022',
223
+ },
224
+ };
225
+ const result = await replaceObjectProperties(projectContent, { models: newModels });
226
+ // Should preserve imports and structure
227
+ expect(result).toContain("import { project } from '@inkeep/agents-sdk'");
228
+ expect(result).toContain("import { weatherGraph } from './graphs/weather-graph'");
229
+ expect(result).toContain("id: 'weather-project'");
230
+ expect(result).toContain("name: 'Weather Project'");
231
+ expect(result).toContain('graphs: () => [weatherGraph]');
232
+ // Should replace all model configurations
233
+ expect(result).toContain('anthropic/claude-3-5-haiku-20241022');
234
+ expect(result).not.toContain('gpt-4o-mini');
235
+ // Should maintain proper structure
236
+ expect(result).toContain("'base': {");
237
+ expect(result).toContain("'structuredOutput': {");
238
+ expect(result).toContain("'summarizer': {");
239
+ });
240
+ it('should handle edge cases in TypeScript syntax', async () => {
241
+ const content = `export const config = {
242
+ models: {
243
+ base: { model: "double-quotes" },
244
+ other: {
245
+ model: 'single-quotes',
246
+ nested: { value: true }
247
+ },
248
+ },
249
+ // Comment here
250
+ data: 'preserve me'
251
+ };`;
252
+ const replacement = {
253
+ base: { model: 'new-base' },
254
+ other: { model: 'new-other', nested: { value: false } },
255
+ };
256
+ const result = await replaceObjectProperties(content, { models: replacement });
257
+ expect(result).toContain("'model': 'new-base'");
258
+ expect(result).toContain("'model': 'new-other'");
259
+ expect(result).toContain("'value': false");
260
+ expect(result).toContain('// Comment here');
261
+ expect(result).toContain("data: 'preserve me'");
262
+ });
263
+ it('should inject models property when it does not exist', async () => {
264
+ const content = `export const myProject = project({
265
+ id: 'test-project',
266
+ name: 'Test Project',
267
+ description: 'A test project without models',
268
+ graphs: () => [],
269
+ });`;
270
+ const replacement = {
271
+ base: { model: 'anthropic/claude-3-5-haiku-20241022' },
272
+ structuredOutput: { model: 'anthropic/claude-3-5-haiku-20241022' },
273
+ };
274
+ const result = await replaceObjectProperties(content, { models: replacement });
275
+ expect(result).toContain('models:');
276
+ expect(result).toContain("'base': {");
277
+ expect(result).toContain("'model': 'anthropic/claude-3-5-haiku-20241022'");
278
+ expect(result).toContain("'structuredOutput': {");
279
+ expect(result).toContain("id: 'test-project'");
280
+ expect(result).toContain('graphs: () => []');
281
+ });
282
+ });
283
+ });
@@ -81,7 +81,14 @@ describe('createAgents - Template and Project ID Logic', () => {
81
81
  // Should clone base template and weather-project template
82
82
  expect(cloneTemplate).toHaveBeenCalledTimes(2);
83
83
  expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/create-agents-template', expect.any(String));
84
- expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents-cookbook/template-projects/weather-project', 'src/weather-project');
84
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents-cookbook/template-projects/weather-project', 'src/weather-project', expect.arrayContaining([
85
+ expect.objectContaining({
86
+ filePath: 'index.ts',
87
+ replacements: expect.objectContaining({
88
+ models: expect.any(Object)
89
+ })
90
+ })
91
+ ]));
85
92
  // Should not call getAvailableTemplates since no template validation needed
86
93
  expect(getAvailableTemplates).not.toHaveBeenCalled();
87
94
  });
@@ -91,8 +98,10 @@ describe('createAgents - Template and Project ID Logic', () => {
91
98
  openAiKey: 'test-openai-key',
92
99
  anthropicKey: 'test-anthropic-key',
93
100
  });
94
- // Check that inkeep.config.ts is created with correct project ID
95
- expect(fs.writeFile).toHaveBeenCalledWith('src/weather-project/inkeep.config.ts', expect.stringContaining('projectId: "weather-project"'));
101
+ // Check that .env file is created
102
+ expect(fs.writeFile).toHaveBeenCalledWith('.env', expect.stringContaining('ENVIRONMENT=development'));
103
+ // Check that inkeep.config.ts is created in src directory
104
+ expect(fs.writeFile).toHaveBeenCalledWith('src/inkeep.config.ts', expect.stringContaining('tenantId: "default"'));
96
105
  });
97
106
  });
98
107
  describe('Template provided', () => {
@@ -108,8 +117,18 @@ describe('createAgents - Template and Project ID Logic', () => {
108
117
  // Should clone base template and the specified template
109
118
  expect(cloneTemplate).toHaveBeenCalledTimes(2);
110
119
  expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/create-agents-template', expect.any(String));
111
- expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents-cookbook/template-projects/chatbot', 'src/chatbot');
112
- expect(fs.writeFile).toHaveBeenCalledWith('src/chatbot/inkeep.config.ts', expect.stringContaining('projectId: "chatbot"'));
120
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents-cookbook/template-projects/chatbot', 'src/chatbot', expect.arrayContaining([
121
+ expect.objectContaining({
122
+ filePath: 'index.ts',
123
+ replacements: expect.objectContaining({
124
+ models: expect.any(Object)
125
+ })
126
+ })
127
+ ]));
128
+ // Check that .env file is created
129
+ expect(fs.writeFile).toHaveBeenCalledWith('.env', expect.stringContaining('ENVIRONMENT=development'));
130
+ // Check that inkeep.config.ts is created in src directory (not in project subdirectory)
131
+ expect(fs.writeFile).toHaveBeenCalledWith('src/inkeep.config.ts', expect.stringContaining('tenantId: "default"'));
113
132
  });
114
133
  it('should exit with error when template does not exist', async () => {
115
134
  vi.mocked(getAvailableTemplates).mockResolvedValue(['weather-project', 'chatbot']);
@@ -153,7 +172,12 @@ describe('createAgents - Template and Project ID Logic', () => {
153
172
  expect(getAvailableTemplates).not.toHaveBeenCalled();
154
173
  // Should create empty project directory
155
174
  expect(fs.ensureDir).toHaveBeenCalledWith('src/my-custom-project');
156
- expect(fs.writeFile).toHaveBeenCalledWith('src/my-custom-project/inkeep.config.ts', expect.stringContaining('projectId: "my-custom-project"'));
175
+ // Check that .env file is created
176
+ expect(fs.writeFile).toHaveBeenCalledWith('.env', expect.stringContaining('ENVIRONMENT=development'));
177
+ // Check that inkeep.config.ts is created in src directory
178
+ expect(fs.writeFile).toHaveBeenCalledWith('src/inkeep.config.ts', expect.stringContaining('tenantId: "default"'));
179
+ // Check that custom project index.ts is created
180
+ expect(fs.writeFile).toHaveBeenCalledWith('src/my-custom-project/index.ts', expect.stringContaining('id: "my-custom-project"'));
157
181
  });
158
182
  it('should prioritize custom project ID over template if both are provided', async () => {
159
183
  await createAgents({
@@ -168,8 +192,12 @@ describe('createAgents - Template and Project ID Logic', () => {
168
192
  expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/create-agents-template', expect.any(String));
169
193
  expect(getAvailableTemplates).not.toHaveBeenCalled();
170
194
  expect(fs.ensureDir).toHaveBeenCalledWith('src/my-custom-project');
171
- // Config should use custom project ID
172
- expect(fs.writeFile).toHaveBeenCalledWith('src/my-custom-project/inkeep.config.ts', expect.stringContaining('projectId: "my-custom-project"'));
195
+ // Check that .env file is created
196
+ expect(fs.writeFile).toHaveBeenCalledWith('.env', expect.stringContaining('ENVIRONMENT=development'));
197
+ // Check that inkeep.config.ts is created in src directory
198
+ expect(fs.writeFile).toHaveBeenCalledWith('src/inkeep.config.ts', expect.stringContaining('tenantId: "default"'));
199
+ // Check that custom project index.ts is created
200
+ expect(fs.writeFile).toHaveBeenCalledWith('src/my-custom-project/index.ts', expect.stringContaining('id: "my-custom-project"'));
173
201
  });
174
202
  });
175
203
  describe('Edge cases and validation', () => {
@@ -185,7 +213,14 @@ describe('createAgents - Template and Project ID Logic', () => {
185
213
  anthropicKey: 'test-key',
186
214
  });
187
215
  expect(cloneTemplate).toHaveBeenCalledTimes(2);
188
- expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents-cookbook/template-projects/my-complex-template', 'src/my-complex-template');
216
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents-cookbook/template-projects/my-complex-template', 'src/my-complex-template', expect.arrayContaining([
217
+ expect.objectContaining({
218
+ filePath: 'index.ts',
219
+ replacements: expect.objectContaining({
220
+ models: expect.any(Object)
221
+ })
222
+ })
223
+ ]));
189
224
  });
190
225
  it('should handle custom project IDs with special characters', async () => {
191
226
  await createAgents({
@@ -195,7 +230,12 @@ describe('createAgents - Template and Project ID Logic', () => {
195
230
  anthropicKey: 'test-key',
196
231
  });
197
232
  expect(fs.ensureDir).toHaveBeenCalledWith('src/my_project-123');
198
- expect(fs.writeFile).toHaveBeenCalledWith('src/my_project-123/inkeep.config.ts', expect.stringContaining('projectId: "my_project-123"'));
233
+ // Check that .env file is created
234
+ expect(fs.writeFile).toHaveBeenCalledWith('.env', expect.stringContaining('ENVIRONMENT=development'));
235
+ // Check that inkeep.config.ts is created in src directory
236
+ expect(fs.writeFile).toHaveBeenCalledWith('src/inkeep.config.ts', expect.stringContaining('tenantId: "default"'));
237
+ // Check that custom project index.ts is created
238
+ expect(fs.writeFile).toHaveBeenCalledWith('src/my_project-123/index.ts', expect.stringContaining('id: "my_project-123"'));
199
239
  });
200
240
  it('should create correct folder structure for all scenarios', async () => {
201
241
  // Test default
@@ -1,2 +1,16 @@
1
- export declare function cloneTemplate(templatePath: string, targetPath: string): Promise<void>;
1
+ export interface ContentReplacement {
2
+ /** Relative file path within the cloned template */
3
+ filePath: string;
4
+ /** Object property replacements - key is the property path, value is the replacement content */
5
+ replacements: Record<string, any>;
6
+ }
7
+ export declare function cloneTemplate(templatePath: string, targetPath: string, replacements?: ContentReplacement[]): Promise<void>;
8
+ /**
9
+ * Replace content in cloned template files
10
+ */
11
+ export declare function replaceContentInFiles(targetPath: string, replacements: ContentReplacement[]): Promise<void>;
12
+ /**
13
+ * Replace object properties in TypeScript code content
14
+ */
15
+ export declare function replaceObjectProperties(content: string, replacements: Record<string, any>): Promise<string>;
2
16
  export declare function getAvailableTemplates(): Promise<string[]>;
package/dist/templates.js CHANGED
@@ -1,17 +1,242 @@
1
+ import path from 'node:path';
1
2
  import degit from 'degit';
2
3
  import fs from 'fs-extra';
3
4
  //Duplicating function here so we dont have to add a dependency on the agents-cli package
4
- export async function cloneTemplate(templatePath, targetPath) {
5
+ export async function cloneTemplate(templatePath, targetPath, replacements) {
5
6
  await fs.mkdir(targetPath, { recursive: true });
6
7
  const templatePathSuffix = templatePath.replace('https://github.com/', '');
7
8
  const emitter = degit(templatePathSuffix);
8
9
  try {
9
10
  await emitter.clone(targetPath);
11
+ // Apply content replacements if provided
12
+ if (replacements && replacements.length > 0) {
13
+ await replaceContentInFiles(targetPath, replacements);
14
+ }
10
15
  }
11
16
  catch (_error) {
12
17
  process.exit(1);
13
18
  }
14
19
  }
20
+ /**
21
+ * Replace content in cloned template files
22
+ */
23
+ export async function replaceContentInFiles(targetPath, replacements) {
24
+ for (const replacement of replacements) {
25
+ const filePath = path.join(targetPath, replacement.filePath);
26
+ // Check if file exists
27
+ if (!(await fs.pathExists(filePath))) {
28
+ console.warn(`Warning: File ${filePath} not found, skipping replacements`);
29
+ continue;
30
+ }
31
+ // Read the file content
32
+ const content = await fs.readFile(filePath, 'utf-8');
33
+ // Apply replacements
34
+ const updatedContent = await replaceObjectProperties(content, replacement.replacements);
35
+ // Write back to file
36
+ await fs.writeFile(filePath, updatedContent, 'utf-8');
37
+ }
38
+ }
39
+ /**
40
+ * Replace object properties in TypeScript code content
41
+ */
42
+ export async function replaceObjectProperties(content, replacements) {
43
+ let updatedContent = content;
44
+ for (const [propertyPath, replacement] of Object.entries(replacements)) {
45
+ updatedContent = replaceObjectProperty(updatedContent, propertyPath, replacement);
46
+ }
47
+ return updatedContent;
48
+ }
49
+ /**
50
+ * Replace a specific object property in TypeScript code
51
+ * This implementation uses line-by-line parsing for better accuracy
52
+ * If the property doesn't exist, it will be added to the object
53
+ */
54
+ function replaceObjectProperty(content, propertyPath, replacement) {
55
+ // Check if this is a single-line object format first (object all on one line)
56
+ const singleLineMatch = content.match(new RegExp(`^(.+{[^{}]*${propertyPath}\\s*:\\s*{[^{}]*}[^{}]*}.*)$`, 'm'));
57
+ if (singleLineMatch) {
58
+ // For single-line objects, handle replacement inline
59
+ const singleLinePattern = new RegExp(`((^|\\s|{)${propertyPath}\\s*:\\s*)({[^}]*})`);
60
+ return content.replace(singleLinePattern, `$1${JSON.stringify(replacement).replace(/"/g, "'").replace(/:/g, ': ').replace(/,/g, ', ')}`);
61
+ }
62
+ // Convert replacement to formatted JSON string with proper indentation
63
+ const replacementStr = JSON.stringify(replacement, null, 2).replace(/"/g, "'"); // Use single quotes for consistency with TS
64
+ const lines = content.split('\n');
65
+ const result = [];
66
+ let inTargetProperty = false;
67
+ let braceCount = 0;
68
+ let targetPropertyIndent = '';
69
+ let foundProperty = false;
70
+ for (let i = 0; i < lines.length; i++) {
71
+ const line = lines[i];
72
+ const trimmedLine = line.trim();
73
+ // Skip if we're currently inside the target property
74
+ if (inTargetProperty) {
75
+ // Count braces to track nesting
76
+ for (const char of line) {
77
+ if (char === '{')
78
+ braceCount++;
79
+ if (char === '}')
80
+ braceCount--;
81
+ }
82
+ // When braceCount reaches 0, we've found the end of the property
83
+ if (braceCount <= 0) {
84
+ // Check if there's a trailing comma on this line or the original property line
85
+ const hasTrailingComma = line.includes(',') ||
86
+ (i + 1 < lines.length &&
87
+ lines[i + 1].trim().startsWith('}') === false &&
88
+ lines[i + 1].trim() !== '');
89
+ // Add the replacement with proper indentation
90
+ const indentedReplacement = replacementStr
91
+ .split('\n')
92
+ .map((replacementLine, index) => {
93
+ if (index === 0) {
94
+ return `${targetPropertyIndent}${propertyPath}: ${replacementLine}`;
95
+ }
96
+ return `${targetPropertyIndent}${replacementLine}`;
97
+ })
98
+ .join('\n');
99
+ result.push(`${indentedReplacement}${hasTrailingComma ? ',' : ''}`);
100
+ inTargetProperty = false;
101
+ foundProperty = true;
102
+ continue;
103
+ }
104
+ // Skip all lines while inside the target property
105
+ continue;
106
+ }
107
+ // Check if this line contains the target property at the right level
108
+ const propertyPattern = new RegExp(`(^|\\s+)${propertyPath}\\s*:`);
109
+ if (trimmedLine.startsWith(`${propertyPath}:`) || propertyPattern.test(line)) {
110
+ inTargetProperty = true;
111
+ braceCount = 0;
112
+ // For single-line objects, use base indentation plus 2 spaces for properties
113
+ if (line.includes(' = { ')) {
114
+ // Single-line format: use base indentation
115
+ targetPropertyIndent = `${line.match(/^\s*/)?.[0] || ''} `;
116
+ }
117
+ else {
118
+ // Multi-line format: calculate from property position
119
+ const propertyMatch = line.match(new RegExp(`(.*?)(^|\\s+)${propertyPath}\\s*:`));
120
+ targetPropertyIndent = propertyMatch ? propertyMatch[1] : line.match(/^\s*/)?.[0] || '';
121
+ }
122
+ // Count braces in the current line
123
+ for (const char of line) {
124
+ if (char === '{')
125
+ braceCount++;
126
+ if (char === '}')
127
+ braceCount--;
128
+ }
129
+ // If the property definition is on a single line (braceCount = 0)
130
+ if (braceCount <= 0) {
131
+ const hasTrailingComma = line.includes(',');
132
+ const indentedReplacement = replacementStr
133
+ .split('\n')
134
+ .map((replacementLine, index) => {
135
+ if (index === 0) {
136
+ return `${targetPropertyIndent}${propertyPath}: ${replacementLine}`;
137
+ }
138
+ return `${targetPropertyIndent}${replacementLine}`;
139
+ })
140
+ .join('\n');
141
+ result.push(`${indentedReplacement}${hasTrailingComma ? ',' : ''}`);
142
+ inTargetProperty = false;
143
+ foundProperty = true;
144
+ continue;
145
+ }
146
+ // Continue to next iteration to process multi-line property
147
+ continue;
148
+ }
149
+ // If we're not in the target property, keep the line as-is
150
+ result.push(line);
151
+ }
152
+ // If property wasn't found, try to inject it into the object
153
+ if (!foundProperty) {
154
+ return injectPropertyIntoObject(result.join('\n'), propertyPath, replacement);
155
+ }
156
+ return result.join('\n');
157
+ }
158
+ /**
159
+ * Inject a new property into a TypeScript object when the property doesn't exist
160
+ */
161
+ function injectPropertyIntoObject(content, propertyPath, replacement) {
162
+ const replacementStr = JSON.stringify(replacement, null, 2).replace(/"/g, "'"); // Use single quotes for consistency with TS
163
+ const lines = content.split('\n');
164
+ const result = [];
165
+ // Find the main object definition (looking for patterns like project({...})
166
+ let foundObjectStart = false;
167
+ let objectDepth = 0;
168
+ let insertionPoint = -1;
169
+ let baseIndent = '';
170
+ for (let i = 0; i < lines.length; i++) {
171
+ const line = lines[i];
172
+ const trimmedLine = line.trim();
173
+ // Look for object patterns like "project({", "= {", etc.
174
+ if (!foundObjectStart &&
175
+ (trimmedLine.includes('({') || trimmedLine.endsWith(' = {') || line.includes(' = { '))) {
176
+ foundObjectStart = true;
177
+ baseIndent = line.match(/^\s*/)?.[0] || '';
178
+ objectDepth = 0;
179
+ // Count braces on this line
180
+ for (const char of line) {
181
+ if (char === '{')
182
+ objectDepth++;
183
+ if (char === '}')
184
+ objectDepth--;
185
+ }
186
+ }
187
+ else if (foundObjectStart) {
188
+ // Track brace depth
189
+ for (const char of line) {
190
+ if (char === '{')
191
+ objectDepth++;
192
+ if (char === '}')
193
+ objectDepth--;
194
+ }
195
+ // If we're at the end of the main object, this is our insertion point
196
+ if (objectDepth === 0 && trimmedLine.startsWith('}')) {
197
+ insertionPoint = i;
198
+ break;
199
+ }
200
+ }
201
+ }
202
+ // If we found an insertion point, add the property
203
+ if (insertionPoint !== -1) {
204
+ const propertyIndent = `${baseIndent} `; // Add 2 spaces for property indent
205
+ // Check if we need a comma before our property
206
+ let needsCommaPrefix = false;
207
+ if (insertionPoint > 0) {
208
+ const prevLine = lines[insertionPoint - 1].trim();
209
+ needsCommaPrefix = prevLine !== '' && !prevLine.endsWith(',') && !prevLine.startsWith('}');
210
+ }
211
+ // Format the property to inject
212
+ const indentedReplacement = replacementStr
213
+ .split('\n')
214
+ .map((replacementLine, index) => {
215
+ if (index === 0) {
216
+ return `${propertyIndent}${propertyPath}: ${replacementLine}`;
217
+ }
218
+ return `${propertyIndent}${replacementLine}`;
219
+ })
220
+ .join('\n');
221
+ // Insert the property before the closing brace
222
+ for (let i = 0; i < lines.length; i++) {
223
+ if (i === insertionPoint) {
224
+ result.push(indentedReplacement);
225
+ }
226
+ // Add comma to previous line if needed and we're at the right position
227
+ if (i === insertionPoint - 1 && needsCommaPrefix) {
228
+ result.push(`${lines[i]},`);
229
+ }
230
+ else {
231
+ result.push(lines[i]);
232
+ }
233
+ }
234
+ return result.join('\n');
235
+ }
236
+ // If we couldn't find a suitable injection point, warn and return original
237
+ console.warn(`Could not inject property "${propertyPath}" - no suitable object found in content`);
238
+ return content;
239
+ }
15
240
  export async function getAvailableTemplates() {
16
241
  // Fetch the list of templates from your repo
17
242
  const response = await fetch('https://api.github.com/repos/inkeep/agents-cookbook/contents/template-projects');
package/dist/utils.js CHANGED
@@ -1,9 +1,9 @@
1
- import * as p from '@clack/prompts';
2
1
  import { exec } from 'node:child_process';
3
- import fs from 'fs-extra';
4
2
  import path from 'node:path';
5
- import color from 'picocolors';
6
3
  import { promisify } from 'node:util';
4
+ import * as p from '@clack/prompts';
5
+ import fs from 'fs-extra';
6
+ import color from 'picocolors';
7
7
  import { cloneTemplate, getAvailableTemplates } from './templates.js';
8
8
  const execAsync = promisify(exec);
9
9
  export const defaultGoogleModelConfigurations = {
@@ -215,7 +215,16 @@ export const createAgents = async (args = {}) => {
215
215
  if (projectTemplateRepo) {
216
216
  s.message('Creating project template folder...');
217
217
  const templateTargetPath = `src/${projectId}`;
218
- await cloneTemplate(projectTemplateRepo, templateTargetPath);
218
+ // Prepare content replacements for model settings
219
+ const contentReplacements = [
220
+ {
221
+ filePath: 'index.ts',
222
+ replacements: {
223
+ models: defaultModelSettings,
224
+ },
225
+ },
226
+ ];
227
+ await cloneTemplate(projectTemplateRepo, templateTargetPath, contentReplacements);
219
228
  }
220
229
  else {
221
230
  s.message('Creating empty project folder...');
@@ -299,14 +308,12 @@ async function createInkeepConfig(config) {
299
308
 
300
309
  const config = defineConfig({
301
310
  tenantId: "${config.tenantId}",
302
- projectId: "${config.projectId}",
303
311
  agentsManageApiUrl: 'http://localhost:3002',
304
312
  agentsRunApiUrl: 'http://localhost:3003',
305
- modelSettings: ${JSON.stringify(config.modelSettings, null, 2)},
306
313
  });
307
314
 
308
315
  export default config;`;
309
- await fs.writeFile(`src/${config.projectId}/inkeep.config.ts`, inkeepConfig);
316
+ await fs.writeFile(`src/inkeep.config.ts`, inkeepConfig);
310
317
  if (config.customProject) {
311
318
  const customIndexContent = `import { project } from '@inkeep/agents-sdk';
312
319
 
@@ -315,6 +322,7 @@ export const myProject = project({
315
322
  name: "${config.projectId}",
316
323
  description: "",
317
324
  graphs: () => [],
325
+ models: ${JSON.stringify(config.modelSettings, null, 2)},
318
326
  });`;
319
327
  await fs.writeFile(`src/${config.projectId}/index.ts`, customIndexContent);
320
328
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkeep/create-agents",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Create an Inkeep Agent Framework project",
5
5
  "type": "module",
6
6
  "bin": {