@mintlify/cli 4.0.1084 → 4.0.1086

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,97 @@
1
+ import chalk from 'chalk';
2
+
3
+ export type OutputFormat = 'table' | 'plain' | 'json' | 'graph';
4
+
5
+ export function resolveFormat(argv: { format?: string; agent?: boolean }): OutputFormat {
6
+ if (argv.agent || process.env.CLAUDECODE === '1') return 'json';
7
+ if (
8
+ argv.format === 'table' ||
9
+ argv.format === 'plain' ||
10
+ argv.format === 'json' ||
11
+ argv.format === 'graph'
12
+ )
13
+ return argv.format;
14
+ return 'plain';
15
+ }
16
+
17
+ export function formatPlainTable(headers: string[], rows: string[][]): string {
18
+ if (rows.length === 0) return '';
19
+
20
+ const colWidths = headers.map((h, i) =>
21
+ Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length))
22
+ );
23
+
24
+ const headerLine = headers.map((h, i) => h.toUpperCase().padEnd(colWidths[i]!)).join('\t');
25
+ const bodyLines = rows.map((row) => row.map((cell, i) => cell.padEnd(colWidths[i]!)).join('\t'));
26
+
27
+ return [headerLine, ...bodyLines].join('\n');
28
+ }
29
+
30
+ export function formatPrettyTable(headers: string[], rows: string[][]): string {
31
+ if (rows.length === 0) return chalk.dim(' No data found.');
32
+
33
+ const colWidths = headers.map((h, i) =>
34
+ Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length))
35
+ );
36
+
37
+ const headerLine = headers.map((h, i) => chalk.bold(h.padEnd(colWidths[i]!))).join(' ');
38
+ const separator = chalk.dim(colWidths.map((w) => '\u2500'.repeat(w)).join('\u2500\u2500'));
39
+ const bodyLines = rows.map((row) => row.map((cell, i) => cell.padEnd(colWidths[i]!)).join(' '));
40
+
41
+ return [headerLine, separator, ...bodyLines].join('\n');
42
+ }
43
+
44
+ function gradientBar(len: number, rgb: [number, number, number]): string {
45
+ let result = '';
46
+ for (let i = 0; i < len; i++) {
47
+ const t = len === 1 ? 1 : i / (len - 1);
48
+ const dim = 0.3 + t * 0.7;
49
+ const r = Math.round(rgb[0] * dim);
50
+ const g = Math.round(rgb[1] * dim);
51
+ const b = Math.round(rgb[2] * dim);
52
+ result += chalk.rgb(r, g, b)('\u2588');
53
+ }
54
+ return result;
55
+ }
56
+
57
+ const COLOR_MAP: Record<string, [number, number, number]> = {
58
+ cyan: [0, 255, 255],
59
+ magenta: [255, 0, 255],
60
+ yellow: [255, 255, 0],
61
+ green: [0, 255, 100],
62
+ blue: [80, 140, 255],
63
+ red: [255, 80, 80],
64
+ };
65
+
66
+ export function formatBarChart(
67
+ items: { label: string; value: number; color?: string }[],
68
+ opts: { maxWidth?: number } = {}
69
+ ): string {
70
+ if (items.length === 0) return chalk.dim(' No data found.');
71
+
72
+ const maxWidth = opts.maxWidth ?? 40;
73
+ const maxVal = Math.max(...items.map((i) => i.value), 1);
74
+ const maxLabel = Math.max(...items.map((i) => i.label.length));
75
+ const maxValStr = Math.max(...items.map((i) => i.value.toLocaleString('en-US').length));
76
+
77
+ return items
78
+ .map((item) => {
79
+ const barLen = Math.round((item.value / maxVal) * maxWidth);
80
+ const rgb = COLOR_MAP[item.color ?? 'cyan'] ?? COLOR_MAP.cyan!;
81
+ const bar = barLen > 0 ? gradientBar(barLen, rgb) : '';
82
+ const pad = ' '.repeat(maxWidth - barLen);
83
+ return ` ${item.label.padEnd(maxLabel)} ${bar}${pad} ${item.value.toLocaleString('en-US').padStart(maxValStr)}`;
84
+ })
85
+ .join('\n');
86
+ }
87
+
88
+ export function formatOutput(
89
+ format: OutputFormat,
90
+ headers: string[],
91
+ rows: string[][],
92
+ jsonData: unknown
93
+ ): string {
94
+ if (format === 'json') return JSON.stringify(jsonData, null, 2);
95
+ if (format === 'plain') return formatPlainTable(headers, rows);
96
+ return formatPrettyTable(headers, rows);
97
+ }
@@ -0,0 +1,132 @@
1
+ export interface KpiResponse {
2
+ humanVisitors: number;
3
+ humanViews: number;
4
+ humanAssistant: number;
5
+ humanSearches: number;
6
+ humanFeedback: number;
7
+ agentVisitors: number;
8
+ agentViews: number;
9
+ agentMcpSearches: number;
10
+ }
11
+
12
+ export interface BucketSummary {
13
+ id: string;
14
+ questionSummary: string;
15
+ size: number;
16
+ status: string;
17
+ lastAsked: string | null;
18
+ lastOccurredAt: string | null;
19
+ createdAt: string;
20
+ }
21
+
22
+ export interface BucketsResponse {
23
+ data: BucketSummary[];
24
+ pagination: { total: number };
25
+ }
26
+
27
+ export interface BucketThread {
28
+ id: string;
29
+ firstUserMessage: string | null;
30
+ feedback: { up: number; down: number };
31
+ resolutionStatus: string | null;
32
+ length: number;
33
+ createdAt: string;
34
+ lastMessageAt: string | null;
35
+ }
36
+
37
+ export interface BucketThreadsResponse {
38
+ data: BucketThread[];
39
+ pagination: { total: number; hasMore: boolean; nextCursor: string | null };
40
+ }
41
+
42
+ export interface FeedbackItem {
43
+ id: string;
44
+ path: string;
45
+ comment: string | null;
46
+ createdAt: string | null;
47
+ source: string;
48
+ status: string;
49
+ helpful?: boolean;
50
+ contact?: string;
51
+ code?: string;
52
+ filename?: string;
53
+ lang?: string;
54
+ }
55
+
56
+ export interface FeedbackResponse {
57
+ feedback: FeedbackItem[];
58
+ nextCursor: string | null;
59
+ hasMore: boolean;
60
+ }
61
+
62
+ export interface FeedbackByPageItem {
63
+ path: string;
64
+ thumbsUp: number;
65
+ thumbsDown: number;
66
+ code: number;
67
+ total: number;
68
+ }
69
+
70
+ export interface FeedbackByPageResponse {
71
+ feedback: FeedbackByPageItem[];
72
+ hasMore: boolean;
73
+ }
74
+
75
+ export interface ConversationSource {
76
+ title: string;
77
+ url: string;
78
+ }
79
+
80
+ export interface Conversation {
81
+ id: string;
82
+ timestamp: string;
83
+ query: string;
84
+ response: string;
85
+ sources: ConversationSource[];
86
+ queryCategory: string | null;
87
+ }
88
+
89
+ export interface ConversationResponse {
90
+ conversations: Conversation[];
91
+ nextCursor: string | null;
92
+ hasMore: boolean;
93
+ }
94
+
95
+ export interface SearchRow {
96
+ searchQuery: string;
97
+ hits: number;
98
+ ctr: number;
99
+ topClickedPage: string | null;
100
+ lastSearchedAt: string;
101
+ }
102
+
103
+ export interface SearchResponse {
104
+ searches: SearchRow[];
105
+ totalSearches: number;
106
+ nextCursor: string | null;
107
+ }
108
+
109
+ export interface TrafficTotals {
110
+ human: number;
111
+ ai: number;
112
+ total: number;
113
+ }
114
+
115
+ export interface TrafficRow {
116
+ path: string;
117
+ human: number;
118
+ ai: number;
119
+ total: number;
120
+ }
121
+
122
+ export interface ViewsResponse {
123
+ totals: TrafficTotals;
124
+ views: TrafficRow[];
125
+ hasMore: boolean;
126
+ }
127
+
128
+ export interface VisitorsResponse {
129
+ totals: TrafficTotals;
130
+ visitors: TrafficRow[];
131
+ hasMore: boolean;
132
+ }
@@ -0,0 +1,64 @@
1
+ import { CALLBACK_PORT, DASHBOARD_URL } from './constants.js';
2
+
3
+ export async function startCallbackServer(): Promise<{
4
+ codePromise: Promise<string>;
5
+ close: () => void;
6
+ }> {
7
+ const { default: http } = await import('http');
8
+
9
+ let resolveCode: (code: string) => void;
10
+ let rejectCode: (err: Error) => void;
11
+ let closed = false;
12
+
13
+ const codePromise = new Promise<string>((res, rej) => {
14
+ resolveCode = res;
15
+ rejectCode = rej;
16
+ });
17
+
18
+ const server = http.createServer((req, res) => {
19
+ res.setHeader('Access-Control-Allow-Origin', DASHBOARD_URL);
20
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
21
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
22
+
23
+ if (req.method === 'OPTIONS') {
24
+ res.writeHead(204);
25
+ res.end();
26
+ return;
27
+ }
28
+
29
+ if (req.method === 'POST') {
30
+ let body = '';
31
+ req.on('data', (chunk) => {
32
+ body += chunk;
33
+ });
34
+ req.on('end', () => {
35
+ try {
36
+ const { code } = JSON.parse(body) as { code: string };
37
+ res.writeHead(200, { 'Content-Type': 'application/json' });
38
+ res.end(JSON.stringify({ ok: true }));
39
+ closed = true;
40
+ server.close();
41
+ resolveCode(code);
42
+ } catch {
43
+ res.writeHead(400);
44
+ res.end(JSON.stringify({ error: 'invalid body' }));
45
+ }
46
+ });
47
+ } else {
48
+ res.writeHead(405);
49
+ res.end();
50
+ }
51
+ });
52
+
53
+ server.listen(CALLBACK_PORT, 'localhost');
54
+
55
+ const close = () => {
56
+ if (!closed) {
57
+ closed = true;
58
+ server.close();
59
+ rejectCode(new Error('Login cancelled'));
60
+ }
61
+ };
62
+
63
+ return { codePromise, close };
64
+ }
package/src/cli.tsx CHANGED
@@ -19,9 +19,11 @@ import yargs from 'yargs';
19
19
  import { hideBin } from 'yargs/helpers';
20
20
 
21
21
  import { accessibilityCheck } from './accessibilityCheck.js';
22
+ import { analyticsBuilder } from './analytics/index.js';
22
23
  import { setTelemetryEnabled } from './config.js';
23
- import { CMD_EXEC_PATH } from './constants.js';
24
+ import { getConfigValue, setConfigValue, clearConfigValue } from './config.js';
24
25
  import {
26
+ CMD_EXEC_PATH,
25
27
  checkPort,
26
28
  checkNodeVersion,
27
29
  autoUpgradeIfNeeded,
@@ -315,7 +317,7 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
315
317
  )
316
318
  .command(
317
319
  'status',
318
- false,
320
+ 'View current authentication status',
319
321
  () => undefined,
320
322
  async () => {
321
323
  await status();
@@ -324,7 +326,7 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
324
326
  )
325
327
  .command(
326
328
  'logout',
327
- false,
329
+ 'logout of your mintlify account',
328
330
  () => undefined,
329
331
  async () => {
330
332
  await logout();
@@ -333,13 +335,98 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
333
335
  )
334
336
  .command(
335
337
  'login',
336
- false,
338
+ 'authenticate your account to mintlify',
337
339
  () => undefined,
338
340
  async () => {
339
341
  await login();
340
342
  await terminate(0);
341
343
  }
342
344
  )
345
+ .command('config', 'manage CLI configuration', (yargs) =>
346
+ yargs
347
+ .command(
348
+ 'set <key> <value>',
349
+ 'set a configuration value',
350
+ (yargs) =>
351
+ yargs
352
+ .positional('key', {
353
+ type: 'string',
354
+ demandOption: true,
355
+ description: 'Config key (e.g. subdomain)',
356
+ })
357
+ .positional('value', {
358
+ type: 'string',
359
+ demandOption: true,
360
+ description: 'Config value',
361
+ }),
362
+ async (argv) => {
363
+ const validKeys = ['subdomain', 'dateFrom', 'dateTo'];
364
+ if (!validKeys.includes(argv.key)) {
365
+ addLog(
366
+ <ErrorLog
367
+ message={`Unknown config key: "${argv.key}". Valid keys: ${validKeys.join(', ')}`}
368
+ />
369
+ );
370
+ await terminate(1);
371
+ return;
372
+ }
373
+ await setConfigValue(argv.key, argv.value);
374
+ addLog(<SuccessLog message={`${argv.key} = "${argv.value}"`} />);
375
+ await terminate(0);
376
+ }
377
+ )
378
+ .command(
379
+ 'get <key>',
380
+ 'get a configuration value',
381
+ (yargs) =>
382
+ yargs.positional('key', {
383
+ type: 'string',
384
+ demandOption: true,
385
+ description: 'Config key (e.g. subdomain, dateFrom, dateTo)',
386
+ }),
387
+ async (argv) => {
388
+ const validKeys = ['subdomain', 'dateFrom', 'dateTo'];
389
+ if (!validKeys.includes(argv.key)) {
390
+ addLog(
391
+ <ErrorLog
392
+ message={`Unknown config key: "${argv.key}". Valid keys: ${validKeys.join(', ')}`}
393
+ />
394
+ );
395
+ await terminate(1);
396
+ return;
397
+ }
398
+ const val = getConfigValue(argv.key);
399
+ addLog(<Text>{val ?? 'not set'}</Text>);
400
+ await terminate(0);
401
+ }
402
+ )
403
+ .command(
404
+ 'clear <key>',
405
+ 'remove a configuration value',
406
+ (yargs) =>
407
+ yargs.positional('key', {
408
+ type: 'string',
409
+ demandOption: true,
410
+ description: 'Config key (e.g. subdomain, dateFrom, dateTo)',
411
+ }),
412
+ async (argv) => {
413
+ const validKeys = ['subdomain', 'dateFrom', 'dateTo'];
414
+ if (!validKeys.includes(argv.key)) {
415
+ addLog(
416
+ <ErrorLog
417
+ message={`Unknown config key: "${argv.key}". Valid keys: ${validKeys.join(', ')}`}
418
+ />
419
+ );
420
+ await terminate(1);
421
+ return;
422
+ }
423
+ await clearConfigValue(argv.key);
424
+ addLog(<SuccessLog message={`${argv.key} cleared`} />);
425
+ await terminate(0);
426
+ }
427
+ )
428
+ .demandCommand(1, 'specify a subcommand: set, get, or clear')
429
+ )
343
430
  .command(
344
431
  'update',
345
432
  'update the CLI to the latest version',
@@ -463,6 +550,7 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
463
550
  }
464
551
  }
465
552
  )
553
+ .command('analytics', 'view analytics for your documentation', analyticsBuilder)
466
554
  // Print the help menu when the user enters an invalid command.
467
555
  .strictCommands()
468
556
  .demandCommand(1, 'unknown command. see above for the list of supported commands.')
package/src/config.ts CHANGED
@@ -5,6 +5,9 @@ import { CLI_CONFIG_FILE, CONFIG_DIR } from './constants.js';
5
5
 
6
6
  interface MintlifyConfig {
7
7
  telemetryEnabled?: boolean;
8
+ subdomain?: string;
9
+ dateFrom?: string;
10
+ dateTo?: string;
8
11
  }
9
12
 
10
13
  function readConfig(): MintlifyConfig {
@@ -32,3 +35,16 @@ export function isTelemetryEnabled(): boolean {
32
35
  export async function setTelemetryEnabled(enabled: boolean): Promise<void> {
33
36
  await writeConfig({ telemetryEnabled: enabled });
34
37
  }
38
+
39
+ export function getConfigValue(key: string): string | undefined {
40
+ const config = readConfig();
41
+ return config[key as keyof MintlifyConfig] as string | undefined;
42
+ }
43
+
44
+ export async function setConfigValue(key: string, value: string): Promise<void> {
45
+ await writeConfig({ [key]: value });
46
+ }
47
+
48
+ export async function clearConfigValue(key: string): Promise<void> {
49
+ await writeConfig({ [key]: undefined });
50
+ }
package/src/constants.ts CHANGED
@@ -1,14 +1,33 @@
1
+ import { LOCAL_LINKED_CLI_VERSION } from '@mintlify/previewing';
1
2
  import os from 'os';
2
3
  import path from 'path';
3
4
 
5
+ import { getCliVersion } from './helpers.js';
6
+
4
7
  export const HOME_DIR = os.homedir();
5
- export const CMD_EXEC_PATH = process.cwd();
6
8
 
7
9
  export const CONFIG_DIR = path.join(HOME_DIR, '.config', 'mintlify');
8
10
  export const CLI_CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
11
  export const TELEMETRY_ASYNC_TIMEOUT_MS = 10_000;
10
12
 
11
- export const DASHBOARD_URL = process.env.MINTLIFY_DASHBOARD_URL ?? 'http://localhost:3000';
12
- export const TOKEN_ENDPOINT =
13
+ export const CALLBACK_PORT = 11582;
14
+
15
+ const IS_LOCAL_BUILD =
16
+ process.env.CLI_TEST_MODE === 'true' ? true : getCliVersion() == LOCAL_LINKED_CLI_VERSION;
17
+
18
+ export const API_URL =
19
+ process.env.MINTLIFY_API_URL ??
20
+ (IS_LOCAL_BUILD ? 'http://localhost:5000' : 'https://leaves.mintlify.com');
21
+ export const DASHBOARD_URL =
22
+ process.env.MINTLIFY_DASHBOARD_URL ??
23
+ (IS_LOCAL_BUILD ? 'http://localhost:3000' : 'https://dashboard.mintlify.com');
24
+
25
+ const DEV_TOKEN_ENDPOINT =
13
26
  'https://test.stytch.com/v1/public/project-test-2d86347b-dfdb-4609-be69-12d8146220bd/oauth2/token';
14
- export const STYTCH_CLIENT_ID = 'connected-app-test-b597afb3-304a-420f-bc13-dacca566c59f';
27
+ const DEV_STYTCH_CLIENT_ID = 'connected-app-test-b597afb3-304a-420f-bc13-dacca566c59f';
28
+ const PROD_TOKEN_ENDPOINT =
29
+ 'https://api.stytch.com/v1/public/project-live-731b7a04-9ac3-4923-90b8-0806d4aa29d4/oauth2/token';
30
+ const PROD_STYTCH_CLIENT_ID = 'connected-app-live-d813eedd-dbb0-434b-a1f9-2ce69e5efc49';
31
+
32
+ export const TOKEN_ENDPOINT = IS_LOCAL_BUILD ? DEV_TOKEN_ENDPOINT : PROD_TOKEN_ENDPOINT;
33
+ export const STYTCH_CLIENT_ID = IS_LOCAL_BUILD ? DEV_STYTCH_CLIENT_ID : PROD_STYTCH_CLIENT_ID;
package/src/helpers.tsx CHANGED
@@ -23,9 +23,10 @@ import path from 'path';
23
23
  import type { ArgumentsCamelCase } from 'yargs';
24
24
  import yargs from 'yargs';
25
25
 
26
- import { CMD_EXEC_PATH } from './constants.js';
27
26
  import { shutdownPostHog } from './telemetry/client.js';
28
27
 
28
+ export const CMD_EXEC_PATH = process.cwd();
29
+
29
30
  export const checkPort = async (argv: ArgumentsCamelCase): Promise<number | undefined> => {
30
31
  const initialPort = typeof argv.port === 'number' ? argv.port : 3000;
31
32
  if (initialPort === (await detect(initialPort))) return initialPort;
package/src/keyring.ts CHANGED
@@ -4,7 +4,9 @@ const REFRESH_TOKEN_ACCOUNT = 'refresh_token';
4
4
 
5
5
  async function getKeytar(): Promise<typeof import('keytar')> {
6
6
  try {
7
- return await import('keytar');
7
+ const mod = await import('keytar');
8
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- ESM wraps CJS in { default: ... } at runtime despite types
9
+ return mod.default ?? mod;
8
10
  } catch {
9
11
  throw new Error(
10
12
  'keytar is required for credential storage but is not installed. Install it with: npm install keytar'
package/src/login.tsx CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  randomState,
11
11
  } from 'openid-client';
12
12
 
13
+ import { startCallbackServer } from './callbackServer.js';
13
14
  import { DASHBOARD_URL, STYTCH_CLIENT_ID, TOKEN_ENDPOINT } from './constants.js';
14
15
  import { storeCredentials } from './keyring.js';
15
16
 
@@ -36,6 +37,8 @@ export async function login(): Promise<void> {
36
37
  authorizeUrl.searchParams.set('code_challenge', codeChallenge);
37
38
  const url = authorizeUrl.toString();
38
39
 
40
+ const { codePromise, close: closeServer } = await startCallbackServer();
41
+
39
42
  addLog(
40
43
  <Box flexDirection="column" gap={1} paddingY={1}>
41
44
  <Text bold>
@@ -60,7 +63,7 @@ export async function login(): Promise<void> {
60
63
  // Let ink finish rendering before inquirer takes over stdout
61
64
  await new Promise((resolve) => setTimeout(resolve, 50));
62
65
 
63
- const code = await input({
66
+ const inputPromise = input({
64
67
  message: '█',
65
68
  theme: {
66
69
  prefix: chalk.dim(' │'),
@@ -70,6 +73,19 @@ export async function login(): Promise<void> {
70
73
  },
71
74
  });
72
75
 
76
+ let code: string;
77
+ try {
78
+ code = await Promise.race([codePromise, inputPromise]);
79
+ } catch {
80
+ closeServer();
81
+ inputPromise.cancel();
82
+ addLog(<Text color="red">✖ Login cancelled.</Text>);
83
+ return;
84
+ }
85
+
86
+ closeServer();
87
+ inputPromise.cancel();
88
+
73
89
  const res = await fetch(TOKEN_ENDPOINT, {
74
90
  method: 'POST',
75
91
  headers: { 'Content-Type': 'application/json' },
@@ -95,6 +111,5 @@ export async function login(): Promise<void> {
95
111
 
96
112
  const token = body as TokenResponse;
97
113
  await storeCredentials(token.access_token, token.refresh_token);
98
-
99
114
  addLog(<Text color="green">✔ Logged in successfully.</Text>);
100
115
  }
package/src/status.tsx CHANGED
@@ -2,10 +2,9 @@ import { addLog } from '@mintlify/previewing';
2
2
  import { Text } from 'ink';
3
3
  import { z } from 'zod';
4
4
 
5
+ import { API_URL } from './constants.js';
5
6
  import { getAccessToken } from './keyring.js';
6
7
 
7
- const API_URL = process.env.MINTLIFY_API_URL ?? 'http://localhost:5000';
8
-
9
8
  const StatusResponseSchema = z.object({
10
9
  user: z.object({ email: z.string() }),
11
10
  org: z.object({ name: z.string() }),
@@ -1,6 +1,6 @@
1
1
  import { PostHog } from 'posthog-node';
2
2
 
3
- import { TELEMETRY_ASYNC_TIMEOUT_MS } from '../constants.js';
3
+ const TELEMETRY_ASYNC_TIMEOUT_MS = 10_000;
4
4
 
5
5
  const POSTHOG_API_KEY = 'phc_eNuN6Ojnk9O7uWfC17z12AK85fNR0BY6IiGVy0Gfwzw';
6
6
  const POSTHOG_HOST = 'https://ph.mintlify.com';
package/src/workflow.tsx CHANGED
@@ -4,7 +4,7 @@ import fse from 'fs-extra';
4
4
  import { Text } from 'ink';
5
5
  import path from 'path';
6
6
 
7
- import { CMD_EXEC_PATH } from './constants.js';
7
+ import { CMD_EXEC_PATH } from './helpers.js';
8
8
 
9
9
  export function slugify(name: string): string {
10
10
  return name