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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1524 -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 +76 -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 +125 -0
  30. package/dist/commands/auth/logout.js +38 -0
  31. package/dist/commands/auth/status.js +86 -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/health.js +22 -0
  41. package/dist/commands/helpdesk/statuses.js +31 -0
  42. package/dist/commands/helpdesk/teams.js +25 -0
  43. package/dist/commands/messages/feedback.js +64 -0
  44. package/dist/commands/messages/list.js +55 -0
  45. package/dist/commands/sources/create.js +134 -0
  46. package/dist/commands/sources/delete.js +28 -0
  47. package/dist/commands/sources/get.js +35 -0
  48. package/dist/commands/sources/list.js +47 -0
  49. package/dist/commands/sources/restore.js +22 -0
  50. package/dist/commands/sources/summary.js +33 -0
  51. package/dist/commands/sources/update.js +76 -0
  52. package/dist/commands/tickets/create.js +58 -0
  53. package/dist/commands/tickets/get.js +44 -0
  54. package/dist/commands/tickets/list.js +96 -0
  55. package/dist/commands/tickets/messages.js +87 -0
  56. package/dist/commands/tickets/reply.js +66 -0
  57. package/dist/commands/tickets/search.js +53 -0
  58. package/dist/commands/tickets/update.js +38 -0
  59. package/dist/config/paths.js +22 -0
  60. package/dist/config/resolve.js +37 -0
  61. package/dist/config/store.js +38 -0
  62. package/dist/errors/errors.js +81 -0
  63. package/dist/hooks/chat-message-hint.js +21 -0
  64. package/dist/output/color.js +30 -0
  65. package/dist/output/mode.js +7 -0
  66. package/dist/output/render.js +0 -0
  67. package/dist/output/spinner.js +37 -0
  68. package/dist/repl/chat-repl.js +129 -0
  69. package/dist/version.js +3 -0
  70. package/oclif.manifest.json +3948 -0
  71. package/package.json +92 -4
  72. package/spec/openapi.json +11843 -0
@@ -0,0 +1,175 @@
1
+ import os from 'node:os';
2
+ import createClient from 'openapi-fetch';
3
+ import { EnvHttpProxyAgent, getGlobalDispatcher, fetch as undiciFetch } from 'undici';
4
+ import { resolveTimeoutMs } from '../config/resolve.js';
5
+ import { parseErrorResponse } from '../errors/errors.js';
6
+ import { VERSION } from '../version.js';
7
+ import { computeRetryDelayMs, shouldRetry } from './retry.js';
8
+ import { getSigintSignal } from './signals.js';
9
+ export const DEFAULT_BASE_URL = 'https://www.chatbase.co/api/v2';
10
+ /**
11
+ * Base-URL resolution: explicit option > CHATBASE_API_URL env > production.
12
+ * The env override exists for developing against a local API server.
13
+ */
14
+ export function resolveBaseUrl(explicit) {
15
+ if (explicit)
16
+ return explicit;
17
+ const env = process.env.CHATBASE_API_URL;
18
+ if (env && env.length > 0)
19
+ return env;
20
+ return DEFAULT_BASE_URL;
21
+ }
22
+ export function buildUserAgent() {
23
+ return `chatbase-cli/${VERSION} (${os.platform()}-${os.arch()}; node/${process.versions.node})`;
24
+ }
25
+ const hasProxyEnv = () => [
26
+ 'HTTP_PROXY',
27
+ 'HTTPS_PROXY',
28
+ 'http_proxy',
29
+ 'https_proxy',
30
+ 'ALL_PROXY'
31
+ ].some((k) => process.env[k] && process.env[k].length > 0);
32
+ let proxyAgent;
33
+ export function dispatcher() {
34
+ // Node's fetch ignores HTTP(S)_PROXY by default; EnvHttpProxyAgent honors it.
35
+ if (!hasProxyEnv())
36
+ return getGlobalDispatcher();
37
+ proxyAgent ??= new EnvHttpProxyAgent();
38
+ return proxyAgent;
39
+ }
40
+ /** setTimeout that also rejects the moment `signal` aborts — the retry
41
+ * backoff must honor Ctrl-C / per-call cancels, not wait out the delay. */
42
+ const sleep = (ms, signal) => new Promise((resolve, reject) => {
43
+ if (signal?.aborted)
44
+ return reject(signal.reason);
45
+ const timer = setTimeout(() => resolve(), ms);
46
+ signal?.addEventListener('abort', () => {
47
+ clearTimeout(timer);
48
+ reject(signal.reason);
49
+ }, { once: true });
50
+ });
51
+ /**
52
+ * Node's global `Request` and the undici package's `Request` are two copies
53
+ * of the same class — and undici rejects objects made by the other copy
54
+ * (it stringifies them and tries to use "[object Request]" as the URL).
55
+ * openapi-fetch hands us Node-flavored Request objects, so we unpack them
56
+ * into plain values (url, method, headers, body) that can't fail any
57
+ * identity check. Also accepts plain URL strings (the rawApiFetch path),
58
+ * normalizing both shapes into one.
59
+ *
60
+ * When touching this: url, METHOD, headers, and body must ALL survive the
61
+ * unpacking — method was once dropped here, silently turning every request
62
+ * into a GET (caught by review; wire-level tests now pin it).
63
+ */
64
+ async function toPlainRequestInit(input, init) {
65
+ if (typeof input === 'string' || input instanceof URL) {
66
+ const method = (init?.method ?? 'GET').toUpperCase();
67
+ return {
68
+ url: String(input),
69
+ method,
70
+ requestInit: { ...init, method },
71
+ signal: init?.signal
72
+ };
73
+ }
74
+ const method = (init?.method ?? input.method ?? 'GET').toUpperCase();
75
+ const headers = {};
76
+ for (const [key, value] of input.headers)
77
+ headers[key] = value;
78
+ const body = input.body ? Buffer.from(await input.arrayBuffer()) : undefined;
79
+ return {
80
+ url: input.url,
81
+ method,
82
+ requestInit: { headers, body, ...init, method },
83
+ // Per-request cancel (e.g. Ctrl-C cancels one chat response without
84
+ // killing the interactive session). Always defined, inert when unused.
85
+ signal: input.signal
86
+ };
87
+ }
88
+ export function makeFetch(opts) {
89
+ const timeoutMs = opts.timeoutMs ?? resolveTimeoutMs();
90
+ return async (input, init) => {
91
+ const { url, method, requestInit, signal } = await toPlainRequestInit(input, init);
92
+ for (let attempt = 1;; attempt++) {
93
+ const attemptSignal = AbortSignal.any([
94
+ getSigintSignal(),
95
+ ...(signal ? [signal] : [AbortSignal.timeout(timeoutMs)])
96
+ ]);
97
+ if (opts.verbose) {
98
+ const retryTag = attempt > 1 ? ` (attempt ${attempt})` : '';
99
+ process.stderr.write(`» ${method} ${url}${retryTag}\n`);
100
+ }
101
+ const started = Date.now();
102
+ const response = await undiciFetch(url, {
103
+ ...requestInit,
104
+ dispatcher: dispatcher(),
105
+ signal: attemptSignal
106
+ });
107
+ if (opts.verbose) {
108
+ const requestId = response.headers.get('x-request-id');
109
+ process.stderr.write(`« ${response.status}${requestId ? ` (request-id: ${requestId})` : ''} in ${Date.now() - started}ms\n`);
110
+ }
111
+ if (response.ok || !shouldRetry(response.status, method, attempt))
112
+ return response;
113
+ // Draining before the retry avoids leaking the unread response
114
+ // body's underlying connection while we sleep and loop.
115
+ await response.body?.cancel();
116
+ // Same signal as the fetch above, so Ctrl-C (or the REPL's
117
+ // per-call cancel) interrupts the backoff wait too instead of
118
+ // only taking effect on the next attempt.
119
+ await sleep(computeRetryDelayMs(attempt, response.headers.get('x-ratelimit-reset'), Date.now()), attemptSignal);
120
+ }
121
+ };
122
+ }
123
+ export function createApiClient(opts = {}) {
124
+ const client = createClient({
125
+ baseUrl: resolveBaseUrl(opts.baseUrl),
126
+ fetch: makeFetch(opts)
127
+ });
128
+ client.use({
129
+ onRequest({ request }) {
130
+ request.headers.set('User-Agent', buildUserAgent());
131
+ if (opts.apiKey)
132
+ request.headers.set('Authorization', `Bearer ${opts.apiKey}`);
133
+ return request;
134
+ }
135
+ });
136
+ return client;
137
+ }
138
+ export function throwIfError(response, errorBody) {
139
+ if (response.ok)
140
+ return;
141
+ throw parseErrorResponse(response.status, errorBody, response.headers.get('x-request-id') ?? undefined);
142
+ }
143
+ /**
144
+ * Untyped HTTP call — the `chatbase api` escape hatch (like `gh api` in
145
+ * GitHub's CLI: any method, any path, JSON in/out). Uses the same
146
+ * proxy/timeout/retry/SIGINT wrappers as the typed client.
147
+ */
148
+ export async function rawApiFetch(method, path, opts = {}) {
149
+ const url = new URL(`${resolveBaseUrl(opts.baseUrl)}${path}`);
150
+ for (const [key, value] of opts.query ?? []) {
151
+ url.searchParams.append(key, value);
152
+ }
153
+ const hasBody = opts.body !== undefined;
154
+ const response = await makeFetch(opts)(url.toString(), {
155
+ method,
156
+ headers: {
157
+ 'User-Agent': buildUserAgent(),
158
+ ...(opts.apiKey ? { Authorization: `Bearer ${opts.apiKey}` } : {}),
159
+ ...(hasBody ? { 'Content-Type': 'application/json' } : {})
160
+ },
161
+ ...(hasBody ? { body: JSON.stringify(opts.body) } : {})
162
+ });
163
+ let body;
164
+ try {
165
+ body = await response.json();
166
+ }
167
+ catch {
168
+ body = undefined;
169
+ }
170
+ return {
171
+ status: response.status,
172
+ requestId: response.headers.get('x-request-id') ?? undefined,
173
+ body
174
+ };
175
+ }
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { FormData as UndiciFormData } from 'undici';
4
+ import { parseErrorResponse } from '../errors/errors.js';
5
+ import { buildUserAgent, makeFetch } from './client.js';
6
+ /** File upload host — documented at chatbase.co/docs/api-v2/sources/create-file-source */
7
+ export const FILES_BASE_URL = 'https://files.chatbase.co/api/v2';
8
+ export function resolveFilesBaseUrl(explicit) {
9
+ if (explicit)
10
+ return explicit;
11
+ const env = process.env.CHATBASE_FILES_URL;
12
+ if (env && env.length > 0)
13
+ return env;
14
+ return FILES_BASE_URL;
15
+ }
16
+ /**
17
+ * The cross-environment trap: CHATBASE_API_URL points somewhere non-default
18
+ * (a preview/staging deployment) while file uploads still go to the
19
+ * production files host — silently sending that environment's credential to
20
+ * a different one. Returns the warning to print, or null when the setup is
21
+ * consistent (no API override, or an explicit CHATBASE_FILES_URL).
22
+ */
23
+ export function filesHostMismatchWarning() {
24
+ const apiOverride = process.env.CHATBASE_API_URL;
25
+ const filesOverride = process.env.CHATBASE_FILES_URL;
26
+ if (!apiOverride || filesOverride)
27
+ return null;
28
+ return `! File uploads go to ${FILES_BASE_URL} while CHATBASE_API_URL is overridden — set CHATBASE_FILES_URL if this environment has its own files host.`;
29
+ }
30
+ export async function uploadFileSource(opts) {
31
+ // Must use undici's FormData, not the global — global silently sends
32
+ // "[object FormData]" as plain text instead of real multipart. No error.
33
+ const form = new UndiciFormData();
34
+ const buffer = fs.readFileSync(opts.filePath);
35
+ const filename = path.basename(opts.filePath);
36
+ form.set('name', opts.name ?? filename);
37
+ form.set('file', new Blob([buffer]), filename);
38
+ const base = resolveFilesBaseUrl(opts.baseUrl);
39
+ const url = opts.sourceId
40
+ ? `${base}/agents/${opts.agentId}/sources/${opts.sourceId}`
41
+ : `${base}/agents/${opts.agentId}/sources`;
42
+ const method = opts.sourceId ? 'PUT' : 'POST';
43
+ const response = await makeFetch({
44
+ timeoutMs: opts.timeoutMs,
45
+ verbose: opts.verbose
46
+ })(url, {
47
+ method,
48
+ headers: {
49
+ Authorization: `Bearer ${opts.apiKey}`,
50
+ 'User-Agent': buildUserAgent()
51
+ },
52
+ body: form
53
+ });
54
+ // Success body is the source object itself: { id, type, name, size, ... }
55
+ const body = (await response.json().catch(() => undefined));
56
+ if (!response.ok) {
57
+ throw parseErrorResponse(response.status, body, response.headers.get('x-request-id') ?? undefined);
58
+ }
59
+ const id = body?.id;
60
+ if (!id) {
61
+ throw new Error('Upload succeeded but the response did not contain a source ID');
62
+ }
63
+ return { id };
64
+ }
@@ -0,0 +1,40 @@
1
+ import { throwIfError } from './client.js';
2
+ /** Pause between pages on `--all` so a long crawl is less likely to share
3
+ * a contested API key's rate-limit window with other traffic. Solo use is
4
+ * well under the v2 limit (1000/10s); this is a courtesy, not a hard pace. */
5
+ const PAGE_DELAY_MS = 200;
6
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
7
+ /**
8
+ * Fetches one page — or, with `opts.all`, every page — of a cursor-paginated
9
+ * endpoint, following `pagination.cursor` until `hasMore` is false.
10
+ *
11
+ * Returns both the raw `pages` (list commands need these verbatim for
12
+ * `--json`, where `--all` must merge `data` across pages but keep the API's
13
+ * envelope shape for the single-page case) and the flattened `items`
14
+ * (display rows, and callers like resolveAgentRef/listAllSources that only
15
+ * need "every item").
16
+ *
17
+ * The caller supplies a `fetcher` that wraps `client.GET(path, { params })`
18
+ * for its specific endpoint and path params — this helper never sees path
19
+ * templates or param shapes, only the `{cursor, limit}` query it asks for
20
+ * and the `{data, error, response}` result every generated GET returns.
21
+ */
22
+ export async function fetchPages(fetcher, opts = {}) {
23
+ const pages = [];
24
+ let cursor = opts.cursor;
25
+ let hasMore = false;
26
+ do {
27
+ if (pages.length > 0)
28
+ await sleep(PAGE_DELAY_MS);
29
+ const { data, error, response } = await fetcher({
30
+ cursor,
31
+ limit: opts.limit
32
+ });
33
+ throwIfError(response, error);
34
+ const page = data;
35
+ pages.push(page);
36
+ cursor = page.pagination.cursor ?? undefined;
37
+ hasMore = page.pagination.hasMore;
38
+ } while (opts.all && hasMore && cursor);
39
+ return { pages, items: pages.flatMap((p) => p.data) };
40
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * CLI-side of the browser pairing login flow:
3
+ * 1. POST /api/cli-pairing/create → get user_code + device_code
4
+ * 2. User approves at verification_uri
5
+ * 3. Poll POST /api/cli-pairing/exchange until approved
6
+ * 4. Receive the minted API key + workspace info
7
+ *
8
+ * These endpoints are internal (not under /api/v2) and unauthenticated.
9
+ */
10
+ import os from 'node:os';
11
+ import { parseErrorResponse, UsageError } from '../errors/errors.js';
12
+ import { rawApiFetch, resolveBaseUrl } from './client.js';
13
+ import { wasInterrupted } from './signals.js';
14
+ function pairingBaseUrl(baseUrl) {
15
+ return new URL(resolveBaseUrl(baseUrl)).origin;
16
+ }
17
+ export async function startPairing(opts) {
18
+ const res = await rawApiFetch('POST', '/api/cli-pairing/create', {
19
+ baseUrl: pairingBaseUrl(opts?.baseUrl),
20
+ body: { device_name: os.hostname() }
21
+ });
22
+ if (res.status >= 400) {
23
+ throw parseErrorResponse(res.status, res.body);
24
+ }
25
+ const d = res.body;
26
+ return {
27
+ deviceCode: d.device_code,
28
+ userCode: d.user_code,
29
+ verificationUri: d.verification_uri,
30
+ expiresIn: d.expires_in,
31
+ interval: d.interval
32
+ };
33
+ }
34
+ export async function pollExchange(deviceCode, opts) {
35
+ const deadline = Date.now() + opts.timeoutMs;
36
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
37
+ const baseUrl = pairingBaseUrl(opts.baseUrl);
38
+ for (;;) {
39
+ opts.onPoll?.();
40
+ const attempt = await rawApiFetch('POST', '/api/cli-pairing/exchange', { baseUrl, body: { device_code: deviceCode } }).then((r) => ({
41
+ ok: true,
42
+ status: r.status,
43
+ body: r.body
44
+ }), (cause) => ({ ok: false, cause }));
45
+ if (!attempt.ok) {
46
+ const name = attempt.cause?.name;
47
+ if (name === 'AbortError' && wasInterrupted())
48
+ throw attempt.cause;
49
+ if (Date.now() >= deadline) {
50
+ throw new UsageError('Pairing request expired. Run `chatbase auth login` to try again.');
51
+ }
52
+ await sleep(opts.intervalMs);
53
+ continue;
54
+ }
55
+ if (attempt.status < 400) {
56
+ const result = attempt.body;
57
+ return {
58
+ apiKey: result.api_key,
59
+ workspace: result.workspace
60
+ };
61
+ }
62
+ const errorBody = attempt.body;
63
+ const code = errorBody?.error?.code;
64
+ if (code === 'PAIRING_PENDING' || code === 'PAIRING_SLOW_DOWN') {
65
+ if (Date.now() >= deadline) {
66
+ throw new UsageError('Pairing request expired. Run `chatbase auth login` to try again.');
67
+ }
68
+ const delay = code === 'PAIRING_SLOW_DOWN'
69
+ ? opts.intervalMs * 2
70
+ : opts.intervalMs;
71
+ await sleep(delay);
72
+ continue;
73
+ }
74
+ throw parseErrorResponse(attempt.status, attempt.body);
75
+ }
76
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Retry policy: 429 (rate limit) retries up to 3 times for ANY method —
3
+ * a rate-limited request was never executed, so repeating it is safe.
4
+ * 5xx retries once, GETs only: reads are safe to repeat, writes are not
5
+ * (the server may have half-done the work before failing).
6
+ */
7
+ export function shouldRetry(status, method, attempt) {
8
+ if (status === 429)
9
+ return attempt <= 3;
10
+ if (status >= 500 && method.toUpperCase() === 'GET')
11
+ return attempt <= 1;
12
+ return false;
13
+ }
14
+ // Epoch timestamps come in seconds (~1.7e9 today) or milliseconds (~1.7e12);
15
+ // the number's size reveals its unit. Anything under 1e11 can only be
16
+ // seconds (1e11 seconds = year 5138; 1e11 ms = 1973, long past). Our API
17
+ // sends seconds — this tolerates ms too in case that ever changes.
18
+ const SECONDS_MS_THRESHOLD = 1e11;
19
+ /**
20
+ * How long to sleep before a retry.
21
+ *
22
+ * Preferred: the X-RateLimit-Reset header says exactly when the rate window
23
+ * reopens — wait precisely until then (capped at 60s so a bad header can't
24
+ * stall the CLI; a past timestamp means the window is already open).
25
+ *
26
+ * Fallback (no usable header): exponential backoff — 500ms, 1s, 2s — plus
27
+ * 0-250ms random jitter so many clients rate-limited together don't all
28
+ * retry at the same instant and collide again.
29
+ */
30
+ export function computeRetryDelayMs(attempt, resetHeader, nowMs) {
31
+ if (resetHeader && /^\d+$/.test(resetHeader)) {
32
+ const raw = Number(resetHeader);
33
+ const resetMs = raw < SECONDS_MS_THRESHOLD ? raw * 1000 : raw;
34
+ const wait = resetMs - nowMs;
35
+ if (wait > 0)
36
+ return Math.min(wait, 60_000);
37
+ }
38
+ const base = 500 * 2 ** (attempt - 1);
39
+ return base + Math.floor(Math.random() * 250);
40
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The Ctrl-C contract. Registering a SIGINT listener replaces Node's
3
+ * die-instantly default, so this file owns the guarantee that Ctrl-C
4
+ * always works:
5
+ *
6
+ * 1st Ctrl-C → say "Interrupted", abort the shared signal (every
7
+ * in-flight request is wired to it via AbortSignal.any),
8
+ * let the code unwind → catch() exits 130 silently.
9
+ * 2nd Ctrl-C → exit immediately, skipping even fast teardown.
10
+ *
11
+ * 130 = 128 + SIGINT's signal number (2) — the Unix convention shells and
12
+ * CI check for "user cancelled", as distinct from "failed".
13
+ */
14
+ const controller = new AbortController();
15
+ let interrupts = 0;
16
+ let installed = false;
17
+ export function getSigintSignal() {
18
+ return controller.signal;
19
+ }
20
+ /** True once SIGINT has been received at least once (see installSigintHandler). */
21
+ export function wasInterrupted() {
22
+ return interrupts > 0;
23
+ }
24
+ /** Idempotent — every command's init() calls this; only one listener ever. */
25
+ export function installSigintHandler() {
26
+ if (installed)
27
+ return;
28
+ installed = true;
29
+ process.on('SIGINT', () => {
30
+ interrupts += 1;
31
+ if (interrupts === 1) {
32
+ process.stderr.write('\nInterrupted\n');
33
+ controller.abort();
34
+ // Insurance: if graceful teardown hangs, force-exit in 2s.
35
+ // unref() is load-bearing — without it this timer would keep
36
+ // the process alive 2s on EVERY Ctrl-C, even after clean exit.
37
+ setTimeout(() => process.exit(130), 2000).unref();
38
+ }
39
+ else {
40
+ process.exit(130);
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Hand-rolled SSE parser (~50 lines) instead of a library (e.g. eventsource-parser)
3
+ * because SSE is trivial (`data: <json>\n\n`) and the real work is Chatbase-specific:
4
+ * idle-timeout racing and mapping our event types (text-delta, message-metadata).
5
+ * A library would replace ~15 lines of string splitting and add a dependency to maintain.
6
+ */
7
+ export async function parseSseStream(body, onEvent, opts = {}) {
8
+ const idleMs = opts.idleTimeoutMs ?? 60_000;
9
+ const reader = body.getReader();
10
+ const decoder = new TextDecoder();
11
+ let buffer = '';
12
+ try {
13
+ for (;;) {
14
+ let timer;
15
+ const idle = new Promise((_, reject) => {
16
+ timer = setTimeout(() => {
17
+ const seconds = Math.round(idleMs / 1000);
18
+ reject(new Error(`Stream idle timeout — no data for ${seconds}s`));
19
+ }, idleMs);
20
+ });
21
+ const result = await Promise.race([reader.read(), idle]).finally(() => clearTimeout(timer));
22
+ const { done, value } = result;
23
+ if (done)
24
+ return;
25
+ buffer += decoder.decode(value, { stream: true });
26
+ for (;;) {
27
+ const idx = buffer.indexOf('\n\n');
28
+ if (idx === -1)
29
+ break;
30
+ const block = buffer.slice(0, idx);
31
+ buffer = buffer.slice(idx + 2);
32
+ for (const line of block.split('\n')) {
33
+ if (!line.startsWith('data: '))
34
+ continue;
35
+ const payload = line.slice(6);
36
+ if (payload === '[DONE]') {
37
+ onEvent({ type: 'done' });
38
+ return;
39
+ }
40
+ let part;
41
+ try {
42
+ part = JSON.parse(payload);
43
+ }
44
+ catch {
45
+ onEvent({
46
+ type: 'warning',
47
+ message: 'Skipped an unparseable stream chunk'
48
+ });
49
+ continue;
50
+ }
51
+ if (part.type === 'text-delta' &&
52
+ typeof part.delta === 'string') {
53
+ onEvent({ type: 'text', text: part.delta });
54
+ }
55
+ else if (part.type === 'error') {
56
+ onEvent({
57
+ type: 'error',
58
+ message: typeof part.errorText === 'string'
59
+ ? part.errorText
60
+ : 'The agent stopped with an error'
61
+ });
62
+ }
63
+ else if (part.type === 'message-metadata' ||
64
+ part.type === 'finish') {
65
+ const meta = (part.messageMetadata ?? {});
66
+ onEvent({
67
+ type: 'metadata',
68
+ conversationId: meta.conversationId,
69
+ messageId: meta.messageId
70
+ });
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ finally {
77
+ await reader.cancel().catch(() => { });
78
+ }
79
+ }
@@ -0,0 +1,46 @@
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 AgentsAutoRetrain extends AgentCommand {
6
+ static description = 'Enable or disable automatic retraining for an agent';
7
+ static examples = [
8
+ '<%= config.bin %> agents auto-retrain agt_123 --enabled',
9
+ '<%= config.bin %> agents auto-retrain --enabled'
10
+ ];
11
+ static args = {
12
+ agentId: Args.string({
13
+ required: false,
14
+ description: 'Agent ID'
15
+ })
16
+ };
17
+ static flags = {
18
+ ...AgentCommand.baseFlags,
19
+ enabled: Flags.boolean({
20
+ description: 'Enable automatic retraining',
21
+ exactlyOne: ['enabled', 'disabled']
22
+ }),
23
+ disabled: Flags.boolean({
24
+ description: 'Disable automatic retraining',
25
+ exactlyOne: ['enabled', 'disabled']
26
+ })
27
+ };
28
+ async run() {
29
+ const { args, flags } = await this.parse(AgentsAutoRetrain);
30
+ if (args.agentId && (flags.agent || flags['agent-name'])) {
31
+ throw new UsageError('Pass the agent ID either positionally or via -a/--agent-name, not both.');
32
+ }
33
+ const body = {
34
+ enabled: flags.enabled === true
35
+ };
36
+ const client = this.apiClient(flags);
37
+ const agentId = args.agentId ?? (await this.agentId(flags, client));
38
+ const { error, response } = await client.PUT('/agents/{agentId}/auto-retrain', {
39
+ params: { path: { agentId } },
40
+ body
41
+ });
42
+ throwIfError(response, error);
43
+ const status = flags.enabled ? 'enabled' : 'disabled';
44
+ this.success(flags, `Auto-retrain ${status} for ${agentId}`);
45
+ }
46
+ }
@@ -0,0 +1,28 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../base/base-command.js';
3
+ import { throwIfError } from '../../client/client.js';
4
+ export default class AgentsClone extends BaseCommand {
5
+ static description = 'Clone an agent, including all its sources (excluding Notion)';
6
+ static examples = ['<%= config.bin %> agents clone agt_123'];
7
+ static args = {
8
+ agentId: Args.string({
9
+ required: true,
10
+ description: 'Agent ID to clone'
11
+ })
12
+ };
13
+ static flags = { ...BaseCommand.baseFlags };
14
+ async run() {
15
+ const { args, flags } = await this.parse(AgentsClone);
16
+ const client = this.apiClient(flags);
17
+ const { data, error, response } = await client.POST('/agents/{agentId}/clone', { params: { path: { agentId: args.agentId } } });
18
+ throwIfError(response, error);
19
+ const id = data.id;
20
+ if (flags.json) {
21
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
22
+ }
23
+ else {
24
+ this.success(flags, `Cloned agent ${args.agentId} → ${id}`);
25
+ process.stdout.write(`${id}\n`);
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,43 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand, bodyFieldFlags } from '../../base/base-command.js';
3
+ import { readBodyData } from '../../base/body-input.js';
4
+ import { throwIfError } from '../../client/client.js';
5
+ export default class AgentsCreate extends BaseCommand {
6
+ static description = 'Create a new agent';
7
+ static examples = [
8
+ '<%= config.bin %> agents create --name "Support Bot" --instructions "Be helpful"',
9
+ '<%= config.bin %> agents create --data @agent.json'
10
+ ];
11
+ static flags = {
12
+ ...BaseCommand.baseFlags,
13
+ ...bodyFieldFlags,
14
+ name: Flags.string({ description: 'Agent name' }),
15
+ instructions: Flags.string({ description: 'System instructions' }),
16
+ model: Flags.string({ description: 'Model ID' }),
17
+ data: Flags.string({
18
+ description: 'JSON body (@file, @-, or inline). Fields: name, instructions, model, visibility, temp'
19
+ })
20
+ };
21
+ async run() {
22
+ const { flags } = await this.parse(AgentsCreate);
23
+ const body = {
24
+ ...(await readBodyData(flags.data, flags.field)),
25
+ ...(flags.name ? { name: flags.name } : {}),
26
+ ...(flags.instructions ? { instructions: flags.instructions } : {}),
27
+ ...(flags.model ? { model: flags.model } : {})
28
+ };
29
+ const client = this.apiClient(flags);
30
+ const { data, error, response } = await client.POST('/agents', {
31
+ body: body
32
+ });
33
+ throwIfError(response, error);
34
+ const id = data.id;
35
+ if (flags.json) {
36
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
37
+ }
38
+ else {
39
+ this.success(flags, `Created agent ${id}`);
40
+ process.stdout.write(`${id}\n`);
41
+ }
42
+ }
43
+ }