@circleback/cli 0.3.5 → 0.4.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.
package/README.md CHANGED
@@ -13,7 +13,7 @@ Requires Node.js 18 or later. The package installs two binaries: `circleback` an
13
13
  ## Quick start
14
14
 
15
15
  ```bash
16
- # Authenticate via browser
16
+ # Log in with your browser
17
17
  cb auth login
18
18
 
19
19
  # Search recent meetings
@@ -39,9 +39,9 @@ cb meetings --json
39
39
 
40
40
  | Command | Description |
41
41
  | ------------------------------ | ------------------------------------------------------------ |
42
- | `cb auth login` | Login with Circleback via browser |
43
- | `cb auth login --api-key` | Authenticate with a Public API key |
44
- | `cb auth logout` | Clear stored login credentials |
42
+ | `cb auth login` | Log in to Circleback with your browser |
43
+ | `cb auth login --api-key` | Log in with an API key |
44
+ | `cb auth logout` | Sign out on this device |
45
45
  | `cb update` | Update CLI to the latest version |
46
46
  | `cb meetings [search]` | Search meetings |
47
47
  | `cb meetings read <ids...>` | Read detailed meeting info, notes, and action items (max 50) |
@@ -86,12 +86,12 @@ Pass `--json` to any command to get raw JSON instead of formatted tables. Useful
86
86
  cb meetings "standup" --json | jq '.[].name'
87
87
  ```
88
88
 
89
- ## Authentication
89
+ ## Login
90
90
 
91
91
  ```bash
92
- cb auth login # Opens the browser for OAuth
93
- cb auth login --api-key # Prompts securely for a Public API key
94
- cb auth logout # Clears stored credentials
92
+ cb auth login # Opens your browser to log in
93
+ cb auth login --api-key # Asks for your API key securely
94
+ cb auth logout # Signs out on this device
95
95
  ```
96
96
 
97
97
  API-key login links to [Settings → API keys](https://circleback.ai/settings?tab=api-access), verifies the key, and stores it locally on your device.
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getCredentialSnapshot = exports.getEnvironmentApiKey = void 0;
3
+ exports.getCredentialSnapshot = exports.getLoginStatus = exports.getEnvironmentApiKey = void 0;
4
+ const node_util_1 = require("node:util");
5
+ const zod_1 = require("zod");
4
6
  const constants_1 = require("../constants");
5
7
  const types_1 = require("../types");
6
8
  const oauth_1 = require("./oauth");
@@ -10,6 +12,49 @@ const getEnvironmentApiKey = () => {
10
12
  return apiKey || undefined;
11
13
  };
12
14
  exports.getEnvironmentApiKey = getEnvironmentApiKey;
15
+ const fetchLoginStatus = async (credentials) => {
16
+ if (credentials.type === types_1.CredentialType.OAuth &&
17
+ Math.floor(Date.now() / 1000) >= credentials.tokens.expiresAt) {
18
+ return 'Unable to check login';
19
+ }
20
+ try {
21
+ const bearerCredential = credentials.type === types_1.CredentialType.OAuth
22
+ ? credentials.tokens.accessToken
23
+ : credentials.apiKey;
24
+ const response = await fetch(`${constants_1.BASE_URL}/api/oauth/test`, {
25
+ headers: { Authorization: `Bearer ${bearerCredential}` },
26
+ signal: AbortSignal.timeout(5_000),
27
+ });
28
+ if (response.status === 401)
29
+ return 'Not logged in';
30
+ if (!response.ok)
31
+ return 'Unable to check login';
32
+ const account = zod_1.z
33
+ .object({ email: zod_1.z.email(), apiKeyName: zod_1.z.string().optional() })
34
+ .parse(await response.json());
35
+ if (credentials.type === types_1.CredentialType.ApiKey) {
36
+ const name = (0, node_util_1.stripVTControlCharacters)(account.apiKeyName ?? '')
37
+ .replace(/[\p{Cc}\p{Cf}]/gu, '')
38
+ .trim();
39
+ return name
40
+ ? `Logged in with API key “${name}”`
41
+ : 'Logged in with an API key';
42
+ }
43
+ return `Logged in as ${account.email}`;
44
+ }
45
+ catch {
46
+ return 'Unable to check login';
47
+ }
48
+ };
49
+ const getLoginStatus = async () => {
50
+ try {
51
+ return await fetchLoginStatus(requireCredentials());
52
+ }
53
+ catch {
54
+ return 'Not logged in';
55
+ }
56
+ };
57
+ exports.getLoginStatus = getLoginStatus;
13
58
  const requireCredentials = () => {
14
59
  const environmentApiKey = (0, exports.getEnvironmentApiKey)();
15
60
  if (environmentApiKey) {
@@ -24,7 +69,7 @@ const requireCredentials = () => {
24
69
  }
25
70
  const storedCredentials = (0, tokenStorage_1.loadCredentials)();
26
71
  if (!storedCredentials) {
27
- throw new Error(`Not authenticated. Run \`circleback auth login\` or set ${constants_1.API_KEY_ENVIRONMENT_VARIABLE}.`);
72
+ throw new Error(`Not logged in. Run \`circleback auth login\` or set ${constants_1.API_KEY_ENVIRONMENT_VARIABLE}.`);
28
73
  }
29
74
  if ((0, types_1.isStoredApiKey)(storedCredentials)) {
30
75
  return {
@@ -13,7 +13,7 @@ const SUCCESS_HTML = `
13
13
  <head><title>Circleback CLI</title></head>
14
14
  <body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
15
15
  <div style="text-align: center;">
16
- <h1>Authentication successful</h1>
16
+ <h1>Login complete</h1>
17
17
  <p>You can close this tab and return to the terminal.</p>
18
18
  </div>
19
19
  </body>
@@ -44,7 +44,7 @@ const startCallbackServer = () => new Promise((resolve, reject) => {
44
44
  .replace(/>/g, '&gt;')
45
45
  .replace(/"/g, '&quot;')
46
46
  .replace(/'/g, '&#039;');
47
- response.end(`<h1>Authentication failed: ${escapedError}</h1>`);
47
+ response.end(`<h1>Login failed: ${escapedError}</h1>`);
48
48
  callbackReject(new Error(`OAuth error: ${error}.`));
49
49
  return;
50
50
  }
@@ -72,7 +72,7 @@ const startCallbackServer = () => new Promise((resolve, reject) => {
72
72
  waitForCallback: async () => {
73
73
  const timeout = setTimeout(() => {
74
74
  shutdown();
75
- callbackReject(new Error('Authentication timed out. Please try again.'));
75
+ callbackReject(new Error('Login timed out. Please try again.'));
76
76
  }, constants_1.CALLBACK_TIMEOUT_MILLISECONDS);
77
77
  server.once('close', () => clearTimeout(timeout));
78
78
  const result = await callbackPromise;
@@ -14,16 +14,16 @@ const constants_1 = require("../constants");
14
14
  const output_1 = require("../helpers/output");
15
15
  const types_1 = require("../types");
16
16
  const loginCommand = new commander_1.Command('login')
17
- .description('Authenticate with Circleback.')
18
- .option('--api-key', 'Authenticate with a Public API key')
17
+ .description('Log in to Circleback.')
18
+ .option('--api-key', 'Log in with an API key')
19
19
  .action(async (options) => {
20
20
  try {
21
21
  if ((0, credentials_1.getEnvironmentApiKey)()) {
22
- (0, output_1.printText)(`${constants_1.API_KEY_ENVIRONMENT_VARIABLE} is active. Unset it before changing stored authentication.`, output_1.TextType.Warning);
22
+ (0, output_1.printText)(`${constants_1.API_KEY_ENVIRONMENT_VARIABLE} is active. Unset it before logging in with another account or API key.`, output_1.TextType.Warning);
23
23
  return;
24
24
  }
25
25
  if ((0, tokenStorage_1.loadCredentials)()) {
26
- (0, output_1.printText)('Already authenticated. Run `circleback auth logout` first to re-authenticate.', output_1.TextType.Warning);
26
+ (0, output_1.printText)('Already logged in. Run `cb auth logout` first to log in again.', output_1.TextType.Warning);
27
27
  return;
28
28
  }
29
29
  if (options.apiKey) {
@@ -33,39 +33,39 @@ const loginCommand = new commander_1.Command('login')
33
33
  const apiKey = await (0, apiKey_1.promptForApiKey)();
34
34
  await (0, apiKey_1.validateApiKey)(apiKey);
35
35
  (0, tokenStorage_1.saveCredentials)({ type: types_1.CredentialType.ApiKey, apiKey });
36
- (0, output_1.printText)('API key authentication successful.', output_1.TextType.Success);
36
+ (0, output_1.printText)('Logged in with your API key.', output_1.TextType.Success);
37
37
  return;
38
38
  }
39
- (0, output_1.printText)('Opening your browser for authentication…', output_1.TextType.Default);
39
+ (0, output_1.printText)('Opening your browser to log in…', output_1.TextType.Default);
40
40
  await (0, oauth_1.login)();
41
- (0, output_1.printText)('Authentication successful.', output_1.TextType.Success);
41
+ (0, output_1.printText)('Logged in successfully.', output_1.TextType.Success);
42
42
  }
43
43
  catch (error) {
44
- const message = error instanceof Error ? error.message : 'Authentication failed.';
44
+ const message = error instanceof Error ? error.message : 'Login failed.';
45
45
  (0, output_1.printText)(message, output_1.TextType.Error);
46
46
  process.exit(1);
47
47
  }
48
48
  });
49
49
  const logoutCommand = new commander_1.Command('logout')
50
- .description('Clear stored authentication credentials.')
50
+ .description('Sign out on this device.')
51
51
  .action(() => {
52
52
  const environmentApiKey = (0, credentials_1.getEnvironmentApiKey)();
53
53
  const didClearCredentials = (0, tokenStorage_1.clearCredentials)();
54
54
  if (!didClearCredentials) {
55
55
  if (environmentApiKey) {
56
- (0, output_1.printText)(`No stored credentials to clear. ${constants_1.API_KEY_ENVIRONMENT_VARIABLE} is still active.`, output_1.TextType.Warning);
56
+ (0, output_1.printText)(`No saved login to clear. ${constants_1.API_KEY_ENVIRONMENT_VARIABLE} is still active.`, output_1.TextType.Warning);
57
57
  return;
58
58
  }
59
- (0, output_1.printText)('Not currently authenticated.', output_1.TextType.Warning);
59
+ (0, output_1.printText)('Not logged in.', output_1.TextType.Warning);
60
60
  return;
61
61
  }
62
62
  if (environmentApiKey) {
63
- (0, output_1.printText)(`Stored credentials cleared. ${constants_1.API_KEY_ENVIRONMENT_VARIABLE} is still active.`, output_1.TextType.Warning);
63
+ (0, output_1.printText)(`Saved login cleared. ${constants_1.API_KEY_ENVIRONMENT_VARIABLE} is still active.`, output_1.TextType.Warning);
64
64
  return;
65
65
  }
66
- (0, output_1.printText)('Logged out successfully.', output_1.TextType.Success);
66
+ (0, output_1.printText)('Signed out successfully.', output_1.TextType.Success);
67
67
  });
68
68
  exports.authCommand = new commander_1.Command('auth')
69
- .description('Manage authentication.')
69
+ .description('Log in or sign out.')
70
70
  .addCommand(loginCommand)
71
71
  .addCommand(logoutCommand);
@@ -33,7 +33,7 @@ exports.searchMeetingsCommand = new commander_1.Command('search')
33
33
  intent: query ?? 'Listing recent meetings.',
34
34
  pageIndex,
35
35
  };
36
- if (query)
36
+ if (typeof query === 'string')
37
37
  args['searchTerm'] = query;
38
38
  if (options.last) {
39
39
  args['startDate'] = (0, date_1.getStartDateFromDaysAgo)((0, parse_1.parseIntOption)(options.last, '--last'));
@@ -17,12 +17,12 @@ const standaloneCommands = [auth_1.authCommand, update_1.updateCommand];
17
17
  const commandGroups = [
18
18
  {
19
19
  name: 'meetings',
20
- description: 'Search meetings with `cb meetings search [term]` and read them with `cb meetings read <id>`.',
20
+ description: 'Find meetings and read notes.',
21
21
  subcommands: [meetings_1.searchMeetingsCommand, meetings_1.readMeetingsCommand],
22
22
  },
23
23
  {
24
24
  name: 'transcripts',
25
- description: 'Search transcripts with `cb transcripts search <term>` and read them with `cb transcripts read <id>`.',
25
+ description: 'Find and read meeting transcripts.',
26
26
  subcommands: [transcripts_1.searchTranscriptsCommand, transcripts_1.getTranscriptsCommand],
27
27
  },
28
28
  {
@@ -9,6 +9,7 @@ const SECTION_HEADERS = {
9
9
  'Options:': 'options',
10
10
  'Commands:': 'commands',
11
11
  'Arguments:': 'arguments',
12
+ 'Examples:': 'commands',
12
13
  };
13
14
  const styleLine = (line, section) => {
14
15
  if (/^Usage:/.test(line)) {
@@ -25,7 +26,7 @@ const styleLine = (line, section) => {
25
26
  const match = line.match(/^(\s+)(.+?)( +)(.*)$/);
26
27
  if (match) {
27
28
  return [
28
- `${match[1]}${chalk_1.default.bold(match[2])}${match[3]}${match[4]}`,
29
+ `${match[1]}${chalk_1.default.cyan(match[2])}${match[3]}${chalk_1.default.dim(match[4])}`,
29
30
  section,
30
31
  ];
31
32
  }
@@ -3,19 +3,38 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.BANNER = exports.tableStyle = exports.brand = void 0;
6
+ exports.formatWelcomeBanner = exports.tableStyle = exports.brand = void 0;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const constants_1 = require("../constants");
9
9
  exports.brand = chalk_1.default.hex(constants_1.BRAND_COLOR);
10
10
  const secondary = chalk_1.default.hex(constants_1.SECONDARY_COLOR);
11
11
  exports.tableStyle = { head: [], border: [] };
12
- exports.BANNER = [
13
- '',
14
- ` ${exports.brand.bold(constants_1.CLIENT_PACKAGE_NAME)} ${chalk_1.default.dim(`v${constants_1.CLIENT_VERSION}`)}`,
15
- constants_1.ASCII_LOGO.split('\n')
16
- .map((line) => line
17
- .replace(/=+/g, (match) => (0, exports.brand)(match))
18
- .replace(/\++/g, (match) => secondary(match)))
19
- .join('\n'),
20
- '',
21
- ].join('\n');
12
+ const formatWelcomeBanner = (width, loginStatus) => {
13
+ const logo = constants_1.ASCII_LOGO.trimEnd().split('\n').slice(1);
14
+ const logoWidth = Math.max(...logo.map((line) => line.length));
15
+ const title = `${constants_1.CLIENT_NAME} v${constants_1.CLIENT_VERSION}`;
16
+ const details = [
17
+ `${exports.brand.bold(constants_1.CLIENT_NAME)} ${chalk_1.default.dim(`v${constants_1.CLIENT_VERSION}`)}`,
18
+ '',
19
+ chalk_1.default.bold(loginStatus),
20
+ ];
21
+ const isWide = width >= logoWidth + 4 + Math.max(title.length, loginStatus.length);
22
+ const detailsStart = Math.floor((logo.length - details.length) / 2);
23
+ const artwork = logo.map((line, index) => {
24
+ const colored = line
25
+ .replace(/=+/g, (match) => (0, exports.brand)(match))
26
+ .replace(/\++/g, (match) => secondary(match));
27
+ const detail = isWide ? details[index - detailsStart] : undefined;
28
+ return detail
29
+ ? `${colored}${' '.repeat(logoWidth - line.length + 4)}${detail}`
30
+ : colored;
31
+ });
32
+ return [
33
+ '',
34
+ ...artwork,
35
+ '',
36
+ ...(isWide ? [] : details.map((line) => ` ${line}`)),
37
+ '',
38
+ ].join('\n');
39
+ };
40
+ exports.formatWelcomeBanner = formatWelcomeBanner;
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const commander_1 = require("commander");
5
+ const credentials_1 = require("./auth/credentials");
5
6
  const registry_1 = require("./commands/registry");
6
7
  const constants_1 = require("./constants");
7
8
  const help_1 = require("./helpers/help");
@@ -11,19 +12,23 @@ const update_1 = require("./helpers/update");
11
12
  const program = new commander_1.Command();
12
13
  const updateCheck = (0, update_1.getUpdateCheckResult)(constants_1.CLIENT_PACKAGE_NAME);
13
14
  const configureCommandOutput = (command) => {
15
+ command.helpOption('-h, --help', 'Display help for command');
16
+ if (command.commands.length > 0) {
17
+ command.addHelpCommand('help [command]', 'Display help for command');
18
+ }
14
19
  command.configureOutput({
15
- writeOut: (text) => (0, output_1.printText)((0, help_1.styleHelpOutput)(text).trimEnd(), output_1.TextType.Default),
16
- writeErr: (text) => (0, output_1.printTextToStderr)((0, help_1.styleHelpOutput)(text).trimEnd(), output_1.TextType.Default),
20
+ writeOut: (text) => process.stdout.write((0, help_1.styleHelpOutput)(text)),
21
+ writeErr: (text) => process.stderr.write((0, help_1.styleHelpOutput)(text)),
17
22
  outputError: (text) => (0, output_1.printText)((0, help_1.styleHelpOutput)(text).trim(), output_1.TextType.Error),
18
23
  });
19
24
  command.commands.forEach(configureCommandOutput);
20
25
  };
21
26
  program
22
27
  .name('circleback')
23
- .description('Search and access meetings, emails, calendar events, and more.')
24
- .version(constants_1.CLIENT_VERSION)
28
+ .version(constants_1.CLIENT_VERSION, '-V, --version', 'Output the version number')
25
29
  .option('--json', 'Output raw JSON instead of formatted text')
26
- .addHelpText('before', style_1.BANNER)
30
+ .allowExcessArguments(false)
31
+ .action(() => program.help())
27
32
  .exitOverride();
28
33
  (0, registry_1.getAllCommands)().reduce((p, command) => p.addCommand(command), program);
29
34
  configureCommandOutput(program);
@@ -35,7 +40,27 @@ program.hook('postAction', async (_thisCommand, actionCommand) => {
35
40
  }
36
41
  catch { }
37
42
  });
38
- program.parseAsync().catch((error) => {
43
+ const run = async () => {
44
+ const isRootHelp = process.argv
45
+ .slice(2)
46
+ .every((argument) => ['--help', '-h', '--json', 'help', '--'].includes(argument));
47
+ if (isRootHelp) {
48
+ const loginStatus = await (0, credentials_1.getLoginStatus)();
49
+ program
50
+ .addHelpText('before', () => (0, style_1.formatWelcomeBanner)(process.stdout.columns ?? 80, loginStatus))
51
+ .addHelpText('before', [
52
+ 'Examples:',
53
+ ' cb auth login Log in to Circleback',
54
+ ' cb meetings See recent meetings',
55
+ ' cb meetings "product review" Find a meeting',
56
+ ' cb transcripts "pricing" Search transcript text',
57
+ ' cb meetings --json Get JSON for scripts',
58
+ '',
59
+ ].join('\n'));
60
+ }
61
+ await program.parseAsync();
62
+ };
63
+ run().catch((error) => {
39
64
  if (error instanceof commander_1.CommanderError)
40
65
  process.exit(error.exitCode);
41
66
  (0, output_1.printText)(error instanceof Error ? error.message : String(error), output_1.TextType.Error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@circleback/cli",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "description": "Circleback CLI. Search and access meetings, emails, calendar events, and more.",
5
5
  "license": "MIT",
6
6
  "author": "Circleback <support@circleback.ai> (https://circleback.ai)",