@aicopycoders/exodus 2026.9.200 → 2026.9.1100-beta.1

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.
@@ -0,0 +1,198 @@
1
+ import { count, dateOrDash, defaultMetaAdsDeps, errorFor, safeText, truncate, } from "../lib/meta-ads.js";
2
+ export const helpText = `
3
+ exodus comments — read the comments on your own synced Meta ads (read-only)
4
+
5
+ The daily Meta sync stores the comments people left on the brand's ads, on both
6
+ Facebook and Instagram. This reads them back. Commenter names and profile links
7
+ are stripped server-side before they are ever stored — you get the words, not
8
+ the person.
9
+
10
+ Usage:
11
+ exodus comments list [--ad <adId>] [--platform x] [--limit n] [--json]
12
+
13
+ Flags:
14
+ --ad <adId> Only this ad's comments (ad ids come from exodus ads list)
15
+ --platform <x> facebook | instagram — one surface only
16
+ --limit <n> How many comments, 1–500 (default 100)
17
+ --json Machine-readable JSON instead of the human list
18
+
19
+ Notes:
20
+ • Newest first.
21
+ • Scopes to your active brand (exodus brand current).
22
+ • Nothing here posts, replies, hides or deletes — reading is all it does.
23
+ • Counts per ad live on \`exodus ads show <adId>\`.
24
+
25
+ Examples:
26
+ exodus comments list
27
+ exodus comments list --ad 120210000000000
28
+ exodus comments list --platform instagram --limit 50
29
+ exodus comments list --ad 120210000000000 --json
30
+ `.trim();
31
+ export const PLATFORMS = ["facebook", "instagram"];
32
+ export const VALUE_FLAGS = new Set(["ad", "platform", "limit"]);
33
+ export const MAX_LIMIT = 500;
34
+ export function parseCommentsFlags(flags) {
35
+ const options = {};
36
+ const ad = flags["ad"];
37
+ if (ad !== undefined) {
38
+ if (typeof ad !== "string" || !ad.trim()) {
39
+ return { ok: false, message: "--ad needs an ad id (see: exodus ads list)" };
40
+ }
41
+ options.adId = ad.trim();
42
+ }
43
+ const platform = flags["platform"];
44
+ if (platform !== undefined) {
45
+ if (typeof platform !== "string" ||
46
+ !PLATFORMS.includes(platform.trim().toLowerCase())) {
47
+ return {
48
+ ok: false,
49
+ message: `--platform must be one of: ${PLATFORMS.join(", ")} (got "${String(platform)}")`,
50
+ };
51
+ }
52
+ options.platform = platform.trim().toLowerCase();
53
+ }
54
+ const limitRaw = flags["limit"];
55
+ if (limitRaw !== undefined) {
56
+ const n = typeof limitRaw === "string" ? Number(limitRaw) : NaN;
57
+ if (!Number.isInteger(n) || n < 1 || n > MAX_LIMIT) {
58
+ return {
59
+ ok: false,
60
+ message: `--limit must be a whole number 1–${MAX_LIMIT} (got "${String(limitRaw)}")`,
61
+ };
62
+ }
63
+ options.limit = n;
64
+ }
65
+ return { ok: true, options };
66
+ }
67
+ export function commentsPath(options) {
68
+ const params = new URLSearchParams();
69
+ if (options.platform)
70
+ params.set("platform", options.platform);
71
+ if (options.limit !== undefined)
72
+ params.set("limit", String(options.limit));
73
+ const qs = params.toString();
74
+ const suffix = qs ? `?${qs}` : "";
75
+ return options.adId
76
+ ? `/api/v2/ads/${encodeURIComponent(options.adId)}/comments${suffix}`
77
+ : `/api/v2/comments${suffix}`;
78
+ }
79
+ const PLATFORM_TAGS = {
80
+ facebook: "fb",
81
+ instagram: "ig",
82
+ };
83
+ export function platformTag(platform) {
84
+ const clean = safeText(platform).toLowerCase();
85
+ if (!clean)
86
+ return "??";
87
+ return PLATFORM_TAGS[clean] ?? clean.slice(0, 2);
88
+ }
89
+ export function commentLines(comment, opts) {
90
+ const meta = [
91
+ `[${platformTag(comment.platform)}]`,
92
+ dateOrDash(comment.createdAt),
93
+ typeof comment.likeCount === "number" && comment.likeCount > 0
94
+ ? `${count(comment.likeCount)} like${comment.likeCount === 1 ? "" : "s"}`
95
+ : null,
96
+ comment.parentId ? "reply" : null,
97
+ opts.showAdId && comment.adId ? `ad ${safeText(comment.adId)}` : null,
98
+ ]
99
+ .filter(Boolean)
100
+ .join(" · ");
101
+ const text = truncate(comment.text, 400) || "(no text — media-only comment)";
102
+ return [` ${meta}`, ` ${text}`];
103
+ }
104
+ export function formatComments(data, options) {
105
+ const comments = Array.isArray(data.comments) ? data.comments : [];
106
+ const scope = options.adId ? `ad ${options.adId}` : "this brand's ads";
107
+ const surface = options.platform ? ` on ${options.platform}` : "";
108
+ if (comments.length === 0) {
109
+ return [
110
+ `No stored comments for ${scope}${surface}.`,
111
+ "The daily Meta sync stores comments as it finds them — an ad with no comments yet, or one synced before comments landed, shows nothing here.",
112
+ ];
113
+ }
114
+ const total = typeof data.count === "number" ? data.count : comments.length;
115
+ const lines = [
116
+ `Comments (${comments.length}${total > comments.length ? ` of ${total}` : ""}) — ${scope}${surface}, newest first`,
117
+ "",
118
+ ];
119
+ const showAdId = !options.adId;
120
+ for (const comment of comments) {
121
+ lines.push(...commentLines(comment, { showAdId }));
122
+ }
123
+ lines.push("");
124
+ lines.push("Commenter names and links are stripped before storage — the words are all Exodus keeps.");
125
+ if (total > comments.length) {
126
+ lines.push(`Raise --limit (max ${MAX_LIMIT}) to see more.`);
127
+ }
128
+ return lines;
129
+ }
130
+ export async function listFlow(options, json, deps) {
131
+ const res = await deps.get(commentsPath(options));
132
+ if (!res.ok && res.status === 404 && options.adId && !json) {
133
+ const laddered = errorFor(res, "comments list", json, deps, (id) => `exodus comments list --account ${id}`);
134
+ if (laddered && laddered.lines[0]?.startsWith("this server does not support")) {
135
+ return laddered;
136
+ }
137
+ return {
138
+ code: 1,
139
+ lines: [
140
+ `No synced ad matches "${options.adId}".`,
141
+ "Ad ids come from `exodus ads list`.",
142
+ ],
143
+ };
144
+ }
145
+ const err = errorFor(res, "comments list", json, deps, (id) => `exodus ads list --account ${id}`);
146
+ if (err)
147
+ return err;
148
+ const data = (res.data ?? {});
149
+ if (json)
150
+ return { code: 0, lines: [JSON.stringify(data)] };
151
+ return { code: 0, lines: formatComments(data, options) };
152
+ }
153
+ export function parsePositional(args = process.argv.slice(3)) {
154
+ const out = [];
155
+ let i = 0;
156
+ while (i < args.length) {
157
+ const arg = args[i];
158
+ if (arg.startsWith("--")) {
159
+ const key = arg.slice(2).split("=", 1)[0] ?? "";
160
+ if (!arg.includes("=") && VALUE_FLAGS.has(key))
161
+ i += 2;
162
+ else
163
+ i++;
164
+ continue;
165
+ }
166
+ out.push(arg);
167
+ i++;
168
+ }
169
+ return out;
170
+ }
171
+ function printResult(result) {
172
+ for (const line of result.lines)
173
+ console.log(line);
174
+ for (const warning of result.warnings ?? [])
175
+ console.error(warning);
176
+ if (result.code !== 0)
177
+ process.exit(result.code);
178
+ }
179
+ export async function run(flags) {
180
+ const [sub] = parsePositional();
181
+ const json = flags["json"] === true;
182
+ if (!sub || sub === "help") {
183
+ console.log(helpText);
184
+ return;
185
+ }
186
+ if (sub === "list") {
187
+ const parsed = parseCommentsFlags(flags);
188
+ if (!parsed.ok) {
189
+ console.error(`Error: ${parsed.message}`);
190
+ console.log("Usage: exodus comments list [--ad <adId>] [--platform x] [--limit n] [--json]");
191
+ process.exit(1);
192
+ }
193
+ return printResult(await listFlow(parsed.options, json, defaultMetaAdsDeps));
194
+ }
195
+ console.error(`Unknown subcommand: "${sub}"\n`);
196
+ console.log(helpText);
197
+ process.exit(1);
198
+ }
@@ -0,0 +1,213 @@
1
+ import { type ApiResponse } from "../lib/client.js";
2
+ export declare const helpText: string;
3
+ export interface ClipWord {
4
+ w: string;
5
+ s: number;
6
+ e: number;
7
+ }
8
+ export interface ClipQc {
9
+ verdict: "pass" | "fail";
10
+ attempts: number;
11
+ }
12
+ export interface ClipFinding {
13
+ check: string;
14
+ code: string;
15
+ severity: "fail" | "warn";
16
+ detail: string;
17
+ }
18
+ export type ArtifactSubset = {
19
+ type: "storyboard";
20
+ storyboard?: unknown;
21
+ storyboardJson?: string;
22
+ } | {
23
+ type: "frames";
24
+ frames?: Array<{
25
+ sceneIndex: number;
26
+ imageUrl?: string;
27
+ }>;
28
+ } | {
29
+ type: "image";
30
+ imageUrl?: string;
31
+ } | {
32
+ type: "video";
33
+ sceneIndex?: number;
34
+ videoUrl?: string;
35
+ durationSec?: number;
36
+ words?: ClipWord[];
37
+ qc?: ClipQc;
38
+ final?: boolean;
39
+ } | {
40
+ type: "audio";
41
+ sceneIndex?: number;
42
+ audioUrl?: string;
43
+ durationSec?: number;
44
+ } | {
45
+ type: "text" | "primer" | "session" | "document";
46
+ };
47
+ export interface VideoRunNode {
48
+ nodeId: string;
49
+ kind: string;
50
+ status: "idle" | "running" | "done" | "failed" | "skipped";
51
+ error?: string;
52
+ outputs?: ArtifactSubset[];
53
+ }
54
+ export type BuilderPauseReason = "taste" | "repair" | "slots" | "call" | "checkpoint";
55
+ export interface VideoRun {
56
+ _id: string;
57
+ status: string;
58
+ isTerminal: boolean;
59
+ error?: string;
60
+ pauseReason?: BuilderPauseReason;
61
+ pausedNodeId?: string;
62
+ nodes: VideoRunNode[];
63
+ }
64
+ export declare function asVideoRun(data: unknown): VideoRun;
65
+ export interface NodeItem {
66
+ nodeId: string;
67
+ sceneIndex: number;
68
+ itemKind: string;
69
+ status: string;
70
+ error?: string;
71
+ attempt?: number;
72
+ flagged?: boolean;
73
+ findings?: ClipFinding[];
74
+ }
75
+ export interface ShowRow {
76
+ id: string;
77
+ name: string;
78
+ status?: string;
79
+ medium?: string;
80
+ styleSlug?: string;
81
+ ready?: boolean;
82
+ setupProgress?: {
83
+ set?: boolean;
84
+ cast?: boolean;
85
+ voices?: boolean;
86
+ };
87
+ }
88
+ export interface FlowResult {
89
+ code: number;
90
+ lines: string[];
91
+ }
92
+ export interface VideoDeps {
93
+ get: (path: string) => Promise<ApiResponse<unknown>>;
94
+ post: (path: string, body: unknown) => Promise<ApiResponse<unknown>>;
95
+ mkdirp: (dir: string) => void;
96
+ writeFile: (filePath: string, text: string) => void;
97
+ downloadToFile: (url: string, filePath: string) => Promise<void>;
98
+ readFile: (filePath: string) => string;
99
+ readFileBytes: (filePath: string) => Uint8Array;
100
+ statFile: (filePath: string) => {
101
+ size: number;
102
+ } | null;
103
+ uploadBytes: (uploadUrl: string, contentType: string, bytes: Uint8Array) => Promise<{
104
+ ok: boolean;
105
+ status: number;
106
+ storageId?: string;
107
+ body?: string;
108
+ }>;
109
+ probeDurationSec: (filePath: string) => number | null;
110
+ sleep: (ms: number) => Promise<void>;
111
+ now: () => number;
112
+ dashboardUrl: string;
113
+ }
114
+ export declare const defaultDeps: VideoDeps;
115
+ export declare const VIDEO_NOT_FOUND_MESSAGE: string;
116
+ export declare function videoApiError(res: ApiResponse<unknown>): string;
117
+ export type RunStop = {
118
+ at: "running";
119
+ stage: string;
120
+ } | {
121
+ at: "storyboard-gate";
122
+ nodeId?: string;
123
+ } | {
124
+ at: "final-watch";
125
+ } | {
126
+ at: "paused";
127
+ nodeId?: string;
128
+ reason?: string;
129
+ } | {
130
+ at: "failed";
131
+ error?: string;
132
+ } | {
133
+ at: "finished";
134
+ status: string;
135
+ };
136
+ export declare function classifyRun(run: VideoRun): RunStop;
137
+ export declare function stageWord(stage: string): string;
138
+ export declare function stopLines(stop: RunStop, runId: string, dashboardUrl: string): string[];
139
+ export interface PullDownload {
140
+ file: string;
141
+ url: string;
142
+ }
143
+ export interface PullTextFile {
144
+ file: string;
145
+ body: string;
146
+ }
147
+ export interface ManifestScene {
148
+ sceneIndex: number;
149
+ durationSec: number | null;
150
+ clip: string | null;
151
+ words: string | null;
152
+ voice: string | null;
153
+ keyframe: string | null;
154
+ qc: ClipQc | null;
155
+ clipStatus: string;
156
+ error: string | null;
157
+ flagged: boolean;
158
+ findings: ClipFinding[];
159
+ }
160
+ export interface PullFailure {
161
+ file: string;
162
+ url: string;
163
+ error: string;
164
+ }
165
+ export interface VideoManifest {
166
+ runId: string;
167
+ pulledAt: string;
168
+ dashboardUrl: string;
169
+ storyboard: string | null;
170
+ reference: string | null;
171
+ music: string | null;
172
+ scenes: ManifestScene[];
173
+ failed: PullFailure[];
174
+ }
175
+ export interface PullPlan {
176
+ downloads: PullDownload[];
177
+ texts: PullTextFile[];
178
+ manifest: VideoManifest;
179
+ }
180
+ export declare function extensionFromUrl(url: string): string | undefined;
181
+ export declare function scenePrefix(sceneIndex: number): string;
182
+ export declare function planPull(run: VideoRun, items: NodeItem[], opts: {
183
+ pulledAt: string;
184
+ dashboardUrl: string;
185
+ }): PullPlan;
186
+ export declare function markPullFailure(manifest: VideoManifest, failure: PullFailure): void;
187
+ export declare function missingSetupLabels(progress: ShowRow["setupProgress"]): string[];
188
+ export declare function showsFlow(json: boolean, deps: VideoDeps): Promise<FlowResult>;
189
+ export interface StartOptions {
190
+ showId: string;
191
+ scriptFile: string;
192
+ voicePath?: string;
193
+ music?: boolean;
194
+ wait: boolean;
195
+ json: boolean;
196
+ }
197
+ export declare function startFlow(opts: StartOptions, deps: VideoDeps): Promise<FlowResult>;
198
+ export declare function waitFlow(runId: string, opts: {
199
+ json: boolean;
200
+ url?: string;
201
+ intervalMs?: number;
202
+ maxPolls?: number;
203
+ }, deps: VideoDeps): Promise<FlowResult>;
204
+ export declare function statusFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
205
+ export declare function storyboardFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
206
+ export declare function approveFlow(runId: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
207
+ export declare function flagFlow(runId: string, note: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
208
+ export declare function pullFlow(runId: string, dir: string, json: boolean, deps: VideoDeps): Promise<FlowResult>;
209
+ export declare function parseMvhdDurationSec(bytes: Uint8Array): number | null;
210
+ export declare const NO_DURATION_MESSAGE: string;
211
+ export declare function uploadFlow(runId: string, filePath: string, durationFlag: string | undefined, json: boolean, deps: VideoDeps): Promise<FlowResult>;
212
+ export declare function parsePositional(args?: string[]): string[];
213
+ export declare function run(flags: Record<string, string | boolean>): Promise<void>;