Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 311 additions & 0 deletions cli-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -35015,6 +35015,317 @@
"navigateBefore": false,
"siteSession": "persistent"
},
{
"site": "tiktok-symphony",
"name": "credits",
"description": "Symphony Creative Studio credit balance and plan",
"access": "read",
"example": "opencli tiktok-symphony credits",
"domain": "ads.tiktok.com",
"strategy": "ui",
"browser": true,
"args": [],
"columns": [
"credits",
"plan",
"account"
],
"type": "js",
"modulePath": "tiktok-symphony/credits.js",
"sourceFile": "tiktok-symphony/credits.js",
"navigateBefore": "https://ads.tiktok.com/creative/creativestudio/image-to-video?subApp=CreativeStudio/ImageGeneration/I2VImageGeneration"
},
{
"site": "tiktok-symphony",
"name": "delete",
"aliases": [
"rm"
],
"description": "Permanently delete a generation from the Library (irreversible; requires --yes)",
"access": "write",
"example": "opencli tiktok-symphony delete 202607205d0d326f7499347f469b9861 --yes",
"domain": "ads.tiktok.com",
"strategy": "ui",
"browser": true,
"args": [
{
"name": "asset",
"type": "string",
"required": true,
"positional": true,
"help": "assetId from `generations`"
},
{
"name": "yes",
"type": "boolean",
"default": false,
"required": false,
"help": "Actually delete. Without it the asset is only located and reported (status: dry-run)"
}
],
"columns": [
"assetId",
"status",
"type",
"url"
],
"type": "js",
"modulePath": "tiktok-symphony/delete.js",
"sourceFile": "tiktok-symphony/delete.js",
"navigateBefore": "https://ads.tiktok.com/creative/creativestudio/create/history",
"defaultWindowMode": "foreground"
},
{
"site": "tiktok-symphony",
"name": "download",
"description": "Download a generated Symphony asset by assetId (see: generations)",
"access": "read",
"example": "opencli tiktok-symphony download 202607195d0d7ea36ed139b74eccb1e4 --out ./downloads",
"domain": "ads.tiktok.com",
"strategy": "ui",
"browser": true,
"args": [
{
"name": "asset",
"type": "string",
"required": true,
"positional": true,
"help": "assetId from `generations`, or a full asset URL"
},
{
"name": "out",
"type": "string",
"default": ".",
"required": false,
"help": "Directory to write the file into"
}
],
"columns": [
"assetId",
"file",
"bytes",
"contentType"
],
"type": "js",
"modulePath": "tiktok-symphony/download.js",
"sourceFile": "tiktok-symphony/download.js",
"navigateBefore": "https://ads.tiktok.com/creative/creativestudio/create/history",
"defaultWindowMode": "foreground"
},
{
"site": "tiktok-symphony",
"name": "generate",
"description": "Generate images from a prompt and up to 4 reference images (spends Symphony credits)",
"access": "write",
"example": "opencli tiktok-symphony generate \"turn this into 3D chibi style\" --refs ./cat.png",
"domain": "ads.tiktok.com",
"strategy": "ui",
"browser": true,
"args": [
{
"name": "prompt",
"type": "string",
"required": true,
"positional": true,
"help": "What to generate"
},
{
"name": "refs",
"type": "string",
"default": "",
"required": false,
"help": "Comma-separated reference image paths (max 4)"
},
{
"name": "model",
"type": "string",
"default": "Nano Banana",
"required": false,
"help": "Model: Nano Banana / Flux Kontext Max"
},
{
"name": "timeout",
"type": "int",
"default": 300,
"required": false,
"help": "Seconds to wait for rendering"
}
],
"columns": [
"index",
"assetId",
"url",
"model",
"prompt"
],
"type": "js",
"modulePath": "tiktok-symphony/generate.js",
"sourceFile": "tiktok-symphony/generate.js",
"navigateBefore": false,
"defaultWindowMode": "foreground"
},
{
"site": "tiktok-symphony",
"name": "generate-video",
"aliases": [
"video"
],
"description": "Generate a video clip from a prompt and optional reference images (spends Symphony credits)",
"access": "write",
"example": "opencli tiktok-symphony generate-video \"a duck floating on a pool, slow zoom out\" --duration 5",
"domain": "ads.tiktok.com",
"strategy": "ui",
"browser": true,
"args": [
{
"name": "prompt",
"type": "string",
"required": true,
"positional": true,
"help": "What to generate"
},
{
"name": "mode",
"type": "string",
"default": "auto",
"required": false,
"help": "Sub-app: text / image / reference. \"auto\" picks text with no refs, image with 1, reference with 2+"
},
{
"name": "refs",
"type": "string",
"default": "",
"required": false,
"help": "Comma-separated reference image paths (image: 1-2, reference: 1-4)"
},
{
"name": "frames",
"type": "string",
"default": "first",
"required": false,
"help": "Image mode only: first / first-last"
},
{
"name": "model",
"type": "string",
"default": "Video 1.5 Pro",
"required": false,
"help": "Model: Video 1.5 Pro"
},
{
"name": "duration",
"type": "string",
"default": "5",
"required": false,
"help": "Clip length: 5s / 10s / 12s"
},
{
"name": "timeout",
"type": "int",
"default": 5400,
"required": false,
"help": "Seconds to wait when --wait true (rendering has been observed to exceed an hour)"
},
{
"name": "wait",
"type": "boolean",
"default": false,
"required": false,
"help": "Block until the clip renders. Off by default: rendering runs for tens of minutes, so the job is submitted and `generations` picks it up later"
}
],
"columns": [
"index",
"status",
"assetId",
"url",
"mode",
"model",
"duration",
"prompt"
],
"type": "js",
"modulePath": "tiktok-symphony/generate-video.js",
"sourceFile": "tiktok-symphony/generate-video.js",
"navigateBefore": false,
"defaultWindowMode": "foreground"
},
{
"site": "tiktok-symphony",
"name": "generations",
"aliases": [
"library"
],
"description": "List generated assets in the Symphony Creative Studio Library",
"access": "read",
"example": "opencli tiktok-symphony generations --limit 10",
"domain": "ads.tiktok.com",
"strategy": "ui",
"browser": true,
"args": [
{
"name": "limit",
"type": "int",
"default": 20,
"required": false,
"help": "Number of assets to return (max 200)"
}
],
"columns": [
"index",
"assetId",
"type",
"url"
],
"type": "js",
"modulePath": "tiktok-symphony/generations.js",
"sourceFile": "tiktok-symphony/generations.js",
"navigateBefore": "https://ads.tiktok.com/creative/creativestudio/create/history",
"defaultWindowMode": "foreground"
},
{
"site": "tiktok-symphony",
"name": "jobs",
"aliases": [
"queue"
],
"description": "Show generations in the Create feed with their render progress",
"access": "read",
"example": "opencli tiktok-symphony jobs",
"domain": "ads.tiktok.com",
"strategy": "ui",
"browser": true,
"args": [
{
"name": "limit",
"type": "int",
"default": 20,
"required": false,
"help": "Number of cards to return (max 50)"
},
{
"name": "pending",
"type": "boolean",
"default": false,
"required": false,
"help": "Only cards that have not produced an output yet (generating or finishing)"
}
],
"columns": [
"index",
"status",
"progress",
"taskId",
"model",
"prompt",
"error"
],
"type": "js",
"modulePath": "tiktok-symphony/jobs.js",
"sourceFile": "tiktok-symphony/jobs.js",
"navigateBefore": "https://ads.tiktok.com/creative/creativestudio/image-to-video?subApp=CreativeStudio/MiniApp/TextToVideo",
"defaultWindowMode": "foreground"
},
{
"site": "toutiao",
"name": "articles",
Expand Down
71 changes: 71 additions & 0 deletions clis/tiktok-symphony/credits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// tiktok-symphony credits — Symphony credit balance and current plan.
import { cli, Strategy } from '@jackwener/opencli/registry';
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { CREATE_URL, DEEP_QUERY_SRC, HOST, waitForValue } from './utils.js';

cli({
site: 'tiktok-symphony',
name: 'credits',
description: 'Symphony Creative Studio credit balance and plan',
access: 'read',
example: 'opencli tiktok-symphony credits',
domain: HOST,
strategy: Strategy.UI,
browser: true,
navigateBefore: CREATE_URL,
args: [],
columns: ['credits', 'plan', 'account'],
func: async (page) => {
// Field names here deliberately avoid the `columns` vocabulary: this is
// a scratch parse object, not a row, and reusing column names would let
// a future edit mistake it for one.
const header = await waitForValue(page, `(() => {
${DEEP_QUERY_SRC}
const coin = __deepAll(document, (el) => __ksTag(el, 'ks-icon') && el.getAttribute('name') === 'coin')[0];
const balanceText = coin && coin.parentElement ? coin.parentElement.textContent.trim() : null;

// "Basic plan" / "Plus plan" lives in the credit popover, which is
// rendered into the DOM without having to open it.
const planEl = __deepAll(document, (el) => /^[A-Z][a-z]+ plan$/.test(__ownText(el)))[0];

const nameEl = __deepAll(document, (el) => el.tagName === 'P' && __ownText(el).length > 0
&& !!(el.closest && el.closest('nav')))[0];

const signedOut = /\\bLog in\\b|\\bSign up\\b/i.test(document.body.innerText || '');

// Keep polling while the header is still hydrating; settle as soon
// as we can tell "signed out" from "balance visible".
if (balanceText === null && !signedOut) return null;

return {
balanceText,
planLabel: planEl ? __ownText(planEl) : null,
accountLabel: nameEl ? __ownText(nameEl) : null,
signedOut,
};
})()`, { label: 'Symphony header', timeoutMs: 30000 });

if (!header) {
throw new CommandExecutionError('Page evaluation returned nothing — the tab may have navigated away');
}
if (header.balanceText === null && header.signedOut) {
throw new AuthRequiredError(HOST, 'Log in to Symphony Creative Studio in this browser first');
}
if (header.balanceText === null) {
throw new CommandExecutionError(
'Could not find the credit indicator (ks-icon[name="coin"]) — the header layout may have changed',
);
}

const digits = String(header.balanceText).replace(/[^\d]/g, '');
if (digits === '') {
throw new CommandExecutionError(`Credit indicator was not numeric: "${header.balanceText}"`);
}

return [{
credits: Number(digits),
plan: header.planLabel,
account: header.accountLabel,
}];
},
});
Loading