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,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
+ }
@@ -0,0 +1,7 @@
1
+ export function selectMode(flags, stream = process.stdout) {
2
+ if (flags.json)
3
+ return 'json';
4
+ if (flags.plain)
5
+ return 'plain';
6
+ return stream.isTTY ? 'pretty' : 'plain';
7
+ }
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
+ }
@@ -0,0 +1,129 @@
1
+ import readline from 'node:readline';
2
+ const GREETING = 'Type /exit or press Ctrl-D to quit. Ctrl-C cancels a response. /help for commands.';
3
+ const HELP_LINES = [
4
+ '/exit Quit the REPL (same as Ctrl-D)',
5
+ '/new Start a fresh conversation (clears the current conversation id)',
6
+ '/retry Regenerate the last response (replaces it in the conversation)',
7
+ '/id Print the current conversation id',
8
+ '/help Show this list'
9
+ ];
10
+ /** True for the AbortError a cancelled `send`/`retry` call rejects with —
11
+ * matches the same duck-typed check `classifyError` uses in base-command.ts. */
12
+ function isAbortError(err) {
13
+ return err?.name === 'AbortError';
14
+ }
15
+ /**
16
+ * Runs the interactive chat REPL. Dependency-injected (send/retry/input/
17
+ * output/info) so tests can drive it with fake streams and fake network
18
+ * calls — the `chat` command wires real stdin/stdout and `sendChat`.
19
+ *
20
+ * Ctrl-C semantics: readline is created with `terminal: true` so it parses
21
+ * Ctrl-C (byte 0x03) itself and emits its own 'SIGINT' event, distinct from
22
+ * the process-wide SIGINT the rest of the CLI listens for (see
23
+ * client/signals.ts) — forcing `terminal: true` is what makes this
24
+ * intercept work even over the plain PassThrough streams used in tests,
25
+ * not just a real TTY. While a `send`/`retry` call is in flight, that event
26
+ * aborts *that call's own* AbortController only; at an idle prompt (nothing
27
+ * in flight) it exits the REPL, same as /exit.
28
+ */
29
+ export async function runChatRepl(deps) {
30
+ const { send, retry, input, output, info } = deps;
31
+ let conversationId = deps.initialConversationId;
32
+ let currentController;
33
+ const rl = readline.createInterface({
34
+ input,
35
+ output,
36
+ terminal: true,
37
+ prompt: '> '
38
+ });
39
+ rl.on('SIGINT', () => {
40
+ if (currentController) {
41
+ currentController.abort();
42
+ return;
43
+ }
44
+ rl.close();
45
+ });
46
+ // Readline closes the moment its input ends (Ctrl-D, or a piped stream
47
+ // finishing), but the `for await` loop below may still be draining lines
48
+ // readline buffered before that. prompt() on a closed interface is a
49
+ // silent no-op on Node 20/22 but throws ERR_USE_AFTER_CLOSE on Node 24+,
50
+ // so every prompt goes through this close-aware wrapper.
51
+ let closed = false;
52
+ rl.once('close', () => {
53
+ closed = true;
54
+ });
55
+ const prompt = () => {
56
+ if (!closed)
57
+ rl.prompt();
58
+ };
59
+ /** Runs `fn` under a fresh per-call AbortController wired to the
60
+ * readline-level Ctrl-C above. Swallows the resulting AbortError (and
61
+ * any other failure) so one bad turn never crashes the whole REPL —
62
+ * it prints a note via `info` and returns to the prompt either way. */
63
+ async function cancelable(fn) {
64
+ const controller = new AbortController();
65
+ currentController = controller;
66
+ try {
67
+ return await fn(controller.signal);
68
+ }
69
+ catch (err) {
70
+ // A cancelled or failed call may have already streamed partial
71
+ // text straight to `output` with no trailing newline (send/retry
72
+ // write tokens as they arrive) — start the note on its own line.
73
+ output.write('\n');
74
+ if (isAbortError(err)) {
75
+ info('[cancelled — response was interrupted]');
76
+ }
77
+ else {
78
+ info(`✗ ${err?.message ?? err}`);
79
+ }
80
+ return undefined;
81
+ }
82
+ finally {
83
+ currentController = undefined;
84
+ }
85
+ }
86
+ info(GREETING);
87
+ prompt();
88
+ for await (const rawLine of rl) {
89
+ const line = rawLine.trim();
90
+ if (line === '') {
91
+ prompt();
92
+ continue;
93
+ }
94
+ if (line.startsWith('/')) {
95
+ const cmd = line.split(/\s+/, 1)[0];
96
+ if (cmd === '/exit')
97
+ break;
98
+ if (cmd === '/new') {
99
+ conversationId = undefined;
100
+ }
101
+ else if (cmd === '/retry') {
102
+ if (!conversationId) {
103
+ info('No conversation yet — send a message first.');
104
+ }
105
+ else {
106
+ await cancelable((signal) => retry(conversationId, signal));
107
+ }
108
+ }
109
+ else if (cmd === '/id') {
110
+ info(conversationId ?? 'none');
111
+ }
112
+ else if (cmd === '/help') {
113
+ for (const helpLine of HELP_LINES)
114
+ info(helpLine);
115
+ }
116
+ else {
117
+ info(`Unknown command: ${cmd} — type /help for a list.`);
118
+ }
119
+ prompt();
120
+ continue;
121
+ }
122
+ const result = await cancelable((signal) => send(line, conversationId, signal));
123
+ if (result?.conversationId)
124
+ conversationId = result.conversationId;
125
+ prompt();
126
+ }
127
+ rl.close();
128
+ return { conversationId };
129
+ }
@@ -0,0 +1,3 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ export const VERSION = require('../package.json').version;