@inkeep/agents-cli 0.39.4 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/_virtual/rolldown_runtime.js +7 -0
  2. package/dist/api.js +185 -0
  3. package/dist/commands/add.js +139 -0
  4. package/dist/commands/config.js +86 -0
  5. package/dist/commands/dev.js +259 -0
  6. package/dist/commands/init.js +360 -0
  7. package/dist/commands/list-agents.js +56 -0
  8. package/dist/commands/login.js +179 -0
  9. package/dist/commands/logout.js +56 -0
  10. package/dist/commands/profile.js +276 -0
  11. package/dist/{component-parser2.js → commands/pull-v3/component-parser.js} +16 -3
  12. package/dist/commands/pull-v3/component-updater.js +710 -0
  13. package/dist/commands/pull-v3/components/agent-generator.js +241 -0
  14. package/dist/commands/pull-v3/components/artifact-component-generator.js +127 -0
  15. package/dist/commands/pull-v3/components/context-config-generator.js +190 -0
  16. package/dist/commands/pull-v3/components/credential-generator.js +89 -0
  17. package/dist/commands/pull-v3/components/data-component-generator.js +102 -0
  18. package/dist/commands/pull-v3/components/environment-generator.js +170 -0
  19. package/dist/commands/pull-v3/components/external-agent-generator.js +75 -0
  20. package/dist/commands/pull-v3/components/function-tool-generator.js +94 -0
  21. package/dist/commands/pull-v3/components/mcp-tool-generator.js +86 -0
  22. package/dist/commands/pull-v3/components/project-generator.js +145 -0
  23. package/dist/commands/pull-v3/components/status-component-generator.js +92 -0
  24. package/dist/commands/pull-v3/components/sub-agent-generator.js +285 -0
  25. package/dist/commands/pull-v3/index.js +510 -0
  26. package/dist/commands/pull-v3/introspect-generator.js +278 -0
  27. package/dist/commands/pull-v3/llm-content-merger.js +192 -0
  28. package/dist/{new-component-generator.js → commands/pull-v3/new-component-generator.js} +14 -3
  29. package/dist/commands/pull-v3/project-comparator.js +914 -0
  30. package/dist/{project-index-generator.js → commands/pull-v3/project-index-generator.js} +1 -2
  31. package/dist/{project-validator.js → commands/pull-v3/project-validator.js} +4 -4
  32. package/dist/commands/pull-v3/targeted-typescript-placeholders.js +173 -0
  33. package/dist/commands/pull-v3/utils/component-registry.js +369 -0
  34. package/dist/commands/pull-v3/utils/component-tracker.js +165 -0
  35. package/dist/commands/pull-v3/utils/generator-utils.js +146 -0
  36. package/dist/commands/pull-v3/utils/model-provider-detector.js +44 -0
  37. package/dist/commands/push.js +326 -0
  38. package/dist/commands/status.js +89 -0
  39. package/dist/commands/update.js +97 -0
  40. package/dist/commands/whoami.js +38 -0
  41. package/dist/config.js +0 -1
  42. package/dist/env.js +30 -0
  43. package/dist/exports.js +3 -0
  44. package/dist/index.js +28 -196514
  45. package/dist/instrumentation.js +47 -0
  46. package/dist/types/agent.js +1 -0
  47. package/dist/types/tsx.d.d.ts +1 -0
  48. package/dist/utils/background-version-check.js +19 -0
  49. package/dist/utils/ci-environment.js +87 -0
  50. package/dist/utils/cli-pipeline.js +158 -0
  51. package/dist/utils/config.js +290 -0
  52. package/dist/utils/credentials.js +132 -0
  53. package/dist/utils/environment-loader.js +28 -0
  54. package/dist/utils/file-finder.js +62 -0
  55. package/dist/utils/json-comparator.js +185 -0
  56. package/dist/utils/json-comparison.js +232 -0
  57. package/dist/utils/mcp-runner.js +120 -0
  58. package/dist/utils/model-config.js +182 -0
  59. package/dist/utils/package-manager.js +58 -0
  60. package/dist/utils/profile-config.js +85 -0
  61. package/dist/utils/profiles/index.js +4 -0
  62. package/dist/utils/profiles/profile-manager.js +219 -0
  63. package/dist/utils/profiles/types.js +62 -0
  64. package/dist/utils/project-directory.js +33 -0
  65. package/dist/utils/project-loader.js +29 -0
  66. package/dist/utils/schema-introspection.js +44 -0
  67. package/dist/utils/templates.js +198 -0
  68. package/dist/utils/tsx-loader.js +27 -0
  69. package/dist/utils/url.js +26 -0
  70. package/dist/utils/version-check.js +79 -0
  71. package/package.json +4 -19
  72. package/dist/component-parser.js +0 -4
  73. package/dist/component-updater.js +0 -4
  74. package/dist/config2.js +0 -4
  75. package/dist/credential-stores.js +0 -4
  76. package/dist/environment-generator.js +0 -4
  77. package/dist/nodefs.js +0 -27
  78. package/dist/opfs-ahp.js +0 -368
  79. package/dist/project-loader.js +0 -4
  80. package/dist/tsx-loader.js +0 -4
@@ -0,0 +1,360 @@
1
+ import { checkKeychainAvailability, loadCredentials } from "../utils/credentials.js";
2
+ import { DEFAULT_PROFILES_CONFIG } from "../utils/profiles/types.js";
3
+ import { ProfileManager } from "../utils/profiles/profile-manager.js";
4
+ import "../utils/profiles/index.js";
5
+ import { loginCommand } from "./login.js";
6
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
7
+ import { basename, dirname, join, resolve } from "node:path";
8
+ import * as p from "@clack/prompts";
9
+ import chalk from "chalk";
10
+
11
+ //#region src/commands/init.ts
12
+ /**
13
+ * Find the most appropriate directory for the config file by looking for
14
+ * common project root indicators
15
+ */
16
+ function findProjectRoot(startPath) {
17
+ let currentPath = resolve(startPath);
18
+ const root = dirname(currentPath);
19
+ const rootIndicators = [
20
+ "package.json",
21
+ ".git",
22
+ ".gitignore",
23
+ "tsconfig.json",
24
+ "package-lock.json",
25
+ "yarn.lock",
26
+ "pnpm-lock.yaml"
27
+ ];
28
+ while (currentPath !== root) {
29
+ const files = readdirSync(currentPath);
30
+ if (rootIndicators.some((indicator) => files.includes(indicator))) return currentPath;
31
+ const parentPath = dirname(currentPath);
32
+ if (parentPath === currentPath) break;
33
+ currentPath = parentPath;
34
+ }
35
+ return startPath;
36
+ }
37
+ async function initCommand(options) {
38
+ if (options?.local) {
39
+ await localInitCommand(options);
40
+ return;
41
+ }
42
+ await cloudInitCommand(options);
43
+ }
44
+ /**
45
+ * Full onboarding wizard for Inkeep Cloud customers
46
+ */
47
+ async function cloudInitCommand(options) {
48
+ console.log();
49
+ console.log(chalk.bold("Welcome to Inkeep!"));
50
+ console.log();
51
+ const s = p.spinner();
52
+ const profileManager = new ProfileManager();
53
+ s.start("Checking authentication...");
54
+ let isAuthenticated = false;
55
+ let credentials = null;
56
+ const { available: keychainAvailable } = await checkKeychainAvailability();
57
+ if (keychainAvailable) try {
58
+ const existingCreds = await loadCredentials("inkeep-cloud");
59
+ if (existingCreds && existingCreds.accessToken && existingCreds.organizationId) {
60
+ credentials = {
61
+ accessToken: existingCreds.accessToken,
62
+ organizationId: existingCreds.organizationId,
63
+ userEmail: existingCreds.userEmail
64
+ };
65
+ isAuthenticated = true;
66
+ s.stop(`Logged in as ${chalk.cyan(existingCreds.userEmail)}`);
67
+ }
68
+ } catch {}
69
+ if (!isAuthenticated) {
70
+ s.stop("Not logged in");
71
+ console.log(chalk.yellow("→ Opening browser for login..."));
72
+ console.log();
73
+ await loginCommand({});
74
+ const newCreds = await loadCredentials("inkeep-cloud");
75
+ if (newCreds && newCreds.accessToken && newCreds.organizationId) {
76
+ credentials = {
77
+ accessToken: newCreds.accessToken,
78
+ organizationId: newCreds.organizationId,
79
+ userEmail: newCreds.userEmail
80
+ };
81
+ isAuthenticated = true;
82
+ } else {
83
+ console.error(chalk.red("Login failed. Please try again."));
84
+ process.exit(1);
85
+ }
86
+ }
87
+ s.start("Fetching your organizations...");
88
+ let selectedTenantId;
89
+ let selectedTenantName;
90
+ try {
91
+ const response = await fetch("https://manage-api.inkeep.com/api/cli/me", { headers: { Authorization: `Bearer ${credentials.accessToken}` } });
92
+ if (!response.ok) {
93
+ s.stop("Failed to fetch organizations");
94
+ console.error(chalk.red("Could not fetch your organizations. Please try logging in again."));
95
+ process.exit(1);
96
+ }
97
+ const data = await response.json();
98
+ selectedTenantId = data.organization.id;
99
+ selectedTenantName = data.organization.name;
100
+ s.stop(`Organization: ${chalk.cyan(selectedTenantName)}`);
101
+ } catch (error) {
102
+ s.stop("Failed to fetch organizations");
103
+ console.error(chalk.red("Network error. Please check your connection."));
104
+ process.exit(1);
105
+ }
106
+ s.start(`Fetching projects for ${selectedTenantName}...`);
107
+ let projects = [];
108
+ try {
109
+ const response = await fetch(`https://manage-api.inkeep.com/tenants/${selectedTenantId}/projects?limit=100`, { headers: { Authorization: `Bearer ${credentials.accessToken}` } });
110
+ if (!response.ok) {
111
+ s.stop("Failed to fetch projects");
112
+ console.error(chalk.red("Could not fetch projects."));
113
+ process.exit(1);
114
+ }
115
+ projects = (await response.json()).data || [];
116
+ s.stop(`Found ${projects.length} project(s)`);
117
+ } catch (error) {
118
+ s.stop("Failed to fetch projects");
119
+ console.error(chalk.red("Network error. Please check your connection."));
120
+ process.exit(1);
121
+ }
122
+ let targetDir;
123
+ if (options?.path) targetDir = resolve(process.cwd(), options.path);
124
+ else {
125
+ const suggestedPath = "./inkeep-agents";
126
+ const confirmedPath = await p.text({
127
+ message: "Where should we create the project files?",
128
+ placeholder: suggestedPath,
129
+ initialValue: suggestedPath,
130
+ validate: (input) => {
131
+ if (!input || input.trim() === "") return "Path is required";
132
+ }
133
+ });
134
+ if (p.isCancel(confirmedPath)) {
135
+ p.cancel("Setup cancelled");
136
+ process.exit(0);
137
+ }
138
+ targetDir = resolve(process.cwd(), confirmedPath);
139
+ }
140
+ if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
141
+ console.log();
142
+ console.log(chalk.bold("Creating directory structure..."));
143
+ const createdProjects = [];
144
+ if (projects.length === 0) {
145
+ const templateDir = join(targetDir, "my-agent");
146
+ mkdirSync(templateDir, { recursive: true });
147
+ const configContent = generateConfigFile(selectedTenantId, "my-agent");
148
+ writeFileSync(join(templateDir, "inkeep.config.ts"), configContent);
149
+ const indexContent = generateIndexFile("my-agent");
150
+ writeFileSync(join(templateDir, "index.ts"), indexContent);
151
+ console.log(chalk.gray(` ${targetDir}/`));
152
+ console.log(chalk.gray(` └── my-agent/`));
153
+ console.log(chalk.gray(` ├── inkeep.config.ts`));
154
+ console.log(chalk.gray(` └── index.ts`));
155
+ createdProjects.push("my-agent");
156
+ } else {
157
+ console.log(chalk.gray(` ${targetDir}/`));
158
+ for (const project of projects) {
159
+ const projectDir = join(targetDir, sanitizeProjectName(project.name || project.id));
160
+ mkdirSync(projectDir, { recursive: true });
161
+ const configContent = generateConfigFile(selectedTenantId, project.id);
162
+ writeFileSync(join(projectDir, "inkeep.config.ts"), configContent);
163
+ const indexContent = generateIndexFile(project.id);
164
+ writeFileSync(join(projectDir, "index.ts"), indexContent);
165
+ const displayName = sanitizeProjectName(project.name || project.id);
166
+ console.log(chalk.gray(` ├── ${displayName}/`));
167
+ console.log(chalk.gray(` │ ├── inkeep.config.ts`));
168
+ console.log(chalk.gray(` │ └── index.ts`));
169
+ createdProjects.push(displayName);
170
+ }
171
+ }
172
+ console.log();
173
+ console.log(chalk.green(`✓ Created ${createdProjects.length} project(s)`));
174
+ console.log();
175
+ console.log(chalk.bold("Creating environment templates..."));
176
+ const envDevContent = generateEnvTemplate("development");
177
+ const envProdContent = generateEnvTemplate("production");
178
+ writeFileSync(join(targetDir, ".env.development"), envDevContent);
179
+ writeFileSync(join(targetDir, ".env.production"), envProdContent);
180
+ console.log(chalk.green(" ✓ .env.development"));
181
+ console.log(chalk.green(" ✓ .env.production"));
182
+ if (!profileManager.profilesFileExists()) {
183
+ console.log();
184
+ console.log(chalk.bold("Setting up profile..."));
185
+ profileManager.saveProfiles(DEFAULT_PROFILES_CONFIG);
186
+ console.log(chalk.green(" ✓ Created cloud profile"));
187
+ }
188
+ console.log();
189
+ console.log(chalk.green.bold("Setup complete!"));
190
+ console.log();
191
+ console.log(chalk.bold("Next steps:"));
192
+ console.log(chalk.gray(` 1. cd ${targetDir}`));
193
+ console.log(chalk.gray(" 2. Add your API keys to .env.development"));
194
+ if (projects.length > 0) console.log(chalk.gray(" 3. Run: inkeep pull --all"));
195
+ else {
196
+ console.log(chalk.gray(" 3. Define your agent in index.ts"));
197
+ console.log(chalk.gray(" 4. Run: inkeep push"));
198
+ }
199
+ console.log();
200
+ }
201
+ /**
202
+ * Simple local init for self-hosted deployments
203
+ */
204
+ async function localInitCommand(options) {
205
+ let configPath;
206
+ if (options?.path) {
207
+ const resolvedPath = resolve(process.cwd(), options.path);
208
+ if (options.path.endsWith(".ts") || options.path.endsWith(".js")) configPath = resolvedPath;
209
+ else configPath = join(resolvedPath, "inkeep.config.ts");
210
+ } else {
211
+ const suggestedPath = join(findProjectRoot(process.cwd()), "inkeep.config.ts");
212
+ if (options?.interactive === false) configPath = suggestedPath;
213
+ else {
214
+ const confirmedPath = await p.text({
215
+ message: "Where should the config file be created?",
216
+ initialValue: suggestedPath,
217
+ validate: (input) => {
218
+ if (!input || input.trim() === "") return "Path is required";
219
+ const dir = input.endsWith(".ts") || input.endsWith(".js") ? dirname(input) : input;
220
+ const resolvedDir = resolve(process.cwd(), dir);
221
+ if (!existsSync(resolvedDir)) return `Directory does not exist: ${resolvedDir}`;
222
+ }
223
+ });
224
+ if (p.isCancel(confirmedPath)) {
225
+ p.cancel("Operation cancelled");
226
+ process.exit(0);
227
+ }
228
+ const resolvedPath = resolve(process.cwd(), confirmedPath);
229
+ configPath = confirmedPath.endsWith(".ts") || confirmedPath.endsWith(".js") ? resolvedPath : join(resolvedPath, "inkeep.config.ts");
230
+ }
231
+ }
232
+ if (existsSync(configPath)) {
233
+ const overwrite = await p.confirm({
234
+ message: `${basename(configPath)} already exists. Overwrite?`,
235
+ initialValue: false
236
+ });
237
+ if (p.isCancel(overwrite)) {
238
+ p.cancel("Operation cancelled");
239
+ process.exit(0);
240
+ }
241
+ if (!overwrite) {
242
+ console.log(chalk.yellow("Init cancelled."));
243
+ return;
244
+ }
245
+ }
246
+ const tenantId = await p.text({
247
+ message: "Enter your tenant ID:",
248
+ validate: (input) => {
249
+ if (!input || input.trim() === "") return "Tenant ID is required";
250
+ }
251
+ });
252
+ if (p.isCancel(tenantId)) {
253
+ p.cancel("Operation cancelled");
254
+ process.exit(0);
255
+ }
256
+ const validateUrl = (input) => {
257
+ try {
258
+ if (input && input.trim() !== "") {
259
+ new URL(input);
260
+ return;
261
+ }
262
+ return;
263
+ } catch {
264
+ return "Please enter a valid URL";
265
+ }
266
+ };
267
+ const manageApiUrl = await p.text({
268
+ message: "Enter the Management API URL:",
269
+ placeholder: "http://localhost:3002",
270
+ initialValue: "http://localhost:3002",
271
+ validate: validateUrl
272
+ });
273
+ if (p.isCancel(manageApiUrl)) {
274
+ p.cancel("Operation cancelled");
275
+ process.exit(0);
276
+ }
277
+ const runApiUrl = await p.text({
278
+ message: "Enter the Run API URL:",
279
+ placeholder: "http://localhost:3003",
280
+ initialValue: "http://localhost:3003",
281
+ validate: validateUrl
282
+ });
283
+ if (p.isCancel(runApiUrl)) {
284
+ p.cancel("Operation cancelled");
285
+ process.exit(0);
286
+ }
287
+ const configContent = `import { defineConfig } from '@inkeep/agents-cli/config';
288
+
289
+ export default defineConfig({
290
+ tenantId: '${tenantId}',
291
+ agentsManageApi: {
292
+ url: '${manageApiUrl}',
293
+ },
294
+ agentsRunApi: {
295
+ url: '${runApiUrl}',
296
+ },
297
+ });
298
+ `;
299
+ try {
300
+ writeFileSync(configPath, configContent);
301
+ console.log(chalk.green("✓"), `Created ${chalk.cyan(configPath)}`);
302
+ console.log(chalk.gray("\nYou can now use the Inkeep CLI commands."));
303
+ const configDir = dirname(configPath);
304
+ if (configDir !== process.cwd()) {
305
+ console.log(chalk.gray(`\nNote: Config file created in ${configDir}`));
306
+ console.log(chalk.gray(`Use --config ${configPath} with commands, or run commands from that directory.`));
307
+ }
308
+ } catch (error) {
309
+ console.error(chalk.red("Failed to create config file:"), error);
310
+ process.exit(1);
311
+ }
312
+ }
313
+ function sanitizeProjectName(name) {
314
+ return name.toLowerCase().replace(/[^a-z0-9-_]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
315
+ }
316
+ function generateConfigFile(tenantId, projectId) {
317
+ return `import { defineConfig } from '@inkeep/agents-cli/config';
318
+
319
+ export default defineConfig({
320
+ tenantId: '${tenantId}',
321
+ projectId: '${projectId}',
322
+ agentsManageApi: {
323
+ url: 'https://manage-api.inkeep.com',
324
+ },
325
+ agentsRunApi: {
326
+ url: 'https://run-api.inkeep.com',
327
+ },
328
+ });
329
+ `;
330
+ }
331
+ function generateIndexFile(projectId) {
332
+ return `import { project } from '@inkeep/agents-sdk';
333
+
334
+ // This file was auto-generated by 'inkeep init'
335
+ // Run 'inkeep pull' to sync with your remote project
336
+
337
+ export default project({
338
+ id: '${projectId}',
339
+ name: '${projectId}',
340
+ agents: {},
341
+ tools: {},
342
+ });
343
+ `;
344
+ }
345
+ function generateEnvTemplate(environment) {
346
+ return `# ${environment.charAt(0).toUpperCase() + environment.slice(1)} Environment
347
+ # Add your API keys and secrets here
348
+
349
+ # OpenAI API Key
350
+ OPENAI_API_KEY=sk-your-key-here
351
+
352
+ # Anthropic API Key
353
+ ANTHROPIC_API_KEY=sk-ant-your-key-here
354
+
355
+ # Add other provider keys as needed
356
+ `;
357
+ }
358
+
359
+ //#endregion
360
+ export { initCommand };
@@ -0,0 +1,56 @@
1
+ import { ManagementApiClient } from "../api.js";
2
+ import { initializeCommand } from "../utils/cli-pipeline.js";
3
+ import * as p from "@clack/prompts";
4
+ import chalk from "chalk";
5
+ import Table from "cli-table3";
6
+
7
+ //#region src/commands/list-agents.ts
8
+ async function listAgentsCommand(options) {
9
+ const configPath = options.config || options.configFilePath;
10
+ const { config } = await initializeCommand({
11
+ configPath,
12
+ showSpinner: false,
13
+ logConfig: true
14
+ });
15
+ console.log();
16
+ const api = await ManagementApiClient.create(config.agentsManageApiUrl, configPath, config.tenantId, options.project);
17
+ const s = p.spinner();
18
+ s.start("Fetching agent...");
19
+ try {
20
+ const agents = await api.listAgents();
21
+ s.stop(`Found ${agents.length} agent(s) in project "${options.project}"`);
22
+ if (agents.length === 0) {
23
+ console.log(chalk.gray(`No agent found in project "${options.project}". Define agent in your project and run: inkeep push`));
24
+ return;
25
+ }
26
+ const table = new Table({
27
+ head: [
28
+ chalk.cyan("Agent ID"),
29
+ chalk.cyan("Name"),
30
+ chalk.cyan("Default Agent"),
31
+ chalk.cyan("Created")
32
+ ],
33
+ style: {
34
+ head: [],
35
+ border: []
36
+ }
37
+ });
38
+ for (const agent of agents) {
39
+ const createdDate = agent.createdAt ? new Date(agent.createdAt).toLocaleDateString() : "Unknown";
40
+ table.push([
41
+ agent.id || "",
42
+ agent.name || agent.id || "",
43
+ agent.defaultSubAgentId || chalk.gray("None"),
44
+ createdDate
45
+ ]);
46
+ }
47
+ console.log(`\n${table.toString()}`);
48
+ } catch (error) {
49
+ s.stop("Failed to fetch agent");
50
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : error);
51
+ process.exit(1);
52
+ }
53
+ }
54
+
55
+ //#endregion
56
+ export { listAgentsCommand };
@@ -0,0 +1,179 @@
1
+ import { checkKeychainAvailability, getKeychainUnavailableMessage, loadCredentials, saveCredentials } from "../utils/credentials.js";
2
+ import { ProfileManager } from "../utils/profiles/profile-manager.js";
3
+ import "../utils/profiles/index.js";
4
+ import * as p from "@clack/prompts";
5
+ import chalk from "chalk";
6
+ import open from "open";
7
+
8
+ //#region src/commands/login.ts
9
+ /**
10
+ * Format user code as XXXX-XXXX for display
11
+ */
12
+ function formatUserCode(code) {
13
+ const cleaned = code.replace(/[^A-Z0-9]/gi, "").toUpperCase();
14
+ if (cleaned.length === 8) return `${cleaned.slice(0, 4)}-${cleaned.slice(4)}`;
15
+ return cleaned;
16
+ }
17
+ /**
18
+ * Sleep for a specified number of milliseconds
19
+ */
20
+ function sleep(ms) {
21
+ return new Promise((resolve) => setTimeout(resolve, ms));
22
+ }
23
+ /**
24
+ * Poll the device token endpoint until authorization is complete
25
+ */
26
+ async function pollForToken(cloudUrl, deviceCode, clientId, initialInterval) {
27
+ let interval = initialInterval;
28
+ while (true) {
29
+ await sleep(interval * 1e3);
30
+ const data = await (await fetch(`${cloudUrl}/api/auth/device/token`, {
31
+ method: "POST",
32
+ headers: { "Content-Type": "application/json" },
33
+ body: JSON.stringify({
34
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
35
+ device_code: deviceCode,
36
+ client_id: clientId
37
+ })
38
+ })).json();
39
+ if (data.access_token) return data.access_token;
40
+ if (data.error === "authorization_pending") continue;
41
+ if (data.error === "slow_down") {
42
+ interval += 5;
43
+ continue;
44
+ }
45
+ if (data.error === "expired_token") throw new Error("Device code expired. Please try again.");
46
+ if (data.error === "access_denied") throw new Error("Authorization denied.");
47
+ throw new Error(data.error || data.message || "Unknown error during authorization");
48
+ }
49
+ }
50
+ /**
51
+ * Fetch user info and organization after authentication
52
+ */
53
+ async function fetchUserInfo(cloudUrl, accessToken) {
54
+ const sessionResponse = await fetch(`${cloudUrl}/api/auth/get-session`, { headers: { Authorization: `Bearer ${accessToken}` } });
55
+ if (!sessionResponse.ok) throw new Error("Failed to fetch user session");
56
+ const user = (await sessionResponse.json()).user;
57
+ if (!user) throw new Error("No user found in session");
58
+ const orgResponse = await fetch(`${cloudUrl}/api/cli/me`, { headers: { Authorization: `Bearer ${accessToken}` } });
59
+ if (!orgResponse.ok) throw new Error("Failed to fetch organization info. Please ensure that you are a member of an organization.");
60
+ const orgData = await orgResponse.json();
61
+ return {
62
+ user: {
63
+ id: user.id,
64
+ email: user.email,
65
+ name: user.name
66
+ },
67
+ organization: orgData.organization
68
+ };
69
+ }
70
+ async function loginCommand(options = {}) {
71
+ const profileManager = new ProfileManager();
72
+ let profileName;
73
+ let credentialKey;
74
+ let manageApiUrl;
75
+ let manageUiUrl;
76
+ try {
77
+ if (options.profile) {
78
+ const profile = profileManager.getProfile(options.profile);
79
+ if (!profile) {
80
+ console.error(chalk.red(`Profile '${options.profile}' not found.`));
81
+ console.log(chalk.gray("Run \"inkeep profile list\" to see available profiles."));
82
+ process.exit(1);
83
+ }
84
+ profileName = options.profile;
85
+ credentialKey = profile.credential;
86
+ manageApiUrl = profile.remote.manageApi;
87
+ manageUiUrl = profile.remote.manageUi;
88
+ } else {
89
+ const activeProfile = profileManager.getActiveProfile();
90
+ profileName = activeProfile.name;
91
+ credentialKey = activeProfile.credential;
92
+ manageApiUrl = activeProfile.remote.manageApi;
93
+ manageUiUrl = activeProfile.remote.manageUi;
94
+ }
95
+ } catch {
96
+ profileName = "default";
97
+ credentialKey = "inkeep-cloud";
98
+ manageApiUrl = "https://manage-api.inkeep.com";
99
+ manageUiUrl = "https://manage.inkeep.com";
100
+ }
101
+ console.log(chalk.gray(`Using profile: ${profileName}`));
102
+ const { available, reason } = await checkKeychainAvailability();
103
+ if (!available) {
104
+ console.error(chalk.red("Error:"), getKeychainUnavailableMessage(reason));
105
+ console.log();
106
+ console.log(chalk.yellow("For CI/CD environments without keychain access:"));
107
+ console.log(chalk.gray(" Set INKEEP_API_KEY environment variable instead of using login."));
108
+ console.log(chalk.gray(" See: https://docs.inkeep.com/cli/cicd"));
109
+ process.exit(1);
110
+ }
111
+ const existingCredentials = await loadCredentials(credentialKey);
112
+ if (existingCredentials) {
113
+ const continueLogin = await p.confirm({
114
+ message: `Already logged in as ${chalk.cyan(existingCredentials.userEmail)} for profile '${profileName}'. Continue with new login?`,
115
+ initialValue: false
116
+ });
117
+ if (p.isCancel(continueLogin)) {
118
+ p.cancel("Login cancelled");
119
+ process.exit(0);
120
+ }
121
+ if (!continueLogin) {
122
+ console.log(chalk.gray("Login cancelled. You are still logged in."));
123
+ return;
124
+ }
125
+ }
126
+ const s = p.spinner();
127
+ try {
128
+ s.start("Requesting device code...");
129
+ const deviceCodeResponse = await fetch(`${manageApiUrl}/api/auth/device/code`, {
130
+ method: "POST",
131
+ headers: { "Content-Type": "application/json" },
132
+ body: JSON.stringify({ client_id: "inkeep-cli" })
133
+ });
134
+ if (!deviceCodeResponse.ok) {
135
+ const errorData = await deviceCodeResponse.json().catch(() => ({}));
136
+ throw new Error(errorData.message || `Failed to get device code: ${deviceCodeResponse.statusText}`);
137
+ }
138
+ const { device_code, user_code, interval } = await deviceCodeResponse.json();
139
+ s.stop("Device code received");
140
+ console.log();
141
+ console.log(chalk.bold("To authenticate, visit:"));
142
+ console.log(chalk.cyan(` ${manageUiUrl}/device?user_code=${user_code}`));
143
+ console.log();
144
+ console.log(chalk.bold("And enter code:"));
145
+ console.log(chalk.yellow.bold(` ${formatUserCode(user_code)}`));
146
+ console.log();
147
+ try {
148
+ await open(`${manageUiUrl}/device?user_code=${user_code}`);
149
+ console.log(chalk.gray(" (Browser opened automatically)"));
150
+ console.log();
151
+ } catch {}
152
+ s.start("Waiting for authorization...");
153
+ const accessToken = await pollForToken(manageApiUrl, device_code, "inkeep-cli", interval || 5);
154
+ s.stop("Authorized!");
155
+ s.start("Fetching account info...");
156
+ const userInfo = await fetchUserInfo(manageApiUrl, accessToken);
157
+ s.stop("Account info retrieved");
158
+ await saveCredentials({
159
+ accessToken,
160
+ userId: userInfo.user.id,
161
+ userEmail: userInfo.user.email,
162
+ organizationId: userInfo.organization.id,
163
+ organizationName: userInfo.organization.name,
164
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
165
+ }, credentialKey);
166
+ console.log();
167
+ console.log(chalk.green("✓"), `Logged in as ${chalk.cyan(userInfo.user.email)}`);
168
+ console.log(chalk.green("✓"), `Organization: ${chalk.cyan(userInfo.organization.name)}`);
169
+ console.log(chalk.green("✓"), `Profile: ${chalk.cyan(profileName)}`);
170
+ } catch (error) {
171
+ s.stop("Login failed");
172
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
173
+ console.error(chalk.red("Error:"), errorMessage);
174
+ process.exit(1);
175
+ }
176
+ }
177
+
178
+ //#endregion
179
+ export { loginCommand };
@@ -0,0 +1,56 @@
1
+ import { clearCredentials, loadCredentials } from "../utils/credentials.js";
2
+ import { ProfileManager } from "../utils/profiles/profile-manager.js";
3
+ import "../utils/profiles/index.js";
4
+ import * as p from "@clack/prompts";
5
+ import chalk from "chalk";
6
+
7
+ //#region src/commands/logout.ts
8
+ async function logoutCommand(options = {}) {
9
+ const profileManager = new ProfileManager();
10
+ const s = p.spinner();
11
+ let profileName;
12
+ let credentialKey;
13
+ try {
14
+ if (options.profile) {
15
+ const profile = profileManager.getProfile(options.profile);
16
+ if (!profile) {
17
+ console.error(chalk.red(`Profile '${options.profile}' not found.`));
18
+ console.log(chalk.gray("Run \"inkeep profile list\" to see available profiles."));
19
+ process.exit(1);
20
+ }
21
+ profileName = options.profile;
22
+ credentialKey = profile.credential;
23
+ } else {
24
+ const activeProfile = profileManager.getActiveProfile();
25
+ profileName = activeProfile.name;
26
+ credentialKey = activeProfile.credential;
27
+ }
28
+ } catch {
29
+ profileName = "default";
30
+ credentialKey = "inkeep-cloud";
31
+ }
32
+ console.log(chalk.gray(`Using profile: ${profileName}`));
33
+ if (!await loadCredentials(credentialKey)) {
34
+ console.log(chalk.yellow(`Not logged in for profile '${profileName}'.`));
35
+ return;
36
+ }
37
+ s.start("Logging out...");
38
+ try {
39
+ if (await clearCredentials(credentialKey)) {
40
+ s.stop("Logged out successfully");
41
+ console.log(chalk.green("✓"), `Logged out from profile '${chalk.cyan(profileName)}'.`);
42
+ console.log(chalk.gray(` • Credential '${credentialKey}' removed from keychain.`));
43
+ } else {
44
+ s.stop("Logout completed");
45
+ console.log(chalk.gray("No credentials were stored."));
46
+ }
47
+ } catch (error) {
48
+ s.stop("Logout failed");
49
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
50
+ console.error(chalk.red("Error:"), errorMessage);
51
+ process.exit(1);
52
+ }
53
+ }
54
+
55
+ //#endregion
56
+ export { logoutCommand };