@sprid/cli 0.1.17 → 0.1.19
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/CHANGELOG.md +19 -0
- package/package.json +1 -1
- package/src/args.mjs +4 -1
- package/src/commands/post-video.mjs +145 -0
- package/src/commands/post.mjs +2 -0
- package/src/commands/publishing.mjs +17 -8
- package/src/docs/commands.mjs +4 -2
- package/src/docs/guides.generated.mjs +2 -2
- package/src/post/cli.mjs +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
sprid follows semver: a breaking change to a command's arguments, its output
|
|
4
4
|
shape or its exit codes is a major release.
|
|
5
5
|
|
|
6
|
+
## 0.1.19 - 2026-09-30
|
|
7
|
+
|
|
8
|
+
- `sprid docs chat` uses the consolidated post and slide tools. Asset creation
|
|
9
|
+
retains retry protection and finished-artwork safeguards; one or many slides
|
|
10
|
+
use the same editing and appending operations.
|
|
11
|
+
|
|
12
|
+
## 0.1.18 - 2026-09-28
|
|
13
|
+
|
|
14
|
+
- `sprid post video <ref>` reads a post's own video: its revision, version
|
|
15
|
+
hash, issues, slide times, which destinations get it, and its renders.
|
|
16
|
+
`sprid post video render <ref>` quotes the render and stops; add `--confirm`
|
|
17
|
+
(optionally `--ceiling <credits>` and `--final`) to render. An unchanged video
|
|
18
|
+
that is already rendered is handed back without a new charge.
|
|
19
|
+
- `sprid post schedule` and `sprid post publish` take `--video-render <id>` to
|
|
20
|
+
book a video destination with that render.
|
|
21
|
+
- TikTok: an agent or access token can send a post to the creator's TikTok
|
|
22
|
+
drafts unattended. A direct post needs the person to confirm it on Sprid's
|
|
23
|
+
posting screen; the command prints the link to open when that is needed.
|
|
24
|
+
|
|
6
25
|
## 0.1.17 - 2026-09-28
|
|
7
26
|
|
|
8
27
|
- `sprid docs chat` says the preview tools return an image of every slide for
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sprid/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "The Sprid command line: connect your app, review results, prepare posts and store screenshots, and release mobile apps.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
package/src/args.mjs
CHANGED
|
@@ -85,9 +85,12 @@ export const VALUE_FLAGS = new Set([
|
|
|
85
85
|
"times",
|
|
86
86
|
"start",
|
|
87
87
|
"time",
|
|
88
|
+
// post video
|
|
89
|
+
"ceiling",
|
|
90
|
+
"video-render",
|
|
88
91
|
]);
|
|
89
92
|
|
|
90
|
-
export const BOOL_FLAGS = new Set(["json", "offline", "check", "no-browser", "from-local", "help", "version", "unanswered", "yes", "clear", "refresh", "cached-only", "next", "queue-end", "shift-later", "dry-run", "skip-busy"]);
|
|
93
|
+
export const BOOL_FLAGS = new Set(["json", "offline", "check", "no-browser", "from-local", "help", "version", "unanswered", "yes", "clear", "refresh", "cached-only", "next", "queue-end", "shift-later", "dry-run", "skip-busy", "confirm", "final"]);
|
|
91
94
|
|
|
92
95
|
/**
|
|
93
96
|
* `sprid connect asc --app myapp --key ./k.p8 --json`
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// sprid post video <postId> read the post's own video
|
|
2
|
+
// sprid post video render <postId> ... render it to MP4 (spends credits)
|
|
3
|
+
//
|
|
4
|
+
// The video is compiled from the post's slides on every read, so what this
|
|
5
|
+
// prints is what would render now: its revision, the hash that names it, the
|
|
6
|
+
// issues in its way, each slide's place in time and the renders of exactly
|
|
7
|
+
// this version. Rendering is the only call here that spends, so it quotes
|
|
8
|
+
// first and renders only with --confirm, against the quote it just read: a
|
|
9
|
+
// video that changed in between is refused by the server, not rendered.
|
|
10
|
+
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { UsageError } from "../args.mjs";
|
|
13
|
+
import { C, OK } from "../format.mjs";
|
|
14
|
+
import { callTool } from "../tools.mjs";
|
|
15
|
+
|
|
16
|
+
const POST_REF = /^([1-9]\d*|[A-Za-z][A-Za-z0-9]{1,4}-\d+)$/;
|
|
17
|
+
const READ_USAGE = "sprid post video <postId>";
|
|
18
|
+
const RENDER_USAGE = "sprid post video render <postId> [--confirm] [--ceiling N] [--final]";
|
|
19
|
+
|
|
20
|
+
export async function postVideo(ctx) {
|
|
21
|
+
const [, sub, maybeRef] = ctx.positionals;
|
|
22
|
+
if (sub === "render") return render(ctx, postRef(maybeRef, RENDER_USAGE));
|
|
23
|
+
return read(ctx, postRef(sub, READ_USAGE));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function postRef(ref, usage) {
|
|
27
|
+
if (!POST_REF.test(String(ref ?? ""))) throw new UsageError(usage);
|
|
28
|
+
return /^\d+$/.test(ref) ? Number(ref) : String(ref).toUpperCase();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A refused tool call answers `{ok: false, error}`; print it and fail. */
|
|
32
|
+
function refused(ctx, res) {
|
|
33
|
+
if (res?.ok !== false) return false;
|
|
34
|
+
if (ctx.json) ctx.out(res);
|
|
35
|
+
else {
|
|
36
|
+
const e = res.error ?? {};
|
|
37
|
+
ctx.warn(` ${C.bad("✗")} ${e.message ?? e.code ?? "The server refused the request."}`);
|
|
38
|
+
for (const i of e.issues ?? []) ctx.warn(` ${i.severity ?? "error"} ${i.code}: ${i.message}`);
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const seconds = (us) => `${(Number(us) / 1e6).toFixed(1)}s`;
|
|
44
|
+
const shortHash = (h) => String(h ?? "").slice(0, 12);
|
|
45
|
+
|
|
46
|
+
function renderLine(r) {
|
|
47
|
+
const url = r?.result?.asset?.url;
|
|
48
|
+
return `${r.mode ?? "?"} ${r.status} ${C.dim(r.id)}${url ? ` ${url}` : ""}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function read(ctx, post) {
|
|
52
|
+
const res = await callTool(ctx, "get_post_video", { post });
|
|
53
|
+
if (refused(ctx, res)) return 1;
|
|
54
|
+
if (ctx.json) {
|
|
55
|
+
ctx.out(res);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
ctx.print(` Post ${post} · video revision ${res.revision} · hash ${shortHash(res.hash)}`);
|
|
59
|
+
const issues = res.issues ?? [];
|
|
60
|
+
const errors = issues.filter((i) => i.severity === "error").length;
|
|
61
|
+
if (!issues.length) ctx.print(` ${C.ok(OK)} No issues`);
|
|
62
|
+
else {
|
|
63
|
+
ctx.print(` ${errors ? C.bad("✗") : "!"} ${errors} error${errors === 1 ? "" : "s"}, ${issues.length - errors} warning${issues.length - errors === 1 ? "" : "s"}`);
|
|
64
|
+
for (const i of issues) ctx.print(` ${i.severity} ${i.code}: ${i.message}`);
|
|
65
|
+
}
|
|
66
|
+
if (res.outputs) ctx.print(` Outputs: carousel ${res.outputs.carousel ? "on" : "off"}, video ${res.outputs.video ? "on" : "off"}`);
|
|
67
|
+
const times = res.slideTimes ?? [];
|
|
68
|
+
if (times.length) {
|
|
69
|
+
ctx.print(` Slides (${seconds(times.at(-1).endUs)}):`);
|
|
70
|
+
times.forEach((t, n) => {
|
|
71
|
+
const length = t.pinned ? `pinned ${seconds(t.endUs - t.startUs)}` : "auto";
|
|
72
|
+
ctx.print(` ${String(n + 1).padStart(2)} slide ${t.slideId} ${seconds(t.startUs)}-${seconds(t.endUs)} ${length}`);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const renders = res.renders ?? [];
|
|
76
|
+
if (renders.length) {
|
|
77
|
+
ctx.print(" Renders of this version:");
|
|
78
|
+
for (const r of renders) ctx.print(` ${renderLine(r)}`);
|
|
79
|
+
} else ctx.print(` Not rendered at this version. Quote it: sprid post video render ${post}`);
|
|
80
|
+
if (res.lastRender?.stale) ctx.print(C.dim(` Last render is of an older version: ${res.lastRender.id}`));
|
|
81
|
+
if (res.editorUrl) ctx.print(C.dim(` Edit: ${res.editorUrl}`));
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function render(ctx, post) {
|
|
86
|
+
const ceiling = ctx.flags.ceiling === undefined ? 30 : Number(ctx.flags.ceiling);
|
|
87
|
+
if (!Number.isInteger(ceiling) || ceiling < 3 || ceiling > 300) {
|
|
88
|
+
throw new UsageError(`--ceiling is the most this render may spend, in credits: a whole number from 3 to 300. ${RENDER_USAGE}`);
|
|
89
|
+
}
|
|
90
|
+
const mode = ctx.flags.final ? "final" : "preview";
|
|
91
|
+
const quote = await callTool(ctx, "quote_post_video_render", { post, creditCeilingCents: ceiling });
|
|
92
|
+
if (refused(ctx, quote)) return 1;
|
|
93
|
+
|
|
94
|
+
// A final render of this exact video already exists: nothing to pay for,
|
|
95
|
+
// and it serves a preview request as well as a final one.
|
|
96
|
+
if (quote.existingRender) {
|
|
97
|
+
if (ctx.json) ctx.out({ quote, render: quote.existingRender, reused: true });
|
|
98
|
+
else {
|
|
99
|
+
ctx.print(` ${C.ok(OK)} Already rendered at this version, nothing to pay: ${renderLine(quote.existingRender)}`);
|
|
100
|
+
ctx.print(C.dim(` Book it: sprid post schedule ${post} --video-render ${quote.existingRender.id} …`));
|
|
101
|
+
}
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!quote.allowed) {
|
|
106
|
+
if (ctx.json) ctx.out({ ok: false, quote });
|
|
107
|
+
else {
|
|
108
|
+
const why = quote.message ?? (quote.plan?.active === false ? quote.plan.reason : null);
|
|
109
|
+
ctx.warn(` ${C.bad("✗")} ${why ?? "This video cannot be rendered yet."}`);
|
|
110
|
+
for (const i of (quote.issues ?? []).filter((x) => x.severity === "error")) ctx.warn(` ${i.code}: ${i.message}`);
|
|
111
|
+
}
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!ctx.json) {
|
|
116
|
+
const est = quote.estimate?.credits != null ? `, about ${quote.estimate.credits} at the recent pace` : "";
|
|
117
|
+
ctx.print(` ${mode === "final" ? "Final" : "Preview"} render of post ${post}, revision ${quote.revision}: up to ${ceiling} credits (${quote.creditsPerStartedMinute} per started minute${est}).`);
|
|
118
|
+
}
|
|
119
|
+
if (!ctx.flags.confirm) {
|
|
120
|
+
const msg = `Rendering spends up to ${ceiling} credits. Re-run with --confirm once that is agreed.`;
|
|
121
|
+
if (ctx.json) ctx.out({ ok: false, needsConfirm: true, error: msg, quote });
|
|
122
|
+
else ctx.warn(` ${msg}`);
|
|
123
|
+
return 2;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const res = await callTool(ctx, "render_post_video", {
|
|
127
|
+
post,
|
|
128
|
+
baseRevision: quote.revision,
|
|
129
|
+
outputHash: quote.hash,
|
|
130
|
+
requestId: randomUUID(),
|
|
131
|
+
mode,
|
|
132
|
+
confirmed: true,
|
|
133
|
+
creditCeilingCents: ceiling,
|
|
134
|
+
});
|
|
135
|
+
if (refused(ctx, res)) return 1;
|
|
136
|
+
if (ctx.json) {
|
|
137
|
+
ctx.out({ quote, render: res });
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
ctx.print(` ${C.ok(OK)} ${res.reused ? "Reused" : "Started"} ${renderLine(res)}`);
|
|
141
|
+
ctx.print(C.dim(` Follow it: sprid post video ${post}`));
|
|
142
|
+
if (mode === "final") ctx.print(C.dim(` Book it: sprid post schedule ${post} --video-render ${res.id} …`));
|
|
143
|
+
else ctx.print(C.dim(" A preview is for review. Bookings publish a --final render."));
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
package/src/commands/post.mjs
CHANGED
|
@@ -3,9 +3,11 @@ import { fork } from "node:child_process";
|
|
|
3
3
|
import { run as runPost } from "../post/cli.mjs";
|
|
4
4
|
import { remotePost } from './studio.mjs';
|
|
5
5
|
import { publishing, PUBLISHING_VERBS } from './publishing.mjs';
|
|
6
|
+
import { postVideo } from './post-video.mjs';
|
|
6
7
|
|
|
7
8
|
export async function post(ctx) {
|
|
8
9
|
if (PUBLISHING_VERBS.includes(ctx.positionals[0])) return publishing(ctx);
|
|
10
|
+
if (ctx.positionals[0] === 'video') return postVideo(ctx);
|
|
9
11
|
if (await remotePost(ctx)) return 0;
|
|
10
12
|
return localPost(ctx);
|
|
11
13
|
}
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
// back (`sprid queue cancel <publishId>`), so scheduling does not ask.
|
|
5
5
|
//
|
|
6
6
|
// TikTok's privacy level, comment/duet/stitch permissions and the
|
|
7
|
-
// commercial-content disclosure
|
|
8
|
-
// a TikTok
|
|
7
|
+
// commercial-content disclosure are the creator's to answer on Sprid's posting
|
|
8
|
+
// screen, never a command's. From here TikTok goes as a draft to the creator's
|
|
9
|
+
// TikTok inbox; a direct post is refused with the review link where the person
|
|
10
|
+
// answers and confirms, and the error line prints it.
|
|
9
11
|
|
|
10
12
|
import { UsageError } from "../args.mjs";
|
|
11
13
|
import { C, OK } from "../format.mjs";
|
|
@@ -13,7 +15,6 @@ import { callTool, listFlag, readJson, show } from "../tools.mjs";
|
|
|
13
15
|
import { confirm } from "./reviews.mjs";
|
|
14
16
|
|
|
15
17
|
const POST_REF = /^([1-9]\d*|[A-Za-z][A-Za-z0-9]{1,4}-\d+)$/;
|
|
16
|
-
const TIKTOK_FIELDS = "privacyLevel, disableComment, disableDuet, disableStitch and the commercial-content answers";
|
|
17
18
|
|
|
18
19
|
export const PUBLISHING_VERBS = ["schedule", "publish", "schedule-batch"];
|
|
19
20
|
|
|
@@ -21,22 +22,30 @@ export async function publishing(ctx) {
|
|
|
21
22
|
const [verb, ref] = ctx.positionals;
|
|
22
23
|
if (verb === "schedule-batch") return scheduleBatch(ctx);
|
|
23
24
|
const usage = verb === "publish"
|
|
24
|
-
? "sprid post publish <postId> [--platforms a,b] [--options <options.json>] [--yes]"
|
|
25
|
-
: "sprid post schedule <postId> (--at ISO|--next|--queue-end) [--platforms a,b] [--time HH:MM] [--shift-later] [--options <options.json>]";
|
|
25
|
+
? "sprid post publish <postId> [--platforms a,b] [--options <options.json>] [--video-render <renderId>] [--yes]"
|
|
26
|
+
: "sprid post schedule <postId> (--at ISO|--next|--queue-end) [--platforms a,b] [--time HH:MM] [--shift-later] [--options <options.json>] [--video-render <renderId>]";
|
|
26
27
|
if (!POST_REF.test(String(ref ?? ""))) throw new UsageError(usage);
|
|
27
28
|
const id = /^\d+$/.test(ref) ? Number(ref) : String(ref).toUpperCase();
|
|
28
29
|
const base = { id, ...(await destination(ctx)) };
|
|
29
30
|
return verb === "publish" ? publish(ctx, base) : schedule(ctx, base, usage);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Platforms, per-platform options and, for destinations that go out as the
|
|
35
|
+
* post's own video, the final render they publish (`sprid post video render
|
|
36
|
+
* --final`). Shared by every verb here.
|
|
37
|
+
*/
|
|
33
38
|
async function destination(ctx) {
|
|
39
|
+
const renderId = ctx.flags["video-render"];
|
|
40
|
+
if (renderId !== undefined && !/^[0-9a-f-]{36}$/i.test(String(renderId))) {
|
|
41
|
+
throw new UsageError("--video-render takes a render id, as sprid post video <postId> lists them.");
|
|
42
|
+
}
|
|
34
43
|
const platforms = listFlag(ctx.flags.platforms);
|
|
35
44
|
const options = typeof ctx.flags.options === "string" ? await readJson(ctx, ctx.flags.options) : undefined;
|
|
36
45
|
if (platforms?.includes("tiktok") && !options?.tiktok) {
|
|
37
|
-
throw new UsageError(`TikTok
|
|
46
|
+
throw new UsageError(`TikTok goes to the creator's drafts from here: put {"tiktok": {"postMode": "MEDIA_UPLOAD"}} in --options <options.json>. To post straight to the profile, schedule the other platforms here and open the post's review link to answer TikTok's questions.`);
|
|
38
47
|
}
|
|
39
|
-
return { ...(platforms ? { platforms } : {}), ...(options ? { options } : {}) };
|
|
48
|
+
return { ...(platforms ? { platforms } : {}), ...(options ? { options } : {}), ...(renderId ? { videoRenderId: renderId } : {}) };
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
async function schedule(ctx, base, usage) {
|
package/src/docs/commands.mjs
CHANGED
|
@@ -33,6 +33,8 @@ export const COMMAND_GROUPS = [
|
|
|
33
33
|
['post', 'sprid post update <postId> --file <changes.json>', 'Apply the existing post-update contract to a shared draft.'],
|
|
34
34
|
['post', 'sprid post preview --id <postId>', 'Preview a shared post. Without --id, historical local preview arguments are unchanged.'],
|
|
35
35
|
['post', 'sprid post deliveries <postId>', 'Read scheduled, failed and delivered publications with platform receipts.'],
|
|
36
|
+
['post', 'sprid post video <postId>', 'Read the post\'s own video, compiled from its slides: its revision and hash, anything that stops it rendering, each slide\'s start, end and whether its length is pinned, which outputs are on, and the renders of this exact version. Edit it with sprid tool call edit_post_video.'],
|
|
37
|
+
['post', 'sprid post video render <postId> [--confirm] [--ceiling N] [--final]', 'Quote a render of the video as it is now, then render it with --confirm, spending at most --ceiling credits (default 30). A render of the same version is returned without charge. Bookings publish a --final render; pass its id to sprid post schedule --video-render.'],
|
|
36
38
|
['post', 'sprid post handoff <postId>', 'Print the prompt to paste into an agent: what the post currently is, which verbs to call, and an empty line for the ask. Same string the editor\'s "Hand off to agent" button copies.'],
|
|
37
39
|
] },
|
|
38
40
|
{ title: 'Installation and updates', entries: [
|
|
@@ -101,9 +103,9 @@ export const COMMAND_GROUPS = [
|
|
|
101
103
|
['queue', 'sprid queue', 'Show what is scheduled and what failed.'],
|
|
102
104
|
['queue', 'sprid queue retry <publishId>', 'Retry a failed or missed publish.'],
|
|
103
105
|
['queue', 'sprid queue cancel <publishId>', 'Cancel a booking. The post itself is unchanged and can be scheduled again.'],
|
|
104
|
-
['post', 'sprid post schedule <postId> (--at ISO|--next|--queue-end) [--platforms a,b] [--time HH:MM] [--shift-later] [--options <options.json>]', 'Book a post: at a time, the next free slot, or after everything already booked. --shift-later moves later bookings down a slot.
|
|
106
|
+
['post', 'sprid post schedule <postId> (--at ISO|--next|--queue-end) [--platforms a,b] [--time HH:MM] [--shift-later] [--options <options.json>] [--video-render <renderId>]', 'Book a post: at a time, the next free slot, or after everything already booked. --shift-later moves later bookings down a slot. A destination that goes out as the post\'s video needs --video-render, a final render of the video as it is now. TikTok goes as a draft to the TikTok inbox: {"tiktok": {"postMode": "MEDIA_UPLOAD"}} in --options. A direct TikTok post is answered and confirmed by the person on the review link the refusal prints. A Pinterest Pin needs its board and destination link saved first with sprid pinterest set.'],
|
|
105
107
|
['post', 'sprid post schedule-batch --account <slug> --ids 1,2,3 [--cadence daily|every-other-day|weekdays] [--times 09:00] [--start YYYY-MM-DD] [--platforms a,b] [--skip-busy] [--dry-run]', 'Place a list of posts on a cadence in one call. --dry-run returns the plan without booking anything.'],
|
|
106
|
-
['post', 'sprid post publish <postId> [--platforms a,b] [--options <options.json>] [--yes]', 'Publish now. Public and not reversible, so it asks unless --yes. Read the receipts with sprid post deliveries.'],
|
|
108
|
+
['post', 'sprid post publish <postId> [--platforms a,b] [--options <options.json>] [--video-render <renderId>] [--yes]', 'Publish now. Public and not reversible, so it asks unless --yes. Read the receipts with sprid post deliveries.'],
|
|
107
109
|
['inbox', 'sprid inbox [--view needs_you|all|done] [--kind all|comment|review] [--accounts a,b] [--app <slug>] [--intent <intent>] [--limit N]', 'Comments on posts and ads plus store reviews, as one ranked queue.'],
|
|
108
110
|
['inbox', 'sprid inbox draft <commentId> [--count N]', 'Draft replies in the account’s reply voice. Nothing is sent.'],
|
|
109
111
|
['inbox', 'sprid inbox reply <commentId> --text "…" [--yes]', 'Send a public reply to a comment. Asks unless --yes. Store reviews are answered with sprid reviews reply.'],
|
|
@@ -1604,8 +1604,8 @@ export const CONTENT_GUIDES = [
|
|
|
1604
1604
|
"title": "Use Sprid in chat",
|
|
1605
1605
|
"summary": "Take an app from setup to a reviewed post and confirmed delivery using only your chat app and a browser. No CLI or installed skills needed.",
|
|
1606
1606
|
"url": "https://sprid.studio/docs/chat",
|
|
1607
|
-
"markdown": "# Use Sprid in chat\n\n**What this guide does:** Take an app from setup to a reviewed post and confirmed delivery using only your chat app and a browser. No CLI or installed skills needed.\n\nYou tell the agent what you want; it handles the tool calls below. The tool names are here so the agent knows the order. Setup should only ask for the connection the current task needs.\n\n## Connect Sprid\n\nChoose your chat at `https://sprid.studio/start#chat` and follow its steps. Your chat may call this an app, connector or integration. The address is `https://api.sprid.studio/api/mcp?surface=core`. Sign in to Sprid and authorize the right workspace.\n\nAgent: when available, start with `get_marketing_plan` and reuse its prepared work and next step rather than starting over. Discovery can start from a website or store URL, with no social account.\n\n## Pick the app and account\n\nAgent: call `get_capabilities`, then `list_apps` and `list_accounts`, and reuse what exists. If the app is missing, get its description or website/store URL, confirm it and create it with `upsert_app_profile`. A content account owns voice and media; a channel is one exact social destination. Never pick a handle just because it is first in a list. A missing account is created in the dashboard.\n\n## Connect a destination or data source\n\n- **Social channel:** `get_documentation` with the platform name, then `connect_channel`. The person opens the returned link and approves in the browser; `channel_status` confirms the saved result.\n- **Store or analytics:** the person enters identifiers and the key in the secure form at `https://app.sprid.studio/settings/app-profiles`. Credentials never go in chat, tool arguments or reports. A saved-key indicator only proves configuration; `get_marketing_review` does a live read.\n\n## Bring images or a finished video\n\nDraft copy in the chat, in the account’s voice. No extra model key needed.\n\nIf the chat exposes the attached files, pass them to `import_assets`: each file has `download_url` and `file_id`, optionally `mime_type` and `file_name`. Public HTTPS URLs and existing `{kind, id}` library references also work. Limits: PNG, JPEG, WebP or finished MP4, up to 32 MiB each. Each position reports on its own; retry the missing ones before building the post. Files are copied to the account and deduplicated by bytes; temporary download URLs are not kept.\n\nA `sandbox:` URL or local path can’t be fetched. For an image the chat generated, use the existing file if the host exposes it (not verified for every host); otherwise download it and use the browser upload. Never ask the agent to recreate an image or transcribe binary data.\n\n**Fallback: browser upload.** Call `create_upload_session` with the account and a new UUID `requestId`. The person opens the link and picks the files. Then read `get_studio_job` with the returned ID; its result keys are zero-based positions. The same session and asset IDs work from any other chat. No image-generation charge.\n\n## Create and review the post\n\nCall `
|
|
1608
|
-
"revision": "
|
|
1607
|
+
"markdown": "# Use Sprid in chat\n\n**What this guide does:** Take an app from setup to a reviewed post and confirmed delivery using only your chat app and a browser. No CLI or installed skills needed.\n\nYou tell the agent what you want; it handles the tool calls below. The tool names are here so the agent knows the order. Setup should only ask for the connection the current task needs.\n\n## Connect Sprid\n\nChoose your chat at `https://sprid.studio/start#chat` and follow its steps. Your chat may call this an app, connector or integration. The address is `https://api.sprid.studio/api/mcp?surface=core`. Sign in to Sprid and authorize the right workspace.\n\nAgent: when available, start with `get_marketing_plan` and reuse its prepared work and next step rather than starting over. Discovery can start from a website or store URL, with no social account.\n\n## Pick the app and account\n\nAgent: call `get_capabilities`, then `list_apps` and `list_accounts`, and reuse what exists. If the app is missing, get its description or website/store URL, confirm it and create it with `upsert_app_profile`. A content account owns voice and media; a channel is one exact social destination. Never pick a handle just because it is first in a list. A missing account is created in the dashboard.\n\n## Connect a destination or data source\n\n- **Social channel:** `get_documentation` with the platform name, then `connect_channel`. The person opens the returned link and approves in the browser; `channel_status` confirms the saved result.\n- **Store or analytics:** the person enters identifiers and the key in the secure form at `https://app.sprid.studio/settings/app-profiles`. Credentials never go in chat, tool arguments or reports. A saved-key indicator only proves configuration; `get_marketing_review` does a live read.\n\n## Bring images or a finished video\n\nDraft copy in the chat, in the account’s voice. No extra model key needed.\n\nIf the chat exposes the attached files, pass them to `import_assets`: each file has `download_url` and `file_id`, optionally `mime_type` and `file_name`. Public HTTPS URLs and existing `{kind, id}` library references also work. Limits: PNG, JPEG, WebP or finished MP4, up to 32 MiB each. Each position reports on its own; retry the missing ones before building the post. Files are copied to the account and deduplicated by bytes; temporary download URLs are not kept.\n\nA `sandbox:` URL or local path can’t be fetched. For an image the chat generated, use the existing file if the host exposes it (not verified for every host); otherwise download it and use the browser upload. Never ask the agent to recreate an image or transcribe binary data.\n\n**Fallback: browser upload.** Call `create_upload_session` with the account and a new UUID `requestId`. The person opens the link and picks the files. Then read `get_studio_job` with the returned ID; its result keys are zero-based positions. The same session and asset IDs work from any other chat. No image-generation charge.\n\n## Create and review the post\n\nCall `create_post` with a new UUID `requestId`, the account, ordered `{kind, id}` assets, captions and aspect ratio. Reuse the same ID and inputs if a response is lost.\n\n- `presentation: \"finished\"` keeps artwork as-is: no text overlay, no added music. Images fit inside the canvas, which can add margins.\n- `presentation: \"editable\"` allows `text` and `subtitle` on image slides.\n- One finished video becomes one reel. Never mix a video with image slides.\n\nEdit with `update_post` (captions) and `update_slides` (slides). Call `preview_post` and check every slide or the video: captions, order, crop, legibility. It returns a small image of each slide for the agent to look at and an embedded preview for the person; a chat without embedded apps still shows the images. A preview is not approval to publish.\n\nAgent: show the person the preview, and link every post you mention with the `links` the tools return, as a Markdown link on its ref or title: `links.review` when the ask is to approve or schedule, `links.editor` when it is to change something. Never draw the slides in your own widget or artifact: those sandboxes block Sprid's media host and the images show broken.\n\n## Make a reel from photos or text\n\nCheck `get_capabilities` for hosted creation first. `create_reel` takes ordered scenes (`imageId`, `text`, `durationMs`), up to 120 seconds, and makes a silent MP4. It costs 3 Sprid credits per started render minute, or uses the plan allowance: check `spend_status`, tell the person, and get their go-ahead before sending `confirmed: true`. Save the job ID and read `get_studio_job`. Retrying with the same ID never starts a second render; a stopped job reports `needs_attention` instead of charging again.\n\nTo turn an existing post's slides into a video instead, use its own video: `get_video_capabilities`, then `get_post_video` and `edit_post_video` for transitions and motion, and `update_post {outputs}` for which destinations get it. Rendering it is metered the same way: `quote_post_video_render`, the person's go-ahead, then `render_post_video` with `confirmed: true`. Book a video destination with the render's `videoRenderId`.\n\nApp captures, simulator recordings and your own build scripts run outside remote MCP. Upload their finished files.\n\n## Approve and confirm delivery\n\nThe person opens the returned `/p/<postId>` link, reviews the real media and captions, and picks the channels, time and platform options. TikTok privacy and interaction choices have no defaults and are made on that screen. An agent-supplied confirmation flag is not a human click.\n\nTikTok works two ways from chat. A draft, `options.tiktok = { \"postMode\": \"MEDIA_UPLOAD\" }`, goes to the creator's TikTok inbox without anyone approving it in Sprid; they finish it in the TikTok app. A post straight to their profile is refused from chat and comes back with `approvalUrl`: send the person that link. It opens the posting screen on TikTok at the time asked for, and once they answer and confirm, the booking publishes on its own.\n\nAfter approval, read `post_queue`. Keep draft, scheduled, failed and delivered apart, and report delivery only with the platform receipt. Read a failure before retrying. Never repeat a publish call just because a response was lost.\n\nFor results, read `get_documentation` for `marketing-review`. Keep the same app, account and post IDs across clients. `next_actions` gives the next step; an empty `verb` means follow its browser link.\n\n## Limits and troubleshooting\n\n- **Attachment can’t be read:** use the browser upload, not the CLI.\n- **Missing account or service:** set it up in the dashboard. Never paste credentials into chat.\n- **Hosted creation:** check `get_capabilities` before promising it. Store screenshots and store releases are local unless it reports otherwise.\n- **File transfer:** whether a host exposes uploaded or generated files has to be checked per host. A supported schema doesn’t prove it.\n",
|
|
1608
|
+
"revision": "203ffeb3b12a34b7"
|
|
1609
1609
|
},
|
|
1610
1610
|
{
|
|
1611
1611
|
"id": "local",
|
package/src/post/cli.mjs
CHANGED
|
@@ -810,8 +810,8 @@ async function cmdInit(argv) {
|
|
|
810
810
|
${C.bold("What are your posts made of?")}
|
|
811
811
|
|
|
812
812
|
${C.bold("--kind text")} words on a background, a template, a hook and a payoff
|
|
813
|
-
${C.dim("-> render in the app repo
|
|
814
|
-
${C.dim(" the
|
|
813
|
+
${C.dim("-> render in the app repo and let Sprid upload the mp4,")}
|
|
814
|
+
${C.dim(" or build it as the post's own video and render it in Sprid.")}
|
|
815
815
|
|
|
816
816
|
${C.bold("--kind stills")} photographs, screenshots, generated images
|
|
817
817
|
${C.dim("-> the built-in recipe. A spec per reel, one command,")}
|