chatbase 0.0.1 → 0.1.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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1651 -6
  3. package/bin/run.js +4 -0
  4. package/dist/base/agent-command.js +40 -0
  5. package/dist/base/agent-ref.js +16 -0
  6. package/dist/base/assert-file.js +21 -0
  7. package/dist/base/base-command.js +203 -0
  8. package/dist/base/body-input.js +77 -0
  9. package/dist/base/list-command.js +16 -0
  10. package/dist/base/sources.js +33 -0
  11. package/dist/client/chat-helpers.js +173 -0
  12. package/dist/client/client.js +175 -0
  13. package/dist/client/files.js +64 -0
  14. package/dist/client/paginate.js +40 -0
  15. package/dist/client/pairing.js +75 -0
  16. package/dist/client/retry.js +40 -0
  17. package/dist/client/signals.js +43 -0
  18. package/dist/client/stream.js +79 -0
  19. package/dist/commands/agents/auto-retrain.js +46 -0
  20. package/dist/commands/agents/clone.js +28 -0
  21. package/dist/commands/agents/create.js +43 -0
  22. package/dist/commands/agents/delete.js +41 -0
  23. package/dist/commands/agents/get.js +52 -0
  24. package/dist/commands/agents/list.js +48 -0
  25. package/dist/commands/agents/styles.js +44 -0
  26. package/dist/commands/agents/train.js +31 -0
  27. package/dist/commands/agents/update.js +47 -0
  28. package/dist/commands/api.js +69 -0
  29. package/dist/commands/auth/login.js +128 -0
  30. package/dist/commands/auth/logout.js +40 -0
  31. package/dist/commands/auth/status.js +88 -0
  32. package/dist/commands/chat/index.js +210 -0
  33. package/dist/commands/chat/retry.js +49 -0
  34. package/dist/commands/config/get.js +40 -0
  35. package/dist/commands/config/list.js +34 -0
  36. package/dist/commands/config/set.js +106 -0
  37. package/dist/commands/conversations/export.js +75 -0
  38. package/dist/commands/conversations/get.js +69 -0
  39. package/dist/commands/conversations/list.js +75 -0
  40. package/dist/commands/conversations/tool-result.js +74 -0
  41. package/dist/commands/health.js +22 -0
  42. package/dist/commands/helpdesk/statuses.js +31 -0
  43. package/dist/commands/helpdesk/teams.js +25 -0
  44. package/dist/commands/messages/feedback.js +64 -0
  45. package/dist/commands/messages/list.js +55 -0
  46. package/dist/commands/sources/create.js +134 -0
  47. package/dist/commands/sources/delete.js +28 -0
  48. package/dist/commands/sources/get.js +35 -0
  49. package/dist/commands/sources/list.js +47 -0
  50. package/dist/commands/sources/restore.js +22 -0
  51. package/dist/commands/sources/summary.js +33 -0
  52. package/dist/commands/sources/update.js +76 -0
  53. package/dist/commands/tickets/create.js +58 -0
  54. package/dist/commands/tickets/get.js +44 -0
  55. package/dist/commands/tickets/list.js +96 -0
  56. package/dist/commands/tickets/messages.js +87 -0
  57. package/dist/commands/tickets/reply.js +66 -0
  58. package/dist/commands/tickets/search.js +53 -0
  59. package/dist/commands/tickets/update.js +38 -0
  60. package/dist/commands/whatsapp/send-template.js +94 -0
  61. package/dist/commands/whatsapp/templates.js +55 -0
  62. package/dist/config/paths.js +22 -0
  63. package/dist/config/resolve.js +37 -0
  64. package/dist/config/store.js +38 -0
  65. package/dist/errors/errors.js +81 -0
  66. package/dist/hooks/chat-message-hint.js +21 -0
  67. package/dist/output/color.js +30 -0
  68. package/dist/output/mode.js +7 -0
  69. package/dist/output/render.js +0 -0
  70. package/dist/output/spinner.js +37 -0
  71. package/dist/repl/chat-repl.js +129 -0
  72. package/dist/version.js +3 -0
  73. package/oclif.manifest.json +4264 -0
  74. package/package.json +96 -4
  75. package/spec/openapi.json +11880 -0
@@ -0,0 +1,41 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base/base-command.js';
3
+ import { throwIfError } from '../../client/client.js';
4
+ import { UsageError } from '../../errors/errors.js';
5
+ export default class AgentsDelete extends BaseCommand {
6
+ static description = 'Permanently delete an agent (cannot be undone)';
7
+ static examples = [
8
+ '<%= config.bin %> agents delete agt_123 --confirm agt_123'
9
+ ];
10
+ static args = {
11
+ agentId: Args.string({ required: true, description: 'Agent ID' })
12
+ };
13
+ static flags = {
14
+ ...BaseCommand.baseFlags,
15
+ confirm: Flags.string({
16
+ description: 'Confirm by repeating the agent ID (required in scripts and CI)'
17
+ })
18
+ };
19
+ async run() {
20
+ const { args, flags } = await this.parse(AgentsDelete);
21
+ if (flags.confirm !== args.agentId) {
22
+ if (flags.confirm)
23
+ throw new UsageError(`--confirm value does not match ${args.agentId}.`);
24
+ if (!process.stdin.isTTY || flags['no-input']) {
25
+ throw new UsageError(`Deleting an agent is permanent. Re-run with --confirm ${args.agentId}`);
26
+ }
27
+ const { input } = await import('@inquirer/prompts');
28
+ const typed = await input({
29
+ message: `Type the agent ID (${args.agentId}) to confirm deletion:`
30
+ });
31
+ if (typed.trim() !== args.agentId)
32
+ throw new UsageError('Confirmation did not match; aborted.');
33
+ }
34
+ const client = this.apiClient(flags);
35
+ const { error, response } = await client.DELETE('/agents/{agentId}', {
36
+ params: { path: { agentId: args.agentId } }
37
+ });
38
+ throwIfError(response, error);
39
+ this.success(flags, `Deleted agent ${args.agentId}`);
40
+ }
41
+ }
@@ -0,0 +1,52 @@
1
+ import { Args } from '@oclif/core';
2
+ import { AgentCommand } from '../../base/agent-command.js';
3
+ import { throwIfError } from '../../client/client.js';
4
+ import { UsageError } from '../../errors/errors.js';
5
+ export default class AgentsGet extends AgentCommand {
6
+ static description = 'Show one agent';
7
+ static examples = [
8
+ '<%= config.bin %> agents get agt_123',
9
+ '<%= config.bin %> agents get'
10
+ ];
11
+ static args = {
12
+ agentId: Args.string({ required: false, description: 'Agent ID' })
13
+ };
14
+ static flags = { ...AgentCommand.baseFlags };
15
+ async run() {
16
+ const { args, flags } = await this.parse(AgentsGet);
17
+ if (args.agentId && (flags.agent || flags['agent-name'])) {
18
+ throw new UsageError('Pass the agent ID either positionally or via -a/--agent-name, not both.');
19
+ }
20
+ const client = this.apiClient(flags);
21
+ const agentId = args.agentId ?? (await this.agentId(flags, client));
22
+ const { data, error, response } = await client.GET('/agents/{agentId}', {
23
+ params: { path: { agentId } }
24
+ });
25
+ throwIfError(response, error);
26
+ const a = data;
27
+ const str = (v) => (v == null ? '' : String(v));
28
+ this.printDetail(flags, data, {
29
+ id: str(a.id),
30
+ name: str(a.name),
31
+ model: str(a.model),
32
+ visibility: str(a.visibility)
33
+ }, [
34
+ { key: 'id', header: 'ID' },
35
+ { key: 'name', header: 'NAME' },
36
+ { key: 'model', header: 'MODEL' },
37
+ { key: 'visibility', header: 'VISIBILITY' }
38
+ ], [
39
+ ['ID', str(a.id)],
40
+ ['Name', str(a.name)],
41
+ ['Model', str(a.model)],
42
+ ['Visibility', str(a.visibility)],
43
+ ['Status', str(a.status)],
44
+ ['Auto-retrain', str(a.autoRetrain)],
45
+ ['Temperature', str(a.temp)],
46
+ ['Size', str(a.size)],
47
+ ['Created', str(a.createdAt)],
48
+ ['Last trained', str(a.lastTrainedAt)],
49
+ ['Instructions', str(a.instructions)]
50
+ ]);
51
+ }
52
+ }
@@ -0,0 +1,48 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base/base-command.js';
3
+ import { fetchPages } from '../../client/paginate.js';
4
+ const COLUMNS = [
5
+ { key: 'id', header: 'ID' },
6
+ { key: 'name', header: 'NAME' },
7
+ { key: 'model', header: 'MODEL' },
8
+ { key: 'visibility', header: 'VISIBILITY' }
9
+ ];
10
+ export default class AgentsList extends BaseCommand {
11
+ static description = 'List all agents in the workspace';
12
+ static examples = [
13
+ '<%= config.bin %> agents list',
14
+ '<%= config.bin %> agents list --json'
15
+ ];
16
+ static flags = {
17
+ ...BaseCommand.baseFlags,
18
+ limit: Flags.integer({
19
+ description: 'Maximum items per page',
20
+ min: 1,
21
+ max: 100
22
+ }),
23
+ cursor: Flags.string({
24
+ description: 'Pagination cursor from a previous page'
25
+ }),
26
+ all: Flags.boolean({ description: 'Fetch every page' })
27
+ };
28
+ async run() {
29
+ const { flags } = await this.parse(AgentsList);
30
+ const client = this.apiClient(flags);
31
+ const { pages, items } = await fetchPages((query) => client.GET('/agents', { params: { query } }), { limit: flags.limit, cursor: flags.cursor, all: flags.all });
32
+ const rows = items.map((a) => ({
33
+ id: String(a.id ?? ''),
34
+ name: String(a.name ?? ''),
35
+ model: String(a.model ?? ''),
36
+ visibility: String(a.visibility ?? '')
37
+ }));
38
+ const last = pages.at(-1);
39
+ // --json must stay the raw API shape even when --all merges pages
40
+ const raw = pages.length === 1
41
+ ? pages[0]
42
+ : { data: items, pagination: last?.pagination };
43
+ this.printData(flags, raw, rows, COLUMNS);
44
+ if (!flags.all && last?.pagination.hasMore && last.pagination.cursor) {
45
+ this.note(flags, `More results: rerun with --cursor ${last.pagination.cursor} (or use --all)`);
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,44 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { AgentCommand } from '../../base/agent-command.js';
3
+ import { bodyFieldFlags } from '../../base/base-command.js';
4
+ import { readBodyData } from '../../base/body-input.js';
5
+ import { throwIfError } from '../../client/client.js';
6
+ import { UsageError } from '../../errors/errors.js';
7
+ export default class AgentsStyles extends AgentCommand {
8
+ static description = 'Update visual styles for an agent';
9
+ static examples = [
10
+ '<%= config.bin %> agents styles agt_123 --data \'{"chat":{"theme":"dark"}}\'',
11
+ '<%= config.bin %> agents styles --data @styles.json'
12
+ ];
13
+ static args = {
14
+ agentId: Args.string({
15
+ required: false,
16
+ description: 'Agent ID'
17
+ })
18
+ };
19
+ static flags = {
20
+ ...AgentCommand.baseFlags,
21
+ ...bodyFieldFlags,
22
+ data: Flags.string({
23
+ required: true,
24
+ description: 'JSON body (@file, @-, or inline). See API docs for style properties'
25
+ })
26
+ };
27
+ async run() {
28
+ const { args, flags } = await this.parse(AgentsStyles);
29
+ if (args.agentId && (flags.agent || flags['agent-name'])) {
30
+ throw new UsageError('Pass the agent ID either positionally or via -a/--agent-name, not both.');
31
+ }
32
+ const stylesData = await readBodyData(flags.data, flags.field);
33
+ const client = this.apiClient(flags);
34
+ const agentId = args.agentId ?? (await this.agentId(flags, client));
35
+ const { error, response } = await client.PUT('/agents/{agentId}/styles', {
36
+ params: { path: { agentId } },
37
+ body: {
38
+ styles: stylesData
39
+ }
40
+ });
41
+ throwIfError(response, error);
42
+ this.success(flags, `Updated styles for ${agentId}`);
43
+ }
44
+ }
@@ -0,0 +1,31 @@
1
+ import { Args } from '@oclif/core';
2
+ import { AgentCommand } from '../../base/agent-command.js';
3
+ import { throwIfError } from '../../client/client.js';
4
+ import { UsageError } from '../../errors/errors.js';
5
+ export default class AgentsTrain extends AgentCommand {
6
+ static description = 'Queue a training job for an agent';
7
+ static examples = [
8
+ '<%= config.bin %> agents train agt_123',
9
+ '<%= config.bin %> agents train'
10
+ ];
11
+ static args = {
12
+ agentId: Args.string({
13
+ required: false,
14
+ description: 'Agent ID to train (defaults to the configured agent, like other commands)'
15
+ })
16
+ };
17
+ static flags = { ...AgentCommand.baseFlags };
18
+ async run() {
19
+ const { args, flags } = await this.parse(AgentsTrain);
20
+ if (args.agentId && (flags.agent || flags['agent-name'])) {
21
+ throw new UsageError('Pass the agent ID either positionally or via -a/--agent-name, not both.');
22
+ }
23
+ const client = this.apiClient(flags);
24
+ const agentId = args.agentId ?? (await this.agentId(flags, client));
25
+ const { error, response } = await client.POST('/agents/{agentId}/train', {
26
+ params: { path: { agentId } }
27
+ });
28
+ throwIfError(response, error);
29
+ this.success(flags, `Training started for ${agentId}`);
30
+ }
31
+ }
@@ -0,0 +1,47 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { AgentCommand } from '../../base/agent-command.js';
3
+ import { bodyFieldFlags } from '../../base/base-command.js';
4
+ import { readBodyData } from '../../base/body-input.js';
5
+ import { throwIfError } from '../../client/client.js';
6
+ import { UsageError } from '../../errors/errors.js';
7
+ export default class AgentsUpdate extends AgentCommand {
8
+ static description = 'Update an existing agent';
9
+ static examples = [
10
+ '<%= config.bin %> agents update agt_123 --name "New Name"',
11
+ '<%= config.bin %> agents update --name "New Name"',
12
+ '<%= config.bin %> agents update agt_123 --data @agent.json'
13
+ ];
14
+ static args = {
15
+ agentId: Args.string({ required: false, description: 'Agent ID' })
16
+ };
17
+ static flags = {
18
+ ...AgentCommand.baseFlags,
19
+ ...bodyFieldFlags,
20
+ name: Flags.string({ description: 'Agent name' }),
21
+ instructions: Flags.string({ description: 'System instructions' }),
22
+ model: Flags.string({ description: 'Model ID' }),
23
+ data: Flags.string({
24
+ description: 'JSON body (@file, @-, or inline). Fields: name, instructions, model, visibility, temp'
25
+ })
26
+ };
27
+ async run() {
28
+ const { args, flags } = await this.parse(AgentsUpdate);
29
+ if (args.agentId && (flags.agent || flags['agent-name'])) {
30
+ throw new UsageError('Pass the agent ID either positionally or via -a/--agent-name, not both.');
31
+ }
32
+ const body = {
33
+ ...(await readBodyData(flags.data, flags.field)),
34
+ ...(flags.name ? { name: flags.name } : {}),
35
+ ...(flags.instructions ? { instructions: flags.instructions } : {}),
36
+ ...(flags.model ? { model: flags.model } : {})
37
+ };
38
+ const client = this.apiClient(flags);
39
+ const agentId = args.agentId ?? (await this.agentId(flags, client));
40
+ const { error, response } = await client.PUT('/agents/{agentId}', {
41
+ params: { path: { agentId } },
42
+ body: body
43
+ });
44
+ throwIfError(response, error);
45
+ this.success(flags, `Updated agent ${agentId}`);
46
+ }
47
+ }
@@ -0,0 +1,69 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand, bodyFieldFlags } from '../base/base-command.js';
3
+ import { parseFields, readBodyData } from '../base/body-input.js';
4
+ import { rawApiFetch } from '../client/client.js';
5
+ import { resolveApiKey } from '../config/resolve.js';
6
+ import { parseErrorResponse, UsageError } from '../errors/errors.js';
7
+ /** k=v -> [k, v], splitting only on the first '=' so values containing '='
8
+ * (base64, JSON snippets, etc.) survive intact. */
9
+ function parseField(field) {
10
+ const idx = field.indexOf('=');
11
+ if (idx === -1) {
12
+ throw new UsageError(`--field must be key=value (got "${field}")`);
13
+ }
14
+ return [field.slice(0, idx), field.slice(idx + 1)];
15
+ }
16
+ export default class Api extends BaseCommand {
17
+ static description = 'Call the Chatbase API directly — an escape hatch for endpoints without a dedicated command';
18
+ static examples = [
19
+ '<%= config.bin %> api GET /agents',
20
+ '<%= config.bin %> api GET /agents --field limit=5',
21
+ '<%= config.bin %> api POST /agents --body \'{"name":"Support Bot"}\'',
22
+ '<%= config.bin %> api PATCH /agents/agt_123 --body @patch.json'
23
+ ];
24
+ static args = {
25
+ method: Args.string({
26
+ required: true,
27
+ options: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
28
+ description: 'HTTP method'
29
+ }),
30
+ path: Args.string({
31
+ required: true,
32
+ description: 'API path relative to /api/v2, e.g. /agents'
33
+ })
34
+ };
35
+ static flags = {
36
+ ...BaseCommand.baseFlags,
37
+ ...bodyFieldFlags,
38
+ query: Flags.string({
39
+ multiple: true,
40
+ description: 'Query param k=v (repeatable)'
41
+ }),
42
+ body: Flags.string({
43
+ description: 'JSON request body (@file, @-, or inline JSON)'
44
+ })
45
+ };
46
+ async run() {
47
+ const { args, flags } = await this.parse(Api);
48
+ const resolved = resolveApiKey();
49
+ if (!resolved) {
50
+ throw new UsageError('Not authenticated. Run `chatbase auth login`, or set CHATBASE_API_KEY.');
51
+ }
52
+ const query = (flags.query ?? []).map(parseField);
53
+ const bodyData = flags.body
54
+ ? await readBodyData(flags.body, flags.field)
55
+ : parseFields(flags.field);
56
+ const hasBody = Object.keys(bodyData).length > 0;
57
+ const res = await rawApiFetch(args.method, args.path, {
58
+ apiKey: resolved.value,
59
+ query,
60
+ body: hasBody ? bodyData : undefined
61
+ });
62
+ if (res.status >= 400) {
63
+ throw parseErrorResponse(res.status, res.body, res.requestId);
64
+ }
65
+ // The escape hatch IS raw by design — always the exact response JSON,
66
+ // regardless of --json/--plain/pretty.
67
+ process.stdout.write(`${JSON.stringify(res.body, null, 2)}\n`);
68
+ }
69
+ }
@@ -0,0 +1,128 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../base/base-command.js';
4
+ import { readStdinToEnd } from '../../base/body-input.js';
5
+ import { rawApiFetch } from '../../client/client.js';
6
+ import { pairingBaseUrl, pollExchange, startPairing } from '../../client/pairing.js';
7
+ import { configFile } from '../../config/paths.js';
8
+ import { writeUserConfig } from '../../config/store.js';
9
+ import { parseErrorResponse, UsageError } from '../../errors/errors.js';
10
+ function tryOpenBrowser(url) {
11
+ try {
12
+ if (process.platform === 'win32') {
13
+ spawn('cmd', ['/c', 'start', '', url], {
14
+ detached: true,
15
+ stdio: 'ignore'
16
+ })
17
+ .on('error', () => { })
18
+ .unref();
19
+ return;
20
+ }
21
+ const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
22
+ spawn(cmd, [url], { detached: true, stdio: 'ignore' })
23
+ .on('error', () => { })
24
+ .unref();
25
+ }
26
+ catch {
27
+ // Synchronous spawn() failure — same best-effort contract.
28
+ }
29
+ }
30
+ export default class AuthLogin extends BaseCommand {
31
+ static description = 'Authenticate with Chatbase — paste an API key or log in via browser';
32
+ static examples = [
33
+ '<%= config.bin %> auth login',
34
+ '<%= config.bin %> auth login --browser',
35
+ 'cat key.txt | <%= config.bin %> auth login --with-token'
36
+ ];
37
+ static flags = {
38
+ ...BaseCommand.baseFlags,
39
+ 'with-token': Flags.boolean({
40
+ description: 'Read the API key from stdin'
41
+ }),
42
+ browser: Flags.boolean({
43
+ description: 'Log in via browser — approve a code at chatbase.co/activate'
44
+ })
45
+ };
46
+ requireAuth = false;
47
+ async run() {
48
+ const { flags } = await this.parse(AuthLogin);
49
+ if (flags['with-token']) {
50
+ if (process.stdin.isTTY)
51
+ throw new UsageError('--with-token reads the key from stdin. Pipe it: chatbase auth login --with-token < key.txt');
52
+ const key = (await readStdinToEnd()).trim();
53
+ if (!key)
54
+ throw new UsageError('No token received on stdin.');
55
+ return this.verifyAndStore(flags, key);
56
+ }
57
+ if (flags.browser) {
58
+ return this.browserLogin(flags);
59
+ }
60
+ if (process.stdin.isTTY && !flags['no-input']) {
61
+ const { select } = await import('@inquirer/prompts');
62
+ const method = await select({
63
+ message: 'How do you want to authenticate?',
64
+ choices: [
65
+ {
66
+ value: 'browser',
67
+ name: 'Log in with browser (recommended)'
68
+ },
69
+ { value: 'paste', name: 'Paste an API key' }
70
+ ]
71
+ });
72
+ if (method === 'browser') {
73
+ return this.browserLogin(flags);
74
+ }
75
+ const { password } = await import('@inquirer/prompts');
76
+ const key = (await password({ message: 'Key:', mask: '●' })).trim();
77
+ if (!key)
78
+ throw new UsageError('No key entered.');
79
+ return this.verifyAndStore(flags, key);
80
+ }
81
+ throw new UsageError('Cannot prompt (no TTY or --no-input). Use: chatbase auth login --with-token < key.txt');
82
+ }
83
+ async browserLogin(flags) {
84
+ const pairing = await startPairing();
85
+ this.note(flags, `\nYour code: ${this.palette(flags).green(pairing.userCode)}\n`);
86
+ this.note(flags, `Open ${pairing.verificationUri} and enter the code to approve.`);
87
+ if (process.stdout.isTTY && !flags['no-input']) {
88
+ tryOpenBrowser(pairing.verificationUri);
89
+ this.note(flags, 'Waiting for approval...');
90
+ }
91
+ const result = await pollExchange(pairing.deviceCode, {
92
+ intervalMs: pairing.interval * 1000,
93
+ timeoutMs: pairing.expiresIn * 1000,
94
+ onPoll: () => {
95
+ if (!flags.quiet) {
96
+ process.stderr.write('.');
97
+ }
98
+ }
99
+ });
100
+ if (!flags.quiet)
101
+ process.stderr.write('\n');
102
+ writeUserConfig({
103
+ apiKey: result.apiKey,
104
+ apiKeySource: 'pairing'
105
+ });
106
+ this.success(flags, `Logged in to workspace ${result.workspace.name}`);
107
+ this.note(flags, `Saved to ${configFile()}`);
108
+ }
109
+ async verifyAndStore(flags, key) {
110
+ const res = await rawApiFetch('GET', '/api/cli-pairing/me', {
111
+ apiKey: key,
112
+ baseUrl: pairingBaseUrl()
113
+ });
114
+ if (res.status === 200) {
115
+ const body = res.body;
116
+ writeUserConfig({ apiKey: key });
117
+ this.success(flags, `Logged in${body.workspace?.name ? ` to workspace ${body.workspace.name}` : ''}`);
118
+ }
119
+ else if (res.status === 404) {
120
+ writeUserConfig({ apiKey: key });
121
+ this.note(flags, 'Key stored (verification unavailable — it will be checked on first use).');
122
+ }
123
+ else {
124
+ throw parseErrorResponse(res.status, res.body, res.requestId);
125
+ }
126
+ this.note(flags, `Saved to ${configFile()}`);
127
+ }
128
+ }
@@ -0,0 +1,40 @@
1
+ import fs from 'node:fs';
2
+ import { BaseCommand } from '../../base/base-command.js';
3
+ import { rawApiFetch } from '../../client/client.js';
4
+ import { pairingBaseUrl } from '../../client/pairing.js';
5
+ import { configFile } from '../../config/paths.js';
6
+ import { readUserConfig } from '../../config/store.js';
7
+ export default class AuthLogout extends BaseCommand {
8
+ static description = 'Remove the stored API key (revokes CLI-paired keys server-side)';
9
+ static examples = ['<%= config.bin %> auth logout'];
10
+ static flags = { ...BaseCommand.baseFlags };
11
+ requireAuth = false;
12
+ async run() {
13
+ const { flags } = await this.parse(AuthLogout);
14
+ const config = readUserConfig();
15
+ if (!config.apiKey) {
16
+ this.note(flags, 'No stored credential — nothing to remove.');
17
+ return;
18
+ }
19
+ if (config.apiKeySource === 'pairing') {
20
+ try {
21
+ const res = await rawApiFetch('DELETE', '/api/cli-pairing/me', {
22
+ apiKey: config.apiKey,
23
+ baseUrl: pairingBaseUrl()
24
+ });
25
+ if (res.status >= 200 && res.status < 300) {
26
+ this.note(flags, 'CLI session revoked server-side.');
27
+ }
28
+ else {
29
+ this.note(flags, this.palette(flags).yellow(`! Could not revoke the key server-side (${res.status}) — revoke it manually at chatbase.co if needed.`));
30
+ }
31
+ }
32
+ catch (err) {
33
+ const detail = err instanceof Error ? err.message : String(err);
34
+ this.note(flags, this.palette(flags).yellow(`! Could not reach the API to revoke the key (${detail}) — revoke it manually at chatbase.co if needed.`));
35
+ }
36
+ }
37
+ fs.rmSync(configFile(), { force: true });
38
+ this.success(flags, 'Logged out (stored config removed).');
39
+ }
40
+ }
@@ -0,0 +1,88 @@
1
+ import { BaseCommand } from '../../base/base-command.js';
2
+ import { DEFAULT_BASE_URL, rawApiFetch, resolveBaseUrl } from '../../client/client.js';
3
+ import { pairingBaseUrl } from '../../client/pairing.js';
4
+ import { resolveApiKey } from '../../config/resolve.js';
5
+ export default class AuthStatus extends BaseCommand {
6
+ static description = 'Show the active credential and where it comes from';
7
+ static examples = ['<%= config.bin %> auth status'];
8
+ static flags = { ...BaseCommand.baseFlags };
9
+ requireAuth = false;
10
+ async run() {
11
+ const { flags } = await this.parse(AuthStatus);
12
+ this.warnIfBaseUrlOverridden(flags);
13
+ const resolved = resolveApiKey();
14
+ if (!resolved) {
15
+ this.note(flags, 'Not authenticated. Run `chatbase auth login`.');
16
+ // Exit 1 so scripts can use `auth status` as an auth probe
17
+ // (same contract as `gh auth status`).
18
+ this.exit(1);
19
+ }
20
+ const tail = resolved.value.length > 8 ? `…${resolved.value.slice(-4)}` : '…****';
21
+ this.note(flags, `Credential: ${tail} (from ${resolved.source})`);
22
+ const res = await rawApiFetch('GET', '/api/cli-pairing/me', {
23
+ apiKey: resolved.value,
24
+ baseUrl: pairingBaseUrl()
25
+ });
26
+ if (res.status === 200) {
27
+ this.renderMe(flags, res.body);
28
+ }
29
+ else if (res.status === 401 || res.status === 403) {
30
+ this.renderAuthError(flags, res.body);
31
+ }
32
+ else {
33
+ this.note(flags, this.palette(flags).yellow(`! Could not verify key (server returned ${res.status})`));
34
+ }
35
+ }
36
+ warnIfBaseUrlOverridden(flags) {
37
+ const baseUrl = resolveBaseUrl();
38
+ if (baseUrl === DEFAULT_BASE_URL)
39
+ return;
40
+ this.note(flags, this.palette(flags).yellow(`! API base overridden: ${baseUrl} (CHATBASE_API_URL)`));
41
+ }
42
+ renderMe(flags, body) {
43
+ this.note(flags, `Workspace: ${body.workspace?.name ?? 'unknown'} (plan: ${body.plan ?? 'unknown'})`);
44
+ const cred = body.credential;
45
+ if (cred?.source === 'cli') {
46
+ this.note(flags, 'Key type: CLI-paired device');
47
+ }
48
+ if (cred?.expiresAt) {
49
+ this.renderExpiry(flags, cred.expiresAt);
50
+ }
51
+ if (cred?.permissions) {
52
+ this.note(flags, `Scopes: ${cred.permissions.join(', ') || 'none'}`);
53
+ }
54
+ else if (cred?.permissions === null) {
55
+ this.note(flags, 'Scopes: full access');
56
+ }
57
+ }
58
+ renderExpiry(flags, expiresAt) {
59
+ const remaining = Math.ceil((new Date(expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
60
+ if (Number.isNaN(remaining)) {
61
+ this.note(flags, this.palette(flags).yellow(`! Could not parse credential expiry (${expiresAt})`));
62
+ }
63
+ else if (remaining <= 0) {
64
+ this.note(flags, this.palette(flags).yellow('! Already expired — re-pair with `chatbase auth login --browser`'));
65
+ }
66
+ else if (remaining <= 7) {
67
+ this.note(flags, this.palette(flags).yellow(`! Expires in ${remaining} day${remaining !== 1 ? 's' : ''} — re-pair with \`chatbase auth login --browser\``));
68
+ }
69
+ else {
70
+ this.note(flags, `Expires in ${remaining} day${remaining !== 1 ? 's' : ''}`);
71
+ }
72
+ }
73
+ renderAuthError(flags, errBody) {
74
+ const code = errBody?.error?.code;
75
+ if (code === 'AUTH_EXPIRED_API_KEY') {
76
+ this.note(flags, this.palette(flags).yellow('! Key has expired — re-pair with `chatbase auth login --browser`'));
77
+ }
78
+ else if (code === 'SUBSCRIPTION_API_RESTRICTED_PLAN') {
79
+ // /me is scope-exempt server-side (any valid key may introspect
80
+ // itself), so the only 403 it returns is the plan restriction —
81
+ // pointing at key scopes here would send users the wrong way.
82
+ this.note(flags, this.palette(flags).yellow("! This workspace's plan does not include API access — a Standard plan or higher is required."));
83
+ }
84
+ else {
85
+ this.note(flags, this.palette(flags).yellow('! Key appears invalid or lacks API access.'));
86
+ }
87
+ }
88
+ }