@spira-lab/cli 0.0.9 → 0.0.11
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/README.md +8 -0
- package/dist/index.cjs +211 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Command line client for Spira.
|
|
4
4
|
|
|
5
|
+
> ⚠️ **Database feature is deprecated.** The Brand Machine "Database" subsystem
|
|
6
|
+
> has been retired from the product. The `spira dataBase glob | read | grep |
|
|
7
|
+
> search` commands still exist for backward compatibility only — do NOT build
|
|
8
|
+
> new workflows against them. Use the Brand Machine web dashboard or the
|
|
9
|
+
> per-user cc-engine workspace (`spira workspace` / dashboard Workspace tab)
|
|
10
|
+
> instead. The bundled `spira-database` skill has been removed so agents are
|
|
11
|
+
> no longer guided toward these commands.
|
|
12
|
+
|
|
5
13
|
## Install
|
|
6
14
|
|
|
7
15
|
```bash
|
package/dist/index.cjs
CHANGED
|
@@ -3457,7 +3457,7 @@ var require_commander = __commonJS({
|
|
|
3457
3457
|
});
|
|
3458
3458
|
|
|
3459
3459
|
// src/program.ts
|
|
3460
|
-
var
|
|
3460
|
+
var import_promises5 = require("node:fs/promises");
|
|
3461
3461
|
|
|
3462
3462
|
// ../../node_modules/.pnpm/commander@14.0.3/node_modules/commander/esm.mjs
|
|
3463
3463
|
var import_index = __toESM(require_commander(), 1);
|
|
@@ -18060,6 +18060,8 @@ var cliResultTypeSchema = external_exports.enum([
|
|
|
18060
18060
|
"scheduleOccurrenceList",
|
|
18061
18061
|
"scheduleExecutionList",
|
|
18062
18062
|
"scheduleAction",
|
|
18063
|
+
"mediaInfo",
|
|
18064
|
+
"mediaAsset",
|
|
18063
18065
|
"dataBaseGlob",
|
|
18064
18066
|
"dataBaseRead",
|
|
18065
18067
|
"dataBaseGrep",
|
|
@@ -20407,7 +20409,7 @@ function getCliAiModelDoc(modelId) {
|
|
|
20407
20409
|
// src/ai/wait.ts
|
|
20408
20410
|
var DEFAULT_WAIT_COMMAND = "spira ai jobs create";
|
|
20409
20411
|
function wait(durationMs) {
|
|
20410
|
-
return new Promise((
|
|
20412
|
+
return new Promise((resolve2) => setTimeout(resolve2, durationMs));
|
|
20411
20413
|
}
|
|
20412
20414
|
function jobStatus(result) {
|
|
20413
20415
|
if (!result.data || typeof result.data !== "object") return void 0;
|
|
@@ -21340,6 +21342,83 @@ var commandDefinitions = [
|
|
|
21340
21342
|
examples: ["spira schedules calendar --from 1781884800000 --to 1782490000000"],
|
|
21341
21343
|
relatedCommands: ["spira schedules get <scheduleId>"]
|
|
21342
21344
|
},
|
|
21345
|
+
{
|
|
21346
|
+
path: ["media", "info"],
|
|
21347
|
+
purpose: "Resolve a TikTok / YouTube / Instagram / X / Facebook link and show its metadata.",
|
|
21348
|
+
arguments: [{ name: "url", description: "Public post or video URL.", required: true }],
|
|
21349
|
+
http: {
|
|
21350
|
+
method: "POST",
|
|
21351
|
+
path: "/api/tools/media/info",
|
|
21352
|
+
authRequired: true,
|
|
21353
|
+
body: [{ name: "url", source: "argument", from: "url" }]
|
|
21354
|
+
},
|
|
21355
|
+
resultType: "mediaInfo",
|
|
21356
|
+
examples: [
|
|
21357
|
+
"spira media info https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
21358
|
+
"spira media info https://www.tiktok.com/@user/video/123 --json"
|
|
21359
|
+
],
|
|
21360
|
+
relatedCommands: ["spira media video <url>", "spira media thumbnail <url>", "spira credits balance"]
|
|
21361
|
+
},
|
|
21362
|
+
{
|
|
21363
|
+
path: ["media", "video"],
|
|
21364
|
+
purpose: "Download the video file behind a social link.",
|
|
21365
|
+
arguments: [{ name: "url", description: "Public post or video URL.", required: true }],
|
|
21366
|
+
options: [
|
|
21367
|
+
{
|
|
21368
|
+
name: "formatId",
|
|
21369
|
+
flag: "--format-id <id>",
|
|
21370
|
+
description: "A format_id from `spira media info`. Omit for the best rung that plays everywhere. Every rung is delivered with audio.",
|
|
21371
|
+
type: "string"
|
|
21372
|
+
},
|
|
21373
|
+
{
|
|
21374
|
+
name: "output",
|
|
21375
|
+
flag: "--output <path>",
|
|
21376
|
+
description: "Also save a local copy from the returned URL.",
|
|
21377
|
+
type: "string"
|
|
21378
|
+
}
|
|
21379
|
+
],
|
|
21380
|
+
http: {
|
|
21381
|
+
method: "POST",
|
|
21382
|
+
path: "/api/tools/media/video",
|
|
21383
|
+
authRequired: true,
|
|
21384
|
+
body: [
|
|
21385
|
+
{ name: "url", source: "argument", from: "url" },
|
|
21386
|
+
{ name: "formatId", source: "option", from: "formatId" }
|
|
21387
|
+
]
|
|
21388
|
+
},
|
|
21389
|
+
resultType: "mediaAsset",
|
|
21390
|
+
examples: [
|
|
21391
|
+
"spira media video https://www.tiktok.com/@user/video/123",
|
|
21392
|
+
"spira media video https://www.youtube.com/watch?v=dQw4w9WgXcQ --output ./downloads/",
|
|
21393
|
+
"spira media video https://www.tiktok.com/@user/video/123 --format-id bytevc1_1080p_612574-0"
|
|
21394
|
+
],
|
|
21395
|
+
relatedCommands: ["spira media info <url>", "spira media thumbnail <url>"]
|
|
21396
|
+
},
|
|
21397
|
+
{
|
|
21398
|
+
path: ["media", "thumbnail"],
|
|
21399
|
+
purpose: "Download the cover image behind a social link.",
|
|
21400
|
+
arguments: [{ name: "url", description: "Public post or video URL.", required: true }],
|
|
21401
|
+
options: [
|
|
21402
|
+
{
|
|
21403
|
+
name: "output",
|
|
21404
|
+
flag: "--output <path>",
|
|
21405
|
+
description: "Also save a local copy from the returned URL.",
|
|
21406
|
+
type: "string"
|
|
21407
|
+
}
|
|
21408
|
+
],
|
|
21409
|
+
http: {
|
|
21410
|
+
method: "POST",
|
|
21411
|
+
path: "/api/tools/media/thumbnail",
|
|
21412
|
+
authRequired: true,
|
|
21413
|
+
body: [{ name: "url", source: "argument", from: "url" }]
|
|
21414
|
+
},
|
|
21415
|
+
resultType: "mediaAsset",
|
|
21416
|
+
examples: [
|
|
21417
|
+
"spira media thumbnail https://x.com/user/status/123",
|
|
21418
|
+
"spira media thumbnail https://www.facebook.com/watch/?v=123 --output ./cover.jpg"
|
|
21419
|
+
],
|
|
21420
|
+
relatedCommands: ["spira media info <url>", "spira media video <url>"]
|
|
21421
|
+
},
|
|
21343
21422
|
{
|
|
21344
21423
|
path: ["dataBase", "glob"],
|
|
21345
21424
|
purpose: "List files and folders in your Brand database matching a glob pattern.",
|
|
@@ -21871,6 +21950,69 @@ async function executeHttpCommand(input) {
|
|
|
21871
21950
|
return successResult(input.command, response.body, response.requestId);
|
|
21872
21951
|
}
|
|
21873
21952
|
|
|
21953
|
+
// src/media/save.ts
|
|
21954
|
+
var import_node_fs = require("node:fs");
|
|
21955
|
+
var import_promises3 = require("node:fs/promises");
|
|
21956
|
+
var import_node_path3 = require("node:path");
|
|
21957
|
+
var import_node_stream = require("node:stream");
|
|
21958
|
+
var import_promises4 = require("node:stream/promises");
|
|
21959
|
+
function assetRecord(data) {
|
|
21960
|
+
if (!data || typeof data !== "object") return null;
|
|
21961
|
+
const asset = data.asset;
|
|
21962
|
+
return asset && typeof asset === "object" ? asset : null;
|
|
21963
|
+
}
|
|
21964
|
+
function fileNameFor(asset) {
|
|
21965
|
+
const key = typeof asset.key === "string" ? (0, import_node_path3.basename)(asset.key) : "";
|
|
21966
|
+
if (key) return key;
|
|
21967
|
+
const fromUrl = typeof asset.url === "string" ? (0, import_node_path3.basename)(new URL(asset.url).pathname) : "";
|
|
21968
|
+
return fromUrl || "download";
|
|
21969
|
+
}
|
|
21970
|
+
async function isDirectory(path) {
|
|
21971
|
+
return (0, import_promises3.stat)(path).then((entry) => entry.isDirectory()).catch(() => false);
|
|
21972
|
+
}
|
|
21973
|
+
async function resolveTargetPath(output, asset) {
|
|
21974
|
+
const target = (0, import_node_path3.resolve)(output);
|
|
21975
|
+
const looksLikeDirectory = /[\\/]$/.test(output) || await isDirectory(target);
|
|
21976
|
+
return looksLikeDirectory ? (0, import_node_path3.join)(target, fileNameFor(asset)) : target;
|
|
21977
|
+
}
|
|
21978
|
+
async function writeAssetToDisk(url2, targetPath) {
|
|
21979
|
+
const response = await fetch(url2);
|
|
21980
|
+
if (!response.ok || !response.body) {
|
|
21981
|
+
throw new Error(`Download failed with status ${response.status}.`);
|
|
21982
|
+
}
|
|
21983
|
+
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(targetPath), { recursive: true });
|
|
21984
|
+
await (0, import_promises4.pipeline)(
|
|
21985
|
+
import_node_stream.Readable.fromWeb(response.body),
|
|
21986
|
+
(0, import_node_fs.createWriteStream)(targetPath)
|
|
21987
|
+
);
|
|
21988
|
+
}
|
|
21989
|
+
async function saveMediaAsset(result, output) {
|
|
21990
|
+
if (result.status !== "success") return result;
|
|
21991
|
+
const asset = assetRecord(result.data);
|
|
21992
|
+
const url2 = asset && typeof asset.url === "string" ? asset.url : null;
|
|
21993
|
+
if (!asset || !url2) {
|
|
21994
|
+
return cliResultSchema.parse({
|
|
21995
|
+
...result,
|
|
21996
|
+
warnings: [...result.warnings, "Response contained no downloadable asset URL; nothing was saved locally."]
|
|
21997
|
+
});
|
|
21998
|
+
}
|
|
21999
|
+
try {
|
|
22000
|
+
const targetPath = await resolveTargetPath(output, asset);
|
|
22001
|
+
await writeAssetToDisk(url2, targetPath);
|
|
22002
|
+
return cliResultSchema.parse({
|
|
22003
|
+
...result,
|
|
22004
|
+
data: { ...result.data, asset: { ...asset, savedTo: targetPath } },
|
|
22005
|
+
impact: [...result.impact, `Saved to ${targetPath}`]
|
|
22006
|
+
});
|
|
22007
|
+
} catch (error51) {
|
|
22008
|
+
const message = error51 instanceof Error && error51.message ? error51.message : "Unknown error.";
|
|
22009
|
+
return cliResultSchema.parse({
|
|
22010
|
+
...result,
|
|
22011
|
+
warnings: [...result.warnings, `Could not save the file locally: ${message} The asset URL is still available.`]
|
|
22012
|
+
});
|
|
22013
|
+
}
|
|
22014
|
+
}
|
|
22015
|
+
|
|
21874
22016
|
// src/auth/login.ts
|
|
21875
22017
|
var import_node_crypto = require("node:crypto");
|
|
21876
22018
|
|
|
@@ -21884,19 +22026,19 @@ function openBrowser(url2) {
|
|
|
21884
22026
|
child.unref();
|
|
21885
22027
|
}
|
|
21886
22028
|
function closeServer(server) {
|
|
21887
|
-
return new Promise((
|
|
22029
|
+
return new Promise((resolve2) => server.close(() => resolve2()));
|
|
21888
22030
|
}
|
|
21889
22031
|
function startLoopbackCallbackServer(input) {
|
|
21890
22032
|
const server = (0, import_node_http.createServer)();
|
|
21891
22033
|
let timer = null;
|
|
21892
22034
|
let resolveCallback = null;
|
|
21893
22035
|
let rejectCallback = null;
|
|
21894
|
-
const callback = new Promise((
|
|
21895
|
-
resolveCallback =
|
|
22036
|
+
const callback = new Promise((resolve2, reject) => {
|
|
22037
|
+
resolveCallback = resolve2;
|
|
21896
22038
|
rejectCallback = reject;
|
|
21897
22039
|
});
|
|
21898
22040
|
const ready = new Promise(
|
|
21899
|
-
(
|
|
22041
|
+
(resolve2, reject) => {
|
|
21900
22042
|
timer = setTimeout(() => {
|
|
21901
22043
|
rejectCallback?.(new Error("Timed out waiting for browser callback."));
|
|
21902
22044
|
void closeServer(server);
|
|
@@ -21923,7 +22065,7 @@ function startLoopbackCallbackServer(input) {
|
|
|
21923
22065
|
reject(new Error("Failed to start local CLI callback server."));
|
|
21924
22066
|
return;
|
|
21925
22067
|
}
|
|
21926
|
-
|
|
22068
|
+
resolve2({
|
|
21927
22069
|
redirectUri: `http://127.0.0.1:${address.port}/callback`,
|
|
21928
22070
|
waitForCallback: () => callback,
|
|
21929
22071
|
close: () => closeServer(server)
|
|
@@ -22387,6 +22529,56 @@ function formatAnchor(anchor) {
|
|
|
22387
22529
|
}
|
|
22388
22530
|
return parts.join(", ") || "-";
|
|
22389
22531
|
}
|
|
22532
|
+
function formatMediaDuration(value) {
|
|
22533
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return "-";
|
|
22534
|
+
const total = Math.round(value);
|
|
22535
|
+
const hours = Math.floor(total / 3600);
|
|
22536
|
+
const minutes = Math.floor(total % 3600 / 60);
|
|
22537
|
+
const seconds = total % 60;
|
|
22538
|
+
const padded = `${String(minutes).padStart(hours ? 2 : 1, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
22539
|
+
return hours ? `${hours}:${padded}` : padded;
|
|
22540
|
+
}
|
|
22541
|
+
function renderMediaInfo(data) {
|
|
22542
|
+
const info = objectRecord2(objectRecord2(data).info);
|
|
22543
|
+
const options = Array.isArray(info.options) ? info.options.map(objectRecord2) : [];
|
|
22544
|
+
const lines = [
|
|
22545
|
+
`platform: ${valueToText(info.platform)}`,
|
|
22546
|
+
`title: ${compactText(info.title)}`,
|
|
22547
|
+
`uploader: ${valueToText(info.uploader ?? info.channel)}`,
|
|
22548
|
+
`duration: ${formatMediaDuration(info.duration)}`,
|
|
22549
|
+
`views: ${valueToText(info.view_count)}`,
|
|
22550
|
+
`likes: ${valueToText(info.like_count)}`,
|
|
22551
|
+
`uploadDate: ${valueToText(info.upload_date)}`,
|
|
22552
|
+
`url: ${valueToText(info.webpage_url ?? info.url)}`,
|
|
22553
|
+
`suggestedFilename: ${valueToText(info.suggested_filename)}`
|
|
22554
|
+
];
|
|
22555
|
+
if (options.length === 0) return `${lines.join("\n")}
|
|
22556
|
+
`;
|
|
22557
|
+
const rows = options.map((option) => ({
|
|
22558
|
+
format_id: option.format_id,
|
|
22559
|
+
resolution: option.label,
|
|
22560
|
+
codec: option.codec,
|
|
22561
|
+
fps: option.fps,
|
|
22562
|
+
size: formatByteSize(option.filesize),
|
|
22563
|
+
plays: option.compatible === true ? "anywhere" : "limited",
|
|
22564
|
+
audio: option.audio_source
|
|
22565
|
+
}));
|
|
22566
|
+
return `${lines.join("\n")}
|
|
22567
|
+
downloadOptions (${rows.length}):
|
|
22568
|
+
${renderRows(rows, ["format_id", "resolution", "codec", "fps", "size", "plays", "audio"])}`;
|
|
22569
|
+
}
|
|
22570
|
+
function renderMediaAsset(data) {
|
|
22571
|
+
const asset = objectRecord2(objectRecord2(data).asset);
|
|
22572
|
+
const lines = [
|
|
22573
|
+
`title: ${compactText(asset.title)}`,
|
|
22574
|
+
`size: ${formatByteSize(asset.size)}`,
|
|
22575
|
+
`contentType: ${valueToText(asset.contentType)}`,
|
|
22576
|
+
`url: ${valueToText(asset.url)}`
|
|
22577
|
+
];
|
|
22578
|
+
if (asset.savedTo) lines.push(`savedTo: ${valueToText(asset.savedTo)}`);
|
|
22579
|
+
return `${lines.join("\n")}
|
|
22580
|
+
`;
|
|
22581
|
+
}
|
|
22390
22582
|
function renderDataBaseGlob(data) {
|
|
22391
22583
|
const record2 = objectRecord2(data);
|
|
22392
22584
|
const pattern = valueToText(record2.pattern);
|
|
@@ -22531,6 +22723,10 @@ function renderSuccess(result) {
|
|
|
22531
22723
|
"startedAt",
|
|
22532
22724
|
"finishedAt"
|
|
22533
22725
|
]);
|
|
22726
|
+
case "mediaInfo":
|
|
22727
|
+
return renderMediaInfo(result.data);
|
|
22728
|
+
case "mediaAsset":
|
|
22729
|
+
return renderMediaAsset(result.data);
|
|
22534
22730
|
case "dataBaseGlob":
|
|
22535
22731
|
return renderDataBaseGlob(result.data);
|
|
22536
22732
|
case "dataBaseRead":
|
|
@@ -22868,12 +23064,12 @@ function renderCommandIndex() {
|
|
|
22868
23064
|
`;
|
|
22869
23065
|
}
|
|
22870
23066
|
async function packageVersion() {
|
|
22871
|
-
if ("0.0.
|
|
22872
|
-
return "0.0.
|
|
23067
|
+
if ("0.0.11".length > 0) {
|
|
23068
|
+
return "0.0.11";
|
|
22873
23069
|
}
|
|
22874
23070
|
try {
|
|
22875
23071
|
const packageUrl = new URL("../package.json", import_meta.url);
|
|
22876
|
-
const pkg = JSON.parse(await (0,
|
|
23072
|
+
const pkg = JSON.parse(await (0, import_promises5.readFile)(packageUrl, "utf8"));
|
|
22877
23073
|
return pkg.version ?? "0.0.0";
|
|
22878
23074
|
} catch {
|
|
22879
23075
|
return "0.0.0";
|
|
@@ -23090,6 +23286,11 @@ ${url2}
|
|
|
23090
23286
|
poll: () => executeHttpCommand({ command: statusCommand, args: { jobId }, options: {} })
|
|
23091
23287
|
});
|
|
23092
23288
|
}
|
|
23289
|
+
if (commandKey(definition.path) === "media video" || commandKey(definition.path) === "media thumbnail") {
|
|
23290
|
+
const downloaded = await executeHttpCommand({ command: definition, args, options });
|
|
23291
|
+
if (typeof options.output !== "string" || options.output.length === 0) return downloaded;
|
|
23292
|
+
return saveMediaAsset(downloaded, options.output);
|
|
23293
|
+
}
|
|
23093
23294
|
return executeHttpCommand({ command: definition, args, options });
|
|
23094
23295
|
}
|
|
23095
23296
|
function addLeafCommand(root, definition, output) {
|