automate-it 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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/ait.mjs +637 -0
  4. package/package.json +27 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Working Dev's Hero LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # automate-it
2
+
3
+ CLI for [Automate It](https://automate.it.com) — AI agents create content tasks, submit them through a human review gate, and publish everywhere.
4
+
5
+ `ait` is a single-file, zero-dependency CLI (Node 18+ or Bun) that speaks Automate It's MCP server. Everything a remote MCP client can do, headlessly: create and work tasks, submit finished content straight into the human review queue, revise after rejection, read workspace skills, and fetch links to published posts.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install -g automate-it
11
+ ait --help
12
+ ```
13
+
14
+ Or without installing: `npx automate-it --help`
15
+
16
+ ## Configure
17
+
18
+ ```sh
19
+ export AUTOMATE_IT_API_KEY="ak_..." # required — create under Profile → API keys
20
+ export AUTOMATE_IT_WORKSPACE="<workspace-id>" # optional; auto-resolved if the key sees one workspace
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ```sh
26
+ ait workspaces # verify access
27
+
28
+ # Delegate: the built-in worker generates the content
29
+ ait task create --title "Post about our launch" \
30
+ --instructions "Write an upbeat announcement." --output-types x
31
+
32
+ # Or bring your own content — one shot, straight into human review
33
+ ait task submit --title "Launch post" --type x \
34
+ --body "We're live — new API, faster everything."
35
+
36
+ ait task get <taskId> # todo → working → review → approved → published
37
+ ait task links <taskId> # live post URLs once published
38
+ ```
39
+
40
+ Every task passes a human review gate before anything publishes — the reviewer approves, rejects with feedback (revise with `ait task update-content`, then `ait task complete`), or publishes. Your agent never needs your social credentials.
41
+
42
+ ## Documentation
43
+
44
+ Full setup for Claude Code, Codex, Cursor, OpenClaw, and Hermes, plus the worker-mode loop and complete command reference: **[docs.automate.it.com](https://docs.automate.it.com)**
45
+
46
+ ## License
47
+
48
+ MIT © Working Dev's Hero LLC
package/ait.mjs ADDED
@@ -0,0 +1,637 @@
1
+ #!/usr/bin/env node
2
+ // ait — Automate It CLI for AI agents.
3
+ //
4
+ // Zero-dependency ESM script (Node 18+ or Bun). Speaks the Model Context
5
+ // Protocol (StreamableHTTP, stateless) to Automate It's public MCP server,
6
+ // authenticating with an API key (ak_*) — so the CLI can do everything a
7
+ // remote MCP client can, headlessly. `ait call <tool>` passes any MCP tool
8
+ // through verbatim; the named commands are ergonomic wrappers over the
9
+ // common ones. Every command goes through MCP — there is no REST path.
10
+ //
11
+ // Tool output is printed verbatim on stdout; errors go to stderr as
12
+ // {"error": "..."} with exit code 1.
13
+
14
+ import { pathToFileURL } from "node:url";
15
+ import { realpathSync } from "node:fs";
16
+
17
+ const DEFAULT_API_URL = "https://api.automate.it.com";
18
+ const REQUEST_TIMEOUT_MS = 120_000;
19
+ const PUBLISH_MODES = ["immediate", "scheduled", "manual"];
20
+
21
+ export const USAGE = `ait — Automate It CLI for AI agents
22
+
23
+ Environment:
24
+ AUTOMATE_IT_API_KEY (required) API key (ak_*), created in Profile → API keys
25
+ AUTOMATE_IT_API_URL API base URL (default: ${DEFAULT_API_URL})
26
+ AUTOMATE_IT_WORKSPACE Default workspace id (else pass --workspace, else
27
+ auto-resolved when the key has exactly one workspace)
28
+
29
+ Discovery:
30
+ ait tools List every available MCP tool
31
+ ait call <tool> [--args '<json>'] Call any MCP tool directly (workspaceId
32
+ is injected automatically if omitted)
33
+ ait workspaces List workspaces the key can access
34
+ ait whoami Show the user this key acts as
35
+
36
+ Tasks (the review-gate loop):
37
+ ait task create --title <t> Create a task
38
+ [--instructions <text>] [--output-types x,linkedin,...]
39
+ [--publish-mode manual|immediate|scheduled] [--publish-at <ISO date>]
40
+ [--no-review] [--claim] [--skills <names or ids>] [--assign <userId|me>]
41
+ --claim creates the task already claimed (status "working") so the
42
+ built-in worker never picks it up — use it when YOU will generate the
43
+ content, then: task add-content → task complete.
44
+ --skills accepts skill names or ids (see \`ait skills list\`).
45
+ --assign gives the task to a workspace member; the built-in worker
46
+ skips assigned tasks and the assignee works them.
47
+ ait task submit --title <t> --body <text>
48
+ [--type <contentType>] [--media '<json array>'] [same flags as create]
49
+ One-shot: creates the task WITH finished content — it skips todo/working
50
+ and lands directly in the human review queue.
51
+ ait task list [--status <s>] [--mine] [--limit <n>] [--offset <n>]
52
+ --mine filters to tasks assigned to you (find your agent's work with
53
+ --mine --status todo).
54
+ Statuses: todo working review approved
55
+ publishing published deleted failed
56
+ ait task get <taskId> Full task detail, incl. reviewer comments
57
+ ait task links <taskId> Published post URLs for a task
58
+ ait task claim-next Atomically claim the oldest todo task
59
+ ait task claim <taskId> Claim a specific todo task
60
+ ait task add-content <taskId> Add generated content to a claimed task
61
+ [--body <text>] [--type <contentType>] [--title <t>]
62
+ [--media '<json array>'] [--sort-order <n>]
63
+ ait task update-content <taskId> <contentItemId>
64
+ [--body <text>] [--title <t>] Rewrite a content item in place
65
+ ait task delete-content <taskId> <contentItemId>
66
+ ait task clear-content <taskId> Remove every content item from a task
67
+ ait task complete <taskId> Finish work (status → review/approved)
68
+ ait task comment <taskId> --comment <text>
69
+ Leave a note for the human reviewer
70
+ ait task next-review Next task waiting for human review
71
+ ait task approve <taskId> Approve (reviewer/admin keys)
72
+ ait task reject <taskId> [--comment <text>]
73
+ ait task publish <taskId> [--platforms a,b]
74
+ ait task delete <taskId>
75
+
76
+ Skills (workspace voice, formatting rules, bundled reference files):
77
+ ait skills list id, name, description for each skill
78
+ ait skills get <skillId> Full instructions + bundled file ids
79
+
80
+ Automations:
81
+ ait automation create --name <n> [--instructions <text>]
82
+ [--output-types x,...] [--schedule <json>] [--no-review]
83
+ ait automation list
84
+ ait automation get <automationId>
85
+ ait automation update <automationId> [--name <n>] [--instructions <text>]
86
+ [--output-types x,...] [--schedule <json>]
87
+ ait automation delete <automationId>
88
+ ait automation run <automationId> Trigger now; returns the spawned task
89
+
90
+ Files:
91
+ ait upload-url --filename <f> --mime-type <m>
92
+ Presigned upload URL for media
93
+ ait files list [--search <s>] [--mime-type <m>] [--limit <n>]
94
+ ait files download-url <fileId> [--expires-in <seconds>]
95
+ Presigned URL — download it yourself
96
+ (folders/integrations: use \`ait call\` — see \`ait tools\`)
97
+
98
+ All workspace commands accept --workspace <id>.`;
99
+
100
+ export class CliError extends Error {}
101
+
102
+ const BOOLEAN_FLAGS = new Set(["no-review", "claim", "mine", "help"]);
103
+
104
+ export function parseArgs(argv) {
105
+ const positional = [];
106
+ const flags = {};
107
+ for (let i = 0; i < argv.length; i++) {
108
+ const arg = argv[i];
109
+ if (!arg.startsWith("--")) {
110
+ positional.push(arg);
111
+ continue;
112
+ }
113
+ let key = arg.slice(2);
114
+ let value;
115
+ const eq = key.indexOf("=");
116
+ if (eq !== -1) {
117
+ value = key.slice(eq + 1);
118
+ key = key.slice(0, eq);
119
+ if (BOOLEAN_FLAGS.has(key)) value = value !== "false";
120
+ } else if (BOOLEAN_FLAGS.has(key)) {
121
+ value = true;
122
+ } else {
123
+ value = argv[++i];
124
+ if (value === undefined) throw new CliError(`Missing value for --${key}`);
125
+ }
126
+ flags[key] = value;
127
+ }
128
+ return { positional, flags };
129
+ }
130
+
131
+ function csv(value) {
132
+ if (!value) return [];
133
+ return String(value)
134
+ .split(",")
135
+ .map((s) => s.trim())
136
+ .filter(Boolean);
137
+ }
138
+
139
+ function parseJsonFlag(value, flagName) {
140
+ try {
141
+ return JSON.parse(value);
142
+ } catch {
143
+ throw new CliError(`--${flagName} must be valid JSON`);
144
+ }
145
+ }
146
+
147
+ // Tool results are text designed for agent consumption; some prefix JSON
148
+ // with a status line ("Successfully created task:\n{...}"). This pulls the
149
+ // first JSON value out of such text.
150
+ export function extractJson(text) {
151
+ try {
152
+ return JSON.parse(text);
153
+ } catch {
154
+ // fall through to bracket scan
155
+ }
156
+ const candidates = ["{", "["]
157
+ .map((ch) => text.indexOf(ch))
158
+ .filter((i) => i !== -1);
159
+ if (candidates.length === 0) return null;
160
+ try {
161
+ return JSON.parse(text.slice(Math.min(...candidates)));
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ function parseRpcResponse(contentType, text) {
168
+ if (contentType.includes("text/event-stream")) {
169
+ for (const line of text.split("\n")) {
170
+ if (!line.startsWith("data:")) continue;
171
+ let message;
172
+ try {
173
+ message = JSON.parse(line.slice(5).trim());
174
+ } catch {
175
+ continue;
176
+ }
177
+ if (message.result !== undefined || message.error !== undefined) return message;
178
+ }
179
+ throw new CliError("No JSON-RPC response found in MCP event stream");
180
+ }
181
+ try {
182
+ return JSON.parse(text);
183
+ } catch {
184
+ throw new CliError(`Unparseable MCP response: ${text.slice(0, 200)}`);
185
+ }
186
+ }
187
+
188
+ function baseUrl(ctx) {
189
+ return (ctx.env.AUTOMATE_IT_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
190
+ }
191
+
192
+ function apiKey(ctx) {
193
+ const key = ctx.env.AUTOMATE_IT_API_KEY;
194
+ if (!key) {
195
+ throw new CliError(
196
+ "AUTOMATE_IT_API_KEY is not set. Create an API key in Automate It (Profile → API keys) and export it."
197
+ );
198
+ }
199
+ return key;
200
+ }
201
+
202
+ async function mcpRequest(ctx, method, params) {
203
+ const url = `${baseUrl(ctx)}/mcp`;
204
+ let res;
205
+ try {
206
+ res = await ctx.fetchFn(url, {
207
+ method: "POST",
208
+ headers: {
209
+ Authorization: `Bearer ${apiKey(ctx)}`,
210
+ "Content-Type": "application/json",
211
+ Accept: "application/json, text/event-stream",
212
+ },
213
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
214
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
215
+ });
216
+ } catch (err) {
217
+ throw new CliError(`Request to ${url} failed: ${err?.message || err}`);
218
+ }
219
+ const text = await res.text();
220
+ if (!res.ok) {
221
+ const detail = extractJson(text)?.error ?? text.slice(0, 200) ?? res.statusText;
222
+ throw new CliError(`MCP HTTP ${res.status}: ${typeof detail === "string" ? detail : JSON.stringify(detail)}`);
223
+ }
224
+ const message = parseRpcResponse(res.headers.get("content-type") || "", text);
225
+ if (message.error) {
226
+ throw new CliError(`MCP error ${message.error.code}: ${message.error.message}`);
227
+ }
228
+ return message.result;
229
+ }
230
+
231
+ async function callTool(ctx, name, args) {
232
+ const result = await mcpRequest(ctx, "tools/call", { name, arguments: args });
233
+ const text = (result?.content ?? [])
234
+ .filter((c) => c.type === "text")
235
+ .map((c) => c.text)
236
+ .join("\n");
237
+ if (result?.isError) throw new CliError(text || `Tool ${name} failed`);
238
+ return text;
239
+ }
240
+
241
+ async function resolveWorkspace(ctx, flags) {
242
+ if (flags.workspace) return flags.workspace;
243
+ if (ctx.env.AUTOMATE_IT_WORKSPACE) return ctx.env.AUTOMATE_IT_WORKSPACE;
244
+ const workspaces = extractJson(await callTool(ctx, "list_workspaces", {}));
245
+ if (!Array.isArray(workspaces) || workspaces.length === 0) {
246
+ throw new CliError("This API key has no workspaces.");
247
+ }
248
+ if (workspaces.length === 1) return workspaces[0].id;
249
+ const options = workspaces.map((w) => `${w.id} (${w.name})`).join(", ");
250
+ throw new CliError(
251
+ `Multiple workspaces available — pass --workspace <id> or set AUTOMATE_IT_WORKSPACE. Options: ${options}`
252
+ );
253
+ }
254
+
255
+ export function shapeLinks(task) {
256
+ const results = Array.isArray(task?.publishResults) ? task.publishResults : [];
257
+ return {
258
+ taskId: task?.id ?? null,
259
+ status: task?.status ?? null,
260
+ links: results.map((r) => ({
261
+ platform: r.platform ?? null,
262
+ postUrl: r.postUrl ?? null,
263
+ postId: r.postId ?? null,
264
+ })),
265
+ };
266
+ }
267
+
268
+ /** Fields shared by `task create` and `task submit`. */
269
+ function buildCreateTaskArgs(flags) {
270
+ if (!flags.title) throw new CliError("--title is required for task create");
271
+ const publishMode = flags["publish-mode"] || "manual";
272
+ if (!PUBLISH_MODES.includes(publishMode)) {
273
+ throw new CliError(`--publish-mode must be one of: ${PUBLISH_MODES.join(", ")}`);
274
+ }
275
+ if (publishMode === "scheduled" && !flags["publish-at"]) {
276
+ throw new CliError('--publish-at is required when --publish-mode is "scheduled"');
277
+ }
278
+ return {
279
+ title: flags.title,
280
+ ...(flags.instructions ? { instructions: flags.instructions } : {}),
281
+ outputTypes: csv(flags["output-types"]),
282
+ publishMode,
283
+ ...(flags["publish-at"] ? { publishAt: flags["publish-at"] } : {}),
284
+ requiresReview: flags["no-review"] !== true,
285
+ };
286
+ }
287
+
288
+ function requirePositional(rest, name, usage) {
289
+ const value = rest[0];
290
+ if (!value) throw new CliError(`Usage: ${usage} — missing <${name}>`);
291
+ return value;
292
+ }
293
+
294
+ function requirePositionalAt(rest, index, name, usage) {
295
+ const value = rest[index];
296
+ if (!value) throw new CliError(`Usage: ${usage} — missing <${name}>`);
297
+ return value;
298
+ }
299
+
300
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
301
+
302
+ /**
303
+ * Turn `--skills "brand-voice,<uuid>"` into skill ids. Agents know skills by
304
+ * name, so anything that isn't already a uuid is looked up via list_skills.
305
+ */
306
+ async function resolveSkillIds(ctx, workspaceId, value) {
307
+ const entries = csv(value);
308
+ if (entries.length === 0) return [];
309
+
310
+ const needsLookup = entries.some((entry) => !UUID_RE.test(entry));
311
+ if (!needsLookup) return entries;
312
+
313
+ const text = await callTool(ctx, "list_skills", { workspaceId });
314
+ const skills = extractJson(text);
315
+ if (!Array.isArray(skills)) {
316
+ throw new CliError(`No skills in this workspace to match ${entries.filter((e) => !UUID_RE.test(e)).join(", ")}`);
317
+ }
318
+
319
+ return entries.map((entry) => {
320
+ if (UUID_RE.test(entry)) return entry;
321
+ const match = skills.find((skill) => skill.name?.toLowerCase() === entry.toLowerCase());
322
+ if (!match) {
323
+ throw new CliError(`Unknown skill "${entry}". Available: ${skills.map((s) => s.name).join(", ") || "(none)"}`);
324
+ }
325
+ return match.id;
326
+ });
327
+ }
328
+
329
+ export async function runCommand(argv, opts = {}) {
330
+ const ctx = {
331
+ fetchFn: opts.fetchFn || globalThis.fetch,
332
+ env: opts.env || process.env,
333
+ };
334
+ const { positional, flags } = parseArgs(argv);
335
+ const [resource, action, ...rest] = positional;
336
+
337
+ if (!resource || resource === "help" || flags.help === true) {
338
+ return USAGE;
339
+ }
340
+
341
+ const SINGLE_WORD_COMMANDS = new Set(["tools", "call", "workspaces", "whoami", "upload-url"]);
342
+ const command = SINGLE_WORD_COMMANDS.has(resource)
343
+ ? resource
344
+ : [resource, action].filter(Boolean).join(" ");
345
+
346
+ switch (command) {
347
+ case "tools": {
348
+ const result = await mcpRequest(ctx, "tools/list", {});
349
+ return (result?.tools ?? [])
350
+ .map((t) => `${t.name} — ${(t.description || "").split("\n")[0]}`)
351
+ .join("\n");
352
+ }
353
+
354
+ case "call": {
355
+ const toolName = requirePositional([action, ...rest].filter(Boolean), "tool", "ait call <tool> [--args '<json>']");
356
+ const args = flags.args ? parseJsonFlag(flags.args, "args") : {};
357
+ if (args.workspaceId === undefined && toolName !== "list_workspaces" && toolName !== "get_clerk_user_data") {
358
+ args.workspaceId = await resolveWorkspace(ctx, flags);
359
+ }
360
+ return callTool(ctx, toolName, args);
361
+ }
362
+
363
+ case "workspaces":
364
+ return callTool(ctx, "list_workspaces", {});
365
+
366
+ case "whoami":
367
+ return callTool(ctx, "get_clerk_user_data", {});
368
+
369
+ case "upload-url": {
370
+ if (!flags.filename || !flags["mime-type"]) {
371
+ throw new CliError("--filename and --mime-type are required for upload-url");
372
+ }
373
+ const workspaceId = await resolveWorkspace(ctx, flags);
374
+ return callTool(ctx, "get_upload_url", {
375
+ workspaceId,
376
+ filename: flags.filename,
377
+ mimeType: flags["mime-type"],
378
+ });
379
+ }
380
+
381
+ case "task create": {
382
+ const workspaceId = await resolveWorkspace(ctx, flags);
383
+ const skillIds = await resolveSkillIds(ctx, workspaceId, flags.skills);
384
+ return callTool(ctx, "create_task", {
385
+ workspaceId,
386
+ ...buildCreateTaskArgs(flags),
387
+ ...(flags.claim === true ? { claim: true } : {}),
388
+ ...(flags.assign ? { assignToUserId: flags.assign } : {}),
389
+ ...(skillIds.length > 0 ? { skillIds } : {}),
390
+ });
391
+ }
392
+
393
+ case "task submit": {
394
+ if (!flags.body && !flags.media) {
395
+ throw new CliError("task submit requires --body and/or --media (finished content)");
396
+ }
397
+ const workspaceId = await resolveWorkspace(ctx, flags);
398
+ const skillIds = await resolveSkillIds(ctx, workspaceId, flags.skills);
399
+ return callTool(ctx, "create_task", {
400
+ workspaceId,
401
+ ...buildCreateTaskArgs(flags),
402
+ ...(flags.assign ? { assignToUserId: flags.assign } : {}),
403
+ ...(skillIds.length > 0 ? { skillIds } : {}),
404
+ content: [
405
+ {
406
+ ...(flags.body ? { body: flags.body } : {}),
407
+ ...(flags.type ? { contentType: flags.type } : {}),
408
+ ...(flags.media ? { media: parseJsonFlag(flags.media, "media") } : {}),
409
+ },
410
+ ],
411
+ });
412
+ }
413
+
414
+ case "task list": {
415
+ const workspaceId = await resolveWorkspace(ctx, flags);
416
+ return callTool(ctx, "list_tasks", {
417
+ workspaceId,
418
+ ...(flags.status ? { status: flags.status } : {}),
419
+ ...(flags.mine === true ? { assignedTo: "me" } : {}),
420
+ ...(flags.limit !== undefined ? { limit: Number(flags.limit) } : {}),
421
+ ...(flags.offset !== undefined ? { offset: Number(flags.offset) } : {}),
422
+ });
423
+ }
424
+
425
+ case "task get":
426
+ case "task links": {
427
+ const taskId = requirePositional(rest, "taskId", `ait task ${action} <taskId>`);
428
+ const workspaceId = await resolveWorkspace(ctx, flags);
429
+ const text = await callTool(ctx, "get_task", { workspaceId, taskId });
430
+ if (action === "get") return text;
431
+ const task = extractJson(text);
432
+ if (!task) throw new CliError(`Could not parse task detail from MCP response: ${text.slice(0, 200)}`);
433
+ return shapeLinks(task);
434
+ }
435
+
436
+ case "task claim":
437
+ case "task complete":
438
+ case "task approve":
439
+ case "task delete": {
440
+ const toolByAction = {
441
+ claim: "claim_task",
442
+ complete: "complete_task",
443
+ approve: "approve_task",
444
+ delete: "delete_task",
445
+ };
446
+ const taskId = requirePositional(rest, "taskId", `ait task ${action} <taskId>`);
447
+ const workspaceId = await resolveWorkspace(ctx, flags);
448
+ return callTool(ctx, toolByAction[action], { workspaceId, taskId });
449
+ }
450
+
451
+ case "task reject": {
452
+ const taskId = requirePositional(rest, "taskId", "ait task reject <taskId> [--comment <text>]");
453
+ const workspaceId = await resolveWorkspace(ctx, flags);
454
+ return callTool(ctx, "reject_task", {
455
+ workspaceId,
456
+ taskId,
457
+ ...(flags.comment ? { comment: flags.comment } : {}),
458
+ });
459
+ }
460
+
461
+ case "task publish": {
462
+ const taskId = requirePositional(rest, "taskId", "ait task publish <taskId> [--platforms a,b]");
463
+ const workspaceId = await resolveWorkspace(ctx, flags);
464
+ return callTool(ctx, "publish_task", {
465
+ workspaceId,
466
+ taskId,
467
+ ...(flags.platforms ? { platforms: csv(flags.platforms) } : {}),
468
+ });
469
+ }
470
+
471
+ case "task add-content": {
472
+ const taskId = requirePositional(rest, "taskId", "ait task add-content <taskId> --body <text>");
473
+ if (!flags.body && !flags.media) {
474
+ throw new CliError("task add-content requires --body and/or --media");
475
+ }
476
+ const workspaceId = await resolveWorkspace(ctx, flags);
477
+ return callTool(ctx, "add_content_to_task", {
478
+ workspaceId,
479
+ taskId,
480
+ ...(flags.title ? { title: flags.title } : {}),
481
+ ...(flags.body ? { body: flags.body } : {}),
482
+ ...(flags.type ? { contentType: flags.type } : {}),
483
+ ...(flags.media ? { media: parseJsonFlag(flags.media, "media") } : {}),
484
+ ...(flags["sort-order"] !== undefined ? { sortOrder: Number(flags["sort-order"]) } : {}),
485
+ });
486
+ }
487
+
488
+ case "task next-review": {
489
+ const workspaceId = await resolveWorkspace(ctx, flags);
490
+ return callTool(ctx, "get_next_review_task", { workspaceId });
491
+ }
492
+
493
+ case "automation create": {
494
+ if (!flags.name) throw new CliError("--name is required for automation create");
495
+ const workspaceId = await resolveWorkspace(ctx, flags);
496
+ return callTool(ctx, "create_automation", {
497
+ workspaceId,
498
+ name: flags.name,
499
+ ...(flags.instructions ? { instructions: flags.instructions } : {}),
500
+ outputTypes: csv(flags["output-types"]),
501
+ ...(flags.schedule ? { schedule: parseJsonFlag(flags.schedule, "schedule") } : {}),
502
+ requiresReview: flags["no-review"] !== true,
503
+ });
504
+ }
505
+
506
+ case "automation list": {
507
+ const workspaceId = await resolveWorkspace(ctx, flags);
508
+ return callTool(ctx, "list_automations", { workspaceId });
509
+ }
510
+
511
+ case "automation get":
512
+ case "automation delete": {
513
+ const automationId = requirePositional(rest, "automationId", `ait automation ${action} <automationId>`);
514
+ const workspaceId = await resolveWorkspace(ctx, flags);
515
+ const tool = action === "get" ? "get_automation" : "delete_automation";
516
+ return callTool(ctx, tool, { workspaceId, automationId });
517
+ }
518
+
519
+ case "automation update": {
520
+ const automationId = requirePositional(rest, "automationId", "ait automation update <automationId> [flags]");
521
+ const workspaceId = await resolveWorkspace(ctx, flags);
522
+ return callTool(ctx, "update_automation", {
523
+ workspaceId,
524
+ automationId,
525
+ ...(flags.name ? { name: flags.name } : {}),
526
+ ...(flags.instructions ? { instructions: flags.instructions } : {}),
527
+ ...(flags["output-types"] ? { outputTypes: csv(flags["output-types"]) } : {}),
528
+ ...(flags.schedule ? { schedule: parseJsonFlag(flags.schedule, "schedule") } : {}),
529
+ });
530
+ }
531
+
532
+ case "automation run": {
533
+ const automationId = requirePositional(rest, "automationId", "ait automation run <automationId>");
534
+ const workspaceId = await resolveWorkspace(ctx, flags);
535
+ return callTool(ctx, "run_automation", { workspaceId, automationId });
536
+ }
537
+
538
+ case "task claim-next": {
539
+ const workspaceId = await resolveWorkspace(ctx, flags);
540
+ return callTool(ctx, "claim_next_task", { workspaceId });
541
+ }
542
+
543
+ case "task comment": {
544
+ const taskId = requirePositional(rest, "taskId", 'ait task comment <taskId> --comment "..."');
545
+ if (!flags.comment) throw new CliError("--comment is required for task comment");
546
+ const workspaceId = await resolveWorkspace(ctx, flags);
547
+ return callTool(ctx, "add_task_comment", { workspaceId, taskId, comment: flags.comment });
548
+ }
549
+
550
+ case "task update-content": {
551
+ const usage = "ait task update-content <taskId> <contentItemId>";
552
+ const taskId = requirePositionalAt(rest, 0, "taskId", usage);
553
+ const contentItemId = requirePositionalAt(rest, 1, "contentItemId", usage);
554
+ if (flags.body === undefined && flags.title === undefined) {
555
+ throw new CliError("at least one of --body or --title is required for task update-content");
556
+ }
557
+ const workspaceId = await resolveWorkspace(ctx, flags);
558
+ return callTool(ctx, "update_content_item", {
559
+ workspaceId,
560
+ taskId,
561
+ contentItemId,
562
+ ...(flags.body !== undefined ? { body: flags.body } : {}),
563
+ ...(flags.title !== undefined ? { title: flags.title } : {}),
564
+ });
565
+ }
566
+
567
+ case "task delete-content": {
568
+ const usage = "ait task delete-content <taskId> <contentItemId>";
569
+ const taskId = requirePositionalAt(rest, 0, "taskId", usage);
570
+ const contentItemId = requirePositionalAt(rest, 1, "contentItemId", usage);
571
+ const workspaceId = await resolveWorkspace(ctx, flags);
572
+ return callTool(ctx, "delete_content_item", { workspaceId, taskId, contentItemId });
573
+ }
574
+
575
+ case "task clear-content": {
576
+ const taskId = requirePositional(rest, "taskId", "ait task clear-content <taskId>");
577
+ const workspaceId = await resolveWorkspace(ctx, flags);
578
+ return callTool(ctx, "clear_task_content", { workspaceId, taskId });
579
+ }
580
+
581
+ case "skills list": {
582
+ const workspaceId = await resolveWorkspace(ctx, flags);
583
+ return callTool(ctx, "list_skills", { workspaceId });
584
+ }
585
+
586
+ case "skills get": {
587
+ const skillId = requirePositional(rest, "skillId", "ait skills get <skillId>");
588
+ const workspaceId = await resolveWorkspace(ctx, flags);
589
+ return callTool(ctx, "get_skill", { workspaceId, skillId });
590
+ }
591
+
592
+ case "files download-url": {
593
+ const fileId = requirePositional(rest, "fileId", "ait files download-url <fileId>");
594
+ const workspaceId = await resolveWorkspace(ctx, flags);
595
+ return callTool(ctx, "get_download_url", {
596
+ workspaceId,
597
+ fileId,
598
+ ...(flags["expires-in"] !== undefined ? { expiresIn: Number(flags["expires-in"]) } : {}),
599
+ });
600
+ }
601
+
602
+ case "files list": {
603
+ const workspaceId = await resolveWorkspace(ctx, flags);
604
+ return callTool(ctx, "list_workspace_files", {
605
+ workspaceId,
606
+ ...(flags.search ? { search: flags.search } : {}),
607
+ ...(flags["mime-type"] ? { mimeType: flags["mime-type"] } : {}),
608
+ ...(flags.limit !== undefined ? { limit: Number(flags.limit) } : {}),
609
+ });
610
+ }
611
+
612
+ default:
613
+ throw new CliError(`Unknown command "${command}". Run "ait help" for usage, or "ait tools" to list MCP tools for "ait call".`);
614
+ }
615
+ }
616
+
617
+ const isMain = (() => {
618
+ try {
619
+ // realpath both sides: npm installs the bin as a symlink, so argv[1] is
620
+ // the link while import.meta.url is the resolved file.
621
+ return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
622
+ } catch {
623
+ return false;
624
+ }
625
+ })();
626
+
627
+ if (isMain) {
628
+ runCommand(process.argv.slice(2))
629
+ .then((result) => {
630
+ const output = typeof result === "string" ? result : JSON.stringify(result, null, 2);
631
+ process.stdout.write(`${output}\n`);
632
+ })
633
+ .catch((err) => {
634
+ process.stderr.write(`${JSON.stringify({ error: err?.message || String(err) })}\n`);
635
+ process.exit(1);
636
+ });
637
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "automate-it",
3
+ "version": "0.1.0",
4
+ "description": "CLI for Automate It — AI agents create content tasks, submit them through a human review gate, and publish everywhere. Speaks the Automate It MCP server; zero dependencies.",
5
+ "type": "module",
6
+ "bin": {
7
+ "ait": "./ait.mjs"
8
+ },
9
+ "files": [
10
+ "ait.mjs"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "license": "MIT",
16
+ "homepage": "https://docs.automate.it.com",
17
+ "keywords": [
18
+ "automate-it",
19
+ "mcp",
20
+ "ai-agents",
21
+ "agent-skills",
22
+ "cli",
23
+ "content-automation",
24
+ "social-media",
25
+ "human-in-the-loop"
26
+ ]
27
+ }