@extuitive/skill 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 +514 -0
- package/bin/cli.mjs +932 -0
- package/package.json +40 -0
- package/skills/extuitive/SKILL.md +63 -0
- package/skills/extuitive/references/connect.md +75 -0
- package/skills/extuitive/references/init.md +83 -0
- package/skills/extuitive/references/select.md +89 -0
- package/skills/extuitive/references/tools.md +252 -0
- package/skills/extuitive/references/upload-status.md +102 -0
- package/skills/extuitive/references/upload.md +160 -0
- package/skills/extuitive/scripts/upload.mjs +340 -0
- package/src/constants.mjs +63 -0
- package/src/doctor.mjs +513 -0
- package/src/exec.mjs +139 -0
- package/src/hosts.mjs +350 -0
- package/src/install.mjs +541 -0
- package/src/mcp-setup.mjs +476 -0
- package/src/zip.mjs +246 -0
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@extuitive/skill",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The Extuitive agent skill and installer for Claude Code, Codex, and Claude Desktop. Run it with `npx extuitive`.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/fl100inc/extuitive-skill.git"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/fl100inc/extuitive-skill#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/fl100inc/extuitive-skill/issues"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"extuitive",
|
|
20
|
+
"mcp",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"claude-desktop",
|
|
23
|
+
"codex",
|
|
24
|
+
"agent-skills",
|
|
25
|
+
"facebook-ads"
|
|
26
|
+
],
|
|
27
|
+
"files": [
|
|
28
|
+
"bin",
|
|
29
|
+
"src",
|
|
30
|
+
"skills",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"doctor": "node bin/cli.mjs doctor"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: extuitive
|
|
3
|
+
description: Work with Extuitive, where each workspace is one Facebook ads account. Check that Extuitive is connected and signed in, choose which ad account to work in, upload images and videos in bulk, check how an upload is going and which files were accepted, connect or repair a Meta connection, or set Extuitive up from scratch. Use when asked to check the Extuitive connection or whether Extuitive is set up, to upload, add, or import creative, ad images, ad videos, or a folder of assets into Extuitive, to check an Extuitive upload, to pick or switch which Extuitive workspace or ad account to work in, or when Extuitive tools are missing or refusing.
|
|
4
|
+
argument-hint: "[init | select | upload | upload-status | connect]"
|
|
5
|
+
arguments: command
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Extuitive
|
|
9
|
+
|
|
10
|
+
One skill, five jobs. Read the reference for the job you are doing and follow it; this page
|
|
11
|
+
only routes.
|
|
12
|
+
|
|
13
|
+
## Which job
|
|
14
|
+
|
|
15
|
+
The requested command is **`$command`**, from the full invocation `$ARGUMENTS`.
|
|
16
|
+
|
|
17
|
+
If that is empty, or still reads as a literal `$command` because this host does not
|
|
18
|
+
substitute arguments, work it out from what was actually asked instead.
|
|
19
|
+
|
|
20
|
+
| Command | Read | For |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| `init` | `references/init.md` | Nothing is set up yet, the tools are missing, or "check my Extuitive connection" — is it set up, signed in, and working |
|
|
23
|
+
| `select` | `references/select.md` | Choose which ad account the rest of this session works in |
|
|
24
|
+
| `upload` | `references/upload.md` | Send local files into a workspace |
|
|
25
|
+
| `upload-status` | `references/upload-status.md` | How is an upload going, what was accepted |
|
|
26
|
+
| `connect` | `references/connect.md` | No workspaces, or ads data has gone stale — the *Meta* connection |
|
|
27
|
+
|
|
28
|
+
Anything else, or nothing at all: pick from the "For" column. An unrecognised command is
|
|
29
|
+
worth one sentence — say what the five are — rather than a guess.
|
|
30
|
+
|
|
31
|
+
"Connection" is ambiguous here, so route it by what is being asked about. Whether *this
|
|
32
|
+
host* can reach Extuitive — tools present, signed in, workspaces listed — is `init`, which
|
|
33
|
+
checks all three in order and hands off to `connect` itself if the only thing missing is a
|
|
34
|
+
Meta connection. Whether a *workspace's* ads data is flowing is `connect`.
|
|
35
|
+
|
|
36
|
+
`references/tools.md` documents every tool's arguments, the error vocabulary, and the status
|
|
37
|
+
lifecycle. Reach for it when a call fails or a field is not what you expected.
|
|
38
|
+
|
|
39
|
+
## True regardless of which job
|
|
40
|
+
|
|
41
|
+
**The MCP tools never carry file bytes.** `create_upload_batch` hands back presigned storage
|
|
42
|
+
URLs and whoever holds the files sends the bytes there directly. No tool accepts a file. If
|
|
43
|
+
you cannot open the person's files, you cannot upload — use `create_browser_upload_link` and
|
|
44
|
+
let them do it from their browser. Being able to run code is not the same as being able to
|
|
45
|
+
read their disk; some hosts give you one without the other.
|
|
46
|
+
|
|
47
|
+
**Signing in happens in a browser and only the person can do it.** The token lands in this
|
|
48
|
+
host's own credential store. You never see it, and no amount of retrying substitutes for it.
|
|
49
|
+
|
|
50
|
+
**If the Extuitive tools are not in this session, stop and read `references/init.md`.** Do not
|
|
51
|
+
invent setup commands. They differ per host and they change.
|
|
52
|
+
|
|
53
|
+
**Uploading files is not an instruction to do anything with them.** Thirty images is not a
|
|
54
|
+
request for thirty ads. Report what landed and wait.
|
|
55
|
+
|
|
56
|
+
**Say what a tool returned, not what it implies.** The fields on a workspace do not predict
|
|
57
|
+
which tools will accept it — `role` and `isOwner` govern reconnecting Meta and nothing else —
|
|
58
|
+
so "use this one for uploads" is a claim you can only make after one worked. Two workspaces
|
|
59
|
+
can point at the same ad account and behave differently. Ask rather than choose.
|
|
60
|
+
|
|
61
|
+
**Ask which workspace once, then stop asking.** A choice made anywhere in the conversation —
|
|
62
|
+
through `select`, or in passing while asking for something else — holds until they change it
|
|
63
|
+
or the session ends. Re-asking on every upload is its own kind of wrong answer.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# `/extuitive connect` — connect or repair Meta
|
|
2
|
+
|
|
3
|
+
Each Extuitive workspace is one Facebook ads account. When something is wrong with that
|
|
4
|
+
connection, the tools tell you what and where to send the person — your job is to route to the
|
|
5
|
+
right one and relay what it says.
|
|
6
|
+
|
|
7
|
+
The reverse does not hold: two workspaces can point at the same ads account, each with its own
|
|
8
|
+
connection and its own `canReconnect`. Repairing one does not repair the other, so check the
|
|
9
|
+
`metaConnection` on the workspace you are actually being asked about.
|
|
10
|
+
|
|
11
|
+
## Two different problems
|
|
12
|
+
|
|
13
|
+
They look similar and have different fixes, so identify which one you have before acting.
|
|
14
|
+
|
|
15
|
+
- **No workspaces at all.** `list_workspaces` returns an empty list. This is a setup problem,
|
|
16
|
+
and it belongs to the person themselves. Use `get_meta_setup_status`.
|
|
17
|
+
- **A workspace exists but its data has gone stale.** `list_workspaces` returns a workspace
|
|
18
|
+
whose `metaConnection.status` is not `healthy`. This is a repair problem, and it may belong
|
|
19
|
+
to someone else. Use `create_meta_reconnect_link`.
|
|
20
|
+
|
|
21
|
+
Do not guess between them. Sending someone who already finished Meta OAuth back through it
|
|
22
|
+
achieves nothing and wastes their time.
|
|
23
|
+
|
|
24
|
+
## No workspaces yet
|
|
25
|
+
|
|
26
|
+
Call `get_meta_setup_status`. It takes no arguments and answers the question an empty
|
|
27
|
+
`list_workspaces` cannot: whether they never connected Meta, or connected it but never chose
|
|
28
|
+
ad accounts, or connected it and Meta returned no ad accounts.
|
|
29
|
+
|
|
30
|
+
It gives you back:
|
|
31
|
+
|
|
32
|
+
- `stage` — which of those situations this is
|
|
33
|
+
- `url` — the page that completes this particular stage
|
|
34
|
+
- `nextStep` — what to tell them, already worded for the stage
|
|
35
|
+
|
|
36
|
+
**Relay `nextStep` rather than writing your own.** It is phrased per stage precisely so that
|
|
37
|
+
someone who is already connected is not told to connect again.
|
|
38
|
+
|
|
39
|
+
Give them the `url`. It requires them to be signed in, so it grants nothing on its own — it is
|
|
40
|
+
an ordinary link to a page, not a credential. Then ask them to call you back and run
|
|
41
|
+
`list_workspaces` again.
|
|
42
|
+
|
|
43
|
+
## A workspace whose connection is broken
|
|
44
|
+
|
|
45
|
+
`list_workspaces` reports `metaConnection` on every workspace:
|
|
46
|
+
|
|
47
|
+
| `status` | Means |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `healthy` | Working. Nothing to do. |
|
|
50
|
+
| `expiring` | The token will expire soon; `daysUntilExpiry` says when. |
|
|
51
|
+
| `token_invalid` | The token stopped working. Ads data is stale until it is renewed. |
|
|
52
|
+
| `missing_scope` | Meta stopped granting the ads permissions this workspace needs. |
|
|
53
|
+
| `ad_account_permission` | Their Meta user has no role on this ad account. |
|
|
54
|
+
|
|
55
|
+
**Check `metaConnection.canReconnect` before calling `create_meta_reconnect_link`.** Only the
|
|
56
|
+
workspace owner can reconnect, because the Meta account is theirs. When `canReconnect` is
|
|
57
|
+
`false`, tell the person to ask the workspace owner — do not call the tool to find out, it
|
|
58
|
+
will refuse with `not_workspace_owner`.
|
|
59
|
+
|
|
60
|
+
`canReconnect` is also `false` for `ad_account_permission` even when the person *is* the
|
|
61
|
+
owner, because reconnecting cannot grant a Business Manager role. That one is fixed in Meta's
|
|
62
|
+
own settings, not in Extuitive.
|
|
63
|
+
|
|
64
|
+
When `canReconnect` is `true`, call `create_meta_reconnect_link` with the `workspaceId`. It
|
|
65
|
+
returns a `url` and a `nextStep`. Same rule as above: relay the `nextStep`, hand over the
|
|
66
|
+
`url`, and note that the page requires sign-in.
|
|
67
|
+
|
|
68
|
+
## What not to do
|
|
69
|
+
|
|
70
|
+
Do not report numbers from a workspace whose connection is broken without saying so. A
|
|
71
|
+
`token_invalid` workspace still has data, but it stopped updating on the day the token died,
|
|
72
|
+
and presenting it as current is worse than reporting nothing.
|
|
73
|
+
|
|
74
|
+
Do not treat any of these URLs as secret. They all point at pages that require the person to
|
|
75
|
+
be signed in.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# `/extuitive init` — set Extuitive up, or check that it is
|
|
2
|
+
|
|
3
|
+
Gets a person from nothing to a working connection, and doubles as the check: "is Extuitive
|
|
4
|
+
connected?" is answered by walking the same three steps and reporting where they stop. Run
|
|
5
|
+
when asked to set up or check the connection, or when the Extuitive tools are missing from
|
|
6
|
+
the session.
|
|
7
|
+
|
|
8
|
+
Work through the obstacles in order. Each has to be true before the next can be. When asked
|
|
9
|
+
only to check, say which step you got to and stop there; do not start fixing what was not
|
|
10
|
+
asked about.
|
|
11
|
+
|
|
12
|
+
## 1. Is this host connected to the Extuitive MCP server?
|
|
13
|
+
|
|
14
|
+
If the Extuitive tools are not available in this session, the connection is not set up.
|
|
15
|
+
|
|
16
|
+
**Some hosts have no command line at all.** If this one is a desktop app where you reached the
|
|
17
|
+
person through a chat window rather than a terminal — Claude Desktop is the one to expect —
|
|
18
|
+
then the connection is a panel they click through, and telling them to run something would
|
|
19
|
+
send them looking for a terminal they may not have:
|
|
20
|
+
|
|
21
|
+
> Settings, then Connectors, then Add custom connector, and paste
|
|
22
|
+
> `https://www.extuitive.com/mcp` as the URL.
|
|
23
|
+
|
|
24
|
+
Everywhere else, tell them to run:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx github:fl100inc/extuitive-skill doctor
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
That checks the endpoint, whether this host has the server registered, and whether sign-in
|
|
31
|
+
has been completed, and it prints the exact next command for their host.
|
|
32
|
+
|
|
33
|
+
**Do not invent setup commands.** They differ per host, they change between versions, and
|
|
34
|
+
`doctor` reads the current ones. A stale command sends someone down a dead end that looks
|
|
35
|
+
like the product is broken. The connector URL above is the one exception, and only because
|
|
36
|
+
it is a URL rather than a command — there is nothing about it to go stale but the address.
|
|
37
|
+
|
|
38
|
+
Relay what `doctor` prints, in the order it prints it. Three things are worth saying plainly
|
|
39
|
+
while they work through it:
|
|
40
|
+
|
|
41
|
+
- **Signing in happens in a browser and only they can do it.** It opens a page, they approve
|
|
42
|
+
access, and the token goes into their host's credential store. You never see it.
|
|
43
|
+
- **Do not run the sign-in step yourself.** If `doctor` prints a slash command, it belongs to
|
|
44
|
+
the person's session, not your shell; if it prints a shell command, it opens a browser and
|
|
45
|
+
waits for a redirect your shell cannot receive. Either way, running it produces a failure
|
|
46
|
+
that reads as a broken install. Print the step and stop.
|
|
47
|
+
- **The tools will not appear in this session.** Hosts connect MCP servers when a session
|
|
48
|
+
starts, so nothing registered while you have been running is visible to you. (The skill
|
|
49
|
+
itself is different: hosts pick up new skills without a restart, which is why you are
|
|
50
|
+
reading this.) Say so, and ask them to start a fresh session — a new chat, in a desktop
|
|
51
|
+
app — before checking. Retrying the tools here will not make them exist.
|
|
52
|
+
|
|
53
|
+
## 2. Do they have an account?
|
|
54
|
+
|
|
55
|
+
They do not need one in advance, and you should not send them off to make one first.
|
|
56
|
+
|
|
57
|
+
The sign-in page in step 1 offers **Sign up** alongside **Log in**, both with a one-time
|
|
58
|
+
email code. Someone with no account creates it there, in the same browser step that grants
|
|
59
|
+
this host access. If they have no Meta connection yet, that same page then shows a panel
|
|
60
|
+
pointing at the right setup step and returns them to the consent screen afterwards.
|
|
61
|
+
|
|
62
|
+
So: let the flow do it. Do not narrate the signup steps or send them to a separate page —
|
|
63
|
+
anything you add is a second, worse copy of a flow that already handles this.
|
|
64
|
+
|
|
65
|
+
## 3. Does it work?
|
|
66
|
+
|
|
67
|
+
Once the tools are available, call `list_workspaces`.
|
|
68
|
+
|
|
69
|
+
- **It returns workspaces.** Setup is done. Say which ones they have and stop.
|
|
70
|
+
- **It returns an empty list.** They are signed in but have no ads account connected yet.
|
|
71
|
+
Read `connect.md` — it covers exactly this.
|
|
72
|
+
- **It refuses.** They are not signed in. Back to step 1.
|
|
73
|
+
|
|
74
|
+
## What "done" means
|
|
75
|
+
|
|
76
|
+
At least one workspace from `list_workspaces`. Until then, do not start on anything else they
|
|
77
|
+
asked for: uploads and reporting both need a `workspaceId`, and every one of those tools will
|
|
78
|
+
refuse without it.
|
|
79
|
+
|
|
80
|
+
When setup started from a session with no Extuitive tools, that call belongs to the next
|
|
81
|
+
session, not this one. Finish by handing over the remaining steps and saying which one they
|
|
82
|
+
are on — not by declaring it working, and not by checking again for tools that cannot arrive
|
|
83
|
+
until they restart.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# `/extuitive select` — choose which ad account to work in
|
|
2
|
+
|
|
3
|
+
An Extuitive workspace is one Facebook ads account. This picks the one the rest of the
|
|
4
|
+
conversation uses, so the person is not asked again on every upload and every status check.
|
|
5
|
+
|
|
6
|
+
Anything they typed after the command — a name, a slug, an ad account id — arrives in
|
|
7
|
+
`$ARGUMENTS`.
|
|
8
|
+
|
|
9
|
+
**The choice lasts this conversation and nothing longer.** Nothing is written to disk, and no
|
|
10
|
+
tool remembers it. A new session starts with none, which is worth saying once when they choose
|
|
11
|
+
so that it is not a surprise tomorrow.
|
|
12
|
+
|
|
13
|
+
If the Extuitive tools are missing from this session, read `init.md` instead. There is nothing
|
|
14
|
+
to select from until they are there.
|
|
15
|
+
|
|
16
|
+
## 1. List what they can actually reach
|
|
17
|
+
|
|
18
|
+
Call `list_workspaces`. It takes no arguments.
|
|
19
|
+
|
|
20
|
+
An **empty list** means they have no workspace, not that they lack access to one. Read
|
|
21
|
+
`connect.md`; there is nothing to select yet.
|
|
22
|
+
|
|
23
|
+
**Exactly one** is not a question. Say which workspace you are using and move on — a menu of
|
|
24
|
+
one spends a turn to tell them something they cannot change.
|
|
25
|
+
|
|
26
|
+
## 2. Show every row, ranked by nothing
|
|
27
|
+
|
|
28
|
+
Give them each workspace with the three fields that actually tell one from another:
|
|
29
|
+
|
|
30
|
+
| Show | From | Why |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| Name | `name` | The only part they will recognise |
|
|
33
|
+
| Ad account | `facebookAdAccountId`, or say it has none when `null` | What they think of as the account |
|
|
34
|
+
| Connection | `metaConnection.status`, when it is not `healthy` | A stale workspace is still selectable, and they should know that before choosing it |
|
|
35
|
+
|
|
36
|
+
**Do not order or recommend by `role` or `isOwner`.** Those govern who may reconnect Meta and
|
|
37
|
+
say nothing about uploading — a workspace where they are `viewer` may take files that the one
|
|
38
|
+
where they are `owner` refuses. Presenting one as the right one to work in is a claim you
|
|
39
|
+
cannot make yet.
|
|
40
|
+
|
|
41
|
+
**Two rows can carry the same `facebookAdAccountId`.** They are two separate workspaces with
|
|
42
|
+
separate content, not one workspace listed twice. Show both, say they point at the same ad
|
|
43
|
+
account, and let them pick. The ad account id does not identify a workspace, and collapsing
|
|
44
|
+
the two loses the distinction that decides where their files end up.
|
|
45
|
+
|
|
46
|
+
## 3. Match what they said, or ask
|
|
47
|
+
|
|
48
|
+
When `$ARGUMENTS` names something, match it against `name`, `slug`, and
|
|
49
|
+
`facebookAdAccountId`, ignoring case.
|
|
50
|
+
|
|
51
|
+
- **One match.** Take it, and confirm which one by name.
|
|
52
|
+
- **Several.** Ask which, showing what differs between them. A shared ad account id lands
|
|
53
|
+
here, and it is the case where a guess costs the most.
|
|
54
|
+
- **None.** Say so and list what they do have, rather than matching loosely onto whichever
|
|
55
|
+
name looks closest.
|
|
56
|
+
|
|
57
|
+
With no argument, ask.
|
|
58
|
+
|
|
59
|
+
## 4. Say what you selected
|
|
60
|
+
|
|
61
|
+
Confirm the workspace by **name**, not by id. The id is for the tools; the name is the part
|
|
62
|
+
they can check. Mention its ad account when it has one.
|
|
63
|
+
|
|
64
|
+
Then hold that `workspaceId` and pass it to every workspace-scoped call for the rest of the
|
|
65
|
+
conversation. Not asking again is the entire point of having asked once.
|
|
66
|
+
|
|
67
|
+
## Selecting is not access
|
|
68
|
+
|
|
69
|
+
**A selected workspace can still refuse a call.** Membership is re-checked on every single
|
|
70
|
+
call, so `workspace_access_denied` can come back from a `workspaceId` that `list_workspaces`
|
|
71
|
+
returned a moment earlier.
|
|
72
|
+
|
|
73
|
+
When it does, the selection is wrong rather than the call. Say plainly which one was refused,
|
|
74
|
+
select another, and carry on in that one — correcting yourself in the same breath if you had
|
|
75
|
+
already told them their files were going somewhere else. Retrying the refused id will not
|
|
76
|
+
change it, and neither `role` nor `isOwner` explains it.
|
|
77
|
+
|
|
78
|
+
## Switching later
|
|
79
|
+
|
|
80
|
+
People change their mind mid-conversation. Run this again and replace the selection.
|
|
81
|
+
|
|
82
|
+
Two things do not follow the switch. A `batchId` belongs to the workspace it was created in,
|
|
83
|
+
so checking on an older batch needs the workspace it was made in, not the current one. And
|
|
84
|
+
files already uploaded stay where they were sent — selecting elsewhere moves nothing.
|
|
85
|
+
|
|
86
|
+
## Choosing without the command
|
|
87
|
+
|
|
88
|
+
Naming a workspace in passing — "upload these to Acme" — is a selection. Take it, say which
|
|
89
|
+
workspace you understood that to mean, and do not ask again in the same breath.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Extuitive MCP tools
|
|
2
|
+
|
|
3
|
+
Every tool the Extuitive MCP server exposes, with its arguments and what it gives back. Read
|
|
4
|
+
this when you need a schema or an error meaning; the `SKILL.md` files carry the procedures.
|
|
5
|
+
|
|
6
|
+
Server endpoint is `https://www.extuitive.com/mcp`. Every tool except `list_workspaces`,
|
|
7
|
+
`get_meta_setup_status`, and `create_meta_reconnect_link`'s sibling takes a `workspaceId`, and
|
|
8
|
+
membership is re-checked on every single call — a token issued before someone left a workspace
|
|
9
|
+
does not still reach it.
|
|
10
|
+
|
|
11
|
+
## Read this first: five things that cause wrong answers
|
|
12
|
+
|
|
13
|
+
1. **`role` and `isOwner` say nothing about uploading.** They decide one thing only: who may
|
|
14
|
+
call `create_meta_reconnect_link`. A workspace where this caller is `viewer` may accept
|
|
15
|
+
uploads that the one where they are `owner` refuses. Nothing in a `list_workspaces` row
|
|
16
|
+
predicts which workspaces the upload tools will accept, so never present one as the right
|
|
17
|
+
one for uploads — ask, or try and report what happened.
|
|
18
|
+
2. **`count` is not the batch size.** In `get_upload_batch_content`, `count` describes only
|
|
19
|
+
the rows returned after filtering. `statusCounts`, `pending`, and `settled` always describe
|
|
20
|
+
the whole batch. Filtering to `["READY"]` and reading `count` makes an unfinished upload
|
|
21
|
+
look complete.
|
|
22
|
+
3. **`complete_upload` uses S3's capitalisation.** `PartNumber` and `ETag`, not `partNumber`
|
|
23
|
+
and `eTag`. They are the only non-camelCase fields in the entire surface.
|
|
24
|
+
4. **`sign_upload_part` does not give you the checksum.** You compute
|
|
25
|
+
`x-amz-checksum-crc32c` yourself over the exact bytes of that part and send it as a header.
|
|
26
|
+
The signature covers that header, so S3 rejects the part when it is missing or wrong. The
|
|
27
|
+
bundled `scripts/upload.mjs` does this for you.
|
|
28
|
+
5. **No tool is annotated.** None declares `readOnlyHint`, `destructiveHint`, or an output
|
|
29
|
+
schema, so nothing can be inferred about safety from the listing. `abort_upload` is
|
|
30
|
+
destructive and carries no marking.
|
|
31
|
+
|
|
32
|
+
Results always arrive as both a JSON text block and `structuredContent` with the same payload.
|
|
33
|
+
|
|
34
|
+
## Workspaces
|
|
35
|
+
|
|
36
|
+
### `list_workspaces`
|
|
37
|
+
|
|
38
|
+
No arguments. Start every session here.
|
|
39
|
+
|
|
40
|
+
Returns `{ workspaces: [...] }`, each with:
|
|
41
|
+
|
|
42
|
+
| Field | Meaning |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `workspaceId` | Pass this to every other tool |
|
|
45
|
+
| `name`, `slug` | Display |
|
|
46
|
+
| `role` | This caller's role in the workspace. Governs reconnect only |
|
|
47
|
+
| `facebookAdAccountId` | The Meta ads account, or `null`. Not unique across workspaces |
|
|
48
|
+
| `isOwner` | Whether this caller owns the workspace. Governs reconnect only |
|
|
49
|
+
| `metaConnection.status` | `healthy`, `expiring`, `token_invalid`, `missing_scope`, `ad_account_permission` |
|
|
50
|
+
| `metaConnection.daysUntilExpiry` | Whole days, or `null` when Meta gave no expiry |
|
|
51
|
+
| `metaConnection.canReconnect` | Whether *this* caller can fix it |
|
|
52
|
+
|
|
53
|
+
An empty list means no workspace, not no access. Follow with `get_meta_setup_status`.
|
|
54
|
+
|
|
55
|
+
Two rows can carry the same `facebookAdAccountId`. They are still two separate workspaces
|
|
56
|
+
with separate content, not one workspace listed twice, so do not merge them or pick between
|
|
57
|
+
them on the person's behalf. Whichever they choose is the one their files will be in.
|
|
58
|
+
|
|
59
|
+
A row in this list is also not a guarantee that every tool will accept its `workspaceId`; see
|
|
60
|
+
`workspace_access_denied` below.
|
|
61
|
+
|
|
62
|
+
## Meta connection
|
|
63
|
+
|
|
64
|
+
### `get_meta_setup_status`
|
|
65
|
+
|
|
66
|
+
No arguments. The right follow-up to an empty `list_workspaces`.
|
|
67
|
+
|
|
68
|
+
Returns `stage`, `connected`, `discoveredAdAccountCount`, `connectedAdAccountCount`, `action`,
|
|
69
|
+
`url` (absent only when setup is complete), and `nextStep`. Relay `nextStep` rather than
|
|
70
|
+
writing your own wording — it differs per stage so that someone already connected is not told
|
|
71
|
+
to connect again.
|
|
72
|
+
|
|
73
|
+
### `create_meta_reconnect_link`
|
|
74
|
+
|
|
75
|
+
| Argument | Required | Notes |
|
|
76
|
+
| --- | --- | --- |
|
|
77
|
+
| `workspaceId` | yes | Caller must **own** it, not merely belong to it |
|
|
78
|
+
|
|
79
|
+
Returns `workspaceId`, `status`, `action`, `url`, `nextStep`. Check
|
|
80
|
+
`metaConnection.canReconnect` first; a non-owner gets `not_workspace_owner` and the right
|
|
81
|
+
answer is to ask the owner instead.
|
|
82
|
+
|
|
83
|
+
## Uploads
|
|
84
|
+
|
|
85
|
+
Bytes never pass through any of these. `create_upload_batch` mints presigned storage URLs and
|
|
86
|
+
the caller sends bytes to them directly.
|
|
87
|
+
|
|
88
|
+
### `get_upload_limits`
|
|
89
|
+
|
|
90
|
+
| Argument | Required |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `workspaceId` | yes |
|
|
93
|
+
|
|
94
|
+
Returns `multipartThresholdBytes`, `recommendedPartBytes`, `maxParts`, `maxFiles`, and
|
|
95
|
+
`media.image` / `media.video`, each with `maxBytes` and `contentTypes`. Image and video
|
|
96
|
+
ceilings differ by orders of magnitude. Never hardcode any of these.
|
|
97
|
+
|
|
98
|
+
### `create_upload_batch`
|
|
99
|
+
|
|
100
|
+
| Argument | Required | Notes |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `workspaceId` | yes | |
|
|
103
|
+
| `files` | yes | 1 to `maxFiles` entries (250 when the server does not say) |
|
|
104
|
+
| `files[].fileName` | yes | Including extension |
|
|
105
|
+
| `files[].contentType` | yes | Must be an allowed type |
|
|
106
|
+
| `files[].bytes` | yes | Whole number, the real size |
|
|
107
|
+
|
|
108
|
+
Returns `batchId`, `metaAdAccountId`, `createdAt`, `fileCount`, and `files` — one destination
|
|
109
|
+
per file, in the order sent. A destination is one of:
|
|
110
|
+
|
|
111
|
+
- **PUT** — `method`, `fileName`, `contentId`, `url`, `fields`, `headers`, `expiresIn` (1800).
|
|
112
|
+
Send the whole file to `url` with `headers` verbatim.
|
|
113
|
+
- **MULTIPART** — `method`, `fileName`, `contentId`, `uploadId`, `key`, `partBytes`. Needs
|
|
114
|
+
`sign_upload_part` per part, then `complete_upload`.
|
|
115
|
+
|
|
116
|
+
### `resign_upload`
|
|
117
|
+
|
|
118
|
+
| Argument | Required |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `workspaceId`, `contentId` | yes |
|
|
121
|
+
|
|
122
|
+
A fresh presigned PUT for one file. Every URL in a batch is signed at the same instant and
|
|
123
|
+
lasts 1800 seconds, so late files in a large batch can expire while early ones are still
|
|
124
|
+
transferring. Call on a 403, or when most of `expiresIn` has passed.
|
|
125
|
+
|
|
126
|
+
### `sign_upload_part`
|
|
127
|
+
|
|
128
|
+
| Argument | Required | Notes |
|
|
129
|
+
| --- | --- | --- |
|
|
130
|
+
| `workspaceId`, `uploadId` | yes | |
|
|
131
|
+
| `partNumber` | yes | 1 to 10000 |
|
|
132
|
+
|
|
133
|
+
Returns `{ url, headers, expiresIn }`. Takes no bytes and no checksum, so all parts can be
|
|
134
|
+
signed up front before any transfer starts. Part count is `ceil(bytes / partBytes)`.
|
|
135
|
+
|
|
136
|
+
### `list_upload_parts`
|
|
137
|
+
|
|
138
|
+
| Argument | Required |
|
|
139
|
+
| --- | --- |
|
|
140
|
+
| `workspaceId`, `uploadId` | yes |
|
|
141
|
+
|
|
142
|
+
Returns a **bare array** of `{ PartNumber, Size, ETag }` — what storage already holds. Use it
|
|
143
|
+
to resume without re-sending parts that landed.
|
|
144
|
+
|
|
145
|
+
### `complete_upload`
|
|
146
|
+
|
|
147
|
+
| Argument | Required | Notes |
|
|
148
|
+
| --- | --- | --- |
|
|
149
|
+
| `workspaceId`, `uploadId` | yes | |
|
|
150
|
+
| `parts` | yes | `{ PartNumber, ETag }[]`, capitalised exactly so |
|
|
151
|
+
|
|
152
|
+
Assembles the object. Returns `VALIDATING` — this does **not** mean accepted. Safe to retry.
|
|
153
|
+
|
|
154
|
+
### `abort_upload`
|
|
155
|
+
|
|
156
|
+
| Argument | Required |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| `workspaceId`, `uploadId` | yes |
|
|
159
|
+
|
|
160
|
+
Discards a multipart upload and its stored parts. Destructive, and not marked as such.
|
|
161
|
+
|
|
162
|
+
### `list_upload_batches`
|
|
163
|
+
|
|
164
|
+
| Argument | Required |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `workspaceId` | yes |
|
|
167
|
+
|
|
168
|
+
Batches newest first, each with `batchId`, `createdAt`, `fileCount`, `pending`, and
|
|
169
|
+
`statusCounts`. No file rows. The cheap way to check on an upload, and the way to find a batch
|
|
170
|
+
someone created through a browser link. May return `truncated: true` at 1000 items, with no
|
|
171
|
+
cursor beyond that.
|
|
172
|
+
|
|
173
|
+
### `get_upload_batch_content`
|
|
174
|
+
|
|
175
|
+
| Argument | Required | Notes |
|
|
176
|
+
| --- | --- | --- |
|
|
177
|
+
| `workspaceId`, `batchId` | yes | |
|
|
178
|
+
| `status` | no | Array of statuses; omit for all |
|
|
179
|
+
| `mediaKind` | no | `image` or `video`; omit for both |
|
|
180
|
+
|
|
181
|
+
Returns `content`, `count`, `statusCounts`, `pending`, `settled`, `batchId`. Each content row:
|
|
182
|
+
`contentId`, `status`, and optionally `rejectionReason`, `fileName`, `mediaKind`, `bytes`,
|
|
183
|
+
`contentType`, `format`, `createdAt`, `updatedAt`, and `url` on a `READY` image.
|
|
184
|
+
|
|
185
|
+
Remember that filtering narrows `content` and `count` only.
|
|
186
|
+
|
|
187
|
+
### `get_upload_content`
|
|
188
|
+
|
|
189
|
+
| Argument | Required |
|
|
190
|
+
| --- | --- |
|
|
191
|
+
| `workspaceId`, `contentId` | yes |
|
|
192
|
+
|
|
193
|
+
One row, same shape as above. For a file uploaded on its own, which has no `batchId`.
|
|
194
|
+
|
|
195
|
+
### `create_browser_upload_link`
|
|
196
|
+
|
|
197
|
+
| Argument | Required |
|
|
198
|
+
| --- | --- |
|
|
199
|
+
| `workspaceId` | yes |
|
|
200
|
+
|
|
201
|
+
Returns `{ workspaceId, url }` — a page where the person uploads from their browser. Use it
|
|
202
|
+
whenever you cannot make HTTP requests, and for very large video where signing every part
|
|
203
|
+
would take too many calls. The page requires them to be signed in, so the link grants nothing
|
|
204
|
+
on its own.
|
|
205
|
+
|
|
206
|
+
## Status lifecycle
|
|
207
|
+
|
|
208
|
+
| Status | Final | Counted in `pending` |
|
|
209
|
+
| --- | --- | --- |
|
|
210
|
+
| `CREATED` | no | yes |
|
|
211
|
+
| `UPLOADING` | no | yes |
|
|
212
|
+
| `VALIDATING` | no | yes |
|
|
213
|
+
| `READY` | **yes** | no |
|
|
214
|
+
| `REJECTED` | yes | no |
|
|
215
|
+
| `ABORTED` | yes | no |
|
|
216
|
+
| `EXPIRED` | no | yes |
|
|
217
|
+
|
|
218
|
+
`READY` is the only status that means a file uploaded successfully. Poll every 5 seconds, give
|
|
219
|
+
up after 5 minutes.
|
|
220
|
+
|
|
221
|
+
## Errors
|
|
222
|
+
|
|
223
|
+
Refusals come back as a result with `isError: true` and a payload of `{ error, message }`, so
|
|
224
|
+
they are yours to read and act on rather than hard failures.
|
|
225
|
+
|
|
226
|
+
| `error` | Meaning |
|
|
227
|
+
| --- | --- |
|
|
228
|
+
| `invalid_arguments` | A required argument is missing, empty, or out of range |
|
|
229
|
+
| `invalid_upload_body` | The `files` manifest was malformed |
|
|
230
|
+
| `missing_workspace_id` | `workspaceId` was blank |
|
|
231
|
+
| `workspace_access_denied` | Caller is not a member of that workspace |
|
|
232
|
+
| `not_workspace_owner` | Caller belongs to the workspace but does not own it |
|
|
233
|
+
| `upload_not_found` | No such content id |
|
|
234
|
+
| `upload_signing_unreachable` | Upstream signing service could not be reached |
|
|
235
|
+
| `upload_signing_timeout` | Upstream signing service timed out |
|
|
236
|
+
| `upload_signing_invalid_json` | Upstream returned something unparseable |
|
|
237
|
+
| `upload_signing_unconfigured` | Server is missing its upstream credentials |
|
|
238
|
+
| `upstream_{status}` | Upstream error with no more specific code |
|
|
239
|
+
| `internal_error` | Server-side fault; the detail is logged, not returned |
|
|
240
|
+
|
|
241
|
+
Genuine protocol faults — an unknown tool, malformed JSON-RPC — arrive as JSON-RPC errors
|
|
242
|
+
instead, and mean something is wrong with the call itself rather than its arguments.
|
|
243
|
+
|
|
244
|
+
### `workspace_access_denied` on an id from `list_workspaces`
|
|
245
|
+
|
|
246
|
+
This happens, and it is not something to solve by reasoning. Membership is re-checked on every
|
|
247
|
+
call against the tool being called, so a workspace can appear in the list and still refuse an
|
|
248
|
+
upload. Retrying will not change it, and neither `role` nor `isOwner` predicts it.
|
|
249
|
+
|
|
250
|
+
Say plainly which id was refused and which one worked, and carry on with the one that worked.
|
|
251
|
+
If you had already told the person to use the refused one, correct that in the same breath —
|
|
252
|
+
they are about to look for their files in a workspace that has none.
|