@inkeep/create-agents 0.50.0 → 0.50.3

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.
@@ -1,9 +1,13 @@
1
1
  import path from 'node:path';
2
2
  import { execa } from 'execa';
3
+ import { chromium } from 'playwright';
3
4
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
4
- import { cleanupDir, createTempDir, linkLocalPackages, runCommand, runCreateAgentsCLI, verifyDirectoryStructure, verifyFile, waitForServerReady, } from './utils';
5
+ import { cleanupDir, createTempDir, linkLocalPackages, runCommand, runCreateAgentsCLI, startDashboardServer, verifyDirectoryStructure, verifyFile, waitForServerReady, } from './utils';
5
6
  // Use 127.0.0.1 instead of localhost to avoid IPv6/IPv4 resolution issues on CI (Ubuntu)
6
7
  const manageApiUrl = 'http://127.0.0.1:3002';
8
+ // Dashboard must use localhost (not 127.0.0.1) so auth cookies share the same domain
9
+ // between the dashboard (localhost:3000) and the API (localhost:3002).
10
+ const dashboardApiUrl = 'http://localhost:3002';
7
11
  // Use a test bypass secret for authentication in CI
8
12
  // This bypasses the need for a real login/API key
9
13
  const TEST_BYPASS_SECRET = 'e2e-test-bypass-secret-for-ci-testing-only';
@@ -29,16 +33,15 @@ describe('create-agents quickstart e2e', () => {
29
33
  console.log(`Working directory: ${testDir}`);
30
34
  const result = await runCreateAgentsCLI([
31
35
  workspaceName,
32
- '--openai-key',
33
- 'test-openai-key',
34
- '--disable-git', // Skip git init for faster tests
36
+ '--skip-provider',
37
+ '--disable-git',
35
38
  '--local-agents-prefix',
36
39
  createAgentsPrefix,
37
40
  '--local-templates-prefix',
38
41
  projectTemplatesPrefix,
39
42
  '--skip-inkeep-cli',
40
43
  '--skip-inkeep-mcp',
41
- '--skip-install', // Skip initial install so we can link local packages first
44
+ '--skip-install',
42
45
  ], testDir);
43
46
  // Verify the CLI completed successfully
44
47
  expect(result.exitCode, `CLI failed with exit code ${result.exitCode}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`).toBe(0);
@@ -62,11 +65,10 @@ describe('create-agents quickstart e2e', () => {
62
65
  console.log('Verifying .env file...');
63
66
  await verifyFile(path.join(projectDir, '.env'), [
64
67
  /ENVIRONMENT=development/,
65
- /OPENAI_API_KEY=test-openai-key/,
66
68
  /INKEEP_AGENTS_MANAGE_DATABASE_URL=postgresql:\/\/appuser:password@localhost:5432\/inkeep_agents/,
67
69
  /INKEEP_AGENTS_RUN_DATABASE_URL=postgresql:\/\/appuser:password@localhost:5433\/inkeep_agents/,
68
70
  /INKEEP_AGENTS_API_URL="http:\/\/127\.0\.0\.1:3002"/,
69
- /INKEEP_AGENTS_JWT_SIGNING_SECRET=\w+/, // Random secret should be generated
71
+ /INKEEP_AGENTS_JWT_SIGNING_SECRET=\w+/,
70
72
  ]);
71
73
  console.log('.env file verified');
72
74
  // Verify inkeep.config.ts was created
@@ -92,6 +94,31 @@ describe('create-agents quickstart e2e', () => {
92
94
  },
93
95
  stream: true,
94
96
  });
97
+ // Run auth init separately to create the "default" organization and admin user.
98
+ // setup-dev:cloud may exit early if its internal migrations fail (e.g. when CI
99
+ // already applied migrations from the monorepo root), skipping auth init entirely.
100
+ console.log('Running auth init to ensure default organization exists');
101
+ const authInitResult = await runCommand({
102
+ command: 'node',
103
+ args: ['node_modules/@inkeep/agents-core/dist/auth/init.js'],
104
+ cwd: projectDir,
105
+ timeout: 120000, // 2 min: SpiceDB schema write retries up to 30x at 1s each
106
+ env: {
107
+ INKEEP_AGENTS_MANAGE_UI_USERNAME: 'admin@example.com',
108
+ INKEEP_AGENTS_MANAGE_UI_PASSWORD: 'adminADMIN!@12',
109
+ BETTER_AUTH_SECRET: 'test-secret-key-for-ci',
110
+ SPICEDB_PRESHARED_KEY: 'dev-secret-key',
111
+ // Explicit DB URLs in case process.env doesn't carry them
112
+ INKEEP_AGENTS_RUN_DATABASE_URL: process.env.INKEEP_AGENTS_RUN_DATABASE_URL ||
113
+ 'postgresql://appuser:password@localhost:5433/inkeep_agents',
114
+ INKEEP_AGENTS_MANAGE_DATABASE_URL: process.env.INKEEP_AGENTS_MANAGE_DATABASE_URL ||
115
+ 'postgresql://appuser:password@localhost:5432/inkeep_agents',
116
+ },
117
+ stream: true,
118
+ });
119
+ if (authInitResult.exitCode !== 0) {
120
+ console.warn(`Auth init exited with code ${authInitResult.exitCode}\nstdout: ${authInitResult.stdout}\nstderr: ${authInitResult.stderr}`);
121
+ }
95
122
  console.log('Project setup in database');
96
123
  console.log('Starting dev servers');
97
124
  // Start dev servers in background with output monitoring
@@ -132,6 +159,27 @@ describe('create-agents quickstart e2e', () => {
132
159
  // Wait for servers to be ready with retries
133
160
  await waitForServerReady(`${manageApiUrl}/health`, 120000); // Increased to 2 minutes for CI
134
161
  console.log('Manage API is ready');
162
+ // Ensure admin user exists for dashboard login.
163
+ // Auth init may fail in CI (SpiceDB schema write, module resolution), so
164
+ // create the user directly via the API's Better Auth signup endpoint as a
165
+ // reliable fallback. Signup is idempotent — returns 200 if user exists.
166
+ console.log('Ensuring admin user exists via API signup');
167
+ try {
168
+ const signupRes = await fetch(`${manageApiUrl}/api/auth/sign-up/email`, {
169
+ method: 'POST',
170
+ headers: { 'Content-Type': 'application/json', Origin: manageApiUrl },
171
+ body: JSON.stringify({
172
+ email: 'admin@example.com',
173
+ password: 'adminADMIN!@12',
174
+ name: 'admin',
175
+ }),
176
+ });
177
+ const signupData = await signupRes.json().catch(() => null);
178
+ console.log(`Signup response: ${signupRes.status}`, signupData ? JSON.stringify(signupData).slice(0, 200) : '');
179
+ }
180
+ catch (signupError) {
181
+ console.warn('Signup request failed (non-fatal):', signupError);
182
+ }
135
183
  console.log('Pushing project');
136
184
  const pushResult = await runCommand({
137
185
  command: 'pnpm',
@@ -158,6 +206,110 @@ describe('create-agents quickstart e2e', () => {
158
206
  const data = await response.json();
159
207
  expect(data.data.tenantId).toBe('default');
160
208
  expect(data.data.id).toBe(projectId);
209
+ // Verify login works at the API level before starting dashboard
210
+ console.log('Testing login API directly');
211
+ try {
212
+ const loginTestRes = await fetch(`${manageApiUrl}/api/auth/sign-in/email`, {
213
+ method: 'POST',
214
+ headers: { 'Content-Type': 'application/json', Origin: manageApiUrl },
215
+ body: JSON.stringify({
216
+ email: 'admin@example.com',
217
+ password: 'adminADMIN!@12',
218
+ }),
219
+ });
220
+ const loginBody = await loginTestRes.text().catch(() => '');
221
+ console.log(`Login API test: ${loginTestRes.status} ${loginBody.slice(0, 300)}`);
222
+ if (!loginTestRes.ok) {
223
+ console.error('Login API test failed — dashboard login will likely fail');
224
+ }
225
+ }
226
+ catch (loginTestError) {
227
+ console.warn('Login API test failed (non-fatal):', loginTestError);
228
+ }
229
+ // --- Dashboard Lap ---
230
+ console.log('Starting dashboard lap');
231
+ const dashboardProcess = await startDashboardServer(projectDir, {
232
+ INKEEP_AGENTS_API_URL: dashboardApiUrl,
233
+ NEXT_PUBLIC_API_URL: dashboardApiUrl,
234
+ PUBLIC_INKEEP_AGENTS_API_URL: dashboardApiUrl,
235
+ BETTER_AUTH_SECRET: 'test-secret-key-for-ci',
236
+ INKEEP_AGENTS_MANAGE_API_BYPASS_SECRET: TEST_BYPASS_SECRET,
237
+ });
238
+ console.log('Dashboard server started');
239
+ const browser = await chromium.launch({ headless: true });
240
+ try {
241
+ const page = await browser.newPage();
242
+ console.log('Navigating to login page');
243
+ await page.goto('http://localhost:3000/login', {
244
+ waitUntil: 'networkidle',
245
+ timeout: 15000,
246
+ });
247
+ console.log('Filling login form');
248
+ await page.fill('input[type="email"]', 'admin@example.com');
249
+ await page.fill('input[type="password"]', 'adminADMIN!@12');
250
+ await page.click('button[type="submit"]');
251
+ console.log('Waiting for redirect to projects page');
252
+ await page.waitForURL('**/default/projects**', {
253
+ timeout: 15000,
254
+ waitUntil: 'domcontentloaded',
255
+ });
256
+ console.log('Redirected to projects page');
257
+ console.log('Clicking activities-planner project');
258
+ // Use force:true because card uses a linkoverlay pattern that intercepts pointer events
259
+ await page.click(`a[href*="${projectId}"]`, { timeout: 15000, force: true });
260
+ await page.waitForURL(`**/default/projects/${projectId}/**`, {
261
+ timeout: 15000,
262
+ waitUntil: 'domcontentloaded',
263
+ });
264
+ console.log('Navigated to project page');
265
+ console.log('Clicking agent card');
266
+ // Use href locator to avoid matching project heading text
267
+ const agentId = 'activities-planner';
268
+ await page.click(`a[href*="/agents/${agentId}"]`, { timeout: 15000, force: true });
269
+ await page.waitForURL(`**/agents/${agentId}**`, {
270
+ timeout: 15000,
271
+ waitUntil: 'domcontentloaded',
272
+ });
273
+ console.log('Navigated to agent page');
274
+ console.log('Clicking Try it button');
275
+ await page.click('button:has-text("Try it")', { timeout: 15000, force: true });
276
+ console.log('Waiting for playground to open');
277
+ await page.waitForSelector('#playground-pane', { timeout: 15000 });
278
+ console.log('Playground panel is visible');
279
+ console.log('Verifying chat widget initialized');
280
+ await page.locator('#inkeep-widget-root').waitFor({ timeout: 15000 });
281
+ console.log('Chat widget is initialized');
282
+ console.log('Dashboard lap complete');
283
+ }
284
+ catch (dashboardError) {
285
+ console.error('Dashboard lap failed:', dashboardError);
286
+ try {
287
+ const activePage = browser.contexts()[0]?.pages()[0];
288
+ if (activePage) {
289
+ const screenshotPath = path.join(testDir, 'dashboard-failure.png');
290
+ await activePage.screenshot({ path: screenshotPath, fullPage: true });
291
+ console.log(`Screenshot saved to: ${screenshotPath}`);
292
+ console.log(`Current URL: ${activePage.url()}`);
293
+ console.log(`Page content: ${await activePage.content()}`);
294
+ }
295
+ }
296
+ catch (screenshotError) {
297
+ console.error('Failed to capture screenshot:', screenshotError);
298
+ }
299
+ throw dashboardError;
300
+ }
301
+ finally {
302
+ await browser.close();
303
+ try {
304
+ dashboardProcess.kill('SIGTERM');
305
+ }
306
+ catch { }
307
+ await new Promise((resolve) => setTimeout(resolve, 1000));
308
+ try {
309
+ dashboardProcess.kill('SIGKILL');
310
+ }
311
+ catch { }
312
+ }
161
313
  }
162
314
  catch (error) {
163
315
  console.error('Test failed with error:', error);
@@ -1,3 +1,4 @@
1
+ import { type ChildProcess } from 'node:child_process';
1
2
  /**
2
3
  * Run the create-agents CLI with the given arguments
3
4
  */
@@ -46,3 +47,4 @@ export declare function linkLocalPackages(projectDir: string, monorepoRoot: stri
46
47
  * Wait for a server to be ready by polling a health endpoint
47
48
  */
48
49
  export declare function waitForServerReady(url: string, timeout: number): Promise<void>;
50
+ export declare function startDashboardServer(projectDir: string, env?: Record<string, string>): Promise<ChildProcess>;
@@ -1,3 +1,4 @@
1
+ import { fork } from 'node:child_process';
1
2
  import os from 'node:os';
2
3
  import path, { dirname } from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
@@ -155,6 +156,7 @@ export async function linkLocalPackages(projectDir, monorepoRoot) {
155
156
  '@inkeep/agents-core': `link:${path.join(monorepoRoot, 'packages/agents-core')}`,
156
157
  '@inkeep/agents-api': `link:${path.join(monorepoRoot, 'agents-api')}`,
157
158
  '@inkeep/agents-cli': `link:${path.join(monorepoRoot, 'agents-cli')}`,
159
+ '@inkeep/agents-manage-ui': `link:${path.join(monorepoRoot, 'agents-manage-ui')}`,
158
160
  };
159
161
  // Replace package versions with local links
160
162
  for (const [pkg, linkPath] of Object.entries(inkeepPackages)) {
@@ -218,3 +220,37 @@ export async function waitForServerReady(url, timeout) {
218
220
  const errorDetails = lastError ? `: ${lastError.message}` : '';
219
221
  throw new Error(`Server not ready at ${url} after ${elapsed}ms (${attempts} attempts)${errorDetails}`);
220
222
  }
223
+ export async function startDashboardServer(projectDir, env = {}) {
224
+ const manageUiPkgJson = path.join(projectDir, 'node_modules/@inkeep/agents-manage-ui/package.json');
225
+ const manageUiRoot = path.dirname(manageUiPkgJson);
226
+ const standaloneDir = path.join(manageUiRoot, '.next/standalone/agents-manage-ui');
227
+ const serverEntry = path.join(standaloneDir, 'server.js');
228
+ if (!(await fs.pathExists(serverEntry))) {
229
+ throw new Error(`Dashboard standalone server not found at ${serverEntry}`);
230
+ }
231
+ const child = fork(serverEntry, [], {
232
+ cwd: standaloneDir,
233
+ env: {
234
+ ...process.env,
235
+ NODE_ENV: 'production',
236
+ PORT: '3000',
237
+ HOSTNAME: '0.0.0.0',
238
+ ...env,
239
+ },
240
+ stdio: 'pipe',
241
+ });
242
+ const outputHandler = (data) => {
243
+ const text = data.toString();
244
+ if (process.env.CI) {
245
+ if (text.includes('Error') || text.includes('EADDRINUSE') || text.includes('ready')) {
246
+ console.log('[Dashboard]:', text.trim());
247
+ }
248
+ }
249
+ };
250
+ if (child.stdout)
251
+ child.stdout.on('data', outputHandler);
252
+ if (child.stderr)
253
+ child.stderr.on('data', outputHandler);
254
+ await waitForServerReady('http://localhost:3000', 30000);
255
+ return child;
256
+ }
@@ -2,7 +2,7 @@ import * as p from '@clack/prompts';
2
2
  import fs from 'fs-extra';
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
4
  import { cloneTemplate, cloneTemplateLocal, getAvailableTemplates } from '../templates';
5
- import { createAgents } from '../utils';
5
+ import { createAgents, defaultMockModelConfigurations } from '../utils';
6
6
  // Create the mock execAsync function that will be used by promisify - hoisted so it's available in mocks
7
7
  const { mockExecAsync } = vi.hoisted(() => ({
8
8
  mockExecAsync: vi.fn().mockResolvedValue({ stdout: '', stderr: '' }),
@@ -278,6 +278,59 @@ describe('createAgents - Template and Project ID Logic', () => {
278
278
  expect(fs.ensureDir).toHaveBeenCalledWith('src/projects/custom');
279
279
  });
280
280
  });
281
+ describe('Skip provider option', () => {
282
+ it('should use mock model configuration when skipProvider is true', async () => {
283
+ await createAgents({
284
+ dirName: 'test-dir',
285
+ skipProvider: true,
286
+ });
287
+ expect(cloneTemplate).toHaveBeenCalledWith(expect.stringContaining('activities-planner'), 'src/projects/activities-planner', expect.arrayContaining([
288
+ expect.objectContaining({
289
+ filePath: 'index.ts',
290
+ replacements: expect.objectContaining({
291
+ models: defaultMockModelConfigurations,
292
+ }),
293
+ }),
294
+ ]));
295
+ const selectCalls = vi.mocked(p.select).mock.calls;
296
+ const providerSelectCalls = selectCalls.filter((call) => call[0] &&
297
+ typeof call[0] === 'object' &&
298
+ 'message' in call[0] &&
299
+ call[0].message.includes('AI provider'));
300
+ expect(providerSelectCalls).toHaveLength(0);
301
+ expect(p.password).not.toHaveBeenCalled();
302
+ });
303
+ it('should use mock model configuration when skip is selected interactively', async () => {
304
+ vi.mocked(p.select).mockResolvedValueOnce('skip');
305
+ await createAgents({
306
+ dirName: 'test-dir',
307
+ });
308
+ expect(cloneTemplate).toHaveBeenCalledWith(expect.stringContaining('activities-planner'), 'src/projects/activities-planner', expect.arrayContaining([
309
+ expect.objectContaining({
310
+ filePath: 'index.ts',
311
+ replacements: expect.objectContaining({
312
+ models: defaultMockModelConfigurations,
313
+ }),
314
+ }),
315
+ ]));
316
+ expect(p.password).not.toHaveBeenCalled();
317
+ });
318
+ it('should take precedence over API key flags when skipProvider is true', async () => {
319
+ await createAgents({
320
+ dirName: 'test-dir',
321
+ skipProvider: true,
322
+ openAiKey: 'test-key',
323
+ });
324
+ expect(cloneTemplate).toHaveBeenCalledWith(expect.stringContaining('activities-planner'), 'src/projects/activities-planner', expect.arrayContaining([
325
+ expect.objectContaining({
326
+ filePath: 'index.ts',
327
+ replacements: expect.objectContaining({
328
+ models: defaultMockModelConfigurations,
329
+ }),
330
+ }),
331
+ ]));
332
+ });
333
+ });
281
334
  describe('Security - Password input for API keys', () => {
282
335
  it('should use password input instead of text input for API keys', async () => {
283
336
  // Mock the select to return 'anthropic' to trigger the API key prompt
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ program
16
16
  .option('--skip-inkeep-cli', 'Skip installing Inkeep CLI globally')
17
17
  .option('--skip-inkeep-mcp', 'Skip installing Inkeep MCP server')
18
18
  .option('--skip-install', 'Skip installing dependencies')
19
+ .option('--skip-provider', 'Skip AI provider setup (uses mock provider)')
19
20
  .parse();
20
21
  async function main() {
21
22
  const options = program.opts();
@@ -33,6 +34,7 @@ async function main() {
33
34
  skipInkeepCli: options.skipInkeepCli,
34
35
  skipInkeepMcp: options.skipInkeepMcp,
35
36
  skipInstall: options.skipInstall,
37
+ skipProvider: options.skipProvider,
36
38
  });
37
39
  }
38
40
  catch (error) {
package/dist/utils.d.ts CHANGED
@@ -31,6 +31,11 @@ export declare const defaultAnthropicModelConfigurations: {
31
31
  model: "anthropic/claude-sonnet-4-5";
32
32
  };
33
33
  };
34
+ export declare const defaultMockModelConfigurations: {
35
+ base: {
36
+ model: string;
37
+ };
38
+ };
34
39
  export declare const createAgents: (args?: {
35
40
  dirName?: string;
36
41
  templateName?: string;
@@ -46,6 +51,7 @@ export declare const createAgents: (args?: {
46
51
  skipInkeepCli?: boolean;
47
52
  skipInkeepMcp?: boolean;
48
53
  skipInstall?: boolean;
54
+ skipProvider?: boolean;
49
55
  }) => Promise<void>;
50
56
  export declare function createCommand(dirName?: string, options?: any): Promise<void>;
51
57
  export declare function addInkeepMcp(): Promise<void>;
package/dist/utils.js CHANGED
@@ -66,8 +66,11 @@ export const defaultAnthropicModelConfigurations = {
66
66
  model: ANTHROPIC_MODELS.CLAUDE_SONNET_4_5,
67
67
  },
68
68
  };
69
+ export const defaultMockModelConfigurations = {
70
+ base: { model: 'mock/default' },
71
+ };
69
72
  export const createAgents = async (args = {}) => {
70
- let { dirName, openAiKey, anthropicKey, googleKey, azureKey, template, customProjectId, disableGit, localAgentsPrefix, localTemplatesPrefix, skipInkeepCli, skipInkeepMcp, skipInstall, } = args;
73
+ let { dirName, openAiKey, anthropicKey, googleKey, azureKey, template, customProjectId, disableGit, localAgentsPrefix, localTemplatesPrefix, skipInkeepCli, skipInkeepMcp, skipInstall, skipProvider, } = args;
71
74
  const tenantId = 'default';
72
75
  let projectId;
73
76
  let templateName;
@@ -111,7 +114,7 @@ export const createAgents = async (args = {}) => {
111
114
  throw new Error(validationError);
112
115
  }
113
116
  }
114
- if (!anthropicKey && !openAiKey && !googleKey && !azureKey) {
117
+ if (!skipProvider && !anthropicKey && !openAiKey && !googleKey && !azureKey) {
115
118
  const providerChoice = await p.select({
116
119
  message: 'Which AI provider would you like to use?',
117
120
  options: [
@@ -119,6 +122,7 @@ export const createAgents = async (args = {}) => {
119
122
  { value: 'openai', label: 'OpenAI' },
120
123
  { value: 'google', label: 'Google' },
121
124
  { value: 'azure', label: 'Azure' },
125
+ { value: 'skip', label: 'Skip', hint: 'scaffolds with mock provider, no API key needed' },
122
126
  ],
123
127
  });
124
128
  if (p.isCancel(providerChoice)) {
@@ -189,9 +193,18 @@ export const createAgents = async (args = {}) => {
189
193
  }
190
194
  azureKey = azureKeyResponse;
191
195
  }
196
+ else if (providerChoice === 'skip') {
197
+ skipProvider = true;
198
+ }
192
199
  }
193
200
  let defaultModelSettings = {};
194
- if (anthropicKey) {
201
+ if (skipProvider) {
202
+ defaultModelSettings = defaultMockModelConfigurations;
203
+ if (openAiKey || anthropicKey || googleKey || azureKey) {
204
+ p.log.warn('--skip-provider is active. Provider key flags are ignored; mock/default model will be used.');
205
+ }
206
+ }
207
+ else if (anthropicKey) {
195
208
  defaultModelSettings = defaultAnthropicModelConfigurations;
196
209
  }
197
210
  else if (openAiKey) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkeep/create-agents",
3
- "version": "0.50.0",
3
+ "version": "0.50.3",
4
4
  "description": "Create an Inkeep Agent Framework project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,13 +33,14 @@
33
33
  "degit": "^2.8.4",
34
34
  "fs-extra": "^11.0.0",
35
35
  "picocolors": "^1.0.0",
36
- "@inkeep/agents-core": "0.50.0"
36
+ "@inkeep/agents-core": "0.50.3"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/degit": "^2.8.6",
40
40
  "@types/fs-extra": "^11.0.0",
41
41
  "@types/node": "^20.12.0",
42
42
  "execa": "^9.6.0",
43
+ "playwright": "^1.58.2",
43
44
  "tsx": "^4.7.0",
44
45
  "typescript": "^5.4.0",
45
46
  "vitest": "^3.2.4"