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,210 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { AgentCommand } from '../../base/agent-command.js';
3
+ import { readStdinToEnd } from '../../base/body-input.js';
4
+ import { fetchRecentHistory, retryChat, runChatTurn, sendChat } from '../../client/chat-helpers.js';
5
+ import { UsageError } from '../../errors/errors.js';
6
+ import { maybeSpinner } from '../../output/spinner.js';
7
+ import { runChatRepl } from '../../repl/chat-repl.js';
8
+ export default class Chat extends AgentCommand {
9
+ static description = 'Send a message to an agent and print its response';
10
+ static examples = [
11
+ '<%= config.bin %> chat -a agt_123 -m "How do I reset my password?"',
12
+ 'echo "summarize our refund policy" | <%= config.bin %> chat -a agt_123',
13
+ '<%= config.bin %> chat -a agt_123 -m "and then?" --conversation conv_123',
14
+ '<%= config.bin %> chat -a agt_123 -m "hi" --no-stream',
15
+ '<%= config.bin %> chat -a agt_123 -m "hi" --json'
16
+ ];
17
+ static flags = {
18
+ ...AgentCommand.baseFlags,
19
+ message: Flags.string({
20
+ char: 'm',
21
+ description: 'Message to send (else read from piped stdin, else an interactive REPL)'
22
+ }),
23
+ conversation: Flags.string({
24
+ description: 'Continue an existing conversation'
25
+ }),
26
+ resume: Flags.boolean({
27
+ description: 'Replay the last few messages when continuing a conversation',
28
+ dependsOn: ['conversation']
29
+ }),
30
+ 'no-stream': Flags.boolean({
31
+ description: 'Wait for the complete response instead of streaming tokens'
32
+ })
33
+ };
34
+ async resolveMessage(flags) {
35
+ if (flags.message)
36
+ return flags.message;
37
+ // A TTY with no -m is handled by run() before this is ever called
38
+ // (it goes to the interactive REPL instead), so reaching here with
39
+ // no message means stdin is a pipe that turned out to be empty.
40
+ const piped = (await readStdinToEnd()).trim();
41
+ if (piped)
42
+ return piped;
43
+ throw new UsageError('No message received on stdin.');
44
+ }
45
+ async run() {
46
+ const { flags } = await this.parse(Chat);
47
+ if (!flags.message && process.stdin.isTTY && !flags['no-input']) {
48
+ await this.runInteractive(flags);
49
+ return;
50
+ }
51
+ if (flags.resume) {
52
+ this.note(flags, this.palette(flags).yellow('! --resume only replays history in the interactive REPL (a TTY with no -m) — ignored here.'));
53
+ }
54
+ // Resolve the message first: it's local (no network), so a failure
55
+ // here doesn't first need working credentials or an agent lookup
56
+ // round trip to fail fast.
57
+ const message = await this.resolveMessage(flags);
58
+ const client = this.apiClient(flags);
59
+ const agentId = await this.agentId(flags, client);
60
+ // --json always forces a non-streaming call so the full envelope is
61
+ // available to print in one shot; --no-stream does the same for
62
+ // plain-text output. Otherwise stream tokens as they arrive.
63
+ const stream = !flags.json && !flags['no-stream'];
64
+ const result = await runChatTurn({
65
+ stream,
66
+ quiet: flags.quiet,
67
+ json: flags.json,
68
+ call: (onText) => sendChat({
69
+ client,
70
+ agentId,
71
+ message,
72
+ conversationId: flags.conversation,
73
+ stream,
74
+ onText
75
+ })
76
+ });
77
+ if (!flags.json) {
78
+ this.printConversationHint(flags, agentId, result.conversationId);
79
+ }
80
+ }
81
+ /**
82
+ * The interactive REPL path: a TTY with no -m and no piped stdin. Builds
83
+ * the `send`/`retry` deps runChatRepl needs — each wraps `sendChat` with
84
+ * streaming to stdout and forwards the per-call `signal` the REPL
85
+ * creates fresh per turn, so Ctrl-C cancels just that one call instead
86
+ * of the process-wide interrupt signal (which would also poison every
87
+ * later request in the session — see client/signals.ts).
88
+ */
89
+ async runInteractive(flags) {
90
+ const client = this.apiClient(flags);
91
+ const agentId = await this.agentId(flags, client);
92
+ // --resume replays the tail of the conversation so the user sees
93
+ // where they left off. Best-effort: a failed history fetch must
94
+ // not block the chat itself.
95
+ if (flags.conversation && flags.resume) {
96
+ try {
97
+ const history = await fetchRecentHistory({
98
+ client,
99
+ agentId,
100
+ conversationId: flags.conversation
101
+ });
102
+ if (history.length > 0) {
103
+ const dim = this.palette(flags).dim;
104
+ this.note(flags, dim(`— resuming ${flags.conversation} —`));
105
+ for (const line of history) {
106
+ const who = line.role === 'user' ? 'you' : 'agent';
107
+ this.note(flags, dim(`${who}: ${line.text}`));
108
+ }
109
+ this.note(flags, dim('—'));
110
+ }
111
+ }
112
+ catch (err) {
113
+ // Still best-effort — the conversation continues either way —
114
+ // but a bad --conversation id and "no history yet" must not
115
+ // look identical.
116
+ const detail = err instanceof Error ? err.message : String(err);
117
+ this.note(flags, this.palette(flags).yellow(`! Could not load history for ${flags.conversation} (${detail}) — continuing without it.`));
118
+ }
119
+ }
120
+ // Streaming still has time-to-first-token dead air — spin until the
121
+ // first token arrives, then let the tokens themselves be the feedback.
122
+ const spinUntilFirstToken = () => {
123
+ const stop = maybeSpinner(flags.quiet, 'Thinking…', 300);
124
+ let stopped = false;
125
+ return () => {
126
+ if (stopped)
127
+ return;
128
+ stopped = true;
129
+ stop();
130
+ };
131
+ };
132
+ // The server returns the assistant message id in the stream's finish
133
+ // metadata — remember it so /retry has a real id to send. The retry
134
+ // endpoint truncates the conversation at that message and re-sends
135
+ // the user message before it; there is no "last" sentinel.
136
+ let lastMessageId;
137
+ const send = async (message, conversationId, signal) => {
138
+ const stop = spinUntilFirstToken();
139
+ try {
140
+ const { conversationId: nextId, messageId } = await sendChat({
141
+ client,
142
+ agentId,
143
+ message,
144
+ conversationId,
145
+ stream: true,
146
+ signal,
147
+ onText: (text) => {
148
+ stop();
149
+ process.stdout.write(text);
150
+ }
151
+ });
152
+ lastMessageId = messageId ?? lastMessageId;
153
+ process.stdout.write('\n');
154
+ return { conversationId: nextId };
155
+ }
156
+ finally {
157
+ stop();
158
+ }
159
+ };
160
+ const retry = async (conversationId, signal) => {
161
+ if (!lastMessageId) {
162
+ // Cold /retry on a --conversation the REPL just resumed:
163
+ // nothing sent this session yet, so look up the last
164
+ // assistant message (history returns the recent tail —
165
+ // enough, since /retry targets the latest response).
166
+ const history = await fetchRecentHistory({
167
+ client,
168
+ agentId,
169
+ conversationId
170
+ });
171
+ const lastAssistant = [...history]
172
+ .reverse()
173
+ .find((line) => line.role === 'assistant' && line.id);
174
+ if (!lastAssistant) {
175
+ throw new UsageError('Nothing to retry yet in this conversation — send a message first.');
176
+ }
177
+ lastMessageId = lastAssistant.id;
178
+ }
179
+ const stop = spinUntilFirstToken();
180
+ try {
181
+ const { messageId } = await retryChat({
182
+ client,
183
+ agentId,
184
+ conversationId,
185
+ messageId: lastMessageId,
186
+ stream: true,
187
+ signal,
188
+ onText: (text) => {
189
+ stop();
190
+ process.stdout.write(text);
191
+ }
192
+ });
193
+ lastMessageId = messageId ?? lastMessageId;
194
+ process.stdout.write('\n');
195
+ }
196
+ finally {
197
+ stop();
198
+ }
199
+ };
200
+ const { conversationId } = await runChatRepl({
201
+ send,
202
+ retry,
203
+ input: process.stdin,
204
+ output: process.stdout,
205
+ info: (msg) => this.note(flags, msg),
206
+ initialConversationId: flags.conversation
207
+ });
208
+ this.printConversationHint(flags, agentId, conversationId);
209
+ }
210
+ }
@@ -0,0 +1,49 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { AgentCommand } from '../../base/agent-command.js';
3
+ import { retryChat, runChatTurn } from '../../client/chat-helpers.js';
4
+ export default class ChatRetry extends AgentCommand {
5
+ static description = 'Retry generating an assistant response (discards that message and everything after it in the conversation)';
6
+ static examples = [
7
+ '<%= config.bin %> chat retry --conversation c_123 -a agt_123 --message-id msg_456',
8
+ '<%= config.bin %> chat retry --conversation c_123 -a agt_123 --message-id msg_456 --no-stream'
9
+ ];
10
+ static flags = {
11
+ ...AgentCommand.baseFlags,
12
+ conversation: Flags.string({
13
+ description: 'The conversation ID to retry in',
14
+ required: true
15
+ }),
16
+ 'message-id': Flags.string({
17
+ description: 'The message ID to retry from',
18
+ required: true
19
+ }),
20
+ 'no-stream': Flags.boolean({
21
+ description: 'Wait for the complete response instead of streaming tokens'
22
+ })
23
+ };
24
+ async run() {
25
+ const { flags } = await this.parse(ChatRetry);
26
+ const client = this.apiClient(flags);
27
+ const agentId = await this.agentId(flags, client);
28
+ // --json always forces a non-streaming call so the full envelope is
29
+ // available to print in one shot; --no-stream does the same for
30
+ // plain-text output. Otherwise stream tokens as they arrive.
31
+ const stream = !flags.json && !flags['no-stream'];
32
+ const result = await runChatTurn({
33
+ stream,
34
+ quiet: flags.quiet,
35
+ json: flags.json,
36
+ call: (onText) => retryChat({
37
+ client,
38
+ agentId,
39
+ conversationId: flags.conversation,
40
+ messageId: flags['message-id'],
41
+ stream,
42
+ onText
43
+ })
44
+ });
45
+ if (!flags.json) {
46
+ this.printConversationHint(flags, agentId, result.conversationId);
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,40 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../base/base-command.js';
3
+ import { resolveAgent, resolveTimeoutMs, resolveTimeoutSource } from '../../config/resolve.js';
4
+ import { UsageError } from '../../errors/errors.js';
5
+ export default class ConfigGet extends BaseCommand {
6
+ static description = 'Print a resolved CLI configuration value and where it comes from';
7
+ static examples = [
8
+ '<%= config.bin %> config get agent',
9
+ '<%= config.bin %> config get timeout'
10
+ ];
11
+ static args = {
12
+ key: Args.string({
13
+ required: true,
14
+ description: 'agent | timeout'
15
+ })
16
+ };
17
+ static flags = { ...BaseCommand.baseFlags };
18
+ requireAuth = false;
19
+ async run() {
20
+ const { args, flags } = await this.parse(ConfigGet);
21
+ const key = args.key.toLowerCase();
22
+ if (key === 'agent') {
23
+ const resolved = resolveAgent();
24
+ if (!resolved) {
25
+ this.note(flags, 'agent is not set.');
26
+ return;
27
+ }
28
+ process.stdout.write(`${resolved.value}\n`);
29
+ this.note(flags, `(from ${resolved.source})`);
30
+ return;
31
+ }
32
+ if (key === 'timeout') {
33
+ const value = resolveTimeoutMs();
34
+ process.stdout.write(`${value}\n`);
35
+ this.note(flags, `(from ${resolveTimeoutSource()})`);
36
+ return;
37
+ }
38
+ throw new UsageError(`Unknown config key "${args.key}". Valid keys: agent, timeout.`);
39
+ }
40
+ }
@@ -0,0 +1,34 @@
1
+ import { BaseCommand } from '../../base/base-command.js';
2
+ import { resolveAgent, resolveTimeoutMs, resolveTimeoutSource } from '../../config/resolve.js';
3
+ export default class ConfigList extends BaseCommand {
4
+ static description = 'List every resolved CLI configuration value and its source';
5
+ static examples = ['<%= config.bin %> config list'];
6
+ static flags = { ...BaseCommand.baseFlags };
7
+ requireAuth = false;
8
+ async run() {
9
+ const { flags } = await this.parse(ConfigList);
10
+ const agent = resolveAgent();
11
+ const timeoutMs = resolveTimeoutMs();
12
+ const rows = [
13
+ {
14
+ key: 'agent',
15
+ value: agent ? agent.value : '<not set>',
16
+ source: agent ? agent.source : 'default'
17
+ },
18
+ {
19
+ key: 'timeout',
20
+ value: `${timeoutMs}ms`,
21
+ source: resolveTimeoutSource()
22
+ }
23
+ ];
24
+ this.printData(flags, rows, rows.map((r) => ({
25
+ key: r.key,
26
+ value: r.value,
27
+ source: r.source
28
+ })), [
29
+ { key: 'key', header: 'KEY' },
30
+ { key: 'value', header: 'VALUE' },
31
+ { key: 'source', header: 'SOURCE' }
32
+ ]);
33
+ }
34
+ }
@@ -0,0 +1,106 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../base/base-command.js';
3
+ import { createApiClient } from '../../client/client.js';
4
+ import { fetchPages } from '../../client/paginate.js';
5
+ import { resolveApiKey } from '../../config/resolve.js';
6
+ import { readUserConfig, writeUserConfig } from '../../config/store.js';
7
+ import { UsageError } from '../../errors/errors.js';
8
+ /** Property names people reach for when about to store a credential —
9
+ * config set never accepts these, no matter the casing. */
10
+ const SECRET_KEYS = new Set(['apikey', 'api_key']);
11
+ export default class ConfigSet extends BaseCommand {
12
+ static description = 'Set a CLI configuration value';
13
+ static examples = [
14
+ '<%= config.bin %> config set agent agt_123',
15
+ '<%= config.bin %> config set agent',
16
+ '<%= config.bin %> config set timeout 60000'
17
+ ];
18
+ static args = {
19
+ key: Args.string({
20
+ required: true,
21
+ description: 'agent | timeout'
22
+ }),
23
+ value: Args.string({
24
+ required: false,
25
+ description: 'New value (omit for agent to pick interactively)'
26
+ })
27
+ };
28
+ static flags = { ...BaseCommand.baseFlags };
29
+ requireAuth = false;
30
+ async run() {
31
+ const { args, flags } = await this.parse(ConfigSet);
32
+ const key = args.key.toLowerCase();
33
+ if (SECRET_KEYS.has(key)) {
34
+ throw new UsageError('Credentials are never stored via `config set` — run `chatbase auth login` instead.');
35
+ }
36
+ if (key === 'agent') {
37
+ if (args.value === '') {
38
+ // Empty value clears — remove the key entirely instead of
39
+ // storing "" and printing the awkward "set to " message.
40
+ const { agent: _cleared, ...rest } = readUserConfig();
41
+ writeUserConfig(rest);
42
+ this.success(flags, 'agent cleared');
43
+ return;
44
+ }
45
+ const agentId = args.value ?? (await this.pickAgent(flags));
46
+ writeUserConfig({ ...readUserConfig(), agent: agentId });
47
+ process.stdout.write(`${agentId}\n`);
48
+ this.success(flags, `agent set to ${agentId}`);
49
+ this.warnIfShadowed(flags);
50
+ return;
51
+ }
52
+ if (key === 'timeout') {
53
+ if (!args.value) {
54
+ throw new UsageError('Usage: chatbase config set timeout <milliseconds>');
55
+ }
56
+ const timeoutMs = Number(args.value);
57
+ if (!/^\d+$/.test(args.value) || timeoutMs < 1) {
58
+ throw new UsageError('timeout must be a positive integer number of milliseconds (>= 1).');
59
+ }
60
+ writeUserConfig({
61
+ ...readUserConfig(),
62
+ timeoutMs
63
+ });
64
+ process.stdout.write(`${args.value}\n`);
65
+ this.success(flags, `timeout set to ${args.value}ms`);
66
+ return;
67
+ }
68
+ throw new UsageError(`Unknown config key "${args.key}". Valid keys: agent, timeout.`);
69
+ }
70
+ warnIfShadowed(flags) {
71
+ const yellow = this.palette(flags).yellow;
72
+ const env = process.env.CHATBASE_AGENT_ID;
73
+ if (env && env.length > 0) {
74
+ this.note(flags, yellow(`! CHATBASE_AGENT_ID=${env} is set and takes precedence — unset it for this value to apply.`));
75
+ }
76
+ }
77
+ /** No value given for `config set agent`: prompt with a picker over
78
+ * GET /agents, but only when there's a TTY to prompt on. */
79
+ async pickAgent(flags) {
80
+ if (!process.stdin.isTTY || flags['no-input']) {
81
+ throw new UsageError('config set agent requires a value in scripts and CI. Usage: chatbase config set agent <agentId>');
82
+ }
83
+ const resolved = resolveApiKey();
84
+ if (!resolved) {
85
+ throw new UsageError('Not authenticated. Run `chatbase auth login`, or set CHATBASE_API_KEY.');
86
+ }
87
+ // Paginate through every page (fetchPages, same helper
88
+ // resolveAgentRef in agent-ref.ts uses) so the picker offers every
89
+ // agent in the workspace, not just whatever fits on the first page.
90
+ const client = createApiClient({ apiKey: resolved.value });
91
+ const { items: agents } = await fetchPages((query) => client.GET('/agents', { params: { query } }), {
92
+ all: true
93
+ });
94
+ if (agents.length === 0) {
95
+ throw new UsageError('No agents found in this workspace — create one first with `chatbase agents create`.');
96
+ }
97
+ const { select } = await import('@inquirer/prompts');
98
+ return select({
99
+ message: 'Select an agent:',
100
+ choices: agents.map((a) => ({
101
+ name: `${a.name ?? a.id} (${a.id})`,
102
+ value: a.id
103
+ }))
104
+ });
105
+ }
106
+ }
@@ -0,0 +1,75 @@
1
+ import fs from 'node:fs';
2
+ import { Flags } from '@oclif/core';
3
+ import { AgentCommand } from '../../base/agent-command.js';
4
+ import { fetchPages } from '../../client/paginate.js';
5
+ import { UsageError } from '../../errors/errors.js';
6
+ export default class ConversationsExport extends AgentCommand {
7
+ static summary = 'Export conversations from every source, with full message history';
8
+ static description = 'Export conversations with full message history, newest first.\n\n' +
9
+ 'This is the only endpoint that returns conversations from every ' +
10
+ 'source — the chat bubble and external integrations (Slack, WhatsApp, ' +
11
+ 'Instagram, Messenger, and the like) as well as API-created ones. ' +
12
+ 'Prefer it over `conversations list` whenever you need real traffic ' +
13
+ 'rather than just programmatically created conversations. Each item ' +
14
+ 'embeds its own `messages` array, so no follow-up `conversations get` ' +
15
+ 'or `messages list` call is needed (and neither works for ' +
16
+ 'bubble/integration conversations).';
17
+ static examples = [
18
+ '<%= config.bin %> conversations export -a agt_123',
19
+ '<%= config.bin %> conversations export -a agt_123 --all -o export.json'
20
+ ];
21
+ // Export is a data-export command, not a display one: it always emits
22
+ // the raw API JSON, in both pretty and --json mode (--plain/--json are
23
+ // inherited but no-ops here — kept only so `-h` documents them like
24
+ // every other command).
25
+ static flags = {
26
+ ...AgentCommand.baseFlags,
27
+ cursor: Flags.string({
28
+ description: 'Opaque cursor from a previous response'
29
+ }),
30
+ // The API caps this endpoint at 20 per page — not the 100 that
31
+ // ListCommand's shared --limit allows — which is why export declares
32
+ // its own flag instead of inheriting that base.
33
+ limit: Flags.integer({
34
+ description: 'Items per page (1-20, default 20)',
35
+ min: 1,
36
+ max: 20
37
+ }),
38
+ all: Flags.boolean({ description: 'Fetch every page' }),
39
+ output: Flags.string({
40
+ char: 'o',
41
+ description: 'Write export JSON to a file instead of stdout'
42
+ })
43
+ };
44
+ async run() {
45
+ const { flags } = await this.parse(ConversationsExport);
46
+ const client = this.apiClient(flags);
47
+ const agentId = await this.agentId(flags, client);
48
+ const { pages, items } = await fetchPages((query) => client.GET('/agents/{agentId}/conversations/export', {
49
+ params: { path: { agentId }, query }
50
+ }), { limit: flags.limit, cursor: flags.cursor, all: flags.all });
51
+ const last = pages.at(-1);
52
+ // Single page stays byte-for-byte the API's envelope; --all merges
53
+ // `data` across pages but keeps that same shape so downstream
54
+ // consumers parse one thing either way.
55
+ const raw = pages.length === 1
56
+ ? pages[0]
57
+ : { data: items, pagination: last?.pagination };
58
+ const json = `${JSON.stringify(raw, null, 2)}\n`;
59
+ if (flags.output) {
60
+ try {
61
+ fs.writeFileSync(flags.output, json);
62
+ }
63
+ catch (e) {
64
+ throw new UsageError(`Cannot write to ${flags.output}: ${e.code ?? e.message}`);
65
+ }
66
+ this.success(flags, `Exported conversations to ${flags.output}`);
67
+ }
68
+ else {
69
+ process.stdout.write(json);
70
+ }
71
+ if (!flags.all && last?.pagination.hasMore && last.pagination.cursor) {
72
+ this.note(flags, `More results: rerun with --cursor ${last.pagination.cursor} (or use --all)`);
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,69 @@
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
+ import { formatEpochSeconds } from '../../output/render.js';
6
+ const COLUMNS = [
7
+ { key: 'id', header: 'ID' },
8
+ { key: 'title', header: 'TITLE' },
9
+ { key: 'status', header: 'STATUS' },
10
+ { key: 'createdAt', header: 'CREATED' },
11
+ { key: 'updatedAt', header: 'UPDATED' }
12
+ ];
13
+ export default class ConversationsGet extends AgentCommand {
14
+ static description = 'Show one conversation';
15
+ static examples = [
16
+ '<%= config.bin %> conversations get conv_123 -a agt_123',
17
+ '<%= config.bin %> conversations get --conversation conv_123 -a agt_123'
18
+ ];
19
+ static args = {
20
+ conversationId: Args.string({
21
+ required: false,
22
+ description: 'Conversation ID (alternative to --conversation)'
23
+ })
24
+ };
25
+ static flags = {
26
+ ...AgentCommand.baseFlags,
27
+ conversation: Flags.string({
28
+ description: 'Conversation ID'
29
+ })
30
+ };
31
+ async run() {
32
+ const { args, flags } = await this.parse(ConversationsGet);
33
+ // Positional and flag are alternatives — `agents get <id>` set the
34
+ // positional convention, the flag predates it and stays supported.
35
+ const conversationId = args.conversationId ?? flags.conversation;
36
+ if (!conversationId) {
37
+ throw new UsageError('Missing conversation ID. Pass it positionally (`conversations get <id>`) or via --conversation.');
38
+ }
39
+ if (args.conversationId && flags.conversation) {
40
+ throw new UsageError('Pass the conversation ID either positionally or via --conversation, not both.');
41
+ }
42
+ const client = this.apiClient(flags);
43
+ const agentId = await this.agentId(flags, client);
44
+ const { data, error, response } = await client.GET('/agents/{agentId}/conversations/{conversationId}', {
45
+ params: {
46
+ path: { agentId, conversationId }
47
+ }
48
+ });
49
+ throwIfError(response, error);
50
+ // GetConversationResponse wraps the conversation (with nested
51
+ // messages) in { data, pagination }; --json prints that whole
52
+ // envelope as-is, but the display row only needs the metadata
53
+ // fields shared with `conversations list`.
54
+ const conversation = data.data;
55
+ // Humans read ISO dates; --plain keeps the raw epoch for scripts.
56
+ const formatTimestamp = this.mode(flags) === 'pretty'
57
+ ? formatEpochSeconds
58
+ : (v) => String(v ?? '');
59
+ this.printData(flags, data, [
60
+ {
61
+ id: String(conversation.id ?? ''),
62
+ title: String(conversation.title ?? ''),
63
+ status: String(conversation.status ?? ''),
64
+ createdAt: formatTimestamp(conversation.createdAt),
65
+ updatedAt: formatTimestamp(conversation.updatedAt)
66
+ }
67
+ ], COLUMNS);
68
+ }
69
+ }
@@ -0,0 +1,75 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { ListCommand } from '../../base/list-command.js';
3
+ import { fetchPages } from '../../client/paginate.js';
4
+ import { formatEpochSeconds } from '../../output/render.js';
5
+ const COLUMNS = [
6
+ { key: 'id', header: 'ID' },
7
+ { key: 'title', header: 'TITLE' },
8
+ { key: 'status', header: 'STATUS' },
9
+ { key: 'createdAt', header: 'CREATED' },
10
+ { key: 'updatedAt', header: 'UPDATED' }
11
+ ];
12
+ /** Printed after every result: the endpoint's scope is the single most
13
+ * common source of "the CLI is broken" reports, because an agent whose
14
+ * traffic is all widget/Slack legitimately lists zero rows here. */
15
+ const SCOPE_NOTE = 'Note: API-created conversations only — use `chatbase conversations export` for widget and integration conversations.';
16
+ export default class ConversationsList extends ListCommand {
17
+ static summary = 'List an agent’s API-created conversations';
18
+ static description = 'List conversations for an agent, newest first.\n\n' +
19
+ 'Scope: the API v2 list endpoint returns only conversations created ' +
20
+ 'programmatically through the API. Conversations from the chat bubble ' +
21
+ 'and external integrations (Slack, WhatsApp, Instagram, Messenger, and ' +
22
+ 'the like) are not accessible here and are not counted in `total`. ' +
23
+ 'Use `chatbase conversations export` to read conversations from every ' +
24
+ 'source — it also embeds full message history, which `conversations ' +
25
+ 'get` and `messages list` cannot retrieve for those conversations.';
26
+ static examples = [
27
+ '<%= config.bin %> conversations list -a agt_123',
28
+ '<%= config.bin %> conversations list -a agt_123 --user usr_456',
29
+ '<%= config.bin %> conversations list -a agt_123 --all --json'
30
+ ];
31
+ static flags = {
32
+ ...ListCommand.baseFlags,
33
+ user: Flags.string({
34
+ description: 'List conversations for a specific user (uses GET /users/{userId}/conversations)'
35
+ })
36
+ };
37
+ async run() {
38
+ const { flags } = await this.parse(ConversationsList);
39
+ const client = this.apiClient(flags);
40
+ const agentId = await this.agentId(flags, client);
41
+ const fetcherPath = flags.user
42
+ ? '/agents/{agentId}/users/{userId}/conversations'
43
+ : '/agents/{agentId}/conversations';
44
+ const { pages, items } = await fetchPages((query) => client.GET(fetcherPath, {
45
+ params: {
46
+ path: {
47
+ agentId,
48
+ ...(flags.user ? { userId: flags.user } : {})
49
+ },
50
+ query
51
+ }
52
+ }), { limit: flags.limit, cursor: flags.cursor, all: flags.all });
53
+ // Humans read ISO dates; --plain keeps the raw epoch for scripts.
54
+ const formatTimestamp = this.mode(flags) === 'pretty'
55
+ ? formatEpochSeconds
56
+ : (v) => String(v ?? '');
57
+ const rows = items.map((c) => ({
58
+ id: String(c.id ?? ''),
59
+ title: String(c.title ?? ''),
60
+ status: String(c.status ?? ''),
61
+ createdAt: formatTimestamp(c.createdAt),
62
+ updatedAt: formatTimestamp(c.updatedAt)
63
+ }));
64
+ const last = pages.at(-1);
65
+ // --json must stay the raw API shape even when --all merges pages
66
+ const raw = pages.length === 1
67
+ ? pages[0]
68
+ : { data: items, pagination: last?.pagination };
69
+ this.printData(flags, raw, rows, COLUMNS);
70
+ if (!flags.all && last?.pagination.hasMore && last.pagination.cursor) {
71
+ this.note(flags, `More results: rerun with --cursor ${last.pagination.cursor} (or use --all)`);
72
+ }
73
+ this.note(flags, SCOPE_NOTE);
74
+ }
75
+ }