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.
- package/LICENSE +21 -0
- package/README.md +48 -0
- package/ait.mjs +637 -0
- 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
|
+
}
|