@inkeep/create-agents 0.1.10 → 0.2.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.
package/README.md CHANGED
@@ -96,10 +96,10 @@ my-agent-directory/
96
96
  inkeep dev
97
97
  ```
98
98
 
99
- 4. **Deploy your first agent graph:**
99
+ 4. **Deploy your project:**
100
100
  ```bash
101
101
  cd src/<project-id>/
102
- pnpm inkeep push hello.graph.ts
102
+ pnpm inkeep push
103
103
  ```
104
104
 
105
105
  5. **Test your agents:**
@@ -120,7 +120,7 @@ After setup, you'll have access to:
120
120
  - `pnpm dev` - Start both API services with hot reload
121
121
  - `pnpm db:push` - Apply database schema changes
122
122
  - `inkeep dev` - Start the Manage UI
123
- - `inkeep push <graph-file>` - Deploy agent configurations
123
+ - `inkeep push` - Deploy project configurations
124
124
  - `inkeep chat` - Interactive chat with your agents
125
125
 
126
126
  ## Environment Variables
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,237 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import fs from 'fs-extra';
3
+ import { createAgents } from '../utils';
4
+ import { getAvailableTemplates, cloneTemplate } from '../templates';
5
+ import * as p from '@clack/prompts';
6
+ // Mock all dependencies
7
+ vi.mock('fs-extra');
8
+ vi.mock('../templates');
9
+ vi.mock('@clack/prompts');
10
+ vi.mock('child_process');
11
+ vi.mock('util');
12
+ // Setup default mocks
13
+ const mockSpinner = {
14
+ start: vi.fn().mockReturnThis(),
15
+ stop: vi.fn().mockReturnThis(),
16
+ message: vi.fn().mockReturnThis(),
17
+ };
18
+ describe('createAgents - Template and Project ID Logic', () => {
19
+ let processExitSpy;
20
+ let processChdirSpy;
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+ // Mock process methods
24
+ processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
25
+ // Only throw for exit(0) which is expected behavior in some tests
26
+ // Let exit(1) pass so we can see the actual error
27
+ if (code === 0) {
28
+ throw new Error('process.exit called');
29
+ }
30
+ // Don't actually exit for exit(1) in tests
31
+ return undefined;
32
+ });
33
+ processChdirSpy = vi.spyOn(process, 'chdir').mockImplementation(() => { });
34
+ // Setup default mocks for @clack/prompts
35
+ vi.mocked(p.intro).mockImplementation(() => { });
36
+ vi.mocked(p.outro).mockImplementation(() => { });
37
+ vi.mocked(p.cancel).mockImplementation(() => { });
38
+ vi.mocked(p.note).mockImplementation(() => { });
39
+ vi.mocked(p.text).mockResolvedValue('test-dir');
40
+ vi.mocked(p.select).mockResolvedValue('dual');
41
+ vi.mocked(p.confirm).mockResolvedValue(false);
42
+ vi.mocked(p.spinner).mockReturnValue(mockSpinner);
43
+ vi.mocked(p.isCancel).mockReturnValue(false);
44
+ // Mock fs-extra
45
+ vi.mocked(fs.pathExists).mockResolvedValue(false);
46
+ vi.mocked(fs.ensureDir).mockResolvedValue(undefined);
47
+ vi.mocked(fs.writeFile).mockResolvedValue(undefined);
48
+ vi.mocked(fs.mkdir).mockResolvedValue(undefined);
49
+ vi.mocked(fs.remove).mockResolvedValue(undefined);
50
+ // Mock templates
51
+ vi.mocked(getAvailableTemplates).mockResolvedValue(['weather-graph', 'chatbot', 'data-analysis']);
52
+ vi.mocked(cloneTemplate).mockResolvedValue(undefined);
53
+ // Mock util.promisify to return a mock exec function
54
+ const mockExecAsync = vi.fn().mockResolvedValue({ stdout: '', stderr: '' });
55
+ const util = require('util');
56
+ util.promisify = vi.fn(() => mockExecAsync);
57
+ // Mock child_process.spawn
58
+ const childProcess = require('child_process');
59
+ childProcess.spawn = vi.fn(() => ({
60
+ pid: 12345,
61
+ stdio: ['pipe', 'pipe', 'pipe'],
62
+ on: vi.fn(),
63
+ kill: vi.fn(),
64
+ }));
65
+ });
66
+ afterEach(() => {
67
+ processExitSpy.mockRestore();
68
+ processChdirSpy.mockRestore();
69
+ });
70
+ describe('Default behavior (no template or customProjectId)', () => {
71
+ it('should use weather-graph as default template and project ID', async () => {
72
+ await createAgents({
73
+ dirName: 'test-dir',
74
+ openAiKey: 'test-openai-key',
75
+ anthropicKey: 'test-anthropic-key'
76
+ });
77
+ // Should clone base template and weather-graph template
78
+ expect(cloneTemplate).toHaveBeenCalledTimes(2);
79
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/create-agents-template', expect.any(String));
80
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents-cookbook/templates/weather-graph', 'src/weather-graph');
81
+ // Should not call getAvailableTemplates since no template validation needed
82
+ expect(getAvailableTemplates).not.toHaveBeenCalled();
83
+ });
84
+ it('should create project with weather-graph as project ID', async () => {
85
+ await createAgents({
86
+ dirName: 'test-dir',
87
+ openAiKey: 'test-openai-key',
88
+ anthropicKey: 'test-anthropic-key'
89
+ });
90
+ // Check that .env file is created with correct project ID path
91
+ expect(fs.writeFile).toHaveBeenCalledWith('src/weather-graph/.env', expect.any(String));
92
+ // Check that inkeep.config.ts is created with correct project ID
93
+ expect(fs.writeFile).toHaveBeenCalledWith('src/weather-graph/inkeep.config.ts', expect.stringContaining('projectId: "weather-graph"'));
94
+ });
95
+ });
96
+ describe('Template provided', () => {
97
+ it('should use template name as project ID when template is provided', async () => {
98
+ await createAgents({
99
+ dirName: 'test-dir',
100
+ template: 'chatbot',
101
+ openAiKey: 'test-openai-key',
102
+ anthropicKey: 'test-anthropic-key'
103
+ });
104
+ // Should validate template exists
105
+ expect(getAvailableTemplates).toHaveBeenCalled();
106
+ // Should clone base template and the specified template
107
+ expect(cloneTemplate).toHaveBeenCalledTimes(2);
108
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/create-agents-template', expect.any(String));
109
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents-cookbook/templates/chatbot', 'src/chatbot');
110
+ // Should create config files with template name as project ID
111
+ expect(fs.writeFile).toHaveBeenCalledWith('src/chatbot/.env', expect.any(String));
112
+ expect(fs.writeFile).toHaveBeenCalledWith('src/chatbot/inkeep.config.ts', expect.stringContaining('projectId: "chatbot"'));
113
+ });
114
+ it('should exit with error when template does not exist', async () => {
115
+ vi.mocked(getAvailableTemplates).mockResolvedValue(['weather-graph', 'chatbot']);
116
+ await expect(createAgents({
117
+ dirName: 'test-dir',
118
+ template: 'non-existent-template',
119
+ openAiKey: 'test-openai-key'
120
+ })).rejects.toThrow('process.exit called');
121
+ expect(p.cancel).toHaveBeenCalledWith(expect.stringContaining('Template "non-existent-template" not found'));
122
+ expect(processExitSpy).toHaveBeenCalledWith(0);
123
+ });
124
+ it('should show available templates when invalid template is provided', async () => {
125
+ vi.mocked(getAvailableTemplates).mockResolvedValue(['weather-graph', 'chatbot', 'data-analysis']);
126
+ await expect(createAgents({
127
+ dirName: 'test-dir',
128
+ template: 'invalid',
129
+ openAiKey: 'test-openai-key'
130
+ })).rejects.toThrow('process.exit called');
131
+ const cancelCall = vi.mocked(p.cancel).mock.calls[0][0];
132
+ expect(cancelCall).toContain('weather-graph');
133
+ expect(cancelCall).toContain('chatbot');
134
+ expect(cancelCall).toContain('data-analysis');
135
+ });
136
+ });
137
+ describe('Custom Project ID provided', () => {
138
+ it('should use custom project ID and not clone any template', async () => {
139
+ await createAgents({
140
+ dirName: 'test-dir',
141
+ customProjectId: 'my-custom-project',
142
+ openAiKey: 'test-openai-key',
143
+ anthropicKey: 'test-anthropic-key'
144
+ });
145
+ // Should clone base template but NOT project template
146
+ expect(cloneTemplate).toHaveBeenCalledTimes(1);
147
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/create-agents-template', expect.any(String));
148
+ // Should NOT validate templates
149
+ expect(getAvailableTemplates).not.toHaveBeenCalled();
150
+ // Should create empty project directory
151
+ expect(fs.ensureDir).toHaveBeenCalledWith('src/my-custom-project');
152
+ // Should create config files with custom project ID
153
+ expect(fs.writeFile).toHaveBeenCalledWith('src/my-custom-project/.env', expect.any(String));
154
+ expect(fs.writeFile).toHaveBeenCalledWith('src/my-custom-project/inkeep.config.ts', expect.stringContaining('projectId: "my-custom-project"'));
155
+ });
156
+ it('should prioritize custom project ID over template if both are provided', async () => {
157
+ await createAgents({
158
+ dirName: 'test-dir',
159
+ template: 'chatbot',
160
+ customProjectId: 'my-custom-project',
161
+ openAiKey: 'test-openai-key',
162
+ anthropicKey: 'test-anthropic-key'
163
+ });
164
+ // Should only clone base template, not project template
165
+ expect(cloneTemplate).toHaveBeenCalledTimes(1);
166
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/create-agents-template', expect.any(String));
167
+ expect(getAvailableTemplates).not.toHaveBeenCalled();
168
+ expect(fs.ensureDir).toHaveBeenCalledWith('src/my-custom-project');
169
+ // Config should use custom project ID
170
+ expect(fs.writeFile).toHaveBeenCalledWith('src/my-custom-project/inkeep.config.ts', expect.stringContaining('projectId: "my-custom-project"'));
171
+ });
172
+ });
173
+ describe('Edge cases and validation', () => {
174
+ it('should handle template names with hyphens correctly', async () => {
175
+ vi.mocked(getAvailableTemplates).mockResolvedValue(['my-complex-template', 'another-template']);
176
+ await createAgents({
177
+ dirName: 'test-dir',
178
+ template: 'my-complex-template',
179
+ openAiKey: 'test-key',
180
+ anthropicKey: 'test-key'
181
+ });
182
+ expect(cloneTemplate).toHaveBeenCalledTimes(2);
183
+ expect(cloneTemplate).toHaveBeenCalledWith('https://github.com/inkeep/agents-cookbook/templates/my-complex-template', 'src/my-complex-template');
184
+ });
185
+ it('should handle custom project IDs with special characters', async () => {
186
+ await createAgents({
187
+ dirName: 'test-dir',
188
+ customProjectId: 'my_project-123',
189
+ openAiKey: 'test-key',
190
+ anthropicKey: 'test-key'
191
+ });
192
+ expect(fs.ensureDir).toHaveBeenCalledWith('src/my_project-123');
193
+ expect(fs.writeFile).toHaveBeenCalledWith('src/my_project-123/inkeep.config.ts', expect.stringContaining('projectId: "my_project-123"'));
194
+ });
195
+ it('should create correct folder structure for all scenarios', async () => {
196
+ // Test default
197
+ await createAgents({
198
+ dirName: 'dir1',
199
+ openAiKey: 'key',
200
+ anthropicKey: 'key'
201
+ });
202
+ expect(fs.ensureDir).toHaveBeenCalledWith('src');
203
+ // Reset mocks
204
+ vi.clearAllMocks();
205
+ setupDefaultMocks();
206
+ // Test with template
207
+ await createAgents({
208
+ dirName: 'dir2',
209
+ template: 'chatbot',
210
+ openAiKey: 'key',
211
+ anthropicKey: 'key'
212
+ });
213
+ expect(fs.ensureDir).toHaveBeenCalledWith('src');
214
+ // Reset mocks
215
+ vi.clearAllMocks();
216
+ setupDefaultMocks();
217
+ // Test with custom ID
218
+ await createAgents({
219
+ dirName: 'dir3',
220
+ customProjectId: 'custom',
221
+ openAiKey: 'key',
222
+ anthropicKey: 'key'
223
+ });
224
+ expect(fs.ensureDir).toHaveBeenCalledWith('src');
225
+ expect(fs.ensureDir).toHaveBeenCalledWith('src/custom');
226
+ });
227
+ });
228
+ });
229
+ // Helper to setup default mocks
230
+ function setupDefaultMocks() {
231
+ vi.mocked(p.spinner).mockReturnValue(mockSpinner);
232
+ vi.mocked(fs.pathExists).mockResolvedValue(false);
233
+ vi.mocked(fs.ensureDir).mockResolvedValue(undefined);
234
+ vi.mocked(fs.writeFile).mockResolvedValue(undefined);
235
+ vi.mocked(getAvailableTemplates).mockResolvedValue(['weather-graph', 'chatbot', 'data-analysis']);
236
+ vi.mocked(cloneTemplate).mockResolvedValue(undefined);
237
+ }
package/dist/index.js CHANGED
@@ -6,9 +6,10 @@ program
6
6
  .description('Create an Inkeep Agent Framework directory')
7
7
  .version('0.1.0')
8
8
  .argument('[directory-name]', 'Name of the directory')
9
- .option('--project-id <project-id>', 'Project ID')
9
+ .option('--template <template>', 'Template to use')
10
10
  .option('--openai-key <openai-key>', 'OpenAI API key')
11
11
  .option('--anthropic-key <anthropic-key>', 'Anthropic API key')
12
+ .option('--custom-project-id <custom-project-id>', 'Custom project id for experienced users who want an empty project directory')
12
13
  .parse();
13
14
  async function main() {
14
15
  const options = program.opts();
@@ -18,7 +19,8 @@ async function main() {
18
19
  dirName: directoryName,
19
20
  openAiKey: options.openaiKey,
20
21
  anthropicKey: options.anthropicKey,
21
- projectId: options.projectId,
22
+ customProjectId: options.customProjectId,
23
+ template: options.template,
22
24
  });
23
25
  }
24
26
  catch (error) {
@@ -0,0 +1,2 @@
1
+ export declare function cloneTemplate(templatePath: string, targetPath: string): Promise<void>;
2
+ export declare function getAvailableTemplates(): Promise<string[]>;
@@ -0,0 +1,22 @@
1
+ import fs from 'fs-extra';
2
+ import degit from 'degit';
3
+ //Duplicating function here so we dont have to add a dependency on the agents-cli package
4
+ export async function cloneTemplate(templatePath, targetPath) {
5
+ await fs.mkdir(targetPath, { recursive: true });
6
+ const templatePathSuffix = templatePath.replace('https://github.com/', '');
7
+ const emitter = degit(templatePathSuffix);
8
+ try {
9
+ await emitter.clone(targetPath);
10
+ }
11
+ catch (error) {
12
+ process.exit(1);
13
+ }
14
+ }
15
+ export async function getAvailableTemplates() {
16
+ // Fetch the list of templates from your repo
17
+ const response = await fetch('https://api.github.com/repos/inkeep/agents-cookbook/contents/templates');
18
+ const contents = await response.json();
19
+ return contents
20
+ .filter((item) => item.type === 'dir')
21
+ .map((item) => item.name);
22
+ }
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const defaultDualModelConfigurations: {
1
+ export declare const defaultGoogleModelConfigurations: {
2
2
  base: {
3
3
  model: string;
4
4
  };
@@ -32,9 +32,12 @@ export declare const defaultAnthropicModelConfigurations: {
32
32
  };
33
33
  };
34
34
  export declare const createAgents: (args?: {
35
- projectId?: string;
36
35
  dirName?: string;
36
+ templateName?: string;
37
37
  openAiKey?: string;
38
38
  anthropicKey?: string;
39
+ googleKey?: string;
40
+ template?: string;
41
+ customProjectId?: string;
39
42
  }) => Promise<void>;
40
43
  export declare function createCommand(dirName?: string, options?: any): Promise<void>;