chatbase 0.0.1 → 0.1.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/LICENSE +21 -0
- package/README.md +1524 -6
- package/bin/run.js +4 -0
- package/dist/base/agent-command.js +40 -0
- package/dist/base/agent-ref.js +16 -0
- package/dist/base/assert-file.js +21 -0
- package/dist/base/base-command.js +203 -0
- package/dist/base/body-input.js +77 -0
- package/dist/base/list-command.js +16 -0
- package/dist/base/sources.js +33 -0
- package/dist/client/chat-helpers.js +173 -0
- package/dist/client/client.js +175 -0
- package/dist/client/files.js +64 -0
- package/dist/client/paginate.js +40 -0
- package/dist/client/pairing.js +76 -0
- package/dist/client/retry.js +40 -0
- package/dist/client/signals.js +43 -0
- package/dist/client/stream.js +79 -0
- package/dist/commands/agents/auto-retrain.js +46 -0
- package/dist/commands/agents/clone.js +28 -0
- package/dist/commands/agents/create.js +43 -0
- package/dist/commands/agents/delete.js +41 -0
- package/dist/commands/agents/get.js +52 -0
- package/dist/commands/agents/list.js +48 -0
- package/dist/commands/agents/styles.js +44 -0
- package/dist/commands/agents/train.js +31 -0
- package/dist/commands/agents/update.js +47 -0
- package/dist/commands/api.js +69 -0
- package/dist/commands/auth/login.js +125 -0
- package/dist/commands/auth/logout.js +38 -0
- package/dist/commands/auth/status.js +86 -0
- package/dist/commands/chat/index.js +210 -0
- package/dist/commands/chat/retry.js +49 -0
- package/dist/commands/config/get.js +40 -0
- package/dist/commands/config/list.js +34 -0
- package/dist/commands/config/set.js +106 -0
- package/dist/commands/conversations/export.js +75 -0
- package/dist/commands/conversations/get.js +69 -0
- package/dist/commands/conversations/list.js +75 -0
- package/dist/commands/health.js +22 -0
- package/dist/commands/helpdesk/statuses.js +31 -0
- package/dist/commands/helpdesk/teams.js +25 -0
- package/dist/commands/messages/feedback.js +64 -0
- package/dist/commands/messages/list.js +55 -0
- package/dist/commands/sources/create.js +134 -0
- package/dist/commands/sources/delete.js +28 -0
- package/dist/commands/sources/get.js +35 -0
- package/dist/commands/sources/list.js +47 -0
- package/dist/commands/sources/restore.js +22 -0
- package/dist/commands/sources/summary.js +33 -0
- package/dist/commands/sources/update.js +76 -0
- package/dist/commands/tickets/create.js +58 -0
- package/dist/commands/tickets/get.js +44 -0
- package/dist/commands/tickets/list.js +96 -0
- package/dist/commands/tickets/messages.js +87 -0
- package/dist/commands/tickets/reply.js +66 -0
- package/dist/commands/tickets/search.js +53 -0
- package/dist/commands/tickets/update.js +38 -0
- package/dist/config/paths.js +22 -0
- package/dist/config/resolve.js +37 -0
- package/dist/config/store.js +38 -0
- package/dist/errors/errors.js +81 -0
- package/dist/hooks/chat-message-hint.js +21 -0
- package/dist/output/color.js +30 -0
- package/dist/output/mode.js +7 -0
- package/dist/output/render.js +0 -0
- package/dist/output/spinner.js +37 -0
- package/dist/repl/chat-repl.js +129 -0
- package/dist/version.js +3 -0
- package/oclif.manifest.json +3948 -0
- package/package.json +92 -4
- package/spec/openapi.json +11843 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { AgentCommand } from '../../base/agent-command.js';
|
|
3
|
+
import { throwIfError } from '../../client/client.js';
|
|
4
|
+
const COLUMNS = [
|
|
5
|
+
{ key: 'ticketNumber', header: 'TICKET' },
|
|
6
|
+
{ key: 'subject', header: 'SUBJECT' },
|
|
7
|
+
{ key: 'statusCategory', header: 'STATUS' },
|
|
8
|
+
{ key: 'channel', header: 'CHANNEL' },
|
|
9
|
+
{ key: 'createdAt', header: 'CREATED' }
|
|
10
|
+
];
|
|
11
|
+
export default class TicketsGet extends AgentCommand {
|
|
12
|
+
static description = 'Show one helpdesk ticket';
|
|
13
|
+
static examples = ['<%= config.bin %> tickets get 42 -a agt_123'];
|
|
14
|
+
static args = {
|
|
15
|
+
ticketNumber: Args.integer({
|
|
16
|
+
required: true,
|
|
17
|
+
description: 'Ticket number'
|
|
18
|
+
})
|
|
19
|
+
};
|
|
20
|
+
static flags = { ...AgentCommand.baseFlags };
|
|
21
|
+
async run() {
|
|
22
|
+
const { args, flags } = await this.parse(TicketsGet);
|
|
23
|
+
const client = this.apiClient(flags);
|
|
24
|
+
const agentId = await this.agentId(flags, client);
|
|
25
|
+
const { data, error, response } = await client.GET('/agents/{agentId}/helpdesk/tickets/{ticketNumber}', {
|
|
26
|
+
params: {
|
|
27
|
+
path: { agentId, ticketNumber: args.ticketNumber }
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
throwIfError(response, error);
|
|
31
|
+
// GET .../tickets/{ticketNumber} returns the Ticket directly (no
|
|
32
|
+
// {data, pagination} envelope) — --json prints it as-is.
|
|
33
|
+
const ticket = data;
|
|
34
|
+
this.printData(flags, data, [
|
|
35
|
+
{
|
|
36
|
+
ticketNumber: String(ticket.ticketNumber ?? ''),
|
|
37
|
+
subject: String(ticket.subject ?? ''),
|
|
38
|
+
statusCategory: String(ticket.statusCategory ?? ''),
|
|
39
|
+
channel: String(ticket.channel ?? ''),
|
|
40
|
+
createdAt: String(ticket.createdAt ?? '')
|
|
41
|
+
}
|
|
42
|
+
], COLUMNS);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { ListCommand } from '../../base/list-command.js';
|
|
3
|
+
import { fetchPages } from '../../client/paginate.js';
|
|
4
|
+
const COLUMNS = [
|
|
5
|
+
{ key: 'ticketNumber', header: 'TICKET' },
|
|
6
|
+
{ key: 'subject', header: 'SUBJECT' },
|
|
7
|
+
{ key: 'statusCategory', header: 'STATUS' },
|
|
8
|
+
{ key: 'channel', header: 'CHANNEL' },
|
|
9
|
+
{ key: 'createdAt', header: 'CREATED' }
|
|
10
|
+
];
|
|
11
|
+
export default class TicketsList extends ListCommand {
|
|
12
|
+
static description = 'List helpdesk tickets for an agent';
|
|
13
|
+
static examples = [
|
|
14
|
+
'<%= config.bin %> tickets list -a agt_123',
|
|
15
|
+
'<%= config.bin %> tickets list -a agt_123 --status new,on_you',
|
|
16
|
+
'<%= config.bin %> tickets list -a agt_123 --all --json'
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
...ListCommand.baseFlags,
|
|
20
|
+
status: Flags.string({
|
|
21
|
+
description: 'Filter by status (comma-separated): new, on_you, on_customer, on_hold, closed, cancelled'
|
|
22
|
+
}),
|
|
23
|
+
channel: Flags.string({
|
|
24
|
+
description: 'Filter by channel (comma-separated, e.g. email,api)'
|
|
25
|
+
}),
|
|
26
|
+
'assignee-id': Flags.string({
|
|
27
|
+
description: 'Filter by assignee UUID, or "none" for unassigned'
|
|
28
|
+
}),
|
|
29
|
+
'team-id': Flags.string({
|
|
30
|
+
description: 'Filter by team UUID, or "none" for no team'
|
|
31
|
+
}),
|
|
32
|
+
'created-after': Flags.string({
|
|
33
|
+
description: 'Only tickets created after this ISO 8601 date'
|
|
34
|
+
}),
|
|
35
|
+
'created-before': Flags.string({
|
|
36
|
+
description: 'Only tickets created before this ISO 8601 date'
|
|
37
|
+
}),
|
|
38
|
+
'sort-by': Flags.string({
|
|
39
|
+
description: 'Sort field',
|
|
40
|
+
options: ['createdAt', 'updatedAt', 'lastMessageAt']
|
|
41
|
+
}),
|
|
42
|
+
order: Flags.string({
|
|
43
|
+
description: 'Sort direction',
|
|
44
|
+
options: ['asc', 'desc']
|
|
45
|
+
}),
|
|
46
|
+
'include-total': Flags.boolean({
|
|
47
|
+
description: 'Include pagination.total in the response'
|
|
48
|
+
})
|
|
49
|
+
};
|
|
50
|
+
async run() {
|
|
51
|
+
const { flags } = await this.parse(TicketsList);
|
|
52
|
+
const client = this.apiClient(flags);
|
|
53
|
+
const agentId = await this.agentId(flags, client);
|
|
54
|
+
const extraQuery = {};
|
|
55
|
+
if (flags.status)
|
|
56
|
+
extraQuery.status = flags.status;
|
|
57
|
+
if (flags.channel)
|
|
58
|
+
extraQuery.channel = flags.channel;
|
|
59
|
+
if (flags['assignee-id'])
|
|
60
|
+
extraQuery.assigneeId = flags['assignee-id'];
|
|
61
|
+
if (flags['team-id'])
|
|
62
|
+
extraQuery.teamId = flags['team-id'];
|
|
63
|
+
if (flags['created-after'])
|
|
64
|
+
extraQuery.createdAfter = flags['created-after'];
|
|
65
|
+
if (flags['created-before'])
|
|
66
|
+
extraQuery.createdBefore = flags['created-before'];
|
|
67
|
+
if (flags['sort-by'])
|
|
68
|
+
extraQuery.sortBy = flags['sort-by'];
|
|
69
|
+
if (flags.order)
|
|
70
|
+
extraQuery.order = flags.order;
|
|
71
|
+
if (flags['include-total'])
|
|
72
|
+
extraQuery.includeTotal = true;
|
|
73
|
+
const { pages, items } = await fetchPages((query) => client.GET('/agents/{agentId}/helpdesk/tickets', {
|
|
74
|
+
params: {
|
|
75
|
+
path: { agentId },
|
|
76
|
+
query: { ...query, ...extraQuery }
|
|
77
|
+
}
|
|
78
|
+
}), { limit: flags.limit, cursor: flags.cursor, all: flags.all });
|
|
79
|
+
const rows = items.map((t) => ({
|
|
80
|
+
ticketNumber: String(t.ticketNumber ?? ''),
|
|
81
|
+
subject: String(t.subject ?? ''),
|
|
82
|
+
statusCategory: String(t.statusCategory ?? ''),
|
|
83
|
+
channel: String(t.channel ?? ''),
|
|
84
|
+
createdAt: String(t.createdAt ?? '')
|
|
85
|
+
}));
|
|
86
|
+
const last = pages.at(-1);
|
|
87
|
+
// --json must stay the raw API shape even when --all merges pages
|
|
88
|
+
const raw = pages.length === 1
|
|
89
|
+
? pages[0]
|
|
90
|
+
: { data: items, pagination: last?.pagination };
|
|
91
|
+
this.printData(flags, raw, rows, COLUMNS);
|
|
92
|
+
if (!flags.all && last?.pagination.hasMore && last.pagination.cursor) {
|
|
93
|
+
this.note(flags, `More results: rerun with --cursor ${last.pagination.cursor} (or use --all)`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { ListCommand } from '../../base/list-command.js';
|
|
3
|
+
import { fetchPages } from '../../client/paginate.js';
|
|
4
|
+
import { UsageError } from '../../errors/errors.js';
|
|
5
|
+
const COLUMNS = [
|
|
6
|
+
{ key: 'id', header: 'ID' },
|
|
7
|
+
{ key: 'type', header: 'TYPE' },
|
|
8
|
+
{ key: 'sender', header: 'SENDER' },
|
|
9
|
+
{ key: 'content', header: 'CONTENT' },
|
|
10
|
+
{ key: 'createdAt', header: 'CREATED' }
|
|
11
|
+
];
|
|
12
|
+
function senderLabel(sender) {
|
|
13
|
+
if (!sender)
|
|
14
|
+
return '';
|
|
15
|
+
return sender.name ?? sender.email ?? sender.type ?? '';
|
|
16
|
+
}
|
|
17
|
+
export default class TicketsMessages extends ListCommand {
|
|
18
|
+
static description = "List a ticket's message thread";
|
|
19
|
+
static examples = [
|
|
20
|
+
'<%= config.bin %> tickets messages 42 -a agt_123',
|
|
21
|
+
'<%= config.bin %> tickets messages 42 -a agt_123 --all --json'
|
|
22
|
+
];
|
|
23
|
+
static args = {
|
|
24
|
+
ticketNumber: Args.integer({
|
|
25
|
+
required: false,
|
|
26
|
+
description: 'Ticket number (alternative to --ticket)'
|
|
27
|
+
})
|
|
28
|
+
};
|
|
29
|
+
static flags = {
|
|
30
|
+
...ListCommand.baseFlags,
|
|
31
|
+
ticket: Flags.integer({
|
|
32
|
+
description: 'Ticket number'
|
|
33
|
+
}),
|
|
34
|
+
types: Flags.string({
|
|
35
|
+
description: 'Message types to include (comma-separated): reply, note, event (default: reply,note)'
|
|
36
|
+
}),
|
|
37
|
+
order: Flags.string({
|
|
38
|
+
description: 'Sort direction',
|
|
39
|
+
options: ['asc', 'desc']
|
|
40
|
+
})
|
|
41
|
+
};
|
|
42
|
+
async run() {
|
|
43
|
+
const { args, flags } = await this.parse(TicketsMessages);
|
|
44
|
+
// Positional and flag are alternatives — `tickets get <n>` and
|
|
45
|
+
// `tickets update <n>` set the positional convention, the flag
|
|
46
|
+
// predates it and stays supported.
|
|
47
|
+
const ticketNumber = args.ticketNumber ?? flags.ticket;
|
|
48
|
+
if (ticketNumber === undefined) {
|
|
49
|
+
throw new UsageError('Missing ticket number. Pass it positionally (`tickets messages <number>`) or via --ticket.');
|
|
50
|
+
}
|
|
51
|
+
if (args.ticketNumber !== undefined && flags.ticket !== undefined) {
|
|
52
|
+
throw new UsageError('Pass the ticket number either positionally or via --ticket, not both.');
|
|
53
|
+
}
|
|
54
|
+
const client = this.apiClient(flags);
|
|
55
|
+
const agentId = await this.agentId(flags, client);
|
|
56
|
+
const extraQuery = {};
|
|
57
|
+
if (flags.types)
|
|
58
|
+
extraQuery.types = flags.types;
|
|
59
|
+
if (flags.order)
|
|
60
|
+
extraQuery.order = flags.order;
|
|
61
|
+
const { pages, items } = await fetchPages((query) => client.GET('/agents/{agentId}/helpdesk/tickets/{ticketNumber}/messages', {
|
|
62
|
+
params: {
|
|
63
|
+
path: {
|
|
64
|
+
agentId,
|
|
65
|
+
ticketNumber
|
|
66
|
+
},
|
|
67
|
+
query: { ...query, ...extraQuery }
|
|
68
|
+
}
|
|
69
|
+
}), { limit: flags.limit, cursor: flags.cursor, all: flags.all });
|
|
70
|
+
const rows = items.map((m) => ({
|
|
71
|
+
id: String(m.id ?? ''),
|
|
72
|
+
type: String(m.type ?? ''),
|
|
73
|
+
sender: senderLabel(m.sender),
|
|
74
|
+
content: String(m.contentText ?? m.content ?? ''),
|
|
75
|
+
createdAt: String(m.createdAt ?? '')
|
|
76
|
+
}));
|
|
77
|
+
const last = pages.at(-1);
|
|
78
|
+
// --json must stay the raw API shape even when --all merges pages
|
|
79
|
+
const raw = pages.length === 1
|
|
80
|
+
? pages[0]
|
|
81
|
+
: { data: items, pagination: last?.pagination };
|
|
82
|
+
this.printData(flags, raw, rows, COLUMNS);
|
|
83
|
+
if (!flags.all && last?.pagination.hasMore && last.pagination.cursor) {
|
|
84
|
+
this.note(flags, `More results: rerun with --cursor ${last.pagination.cursor} (or use --all)`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Args, Flags } 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 TicketsReply extends AgentCommand {
|
|
6
|
+
static description = "Post an agent reply to a ticket's message thread";
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> tickets reply 42 -m "On it" --author-email sam@example.com -a agt_123'
|
|
9
|
+
];
|
|
10
|
+
static args = {
|
|
11
|
+
ticketNumber: Args.integer({
|
|
12
|
+
required: false,
|
|
13
|
+
description: 'Ticket number (alternative to --ticket)'
|
|
14
|
+
})
|
|
15
|
+
};
|
|
16
|
+
static flags = {
|
|
17
|
+
...AgentCommand.baseFlags,
|
|
18
|
+
ticket: Flags.integer({
|
|
19
|
+
description: 'Ticket number'
|
|
20
|
+
}),
|
|
21
|
+
message: Flags.string({
|
|
22
|
+
char: 'm',
|
|
23
|
+
required: true,
|
|
24
|
+
description: 'Reply body as GitHub-flavored Markdown'
|
|
25
|
+
}),
|
|
26
|
+
'author-id': Flags.string({
|
|
27
|
+
description: 'Platform user id of the team member the reply is attributed to (exactly one of --author-id/--author-email)',
|
|
28
|
+
exactlyOne: ['author-id', 'author-email']
|
|
29
|
+
}),
|
|
30
|
+
'author-email': Flags.string({
|
|
31
|
+
description: 'Email of the team member the reply is attributed to (exactly one of --author-id/--author-email)',
|
|
32
|
+
exactlyOne: ['author-id', 'author-email']
|
|
33
|
+
})
|
|
34
|
+
};
|
|
35
|
+
async run() {
|
|
36
|
+
const { args, flags } = await this.parse(TicketsReply);
|
|
37
|
+
// Positional and flag are alternatives — `tickets get <n>` and
|
|
38
|
+
// `tickets update <n>` set the positional convention, the flag
|
|
39
|
+
// predates it and stays supported.
|
|
40
|
+
const ticketNumber = args.ticketNumber ?? flags.ticket;
|
|
41
|
+
if (ticketNumber === undefined) {
|
|
42
|
+
throw new UsageError('Missing ticket number. Pass it positionally (`tickets reply <number>`) or via --ticket.');
|
|
43
|
+
}
|
|
44
|
+
if (args.ticketNumber !== undefined && flags.ticket !== undefined) {
|
|
45
|
+
throw new UsageError('Pass the ticket number either positionally or via --ticket, not both.');
|
|
46
|
+
}
|
|
47
|
+
const client = this.apiClient(flags);
|
|
48
|
+
const agentId = await this.agentId(flags, client);
|
|
49
|
+
const body = {
|
|
50
|
+
type: 'reply',
|
|
51
|
+
content: flags.message,
|
|
52
|
+
...(flags['author-id'] ? { authorId: flags['author-id'] } : {}),
|
|
53
|
+
...(flags['author-email']
|
|
54
|
+
? { authorEmail: flags['author-email'] }
|
|
55
|
+
: {})
|
|
56
|
+
};
|
|
57
|
+
const { error, response } = await client.POST('/agents/{agentId}/helpdesk/tickets/{ticketNumber}/messages', {
|
|
58
|
+
params: {
|
|
59
|
+
path: { agentId, ticketNumber }
|
|
60
|
+
},
|
|
61
|
+
body
|
|
62
|
+
});
|
|
63
|
+
throwIfError(response, error);
|
|
64
|
+
this.success(flags, `Reply posted to ticket ${ticketNumber}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { AgentCommand } from '../../base/agent-command.js';
|
|
3
|
+
import { throwIfError } from '../../client/client.js';
|
|
4
|
+
const COLUMNS = [
|
|
5
|
+
{ key: 'ticketNumber', header: 'TICKET' },
|
|
6
|
+
{ key: 'subject', header: 'SUBJECT' },
|
|
7
|
+
{ key: 'statusCategory', header: 'STATUS' },
|
|
8
|
+
{ key: 'channel', header: 'CHANNEL' },
|
|
9
|
+
{ key: 'createdAt', header: 'CREATED' }
|
|
10
|
+
];
|
|
11
|
+
export default class TicketsSearch extends AgentCommand {
|
|
12
|
+
static description = 'Search tickets by message content';
|
|
13
|
+
static examples = [
|
|
14
|
+
'<%= config.bin %> tickets search "refund not received" -a agt_123',
|
|
15
|
+
'<%= config.bin %> tickets search "refund" -a agt_123 --limit 10 --json'
|
|
16
|
+
];
|
|
17
|
+
static args = {
|
|
18
|
+
query: Args.string({
|
|
19
|
+
required: true,
|
|
20
|
+
description: 'Free-text search terms (matched against ticket messages)'
|
|
21
|
+
})
|
|
22
|
+
};
|
|
23
|
+
static flags = {
|
|
24
|
+
...AgentCommand.baseFlags,
|
|
25
|
+
limit: Flags.integer({
|
|
26
|
+
description: 'Number of results (1–50, default 20)',
|
|
27
|
+
min: 1,
|
|
28
|
+
max: 50
|
|
29
|
+
})
|
|
30
|
+
};
|
|
31
|
+
async run() {
|
|
32
|
+
const { args, flags } = await this.parse(TicketsSearch);
|
|
33
|
+
const client = this.apiClient(flags);
|
|
34
|
+
const agentId = await this.agentId(flags, client);
|
|
35
|
+
const { data, error, response } = await client.POST('/agents/{agentId}/helpdesk/tickets/search', {
|
|
36
|
+
params: { path: { agentId } },
|
|
37
|
+
body: {
|
|
38
|
+
query: args.query,
|
|
39
|
+
limit: flags.limit ?? 20
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
throwIfError(response, error);
|
|
43
|
+
const result = data;
|
|
44
|
+
const rows = result.data.map((t) => ({
|
|
45
|
+
ticketNumber: String(t.ticketNumber ?? ''),
|
|
46
|
+
subject: String(t.subject ?? ''),
|
|
47
|
+
statusCategory: String(t.statusCategory ?? ''),
|
|
48
|
+
channel: String(t.channel ?? ''),
|
|
49
|
+
createdAt: String(t.createdAt ?? '')
|
|
50
|
+
}));
|
|
51
|
+
this.printData(flags, data, rows, COLUMNS);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
export default class TicketsUpdate extends AgentCommand {
|
|
7
|
+
static description = "Update a ticket's status, assignee, and/or team";
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> tickets update 42 --data \'{"statusCategory":"closed"}\' -a agt_123'
|
|
10
|
+
];
|
|
11
|
+
static args = {
|
|
12
|
+
ticketNumber: Args.integer({
|
|
13
|
+
required: true,
|
|
14
|
+
description: 'Ticket number'
|
|
15
|
+
})
|
|
16
|
+
};
|
|
17
|
+
static flags = {
|
|
18
|
+
...AgentCommand.baseFlags,
|
|
19
|
+
...bodyFieldFlags,
|
|
20
|
+
data: Flags.string({
|
|
21
|
+
description: 'JSON body (@file, @-, or inline). Fields: statusId, statusCategory, assigneeId, assigneeEmail, teamId'
|
|
22
|
+
})
|
|
23
|
+
};
|
|
24
|
+
async run() {
|
|
25
|
+
const { args, flags } = await this.parse(TicketsUpdate);
|
|
26
|
+
const body = await readBodyData(flags.data, flags.field);
|
|
27
|
+
const client = this.apiClient(flags);
|
|
28
|
+
const agentId = await this.agentId(flags, client);
|
|
29
|
+
const { error, response } = await client.PATCH('/agents/{agentId}/helpdesk/tickets/{ticketNumber}', {
|
|
30
|
+
params: {
|
|
31
|
+
path: { agentId, ticketNumber: args.ticketNumber }
|
|
32
|
+
},
|
|
33
|
+
body: body
|
|
34
|
+
});
|
|
35
|
+
throwIfError(response, error);
|
|
36
|
+
this.success(flags, `Updated ticket ${args.ticketNumber}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every path the CLI touches on disk, per the XDG Base Directory spec —
|
|
3
|
+
* config (user-created, not regenerable), state/logs (machine-generated
|
|
4
|
+
* history), cache (disposable — cleanup tools may purge it anytime).
|
|
5
|
+
* These three directories are the CLI's ENTIRE disk footprint; the README's
|
|
6
|
+
* uninstall instructions promise exactly them, so never write anywhere else.
|
|
7
|
+
*
|
|
8
|
+
* Deliberate choices: XDG applies on macOS too (matching `gh`, not
|
|
9
|
+
* ~/Library), and these are functions rather than constants because the
|
|
10
|
+
* env overrides must be read at call time — tests stub XDG_* per test.
|
|
11
|
+
*/
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
function xdg(envVar, fallback) {
|
|
15
|
+
const v = process.env[envVar];
|
|
16
|
+
return v && v.length > 0 ? v : path.join(os.homedir(), fallback);
|
|
17
|
+
}
|
|
18
|
+
export const configDir = () => path.join(xdg('XDG_CONFIG_HOME', '.config'), 'chatbase');
|
|
19
|
+
export const configFile = () => path.join(configDir(), 'config.json');
|
|
20
|
+
export const stateDir = () => path.join(xdg('XDG_STATE_HOME', '.local/state'), 'chatbase');
|
|
21
|
+
export const logsDir = () => path.join(stateDir(), 'logs');
|
|
22
|
+
export const cacheDir = () => path.join(xdg('XDG_CACHE_HOME', '.cache'), 'chatbase');
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readUserConfig } from './store.js';
|
|
2
|
+
export function resolveApiKey() {
|
|
3
|
+
const env = process.env.CHATBASE_API_KEY;
|
|
4
|
+
if (env && env.length > 0)
|
|
5
|
+
return { value: env, source: 'CHATBASE_API_KEY' };
|
|
6
|
+
const stored = readUserConfig().apiKey;
|
|
7
|
+
if (stored)
|
|
8
|
+
return { value: stored, source: 'user config' };
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
export function resolveAgent(flag) {
|
|
12
|
+
if (flag)
|
|
13
|
+
return { value: flag, source: 'flag' };
|
|
14
|
+
const env = process.env.CHATBASE_AGENT_ID;
|
|
15
|
+
if (env && env.length > 0)
|
|
16
|
+
return { value: env, source: 'CHATBASE_AGENT_ID' };
|
|
17
|
+
const stored = readUserConfig().agent;
|
|
18
|
+
if (stored)
|
|
19
|
+
return { value: stored, source: 'user config' };
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
export function resolveTimeoutMs() {
|
|
23
|
+
const env = process.env.CHATBASE_TIMEOUT;
|
|
24
|
+
if (env && /^\d+$/.test(env))
|
|
25
|
+
return Number(env);
|
|
26
|
+
return readUserConfig().timeoutMs ?? 30000;
|
|
27
|
+
}
|
|
28
|
+
/** Where resolveTimeoutMs()'s value came from — split out for `config get/list`,
|
|
29
|
+
* which need to name the source without duplicating the precedence logic above. */
|
|
30
|
+
export function resolveTimeoutSource() {
|
|
31
|
+
const env = process.env.CHATBASE_TIMEOUT;
|
|
32
|
+
if (env && /^\d+$/.test(env))
|
|
33
|
+
return 'CHATBASE_TIMEOUT';
|
|
34
|
+
if (readUserConfig().timeoutMs !== undefined)
|
|
35
|
+
return 'user config';
|
|
36
|
+
return 'default';
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { configDir, configFile } from './paths.js';
|
|
4
|
+
export function readUserConfig() {
|
|
5
|
+
let contents;
|
|
6
|
+
try {
|
|
7
|
+
contents = fs.readFileSync(configFile(), 'utf8');
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(contents);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
fs.rmSync(configFile(), { force: true });
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Atomic write: full JSON into a temp file, then rename over config.json.
|
|
22
|
+
* A crash/Ctrl-C at any line leaves either the complete old file or the
|
|
23
|
+
* complete new one — never a truncated config holding half an API key.
|
|
24
|
+
*
|
|
25
|
+
* Load-bearing details: the temp file MUST live in the destination
|
|
26
|
+
* directory (rename is only atomic within one filesystem — /tmp may be
|
|
27
|
+
* another); the PID suffix keeps concurrent CLI processes from clobbering
|
|
28
|
+
* each other's in-flight writes. Modes: dir 0700 / file 0600, since this
|
|
29
|
+
* file stores the credential.
|
|
30
|
+
*/
|
|
31
|
+
export function writeUserConfig(config) {
|
|
32
|
+
fs.mkdirSync(configDir(), { recursive: true, mode: 0o700 });
|
|
33
|
+
const tmp = path.join(configDir(), `.config.json.tmp-${process.pid}`);
|
|
34
|
+
fs.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}\n`, {
|
|
35
|
+
mode: 0o600
|
|
36
|
+
});
|
|
37
|
+
fs.renameSync(tmp, configFile());
|
|
38
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export class UsageError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export class ApiError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
status;
|
|
6
|
+
requestId;
|
|
7
|
+
details;
|
|
8
|
+
remediation;
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
super(opts.message);
|
|
11
|
+
this.code = opts.code;
|
|
12
|
+
this.status = opts.status;
|
|
13
|
+
this.requestId = opts.requestId;
|
|
14
|
+
this.details = opts.details;
|
|
15
|
+
this.remediation = opts.remediation;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Remediation advice appended to API errors, resolved in two tiers:
|
|
20
|
+
* exact error code first (this map), HTTP status fallback second
|
|
21
|
+
* (STATUS_REMEDIATIONS). The server's own message always prints; these
|
|
22
|
+
* only add "here's what to do about it".
|
|
23
|
+
*/
|
|
24
|
+
const REMEDIATIONS = {
|
|
25
|
+
AUTH_MISSING_API_KEY: 'Run `chatbase auth login`, or set CHATBASE_API_KEY. Keys live in chatbase.co → Workspace Settings → API Keys.',
|
|
26
|
+
AUTH_INVALID_API_KEY: 'Your key was rejected. Run `chatbase auth login` with a fresh key, or check CHATBASE_API_KEY.',
|
|
27
|
+
AUTH_EXPIRED_API_KEY: 'This API key has expired. Run `chatbase auth login` to authenticate again.',
|
|
28
|
+
AUTH_INSUFFICIENT_PERMISSIONS: 'This API key does not have permission for this operation. Check its scopes with `chatbase auth status`, re-pair with broader access via `chatbase auth login`, or ask a workspace admin.',
|
|
29
|
+
SUBSCRIPTION_API_RESTRICTED_PLAN: 'API access requires the Standard plan or higher — upgrade at chatbase.co.',
|
|
30
|
+
VALIDATION_INVALID_BODY: 'Fix the fields above and retry.'
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Fallback tier: best-guess advice for error CODES this CLI version doesn't
|
|
34
|
+
* know (e.g. the API added one after this release), keyed by HTTP status.
|
|
35
|
+
* No 403 fallback: the API has many distinct 403 codes (AGENT_LIMIT_REACHED,
|
|
36
|
+
* HELPDESK_NOT_ENABLED, plan gates, ...) whose server messages are already
|
|
37
|
+
* self-explanatory — a blanket guess was actively misleading for most.
|
|
38
|
+
*/
|
|
39
|
+
const STATUS_REMEDIATIONS = {
|
|
40
|
+
404: 'Resource not found — check the ID (agent IDs live in your dashboard).',
|
|
41
|
+
429: 'Rate limited — the CLI already retried; wait for the reset and try again.'
|
|
42
|
+
};
|
|
43
|
+
function isErrorEnvelope(body) {
|
|
44
|
+
return (typeof body === 'object' &&
|
|
45
|
+
body !== null &&
|
|
46
|
+
typeof body.error === 'object' &&
|
|
47
|
+
typeof body.error?.code === 'string');
|
|
48
|
+
}
|
|
49
|
+
export function parseErrorResponse(status, body, requestId) {
|
|
50
|
+
if (isErrorEnvelope(body)) {
|
|
51
|
+
const { code, message, details } = body.error;
|
|
52
|
+
return new ApiError({
|
|
53
|
+
code,
|
|
54
|
+
message,
|
|
55
|
+
status,
|
|
56
|
+
requestId,
|
|
57
|
+
details,
|
|
58
|
+
remediation: REMEDIATIONS[code] ?? STATUS_REMEDIATIONS[status]
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return new ApiError({
|
|
62
|
+
code: `HTTP_${status}`,
|
|
63
|
+
message: `Request failed with status ${status}`,
|
|
64
|
+
status,
|
|
65
|
+
requestId,
|
|
66
|
+
remediation: STATUS_REMEDIATIONS[status]
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export function formatApiError(err, color) {
|
|
70
|
+
const lines = [color.red(`✗ ${err.message} (${err.code})`)];
|
|
71
|
+
if (err.details && typeof err.details === 'object') {
|
|
72
|
+
for (const [field, problem] of Object.entries(err.details)) {
|
|
73
|
+
lines.push(` ${field} ${String(problem)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (err.requestId)
|
|
77
|
+
lines.push(color.dim(` request id: ${err.requestId}`));
|
|
78
|
+
if (err.remediation)
|
|
79
|
+
lines.push(` ${err.remediation}`);
|
|
80
|
+
return lines.join('\n');
|
|
81
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chat` is both a command and a topic (`chat retry`), so oclif resolves
|
|
3
|
+
* `chatbase chat "hello"` by looking for a subcommand named "hello" and
|
|
4
|
+
* lands in command_not_found. Without this hook, plugin-not-found prints
|
|
5
|
+
* a baffling `"chat hello" is not a chatbase command` — catch that one
|
|
6
|
+
* miss and point at the real input forms instead.
|
|
7
|
+
*/
|
|
8
|
+
const hook = async function (opts) {
|
|
9
|
+
// oclif joins the attempted command with its internal ':' separator —
|
|
10
|
+
// `chatbase chat "what are your hours?"` arrives as
|
|
11
|
+
// { id: 'chat:what are your hours?', argv: [] }.
|
|
12
|
+
const argv = opts.argv ?? [];
|
|
13
|
+
const parts = [...(opts.id ?? '').split(':'), ...argv].filter(Boolean);
|
|
14
|
+
if (parts[0] !== 'chat' || parts.length < 2)
|
|
15
|
+
return;
|
|
16
|
+
const attempted = parts.slice(1).join(' ');
|
|
17
|
+
this.error(`chat takes its message via -m, not as an argument:\n` +
|
|
18
|
+
` chatbase chat -m ${JSON.stringify(attempted)}\n` +
|
|
19
|
+
`You can also pipe stdin, or run \`chatbase chat\` alone for the interactive REPL.`, { exit: 2 });
|
|
20
|
+
};
|
|
21
|
+
export default hook;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function colorEnabled(stream, noColorFlag = false) {
|
|
2
|
+
const force = process.env.FORCE_COLOR;
|
|
3
|
+
if (force && force.length > 0 && force !== '0')
|
|
4
|
+
return true;
|
|
5
|
+
if (noColorFlag)
|
|
6
|
+
return false;
|
|
7
|
+
const no = process.env.NO_COLOR;
|
|
8
|
+
if (no && no.length > 0)
|
|
9
|
+
return false;
|
|
10
|
+
if (process.env.TERM === 'dumb')
|
|
11
|
+
return false;
|
|
12
|
+
return stream.isTTY === true;
|
|
13
|
+
}
|
|
14
|
+
const wrap = (open) => (s) => `\x1b[${open}m${s}\x1b[0m`;
|
|
15
|
+
const identity = (s) => s;
|
|
16
|
+
export function paint(enabled) {
|
|
17
|
+
if (!enabled)
|
|
18
|
+
return {
|
|
19
|
+
red: identity,
|
|
20
|
+
green: identity,
|
|
21
|
+
yellow: identity,
|
|
22
|
+
dim: identity
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
red: wrap('31'),
|
|
26
|
+
green: wrap('32'),
|
|
27
|
+
yellow: wrap('33'),
|
|
28
|
+
dim: wrap('2')
|
|
29
|
+
};
|
|
30
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
2
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
3
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
4
|
+
const CLEAR_LINE = '\r\x1b[2K';
|
|
5
|
+
/**
|
|
6
|
+
* Transient stderr spinner for operations with dead air (file uploads,
|
|
7
|
+
* non-streaming chat). Returns a stop() that erases the line. No-op when
|
|
8
|
+
* stderr isn't a TTY — CI logs and pipes never see spinner frames.
|
|
9
|
+
* `delayMs` holds the spinner back so sub-second operations never flicker.
|
|
10
|
+
*/
|
|
11
|
+
/** Convenience for the common `<suppress> ? noop : startSpinner(...)`
|
|
12
|
+
* guard — --quiet (and streaming, where tokens are their own feedback)
|
|
13
|
+
* suppress the spinner entirely. */
|
|
14
|
+
export function maybeSpinner(suppress, text, delayMs = 0) {
|
|
15
|
+
return suppress ? () => { } : startSpinner(text, delayMs);
|
|
16
|
+
}
|
|
17
|
+
export function startSpinner(text, delayMs = 0) {
|
|
18
|
+
if (!process.stderr.isTTY)
|
|
19
|
+
return () => { };
|
|
20
|
+
let i = 0;
|
|
21
|
+
let timer;
|
|
22
|
+
const delay = setTimeout(() => {
|
|
23
|
+
process.stderr.write(HIDE_CURSOR);
|
|
24
|
+
timer = setInterval(() => {
|
|
25
|
+
process.stderr.write(`\r${FRAMES[i++ % FRAMES.length]} ${text}`);
|
|
26
|
+
}, 80);
|
|
27
|
+
timer.unref();
|
|
28
|
+
}, delayMs);
|
|
29
|
+
delay.unref();
|
|
30
|
+
return () => {
|
|
31
|
+
clearTimeout(delay);
|
|
32
|
+
if (timer) {
|
|
33
|
+
clearInterval(timer);
|
|
34
|
+
process.stderr.write(CLEAR_LINE + SHOW_CURSOR);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|