@autumnai/cli 0.0.0-stage → 0.1.2

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/README.md CHANGED
@@ -1,3 +1,28 @@
1
- # Temporary Holding Version
1
+ # Autumn CLI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/@autumnai/cli)](https://www.npmjs.com/package/@autumnai/cli)
4
+
5
+ Install the Autumn Skill and sign in:
6
+
7
+ ```sh
8
+ npm install -g @autumnai/cli
9
+ autumn setup
10
+ autumn login
11
+ ```
12
+
13
+ Start a research task and save its rows:
14
+
15
+ ```sh
16
+ autumn run "Find recent funding rounds for climate software companies" --out rounds.csv
17
+ ```
18
+
19
+ If the task asks for input, reply and resume it with the task id:
20
+
21
+ ```sh
22
+ autumn reply <task-id> "Focus on North American companies"
23
+ autumn wait <task-id>
24
+ ```
25
+
26
+ The CLI stores its API key in `${XDG_CONFIG_HOME:-~/.config}/autumn/config.json` with mode `0600`; the config directory is mode `0700`. `AUTUMN_API_KEY` and `AUTUMN_API_URL` override saved configuration.
27
+
28
+ Exit codes: `0` completed or successful, `1` error/failed/canceled, `2` input required, `3` still working or approval pending, and `4` authentication required.
package/bin/autumn.mjs ADDED
@@ -0,0 +1,997 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { randomUUID } from "node:crypto";
5
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const VERSION = "0.1.2";
11
+ const DEFAULT_API_URL = "https://api.autumn.ai";
12
+ const USER_AGENT = `autumn-cli/${VERSION}`;
13
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
14
+ const CONFIG_DIR = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "autumn");
15
+ const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
16
+
17
+ class CliError extends Error {
18
+ constructor(message, { exitCode = 1, status = 0, payload = null } = {}) {
19
+ super(message);
20
+ this.name = "CliError";
21
+ this.exitCode = exitCode;
22
+ this.status = status;
23
+ this.payload = payload;
24
+ }
25
+ }
26
+
27
+ let jsonMode = false;
28
+
29
+ function writeOut(text) {
30
+ process.stdout.write(`${text}\n`);
31
+ }
32
+
33
+ function writeErr(text) {
34
+ process.stderr.write(`${text}\n`);
35
+ }
36
+
37
+ function emit(value) {
38
+ writeOut(jsonMode ? JSON.stringify(value) : typeof value === "string" ? value : JSON.stringify(value, null, 2));
39
+ }
40
+
41
+ function compactTaskStatus(status) {
42
+ const fields = [
43
+ "task_id",
44
+ "state",
45
+ "done",
46
+ "outcome",
47
+ "message",
48
+ "progress",
49
+ "credits_used",
50
+ "elapsed_s",
51
+ "queue_position",
52
+ "error",
53
+ "app_url",
54
+ ];
55
+ const compact = Object.fromEntries(fields.map((field) => [field, status[field] ?? null]));
56
+ for (const field of ["saved", "timed_out"]) {
57
+ if (Object.hasOwn(status, field)) compact[field] = status[field];
58
+ }
59
+ return compact;
60
+ }
61
+
62
+ function taskExitCode(state) {
63
+ if (state === "completed") return 0;
64
+ if (state === "failed" || state === "canceled") return 1;
65
+ if (state === "input-required") return 2;
66
+ return 3;
67
+ }
68
+
69
+ function creditsSummary(credits) {
70
+ if (credits.unlimited) return "Unlimited credits";
71
+ const remaining = Number(credits.credits_remaining || 0);
72
+ const used = Number(credits.credits_used || 0);
73
+ const total = credits.credits_total == null
74
+ ? remaining + used
75
+ : Number(credits.credits_total);
76
+ return `${remaining} credits left (${used} used of ${total})`;
77
+ }
78
+
79
+ function errorPayloadMessage(payload, fallback = "") {
80
+ if (payload && typeof payload === "object" && typeof payload.message === "string" && payload.message.trim()) {
81
+ return payload.message;
82
+ }
83
+ if (payload && typeof payload === "object" && typeof payload.error === "string" && payload.error.trim()) {
84
+ return payload.error;
85
+ }
86
+ return fallback;
87
+ }
88
+
89
+ function sleep(ms) {
90
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
91
+ }
92
+
93
+ function configApiKey(config) {
94
+ return process.env.AUTUMN_API_KEY || config.api_key || "";
95
+ }
96
+
97
+ function apiBase(config) {
98
+ return (process.env.AUTUMN_API_URL || config.api_url || DEFAULT_API_URL).replace(/\/+$/, "");
99
+ }
100
+
101
+ async function readConfig() {
102
+ try {
103
+ const parsed = JSON.parse(await readFile(CONFIG_PATH, "utf8"));
104
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
105
+ } catch (error) {
106
+ if (error && error.code === "ENOENT") return {};
107
+ throw new CliError(`Could not read ${CONFIG_PATH}: ${error.message}`);
108
+ }
109
+ }
110
+
111
+ async function saveConfig(config) {
112
+ await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
113
+ await chmod(CONFIG_DIR, 0o700);
114
+ await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
115
+ await chmod(CONFIG_PATH, 0o600);
116
+ }
117
+
118
+ function apiKeyOrThrow(config) {
119
+ const key = configApiKey(config);
120
+ if (!key) throw new CliError("Not logged in. Run: autumn login", { exitCode: 4, status: 401 });
121
+ return key;
122
+ }
123
+
124
+ function retryAfterMs(headers) {
125
+ const value = headers.get("retry-after");
126
+ if (value !== null) {
127
+ const seconds = Number(value);
128
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
129
+ const dateMs = Date.parse(value) - Date.now();
130
+ if (Number.isFinite(dateMs) && dateMs >= 0) return dateMs;
131
+ }
132
+ return 5000;
133
+ }
134
+
135
+ async function parseResponse(response) {
136
+ const text = await response.text();
137
+ if (!text) return { text, data: null };
138
+ if ((response.headers.get("content-type") || "").includes("text/csv")) return { text, data: text };
139
+ try {
140
+ return { text, data: JSON.parse(text) };
141
+ } catch {
142
+ return { text, data: text };
143
+ }
144
+ }
145
+
146
+ async function requestRaw(config, route, {
147
+ method = "GET",
148
+ body,
149
+ headers = {},
150
+ authenticated = true,
151
+ } = {}) {
152
+ const requestHeaders = { "User-Agent": USER_AGENT, ...headers };
153
+ if (authenticated) requestHeaders["X-API-Key"] = apiKeyOrThrow(config);
154
+ if (body !== undefined) requestHeaders["Content-Type"] = "application/json";
155
+ let statusRetries = 0;
156
+ let networkRetries = 0;
157
+
158
+ while (true) {
159
+ let response;
160
+ try {
161
+ response = await fetch(`${apiBase(config)}${route}`, {
162
+ method,
163
+ headers: requestHeaders,
164
+ body: body === undefined ? undefined : JSON.stringify(body),
165
+ });
166
+ } catch (error) {
167
+ if (method !== "GET") {
168
+ throw new CliError(`Network error: ${error.message || "request failed"}`);
169
+ }
170
+ networkRetries += 1;
171
+ if (networkRetries >= 3) {
172
+ throw new CliError(`Network error: ${error.message || "request failed"}`);
173
+ }
174
+ await sleep(250 * (2 ** (networkRetries - 1)));
175
+ continue;
176
+ }
177
+
178
+ const parsed = await parseResponse(response);
179
+ const createdTaskId = method !== "GET" && parsed.data && typeof parsed.data === "object" ? parsed.data.task_id : undefined;
180
+ if (response.status === 401) {
181
+ throw new CliError("Not logged in. Run: autumn login", {
182
+ exitCode: 4,
183
+ status: 401,
184
+ payload: parsed.data,
185
+ });
186
+ }
187
+ if ((response.status === 429 || response.status === 503) && !createdTaskId) {
188
+ statusRetries += 1;
189
+ if (statusRetries < 5) {
190
+ await sleep(retryAfterMs(response.headers));
191
+ continue;
192
+ }
193
+ }
194
+ if (!response.ok) {
195
+ const message = errorPayloadMessage(parsed.data, typeof parsed.data === "string" ? parsed.data : `HTTP ${response.status}`);
196
+ const taskNotice = createdTaskId
197
+ ? ` (task ${createdTaskId} was created; check it with: autumn status ${createdTaskId})`
198
+ : "";
199
+ throw new CliError(`${message}${taskNotice}`, { status: response.status, payload: parsed.data });
200
+ }
201
+ return {
202
+ data: parsed.data,
203
+ text: parsed.text,
204
+ headers: response.headers,
205
+ status: response.status,
206
+ };
207
+ }
208
+ }
209
+
210
+ async function request(config, route, options) {
211
+ return (await requestRaw(config, route, options)).data;
212
+ }
213
+
214
+ function parseArgs(tokens) {
215
+ const flags = {};
216
+ const positionals = [];
217
+ const booleanFlags = new Set(["detach", "force", "json", "no-browser", "wait", "help", "h", "yes"]);
218
+ const valueFlags = new Set(["out", "timeout", "limit", "format", "days", "name", "website", "share-by-default", "owners-can-demote"]);
219
+ for (let index = 0; index < tokens.length; index += 1) {
220
+ const token = tokens[index];
221
+ if (token === "--") {
222
+ positionals.push(...tokens.slice(index + 1));
223
+ break;
224
+ }
225
+ if (token.startsWith("--")) {
226
+ const equals = token.indexOf("=");
227
+ const name = token.slice(2, equals === -1 ? undefined : equals);
228
+ if (booleanFlags.has(name)) {
229
+ flags[name] = equals === -1 ? true : token.slice(equals + 1) !== "false";
230
+ } else if (valueFlags.has(name)) {
231
+ if (equals !== -1) flags[name] = token.slice(equals + 1);
232
+ else if (index + 1 < tokens.length) flags[name] = tokens[++index];
233
+ else throw new CliError(`Missing value for --${name}`);
234
+ } else {
235
+ throw new CliError(`Unknown option: ${token}`);
236
+ }
237
+ } else if (token === "-h") {
238
+ flags.help = true;
239
+ } else {
240
+ positionals.push(token);
241
+ }
242
+ }
243
+ return { flags, positionals };
244
+ }
245
+
246
+ function globalHelp() {
247
+ return `Autumn CLI — research from an agent workflow
248
+
249
+ autumn login
250
+ autumn run "Research the latest funding rounds for..." --out rows.csv
251
+ # Exit 2: Autumn needs input; use the task id from the JSON result:
252
+ autumn reply <id> "..."
253
+ # Exit 3: still working; resume the same task:
254
+ autumn wait <id>
255
+
256
+ Commands:
257
+ login [--force] [--no-browser] [--timeout S] Connect this CLI to your Autumn account
258
+ logout Remove the saved API key
259
+ credits Show credit balance (alias: whoami)
260
+ buy <credits> Get a link to buy credits
261
+ run <prompt> [--out FILE] [--detach] [--timeout S] [--json]
262
+ wait <id> [--out FILE] [--timeout S] [--json]
263
+ reply <id> <message> [--out FILE] [--timeout S] [--wait] [--json]
264
+ status <id> [--json] Show current task status once
265
+ rows <id> [--out FILE] [--format csv|json]
266
+ stop <id> Stop a running task
267
+ list [query] [--limit N] [--json] List or search recent tasks (default limit: 20)
268
+ team [invite|revoke|role|remove|set] Manage your team (writes are owner-only)
269
+ usage [--days N] [--json] Read usage for the last N days (1–90, default: 30)
270
+ setup [claude|codex|all] Install the Autumn Skill
271
+ mcp-config [claude|codex|cursor] Print MCP connection configuration
272
+
273
+ Exit codes:
274
+ 0 Success or completed
275
+ 1 Error, failed task, or canceled task
276
+ 2 Input required
277
+ 3 Still working or login approval pending
278
+ 4 Authentication needed`;
279
+ }
280
+
281
+ function commandHelp(command) {
282
+ const messages = {
283
+ login: "autumn login [--force] [--no-browser] [--timeout S] [--json]",
284
+ logout: "autumn logout",
285
+ credits: "autumn credits [--json]",
286
+ buy: "autumn buy <credits>",
287
+ whoami: "autumn whoami [--json]",
288
+ run: "autumn run <prompt> [--out FILE] [--detach] [--timeout S] [--json]",
289
+ wait: "autumn wait <id> [--out FILE] [--timeout S] [--json]",
290
+ reply: "autumn reply <id> <message> [--out FILE] [--timeout S] [--wait] [--json]",
291
+ status: "autumn status <id> [--json]",
292
+ rows: "autumn rows <id> [--out FILE] [--format csv|json]",
293
+ stop: "autumn stop <id>",
294
+ list: "autumn list [query] [--limit N] [--json] (default limit: 20)",
295
+ team: "autumn team [invite <email>|revoke <invitation_id>|role <user_id> owner|member|remove <user_id> --yes|set [--name N] [--website W] [--share-by-default true|false] [--owners-can-demote true|false]]",
296
+ usage: "autumn usage [--days N] [--json] (1–90, default: 30)",
297
+ setup: "autumn setup [claude|codex|all]",
298
+ "mcp-config": "autumn mcp-config [claude|codex|cursor]",
299
+ };
300
+ return messages[command] || globalHelp();
301
+ }
302
+
303
+ function requirePositionals(positionals, count, usage) {
304
+ if (positionals.length < count) throw new CliError(`Usage: ${usage}`);
305
+ }
306
+
307
+ function teamBoolean(value, option) {
308
+ if (value !== "true" && value !== "false") {
309
+ throw new CliError(`--${option} must be true or false.`);
310
+ }
311
+ return value === "true";
312
+ }
313
+
314
+ async function teamCommand(config, positionals, options) {
315
+ const action = positionals[0] || "";
316
+ if (!action) {
317
+ const result = await request(config, "/team");
318
+ if (jsonMode) emit(result);
319
+ else {
320
+ const account = result.account || {};
321
+ writeOut(`account\t${account.type || ""}\t${account.name || ""}\t${account.role || ""}`);
322
+ writeOut("user_id\tname\temail\trole");
323
+ for (const member of result.members || []) {
324
+ writeOut([member.user_id, member.name, member.email, member.role].map((value) => String(value || "").replace(/[\t\r\n]/g, " ")).join("\t"));
325
+ }
326
+ if (account.type === "team" && account.role === "owner") {
327
+ writeOut("\nPending invitations");
328
+ writeOut("id\temail\tcreated_at");
329
+ for (const invitation of result.invitations || []) {
330
+ writeOut([invitation.id, invitation.email, invitation.created_at].map((value) => String(value || "").replace(/[\t\r\n]/g, " ")).join("\t"));
331
+ }
332
+ }
333
+ }
334
+ return 0;
335
+ }
336
+
337
+ let result;
338
+ if (action === "invite") {
339
+ requirePositionals(positionals, 2, commandHelp("team"));
340
+ result = await request(config, "/team/invitations", { method: "POST", body: { email: positionals[1] } });
341
+ } else if (action === "revoke") {
342
+ requirePositionals(positionals, 2, commandHelp("team"));
343
+ result = await request(config, `/team/invitations/${encodeURIComponent(positionals[1])}`, { method: "DELETE" });
344
+ } else if (action === "role") {
345
+ requirePositionals(positionals, 3, commandHelp("team"));
346
+ if (!["owner", "member"].includes(positionals[2])) throw new CliError("Role must be owner or member.");
347
+ result = await request(config, `/team/members/${encodeURIComponent(positionals[1])}`, { method: "PATCH", body: { role: positionals[2] } });
348
+ } else if (action === "remove") {
349
+ requirePositionals(positionals, 2, commandHelp("team"));
350
+ if (!options.yes) throw new CliError(`Rerun with --yes to remove ${positionals[1]}`);
351
+ result = await request(config, `/team/members/${encodeURIComponent(positionals[1])}`, { method: "DELETE" });
352
+ } else if (action === "set") {
353
+ if (positionals.length !== 1) throw new CliError(`Usage: ${commandHelp("team")}`);
354
+ const body = {};
355
+ if (options.name !== undefined || options.website !== undefined) {
356
+ if (options.name !== undefined) body.name = options.name;
357
+ if (options.website !== undefined) body.website = options.website;
358
+ }
359
+ const settings = {};
360
+ if (options["share-by-default"] !== undefined) settings.share_chats_by_default = teamBoolean(options["share-by-default"], "share-by-default");
361
+ if (options["owners-can-demote"] !== undefined) settings.owners_can_demote_owners = teamBoolean(options["owners-can-demote"], "owners-can-demote");
362
+ if (Object.keys(settings).length) {
363
+ const current = await request(config, "/team");
364
+ body.settings = settings;
365
+ body.version = current.settings?.version;
366
+ }
367
+ if (!Object.keys(body).length) throw new CliError(`Usage: ${commandHelp("team")}`);
368
+ result = await request(config, "/team", { method: "PATCH", body });
369
+ } else {
370
+ throw new CliError(`Unknown team command: ${action}\n${commandHelp("team")}`);
371
+ }
372
+ if (jsonMode) emit(result);
373
+ else writeOut(action === "invite" ? `Invitation sent to ${positionals[1]}.` : action === "revoke" ? "Invitation revoked." : action === "remove" ? `Removed ${positionals[1]} from the team.` : action === "role" ? `Set ${positionals[1]} to ${positionals[2]}.` : "Team updated.");
374
+ return 0;
375
+ }
376
+
377
+ async function usageCommand(config, options) {
378
+ const days = Number(options.days || 30);
379
+ if (!Number.isInteger(days) || days < 1 || days > 90) throw new CliError("--days must be an integer from 1 to 90.");
380
+ const result = await request(config, `/team/usage?days=${days}`);
381
+ if (jsonMode) emit(result);
382
+ else {
383
+ writeOut(`${result.credits_used || 0} credits used across ${result.tasks || 0} tasks in the last ${result.days || days} days.`);
384
+ for (const member of result.members || []) writeOut(`${member.name || member.user_id}: ${member.credits_used || 0} credits, ${member.tasks || 0} tasks`);
385
+ }
386
+ return 0;
387
+ }
388
+
389
+ function taskRoute(taskId) {
390
+ return `/task/${encodeURIComponent(taskId)}`;
391
+ }
392
+
393
+ function formatElapsed(value) {
394
+ if (value === null || value === undefined || !Number.isFinite(Number(value))) return "?";
395
+ const seconds = Math.max(0, Math.floor(Number(value)));
396
+ if (seconds < 60) return `${seconds}s`;
397
+ return `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, "0")}s`;
398
+ }
399
+
400
+ function progressLine(status) {
401
+ const state = status.state || status.stage || "working";
402
+ const message = typeof status.message === "string" ? status.message.trim() : "";
403
+ const progress = status.progress && typeof status.progress === "object" ? status.progress : {};
404
+ const rows = progress.rows ?? 0;
405
+ const target = progress.target;
406
+ const rowsText = target == null ? `${rows} rows` : `${rows}/${target} rows`;
407
+ return [state, message, rowsText, formatElapsed(status.elapsed_s)].filter(Boolean).join(" · ");
408
+ }
409
+
410
+ function printProgress(status, lastVersion) {
411
+ if (jsonMode) return lastVersion;
412
+ const progress = status.progress && typeof status.progress === "object" ? status.progress : {};
413
+ const version = status.version ?? JSON.stringify([
414
+ status.state,
415
+ status.stage,
416
+ status.message,
417
+ progress.rows,
418
+ progress.target,
419
+ status.queue_position,
420
+ ]);
421
+ if (!process.stderr.isTTY && version === lastVersion) return lastVersion;
422
+ const line = progressLine(status);
423
+ if (process.stderr.isTTY) {
424
+ process.stderr.write(`\r\x1b[2K${line}`);
425
+ } else {
426
+ writeErr(line);
427
+ }
428
+ return version;
429
+ }
430
+
431
+ function clearProgressLine() {
432
+ if (process.stderr.isTTY && !jsonMode) process.stderr.write("\r\x1b[2K");
433
+ }
434
+
435
+ function errorFromStatus(status) {
436
+ const error = status.error;
437
+ if (status.state === "canceled" && !(error && typeof error === "object" && error.message)) {
438
+ return "Task canceled.";
439
+ }
440
+ if (error && typeof error === "object") return error.message || error.code || "Task failed.";
441
+ return "Task failed.";
442
+ }
443
+
444
+ function parseTimeout(value) {
445
+ if (value === undefined) return null;
446
+ const seconds = Number(value);
447
+ if (!Number.isFinite(seconds) || seconds < 0) throw new CliError("--timeout must be a non-negative number of seconds.");
448
+ return seconds;
449
+ }
450
+
451
+ async function fetchOutputPages(config, taskId, format) {
452
+ const rows = [];
453
+ const csvPages = [];
454
+ let cursor = "";
455
+ do {
456
+ const query = new URLSearchParams({ limit: "200", format });
457
+ if (cursor) query.set("cursor", cursor);
458
+ const response = await requestRaw(config, `${taskRoute(taskId)}/output?${query}`);
459
+ if (format === "csv") {
460
+ csvPages.push(response.text);
461
+ cursor = response.headers.get("x-next-cursor") || "";
462
+ } else {
463
+ const pageRows = Array.isArray(response.data?.rows) ? response.data.rows : [];
464
+ rows.push(...pageRows);
465
+ cursor = response.data?._next_cursor || response.data?.next_cursor || response.headers.get("x-next-cursor") || "";
466
+ }
467
+ } while (cursor);
468
+
469
+ if (format === "csv") {
470
+ const content = csvPages.length === 1 ? csvPages[0] : mergeCsvPages(csvPages);
471
+ return { content, rows: csvRecordCount(content), format };
472
+ }
473
+ return { content: rows, rows: rows.length, format };
474
+ }
475
+
476
+ function parseCsvRecords(csv) {
477
+ const records = [];
478
+ let record = [];
479
+ let field = "";
480
+ let quoted = false;
481
+
482
+ for (let index = 0; index < csv.length; index += 1) {
483
+ const char = csv[index];
484
+ if (quoted) {
485
+ if (char === '"') {
486
+ if (csv[index + 1] === '"') {
487
+ field += '"';
488
+ index += 1;
489
+ } else {
490
+ quoted = false;
491
+ }
492
+ } else {
493
+ field += char;
494
+ }
495
+ } else if (char === '"' && field === "") {
496
+ quoted = true;
497
+ } else if (char === ",") {
498
+ record.push(field);
499
+ field = "";
500
+ } else if (char === "\n" || char === "\r") {
501
+ if (char === "\r" && csv[index + 1] === "\n") index += 1;
502
+ record.push(field);
503
+ records.push(record);
504
+ record = [];
505
+ field = "";
506
+ } else {
507
+ field += char;
508
+ }
509
+ }
510
+
511
+ if (quoted) throw new CliError("Invalid CSV output: unterminated quoted field.");
512
+ if (field !== "" || record.length > 0) {
513
+ record.push(field);
514
+ records.push(record);
515
+ }
516
+ return records;
517
+ }
518
+
519
+ function serializeCsvRecord(fields) {
520
+ return fields.map((field) => {
521
+ const value = String(field ?? "");
522
+ const quote = (fields.length === 1 && value === "") || /[",\r\n]/.test(value);
523
+ return quote ? `"${value.replace(/"/g, '""')}"` : value;
524
+ }).join(",");
525
+ }
526
+
527
+ function mergeCsvPages(pages) {
528
+ const columns = [];
529
+ const seen = new Set();
530
+ const rows = [];
531
+
532
+ for (const page of pages) {
533
+ const [header, ...records] = parseCsvRecords(page);
534
+ if (!header) continue;
535
+ for (const column of header) {
536
+ if (!seen.has(column)) {
537
+ seen.add(column);
538
+ columns.push(column);
539
+ }
540
+ }
541
+ for (const record of records) {
542
+ rows.push(new Map(header.map((column, index) => [column, record[index] ?? ""])));
543
+ }
544
+ }
545
+
546
+ if (columns.length === 0) return "";
547
+ const output = [serializeCsvRecord(columns)];
548
+ for (const row of rows) {
549
+ output.push(serializeCsvRecord(columns.map((column) => row.get(column) ?? "")));
550
+ }
551
+ return `${output.join("\n")}\n`;
552
+ }
553
+
554
+ function csvRecordCount(csv) {
555
+ if (!csv) return 0;
556
+ let quoted = false;
557
+ let records = 0;
558
+ let hasValue = false;
559
+ for (let index = 0; index < csv.length; index += 1) {
560
+ const char = csv[index];
561
+ if (char === '"') {
562
+ hasValue = true;
563
+ if (quoted && csv[index + 1] === '"') {
564
+ index += 1;
565
+ } else {
566
+ quoted = !quoted;
567
+ }
568
+ } else if (char === "\n" && !quoted) {
569
+ records += 1;
570
+ hasValue = false;
571
+ } else {
572
+ hasValue ||= char !== "\r";
573
+ }
574
+ }
575
+ if (hasValue) records += 1;
576
+ return Math.max(0, records - 1);
577
+ }
578
+
579
+ async function saveOutputFile(filepath, result) {
580
+ const resolved = path.resolve(filepath);
581
+ await mkdir(path.dirname(resolved), { recursive: true });
582
+ const content = result.format === "csv"
583
+ ? result.content
584
+ : `${JSON.stringify(result.content, null, 2)}\n`;
585
+ await writeFile(resolved, content);
586
+ return { path: filepath, rows: result.rows };
587
+ }
588
+
589
+ function watchTimeout(options) {
590
+ const requested = parseTimeout(options.timeout);
591
+ if (requested !== null) return requested * 1000;
592
+ return process.stdout.isTTY ? Infinity : 100_000;
593
+ }
594
+
595
+ async function watchTask(config, taskId, options) {
596
+ const timeoutMs = watchTimeout(options);
597
+ const startedAt = Date.now();
598
+ const deadline = Number.isFinite(timeoutMs) ? startedAt + timeoutMs : Infinity;
599
+ let lastVersion = null;
600
+ let latest = null;
601
+
602
+ while (Date.now() <= deadline) {
603
+ const remainingSeconds = Number.isFinite(deadline) ? Math.max(1, Math.ceil((deadline - Date.now()) / 1000)) : 50;
604
+ const waitSeconds = Math.min(50, remainingSeconds);
605
+ latest = await request(config, `${taskRoute(taskId)}?wait=${waitSeconds}`);
606
+ lastVersion = printProgress(latest, lastVersion);
607
+ if (latest.done) {
608
+ clearProgressLine();
609
+ const status = latest;
610
+ if (status.state === "completed") {
611
+ let saved;
612
+ if (status.outcome === "blocked" || status.outcome === "partial") {
613
+ const label = status.outcome === "blocked" ? "Blocked" : "Partial";
614
+ writeErr(`${label}: ${status.message || ""}`);
615
+ writeErr(`Add detail: autumn reply ${taskId} "<more info>"`);
616
+ }
617
+ if (options.out) {
618
+ const format = path.extname(options.out).toLowerCase() === ".json" ? "json" : "csv";
619
+ saved = await saveOutputFile(options.out, await fetchOutputPages(config, taskId, format));
620
+ if (!jsonMode) writeErr(`Saved ${saved.rows} rows to ${saved.path}`);
621
+ } else if (!jsonMode) {
622
+ if (status.message) writeOut(status.message);
623
+ writeErr(`Rows: autumn rows ${taskId} --out rows.csv`);
624
+ }
625
+ if (jsonMode) {
626
+ emit(compactTaskStatus({
627
+ ...status,
628
+ task_id: status.task_id || taskId,
629
+ ...(saved ? { saved } : {}),
630
+ }));
631
+ }
632
+ return 0;
633
+ }
634
+ if (status.state === "input-required") {
635
+ if (!jsonMode) {
636
+ writeErr(`Autumn asks: ${status.message || "Send a message to continue."}`);
637
+ writeErr(`Reply: autumn reply ${taskId} "<answer>"`);
638
+ }
639
+ if (jsonMode) emit(compactTaskStatus({ ...status, task_id: status.task_id || taskId }));
640
+ return 2;
641
+ }
642
+ if (status.state === "failed" || status.state === "canceled") {
643
+ if (!jsonMode) writeErr(String(errorFromStatus(status)));
644
+ if (jsonMode) emit(compactTaskStatus({ ...status, task_id: status.task_id || taskId }));
645
+ return 1;
646
+ }
647
+ }
648
+ if (Date.now() >= deadline) break;
649
+ if (!latest.done) {
650
+ await sleep(Math.min(250, Math.max(0, deadline - Date.now())));
651
+ }
652
+ }
653
+
654
+ clearProgressLine();
655
+ const status = latest || { task_id: taskId, state: "working", done: false };
656
+ if (!jsonMode) {
657
+ writeErr(`Still working: ${status.message || status.state || "task in progress"}. Continue: autumn wait ${taskId}`);
658
+ }
659
+ if (jsonMode) {
660
+ emit(compactTaskStatus({ ...status, task_id: status.task_id || taskId, timed_out: true }));
661
+ }
662
+ return 3;
663
+ }
664
+
665
+ async function browserOpen(url) {
666
+ let command;
667
+ let args;
668
+ if (process.platform === "darwin") {
669
+ command = "open";
670
+ args = [url];
671
+ } else if (process.platform === "win32") {
672
+ command = "cmd";
673
+ args = ["/c", "start", "", url];
674
+ } else {
675
+ command = "xdg-open";
676
+ args = [url];
677
+ }
678
+ try {
679
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
680
+ child.on("error", () => {});
681
+ child.unref();
682
+ } catch {}
683
+ }
684
+
685
+ async function login(config, options) {
686
+ if (!options.force && configApiKey(config)) {
687
+ try {
688
+ await request(config, "/credits");
689
+ if (jsonMode) emit({ status: "already_logged_in" });
690
+ else writeErr("Already logged in. Use --force to log in again.");
691
+ return 0;
692
+ } catch (error) {
693
+ if (!(error instanceof CliError && error.status === 401)) throw error;
694
+ }
695
+ }
696
+
697
+ let pending = config.pending_login;
698
+ const nowSeconds = Date.now() / 1000;
699
+ if (!pending || !pending.device_code || Number(pending.expires_at) <= nowSeconds) {
700
+ const started = await requestRaw(config, "/auth/device", {
701
+ method: "POST",
702
+ body: { client: "Autumn CLI", host: os.hostname() },
703
+ authenticated: false,
704
+ });
705
+ pending = {
706
+ device_code: started.data.device_code,
707
+ user_code: started.data.user_code,
708
+ verification_uri_complete: started.data.verification_uri_complete,
709
+ interval: Number(started.data.interval) > 0 ? Number(started.data.interval) : 2,
710
+ expires_at: nowSeconds + Number(started.data.expires_in || 600),
711
+ };
712
+ config.pending_login = pending;
713
+ await saveConfig(config);
714
+ }
715
+
716
+ if (!jsonMode) {
717
+ writeErr(`Open this link to connect Autumn:\n ${pending.verification_uri_complete}\nCode: ${pending.user_code}`);
718
+ }
719
+ if (process.stdout.isTTY && !options["no-browser"]) void browserOpen(pending.verification_uri_complete);
720
+
721
+ const requestedTimeout = parseTimeout(options.timeout);
722
+ const availableSeconds = Math.max(0, Number(pending.expires_at) - Date.now() / 1000);
723
+ const timeoutSeconds = requestedTimeout === null
724
+ ? (process.stdout.isTTY ? availableSeconds : 90)
725
+ : requestedTimeout;
726
+ const deadline = Date.now() + Math.min(timeoutSeconds, availableSeconds) * 1000;
727
+
728
+ while (Date.now() <= deadline) {
729
+ let polled;
730
+ try {
731
+ polled = await requestRaw(config, "/auth/device/token", {
732
+ method: "POST",
733
+ body: { device_code: pending.device_code },
734
+ authenticated: false,
735
+ });
736
+ } catch (error) {
737
+ if (error instanceof CliError && (error.status === 403 || error.status === 410)) {
738
+ delete config.pending_login;
739
+ await saveConfig(config);
740
+ }
741
+ throw error;
742
+ }
743
+ if (polled.data?.status === "approved") {
744
+ config.api_key = polled.data.api_key;
745
+ delete config.pending_login;
746
+ await saveConfig(config);
747
+ if (jsonMode) emit({ status: "logged_in" });
748
+ else writeErr(`Logged in. Key saved to ${CONFIG_PATH}.`);
749
+ return 0;
750
+ }
751
+ if (polled.data?.status !== "pending") {
752
+ throw new CliError(errorPayloadMessage(polled.data, "Unexpected device-login response."));
753
+ }
754
+ const remaining = deadline - Date.now();
755
+ if (remaining <= 0) break;
756
+ await sleep(Math.min(Number(pending.interval || 2) * 1000, remaining));
757
+ }
758
+
759
+ if (jsonMode) {
760
+ emit({
761
+ status: "pending",
762
+ verification_uri_complete: pending.verification_uri_complete,
763
+ user_code: pending.user_code,
764
+ });
765
+ } else {
766
+ writeErr("Waiting for approval. After approving, run `autumn login` again.");
767
+ }
768
+ return 3;
769
+ }
770
+
771
+ async function logout(config) {
772
+ delete config.api_key;
773
+ await saveConfig(config);
774
+ writeOut(`Removed the saved API key. It remains valid until revoked in Settings > Developer.`);
775
+ return 0;
776
+ }
777
+
778
+ async function runTask(config, prompt, options) {
779
+ const started = await request(config, "/task", { method: "POST", body: { prompt } });
780
+ const taskId = started.task_id;
781
+ if (!taskId) throw new CliError("The API did not return a task_id.");
782
+ if (!jsonMode) writeErr(`Started ${taskId} · ${started.app_url || ""}`.trim());
783
+ if (options.detach) {
784
+ if (jsonMode) {
785
+ emit(compactTaskStatus({
786
+ ...started,
787
+ state: started.state || "submitted",
788
+ done: started.done ?? false,
789
+ }));
790
+ } else {
791
+ emit(taskId);
792
+ }
793
+ return 0;
794
+ }
795
+ return watchTask(config, taskId, options);
796
+ }
797
+
798
+ async function replyTask(config, taskId, message, options) {
799
+ const accepted = await request(config, `${taskRoute(taskId)}/continue`, {
800
+ method: "POST",
801
+ body: { message },
802
+ headers: { "Idempotency-Key": randomUUID() },
803
+ });
804
+ if (!jsonMode) {
805
+ if (accepted.queue_position != null) writeErr(`Queued (position ${accepted.queue_position})`);
806
+ else writeErr("Sent");
807
+ }
808
+ return watchTask(config, taskId, options);
809
+ }
810
+
811
+ function mcpConfig(kind, config) {
812
+ const key = configApiKey(config) || "$AUTUMN_API_KEY";
813
+ const endpoint = `${apiBase(config)}/mcp`;
814
+ if (kind === "claude") {
815
+ return `claude mcp add --transport http autumn ${endpoint} --header "Authorization: Bearer ${key}"`;
816
+ }
817
+ if (kind === "cursor") {
818
+ return JSON.stringify({
819
+ mcpServers: {
820
+ autumn: {
821
+ url: endpoint,
822
+ headers: { Authorization: `Bearer ${key}` },
823
+ },
824
+ },
825
+ }, null, 2);
826
+ }
827
+ if (kind === "codex") {
828
+ if (configApiKey(config)) {
829
+ return `[mcp_servers.autumn]\nurl = ${JSON.stringify(endpoint)}\n\n[mcp_servers.autumn.http_headers]\nAuthorization = ${JSON.stringify(`Bearer ${key}`)}`;
830
+ }
831
+ return `[mcp_servers.autumn]\nurl = ${JSON.stringify(endpoint)}\nbearer_token_env_var = "AUTUMN_API_KEY"`;
832
+ }
833
+ throw new CliError("Choose one of: claude, codex, cursor.");
834
+ }
835
+
836
+ async function readBundledSkill() {
837
+ return readFile(path.resolve(SCRIPT_DIR, "../skill.md"), "utf8");
838
+ }
839
+
840
+ async function installSkill(destination, content) {
841
+ await mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
842
+ await writeFile(destination, content, { mode: 0o600 });
843
+ return destination;
844
+ }
845
+
846
+ async function setup(kind, config) {
847
+ const selected = kind || "all";
848
+ if (!["claude", "codex", "all"].includes(selected)) {
849
+ throw new CliError("Choose one of: claude, codex, all.");
850
+ }
851
+ const content = await readBundledSkill();
852
+ const installed = [];
853
+ if (selected === "claude" || selected === "all") {
854
+ installed.push(await installSkill(
855
+ path.join(os.homedir(), ".claude", "skills", "autumn", "SKILL.md"),
856
+ content,
857
+ ));
858
+ }
859
+ if (selected === "codex" || selected === "all") {
860
+ installed.push(await installSkill(
861
+ path.join(os.homedir(), ".agents", "skills", "autumn", "SKILL.md"),
862
+ content,
863
+ ));
864
+ }
865
+ if (jsonMode) emit({ installed });
866
+ else {
867
+ for (const destination of installed) writeErr(`Installed Autumn Skill: ${destination}`);
868
+ if (!configApiKey(config)) writeErr("Next: autumn login");
869
+ }
870
+ return 0;
871
+ }
872
+
873
+ async function commandMain(command, options, positionals, config) {
874
+ if (command === "login") return login(config, options);
875
+ if (command === "logout") return logout(config);
876
+ if (command === "setup") return setup(positionals[0], config);
877
+ if (command === "mcp-config") {
878
+ const kind = positionals[0] || "claude";
879
+ emit(mcpConfig(kind, config));
880
+ return 0;
881
+ }
882
+ if (command === "credits" || command === "whoami") {
883
+ const result = await request(config, "/credits");
884
+ if (jsonMode) emit(result);
885
+ else writeOut(creditsSummary(result));
886
+ return 0;
887
+ }
888
+ if (command === "buy") {
889
+ const allowed = [5000, 10000, 20000, 50000];
890
+ const credits = Number(positionals[0]);
891
+ if (!positionals[0] || !Number.isInteger(credits) || !allowed.includes(credits)) {
892
+ throw new CliError(`Choose one of these credit amounts: ${allowed.join(", ")}.`);
893
+ }
894
+ const result = await request(config, "/credits/checkout", {
895
+ method: "POST",
896
+ body: { credits },
897
+ });
898
+ if (jsonMode) emit(result);
899
+ else {
900
+ writeOut(`Open this link to pay: ${result.url}`);
901
+ if (process.stdout.isTTY) void browserOpen(result.url);
902
+ }
903
+ return 0;
904
+ }
905
+ if (command === "run") {
906
+ requirePositionals(positionals, 1, commandHelp(command));
907
+ return runTask(config, positionals.join(" "), options);
908
+ }
909
+ if (command === "wait") {
910
+ requirePositionals(positionals, 1, commandHelp(command));
911
+ return watchTask(config, positionals[0], options);
912
+ }
913
+ if (command === "reply") {
914
+ requirePositionals(positionals, 2, commandHelp(command));
915
+ return replyTask(config, positionals[0], positionals.slice(1).join(" "), options);
916
+ }
917
+ if (command === "status") {
918
+ requirePositionals(positionals, 1, commandHelp(command));
919
+ const status = await request(config, taskRoute(positionals[0]));
920
+ if (jsonMode) emit(compactTaskStatus({ ...status, task_id: status.task_id || positionals[0] }));
921
+ else writeErr(progressLine(status));
922
+ return taskExitCode(status.state);
923
+ }
924
+ if (command === "rows") {
925
+ requirePositionals(positionals, 1, commandHelp(command));
926
+ const format = options.format || (path.extname(options.out || "").toLowerCase() === ".json" ? "json" : "csv");
927
+ if (format !== "csv" && format !== "json") throw new CliError("--format must be csv or json.");
928
+ const result = await fetchOutputPages(config, positionals[0], format);
929
+ if (options.out) {
930
+ const saved = await saveOutputFile(options.out, result);
931
+ if (jsonMode) emit(saved);
932
+ else writeErr(`Saved ${saved.rows} rows to ${saved.path}`);
933
+ } else if (format === "csv") {
934
+ process.stdout.write(result.content);
935
+ } else {
936
+ emit(result.content);
937
+ }
938
+ return 0;
939
+ }
940
+ if (command === "stop") {
941
+ requirePositionals(positionals, 1, commandHelp(command));
942
+ const result = await request(config, `${taskRoute(positionals[0])}/stop`, { method: "POST" });
943
+ if (jsonMode) emit(result);
944
+ else writeOut(`Stopping ${positionals[0]}.`);
945
+ return 0;
946
+ }
947
+ if (command === "list") {
948
+ const limit = Number(options.limit || 20);
949
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
950
+ throw new CliError("--limit must be an integer from 1 to 200.");
951
+ }
952
+ const query = positionals.join(" ").trim();
953
+ const params = new URLSearchParams({ limit: String(limit) });
954
+ if (query) params.set("q", query);
955
+ const result = await request(config, `/task?${params}`);
956
+ if (jsonMode) emit(result);
957
+ else {
958
+ const tasks = Array.isArray(result) ? result : result.tasks || [];
959
+ writeOut("task_id\tname\tupdated_at");
960
+ for (const task of tasks) writeOut(`${task.task_id || ""}\t${task.name || ""}\t${task.updated_at || ""}`);
961
+ }
962
+ return 0;
963
+ }
964
+ if (command === "team") return teamCommand(config, positionals, options);
965
+ if (command === "usage") return usageCommand(config, options);
966
+ throw new CliError(`Unknown command: ${command}\n\n${globalHelp()}`);
967
+ }
968
+
969
+ async function main() {
970
+ const tokens = process.argv.slice(2);
971
+ if (tokens.includes("--version")) {
972
+ writeOut(VERSION);
973
+ return 0;
974
+ }
975
+ if (!tokens.length || tokens[0] === "--help" || tokens[0] === "-h" || tokens[0] === "help") {
976
+ writeOut(globalHelp());
977
+ return 0;
978
+ }
979
+ const command = tokens.shift();
980
+ const { flags, positionals } = parseArgs(tokens);
981
+ jsonMode = Boolean(flags.json);
982
+ if (flags.help) {
983
+ writeOut(commandHelp(command));
984
+ return 0;
985
+ }
986
+ const config = await readConfig();
987
+ return commandMain(command, flags, positionals, config);
988
+ }
989
+
990
+ main().then((exitCode) => {
991
+ process.exitCode = exitCode;
992
+ }).catch((error) => {
993
+ const message = error instanceof Error ? error.message : String(error);
994
+ if (jsonMode) writeOut(JSON.stringify({ error: message }));
995
+ else writeErr(message);
996
+ process.exitCode = error instanceof CliError ? error.exitCode : 1;
997
+ });
package/package.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "name": "@autumnai/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.2",
4
+ "description": "Autumn research agent from your terminal or coding agent",
5
+ "homepage": "https://www.autumn.ai/docs",
6
+ "type": "module",
7
+ "bin": {
8
+ "autumn": "bin/autumn.mjs"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "skill.md"
16
+ ],
17
+ "scripts": {
18
+ "test": "node --test",
19
+ "prepack": "cp ../landing/public/skill.md ./skill.md"
20
+ },
21
+ "license": "MIT"
22
+ }
package/skill.md ADDED
@@ -0,0 +1,158 @@
1
+ ---
2
+ name: autumn
3
+ description: Use when a user wants web research done as rows - finding or enriching companies, people, jobs, products, filings or contacts; building or verifying a list; or scraping structured data from public sources. Also use to check, refine, stop or read an existing Autumn task, or to check Autumn credits.
4
+ ---
5
+
6
+ # Autumn
7
+
8
+ Autumn is a research agent that runs in the cloud. You give it a brief, it searches,
9
+ verifies and saves rows with sources, and you read the rows. A task takes minutes, not
10
+ seconds, and a task is a conversation: you can answer its questions and refine it on the
11
+ same `task_id`.
12
+
13
+ No task_id? Find it with `list_tasks` (MCP), `autumn list "<words>"` (CLI) or `GET /task?q=<words>`.
14
+
15
+ Don't use Autumn for a quick fact you can answer yourself.
16
+
17
+ ## Fastest path: the CLI
18
+
19
+ If `autumn` isn't installed, run:
20
+
21
+ ```bash
22
+ npm install -g @autumnai/cli
23
+ autumn setup
24
+ autumn login
25
+ ```
26
+
27
+ Start a task and save its rows:
28
+
29
+ ```bash
30
+ autumn run "Find recent funding rounds for climate software companies" --out rounds.csv
31
+ ```
32
+
33
+ If the task needs an answer, ask the user and reply on the same task. If it is still
34
+ working, resume waiting:
35
+
36
+ ```bash
37
+ autumn reply <task-id> "Focus on North American companies"
38
+ autumn wait <task-id>
39
+ ```
40
+
41
+ Exit codes: `0` completed, `1` failed or request error, `2` input required, `3` still
42
+ working or approval pending, and `4` authentication required.
43
+
44
+ ## Setup (once)
45
+
46
+ - Base URL `https://api.autumn.ai`. Send `X-API-Key: $AUTUMN_API_KEY` (keys start with `ak_`).
47
+ - No key yet? Follow https://www.autumn.ai/agent-quickstart.md: `autumn login` or two curl calls get a key once the user approves in their browser. Never ask the user to paste a key into chat, and never print, log or commit it.
48
+ - Check it works: `curl -sS https://api.autumn.ai/credits -H "X-API-Key: $AUTUMN_API_KEY"`.
49
+ - To buy credits, use MCP `buy_credits` or `autumn buy 10000` to get a payment link for the user.
50
+
51
+ ## Team and usage
52
+
53
+ - MCP: `get_team`, `get_usage`, `invite_member`, `revoke_invitation`,
54
+ `set_member_role`, `update_team`, `remove_member`.
55
+ - CLI: `autumn team`, `autumn team invite|revoke|role|remove|set`, `autumn usage`.
56
+ - REST: `GET /team`, `PATCH /team`, `POST /team/invitations`,
57
+ `DELETE /team/invitations/{invitation_id}`, `PATCH` / `DELETE /team/members/{user_id}`,
58
+ and `GET /team/usage?days=30`.
59
+ - Team writes are owner-only. Creating or leaving a team is done in the app.
60
+
61
+ ## The loop
62
+
63
+ ```bash
64
+ # 1. Start
65
+ curl -sS -X POST https://api.autumn.ai/task -H "X-API-Key: $AUTUMN_API_KEY" \
66
+ -H "Content-Type: application/json" -d @brief.json # -> {"task_id": ..., "state": "submitted", ...}
67
+
68
+ # 2. Wait. Returns as soon as anything changes (max 50s). Repeat until "done": true.
69
+ curl -sS "https://api.autumn.ai/task/$ID?wait=50" -H "X-API-Key: $AUTUMN_API_KEY"
70
+
71
+ # 3. Answer or refine (queues if a turn is running, returns 202)
72
+ curl -sS -X POST https://api.autumn.ai/task/$ID/continue -H "X-API-Key: $AUTUMN_API_KEY" \
73
+ -H "Content-Type: application/json" -d '{"message": "..."}'
74
+
75
+ # 4. Read rows (paged; use cursor for more). CSV keeps big results out of your context.
76
+ curl -sS "https://api.autumn.ai/task/$ID/output?format=csv" -H "X-API-Key: $AUTUMN_API_KEY" -o rows.csv
77
+ ```
78
+
79
+ Every status response has: `state`, `done`, `message` (progress line, the agent's
80
+ question, or its final reply), `progress` (`rows` / `target`), `credits_used`, `next.hint`
81
+ and `app_url`.
82
+
83
+ | `state` | `done` | What you do |
84
+ |---|---|---|
85
+ | `submitted` | no | Wait. `queue_position` says where it is. |
86
+ | `working` | no | Wait. Relay `message` to the user as a one-line update. |
87
+ | `input-required` | yes | Show `message` (a question) to the user, send their answer with `/continue`. |
88
+ | `completed` | yes | Read `/output`. `outcome: partial` means it stopped short of the target. |
89
+ | `failed` | yes | Report `error.message`. For `out_of_credits`, get a pay link with MCP `buy_credits`, `autumn buy 10000`, or `POST /credits/checkout {"credits":10000}` and give it to the user; https://platform.autumn.ai/?settings=usage is a fallback. |
90
+ | `canceled` | yes | Stopped by `POST /task/$ID/stop`. Rows saved so far are still in `/output`. |
91
+
92
+ Call the wait in a loop rather than sleeping. Each call is one update for the user.
93
+
94
+ ## How to research well with Autumn
95
+
96
+ Autumn plans from your brief. A vague brief doesn't fail; it quietly answers a slightly
97
+ different question. Write the brief like a contract:
98
+
99
+ 1. **Keep every specific the user gave you, verbatim.** Names, URLs, geography, time window,
100
+ exclusions, examples. Paraphrasing is where detail gets lost.
101
+ 2. **Say how many.** `"count": 25` (or a number in the prompt). Without one, "find companies"
102
+ has no natural end.
103
+ 3. **Name the columns.** If you know the shape, send a schema instead of describing it:
104
+ ```json
105
+ {"prompt": "Find 25 US fintechs that raised a Series A in 2025. Exclude crypto.",
106
+ "count": 25,
107
+ "output": {"kind": "research",
108
+ "schema": {"company": {"type": "name"}, "domain": {"type": "domain"},
109
+ "round_date": {"type": "date"}, "lead_investor": {"type": "str"},
110
+ "source_url": {"type": "url"}}}}
111
+ ```
112
+ 4. **State the per-row bar as rules.** Rules are checks each row must pass, not goals:
113
+ `{"rules": {"series_a_2025": "Raised a Series A in 2025, with a source", "us": "Headquartered in the US"}}`.
114
+ Without a bar the agent decides what counts.
115
+ 5. **Pick the output kind.** `research` for lists and tables. `person` or `company` for a
116
+ deep profile per known entity. Pass the known entities as `input_rows`
117
+ (e.g. `[{"name": "Ada Lovelace", "company": "Analytical Engines"}]`) or upload a file.
118
+ 6. **Pick the tier when it matters.** `"version": "ranger"` for open-ended research where the
119
+ agent must decide where to look or cross-check facts. `"version": "scout"` for bounded,
120
+ well-specified work where speed matters. The tier is fixed for the life of the task.
121
+ 7. **Questions.** `clarify` is `false` by default: the agent picks sensible defaults and runs.
122
+ Set `"clarify": true` only when the user is present and an answer would change the work.
123
+
124
+ For complex or relayed requests, fetch `GET /task/metaprompt`. It returns the task spec
125
+ schema and a metaprompt for turning a request into a spec for `POST /task/start`.
126
+
127
+ ## Iterate on the same task
128
+
129
+ - More rows, a new column, a narrower list, fixing gaps: `/continue` the same task. Never start
130
+ a new task for a follow-up; that loses the rows and the context.
131
+ - Restate the goal as one clear instruction, not the user's raw words.
132
+ "also linkedin" -> "Add each founder's LinkedIn URL to every existing row."
133
+ - After `outcome: partial`, look at what's missing and ask for exactly that:
134
+ "Find 8 more that meet the same rules" or "Fill lead_investor where it's empty".
135
+
136
+ ## Judge the result before reporting it
137
+
138
+ - Report from `/output` rows, never from status text.
139
+ - When a completed task has outcome `blocked` or `partial`, read `message`; if it asks for information, get it from the user and reply on the same task.
140
+ - A shorter verified list beats a padded one. Autumn leaves a cell empty or `unclear` rather
141
+ than guess. Say so, don't fill it in yourself.
142
+ - Spot-check a few rows against their source links for anything the user will act on.
143
+ - Show the most useful rows, the count vs target, and the `app_url` for the full table.
144
+
145
+ ## Style
146
+
147
+ - Terse and status-first. One line per update, e.g. `working - 14/25 rows - verifying funding rounds`.
148
+ - Keep the `task_id` and reuse it.
149
+ - Don't show raw traces, internal errors or the API key.
150
+ - Tasks spend credits. For a large or broad task, check `GET /credits` first and confirm
151
+ with the user if the cost looks high.
152
+
153
+ ## Reference
154
+
155
+ - API contract: https://api.autumn.ai/openapi.json
156
+ - Docs index: https://www.autumn.ai/docs/llms.txt
157
+ - App and API keys: https://platform.autumn.ai/?settings=connect
158
+ - Help: founders@autumnai.com