automate-it 0.1.1 → 0.3.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 (2) hide show
  1. package/ait.mjs +179 -40
  2. package/package.json +1 -1
package/ait.mjs CHANGED
@@ -3,10 +3,15 @@
3
3
  //
4
4
  // Zero-dependency ESM script (Node 18+ or Bun). Speaks the Model Context
5
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.
6
+ // authenticating with an API key (ak_*). Every command goes through MCP
7
+ // there is no REST path.
8
+ //
9
+ // The command surface is a closed set: every tool the workspace MCP server
10
+ // exposes has exactly one named command here, and there is no generic
11
+ // passthrough. Adding a tool to the server means adding its command here —
12
+ // that's deliberate, so the skill's capabilities can be read off its docs.
13
+ // Scopes and workspace role still bound what runs, enforced server-side, and
14
+ // tools that destroy data additionally require an explicit --yes.
10
15
  //
11
16
  // Tool output is printed verbatim on stdout; errors go to stderr as
12
17
  // {"error": "..."} with exit code 1.
@@ -26,17 +31,21 @@ Environment:
26
31
  AUTOMATE_IT_WORKSPACE Default workspace id (else pass --workspace, else
27
32
  auto-resolved when the key has exactly one workspace)
28
33
 
34
+ Destructive commands (task delete, task delete-content, task clear-content,
35
+ automation delete, folders delete) permanently remove data and refuse to run
36
+ without --yes. Only pass it when the operator asked for that deletion by name.
37
+
29
38
  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
39
  ait workspaces List workspaces the key can access
34
40
  ait whoami Show the user this key acts as
41
+ ait integrations Platforms connected for publishing
35
42
 
36
43
  Tasks (the review-gate loop):
37
44
  ait task create --title <t> Create a task
38
45
  [--instructions <text>] [--output-types x,linkedin,...]
39
46
  [--publish-mode manual|immediate|scheduled] [--publish-at <ISO date>]
47
+ Omitted --publish-mode falls back to the workspace's default publish
48
+ mode; scheduled without --publish-at auto-schedules at approval.
40
49
  [--no-review] [--claim] [--skills <names or ids>] [--assign <userId|me>]
41
50
  --claim creates the task already claimed (status "working") so the
42
51
  built-in worker never picks it up — use it when YOU will generate the
@@ -62,8 +71,9 @@ Tasks (the review-gate loop):
62
71
  [--media '<json array>'] [--sort-order <n>]
63
72
  ait task update-content <taskId> <contentItemId>
64
73
  [--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
74
+ ait task delete-content <taskId> <contentItemId> --yes
75
+ ait task clear-content <taskId> --yes
76
+ Remove every content item from a task
67
77
  ait task complete <taskId> Finish work (status → review/approved)
68
78
  ait task comment <taskId> --comment <text>
69
79
  Leave a note for the human reviewer
@@ -71,7 +81,12 @@ Tasks (the review-gate loop):
71
81
  ait task approve <taskId> Approve (reviewer/admin keys)
72
82
  ait task reject <taskId> [--comment <text>]
73
83
  ait task publish <taskId> [--platforms a,b]
74
- ait task delete <taskId>
84
+ ait task schedule <taskId> --at <ISO date> | --auto | --clear
85
+ Schedule an approved task to publish
86
+ later; --auto picks the next open slot
87
+ per the workspace's scheduling rules;
88
+ --clear returns it to manual
89
+ ait task delete <taskId> --yes
75
90
 
76
91
  Skills (workspace voice, formatting rules, bundled reference files):
77
92
  ait skills list id, name, description for each skill
@@ -84,7 +99,9 @@ Automations:
84
99
  ait automation get <automationId>
85
100
  ait automation update <automationId> [--name <n>] [--instructions <text>]
86
101
  [--output-types x,...] [--schedule <json>]
87
- ait automation delete <automationId>
102
+ ait automation delete <automationId> --yes
103
+ Also deletes every task it spawned,
104
+ published ones included
88
105
  ait automation run <automationId> Trigger now; returns the spawned task
89
106
 
90
107
  Files:
@@ -93,13 +110,52 @@ Files:
93
110
  ait files list [--search <s>] [--mime-type <m>] [--limit <n>]
94
111
  ait files download-url <fileId> [--expires-in <seconds>]
95
112
  Presigned URL — download it yourself
96
- (folders/integrations: use \`ait call\` see \`ait tools\`)
113
+ ait files move <fileId> [--folder <folderId>]
114
+ Move into a folder; omit --folder for
115
+ the workspace root
116
+ ait files copy <fileId> [--folder <folderId>]
117
+ Duplicate the file (new file + object)
118
+
119
+ Folders:
120
+ ait folders list [--parent <folderId>] [--root]
121
+ --root lists top-level folders only
122
+ ait folders create --name <n> [--parent <folderId>]
123
+ ait folders rename <folderId> --name <n>
124
+ ait folders delete <folderId> --yes Also deletes every file and nested
125
+ folder inside it
126
+
127
+ Composing posts:
128
+ ait x-limit [--text <draft>] X's limit for the connected account
129
+ (280 or 25000). With --text, returns the
130
+ draft's weighted length and whether it
131
+ fits — a URL counts 23, emoji 2.
132
+ ait shorten <url> [--title <t>] Shorten a link for a post
97
133
 
98
134
  All workspace commands accept --workspace <id>.`;
99
135
 
100
136
  export class CliError extends Error {}
101
137
 
102
- const BOOLEAN_FLAGS = new Set(["no-review", "claim", "mine", "help"]);
138
+ const BOOLEAN_FLAGS = new Set(["no-review", "claim", "mine", "clear", "auto", "help", "yes", "root"]);
139
+
140
+ // MCP tools that permanently remove data, keyed to what they destroy. Keyed by
141
+ // tool rather than by command so the gate lives next to the thing that does
142
+ // the damage — a second command routing to one of these inherits it.
143
+ const DESTRUCTIVE_TOOLS = new Map([
144
+ ["delete_task", "delete the task and everything attached to it"],
145
+ ["delete_content_item", "delete the content item"],
146
+ ["clear_task_content", "remove every content item from the task"],
147
+ ["delete_automation", "delete the automation and every task it ever spawned, published ones included"],
148
+ ["delete_folder", "delete the folder and every file and folder inside it"],
149
+ ]);
150
+
151
+ /** Destructive tools need an explicit --yes; nothing else is affected. */
152
+ function requireConfirmation(flags, toolName) {
153
+ const effect = DESTRUCTIVE_TOOLS.get(toolName);
154
+ if (!effect || flags.yes === true) return;
155
+ throw new CliError(
156
+ `Refusing to run without --yes: this would ${effect}. It cannot be undone — confirm with the operator, then re-run with --yes.`
157
+ );
158
+ }
103
159
 
104
160
  export function parseArgs(argv) {
105
161
  const positional = [];
@@ -268,18 +324,15 @@ export function shapeLinks(task) {
268
324
  /** Fields shared by `task create` and `task submit`. */
269
325
  function buildCreateTaskArgs(flags) {
270
326
  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)) {
327
+ const publishMode = flags["publish-mode"];
328
+ if (publishMode && !PUBLISH_MODES.includes(publishMode)) {
273
329
  throw new CliError(`--publish-mode must be one of: ${PUBLISH_MODES.join(", ")}`);
274
330
  }
275
- if (publishMode === "scheduled" && !flags["publish-at"]) {
276
- throw new CliError('--publish-at is required when --publish-mode is "scheduled"');
277
- }
278
331
  return {
279
332
  title: flags.title,
280
333
  ...(flags.instructions ? { instructions: flags.instructions } : {}),
281
334
  outputTypes: csv(flags["output-types"]),
282
- publishMode,
335
+ ...(publishMode ? { publishMode } : {}),
283
336
  ...(flags["publish-at"] ? { publishAt: flags["publish-at"] } : {}),
284
337
  requiresReview: flags["no-review"] !== true,
285
338
  };
@@ -338,28 +391,19 @@ export async function runCommand(argv, opts = {}) {
338
391
  return USAGE;
339
392
  }
340
393
 
341
- const SINGLE_WORD_COMMANDS = new Set(["tools", "call", "workspaces", "whoami", "upload-url"]);
394
+ const SINGLE_WORD_COMMANDS = new Set([
395
+ "workspaces",
396
+ "whoami",
397
+ "upload-url",
398
+ "integrations",
399
+ "x-limit",
400
+ "shorten",
401
+ ]);
342
402
  const command = SINGLE_WORD_COMMANDS.has(resource)
343
403
  ? resource
344
404
  : [resource, action].filter(Boolean).join(" ");
345
405
 
346
406
  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
407
  case "workspaces":
364
408
  return callTool(ctx, "list_workspaces", {});
365
409
 
@@ -444,6 +488,7 @@ export async function runCommand(argv, opts = {}) {
444
488
  delete: "delete_task",
445
489
  };
446
490
  const taskId = requirePositional(rest, "taskId", `ait task ${action} <taskId>`);
491
+ requireConfirmation(flags, toolByAction[action]);
447
492
  const workspaceId = await resolveWorkspace(ctx, flags);
448
493
  return callTool(ctx, toolByAction[action], { workspaceId, taskId });
449
494
  }
@@ -468,6 +513,27 @@ export async function runCommand(argv, opts = {}) {
468
513
  });
469
514
  }
470
515
 
516
+ case "task schedule": {
517
+ const taskId = requirePositional(rest, "taskId", "ait task schedule <taskId> --at <ISO date> | --auto | --clear");
518
+ const modes = [flags.at ? "--at" : null, flags.auto === true ? "--auto" : null, flags.clear === true ? "--clear" : null].filter(Boolean);
519
+ if (modes.length > 1) {
520
+ throw new CliError(`Use only one of --at, --auto, --clear (got ${modes.join(" and ")})`);
521
+ }
522
+ let publishAt = null;
523
+ if (flags.auto === true) {
524
+ publishAt = "auto";
525
+ } else if (flags.clear !== true) {
526
+ if (!flags.at) {
527
+ throw new CliError("task schedule requires --at <ISO date>, --auto, or --clear");
528
+ }
529
+ const at = new Date(flags.at);
530
+ if (isNaN(at.getTime())) throw new CliError(`--at is not a valid date: ${flags.at}`);
531
+ publishAt = at.toISOString();
532
+ }
533
+ const workspaceId = await resolveWorkspace(ctx, flags);
534
+ return callTool(ctx, "schedule_task", { workspaceId, taskId, publishAt });
535
+ }
536
+
471
537
  case "task add-content": {
472
538
  const taskId = requirePositional(rest, "taskId", "ait task add-content <taskId> --body <text>");
473
539
  if (!flags.body && !flags.media) {
@@ -511,8 +577,9 @@ export async function runCommand(argv, opts = {}) {
511
577
  case "automation get":
512
578
  case "automation delete": {
513
579
  const automationId = requirePositional(rest, "automationId", `ait automation ${action} <automationId>`);
514
- const workspaceId = await resolveWorkspace(ctx, flags);
515
580
  const tool = action === "get" ? "get_automation" : "delete_automation";
581
+ requireConfirmation(flags, tool);
582
+ const workspaceId = await resolveWorkspace(ctx, flags);
516
583
  return callTool(ctx, tool, { workspaceId, automationId });
517
584
  }
518
585
 
@@ -565,15 +632,17 @@ export async function runCommand(argv, opts = {}) {
565
632
  }
566
633
 
567
634
  case "task delete-content": {
568
- const usage = "ait task delete-content <taskId> <contentItemId>";
635
+ const usage = "ait task delete-content <taskId> <contentItemId> --yes";
569
636
  const taskId = requirePositionalAt(rest, 0, "taskId", usage);
570
637
  const contentItemId = requirePositionalAt(rest, 1, "contentItemId", usage);
638
+ requireConfirmation(flags, "delete_content_item");
571
639
  const workspaceId = await resolveWorkspace(ctx, flags);
572
640
  return callTool(ctx, "delete_content_item", { workspaceId, taskId, contentItemId });
573
641
  }
574
642
 
575
643
  case "task clear-content": {
576
- const taskId = requirePositional(rest, "taskId", "ait task clear-content <taskId>");
644
+ const taskId = requirePositional(rest, "taskId", "ait task clear-content <taskId> --yes");
645
+ requireConfirmation(flags, "clear_task_content");
577
646
  const workspaceId = await resolveWorkspace(ctx, flags);
578
647
  return callTool(ctx, "clear_task_content", { workspaceId, taskId });
579
648
  }
@@ -609,8 +678,78 @@ export async function runCommand(argv, opts = {}) {
609
678
  });
610
679
  }
611
680
 
681
+ case "files move":
682
+ case "files copy": {
683
+ const usage = `ait files ${action} <fileId> [--folder <folderId>]`;
684
+ const fileId = requirePositional(rest, "fileId", usage);
685
+ const workspaceId = await resolveWorkspace(ctx, flags);
686
+ // Omitting --folder targets the workspace root, matching the tools'
687
+ // own default — for `move` that relocates the file, it isn't a no-op.
688
+ return callTool(ctx, action === "move" ? "move_file" : "copy_file", {
689
+ workspaceId,
690
+ fileId,
691
+ folderId: flags.folder ?? null,
692
+ });
693
+ }
694
+
695
+ case "folders list": {
696
+ const workspaceId = await resolveWorkspace(ctx, flags);
697
+ return callTool(ctx, "list_folders", {
698
+ workspaceId,
699
+ ...(flags.parent ? { parentId: flags.parent } : {}),
700
+ ...(flags.root === true ? { root: true } : {}),
701
+ });
702
+ }
703
+
704
+ case "folders create": {
705
+ if (!flags.name) throw new CliError("--name is required for folders create");
706
+ const workspaceId = await resolveWorkspace(ctx, flags);
707
+ return callTool(ctx, "create_folder", {
708
+ workspaceId,
709
+ name: flags.name,
710
+ ...(flags.parent ? { parentId: flags.parent } : {}),
711
+ });
712
+ }
713
+
714
+ case "folders rename": {
715
+ const folderId = requirePositional(rest, "folderId", "ait folders rename <folderId> --name <n>");
716
+ if (!flags.name) throw new CliError("--name is required for folders rename");
717
+ const workspaceId = await resolveWorkspace(ctx, flags);
718
+ return callTool(ctx, "rename_folder", { workspaceId, folderId, name: flags.name });
719
+ }
720
+
721
+ case "folders delete": {
722
+ const folderId = requirePositional(rest, "folderId", "ait folders delete <folderId> --yes");
723
+ requireConfirmation(flags, "delete_folder");
724
+ const workspaceId = await resolveWorkspace(ctx, flags);
725
+ return callTool(ctx, "delete_folder", { workspaceId, folderId });
726
+ }
727
+
728
+ case "integrations": {
729
+ const workspaceId = await resolveWorkspace(ctx, flags);
730
+ return callTool(ctx, "list_integrations", { workspaceId });
731
+ }
732
+
733
+ case "x-limit": {
734
+ const workspaceId = await resolveWorkspace(ctx, flags);
735
+ return callTool(ctx, "get_x_character_limit", {
736
+ workspaceId,
737
+ ...(flags.text !== undefined ? { text: flags.text } : {}),
738
+ });
739
+ }
740
+
741
+ case "shorten": {
742
+ const url = requirePositional([action, ...rest].filter(Boolean), "url", "ait shorten <url> [--title <t>]");
743
+ const workspaceId = await resolveWorkspace(ctx, flags);
744
+ return callTool(ctx, "shorten_url", {
745
+ workspaceId,
746
+ url,
747
+ ...(flags.title ? { title: flags.title } : {}),
748
+ });
749
+ }
750
+
612
751
  default:
613
- throw new CliError(`Unknown command "${command}". Run "ait help" for usage, or "ait tools" to list MCP tools for "ait call".`);
752
+ throw new CliError(`Unknown command "${command}". Run "ait help" for the full command list.`);
614
753
  }
615
754
  }
616
755
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automate-it",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
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
5
  "type": "module",
6
6
  "bin": {