@mevdragon/vidfarm-devcli 0.17.0 → 0.18.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/dist/src/app.js CHANGED
@@ -30,7 +30,7 @@ import { renderReskinLibrary } from "./reskin/library-page.js";
30
30
  import { renderReskinDiscover } from "./reskin/discover-page.js";
31
31
  import { renderReskinChat } from "./reskin/chat-page.js";
32
32
  import { renderReskinCalendar } from "./reskin/calendar-page.js";
33
- import { renderReskinJobRuns } from "./reskin/job-runs-page.js";
33
+ import { renderReskinInpaint } from "./reskin/inpaint-page.js";
34
34
  import { renderReskinAgency } from "./reskin/agency-page.js";
35
35
  import { renderReskinPricing } from "./reskin/pricing-page.js";
36
36
  import { renderReskinLogin } from "./reskin/login-page.js";
@@ -101,7 +101,10 @@ const COMPOSITIONS_PREFIX = `${API_PREFIX}/compositions`;
101
101
  const INSPIRATIONS_PREFIX = `${API_PREFIX}/inspirations`;
102
102
  const VIDEOS_PREFIX = `${API_PREFIX}/videos`;
103
103
  const PRIMITIVES_PREFIX = `${API_PREFIX}/primitives`;
104
- const CLIPS_PREFIX = "/clips";
104
+ const RAWS_PREFIX = "/raws";
105
+ // Back-compat: the clip library was renamed "raws". Old /clips callers are
106
+ // transparently redispatched to /raws (see the alias registered below).
107
+ const CLIPS_COMPAT_PREFIX = "/clips";
105
108
  // Temp folder ("My Files → temporary") hard-TTL. Keep in sync with the
106
109
  // tag-scoped S3 lifecycle rule (30 days) in the CDK stacks and the clip-scan
107
110
  // Lambda's TEMP_SOURCE_TTL_DAYS — records, sweeps, and object lifecycle must
@@ -3259,11 +3262,11 @@ async function buildClipsEditorChatBoot(input) {
3259
3262
  const directorSkill = readRootSkillFile("SKILL.director.md", "SKILL.user.md") || null;
3260
3263
  const availableProviders = (await normalizeEditorChatProviderRows(input.customer.id)).map((entry) => entry.provider);
3261
3264
  const docsRoutes = [
3262
- { method: "GET", path: `${CLIPS_PREFIX}/feed`, summary: "List the caller's reusable clip library (optional ?q, ?source, ?limit)." },
3263
- { method: "POST", path: `${CLIPS_PREFIX}/search`, summary: "Hybrid structured + semantic clip search. Body is { query } (natural language) or { criteria } (structured filter)." },
3264
- { method: "POST", path: `${CLIPS_PREFIX}/scan`, summary: "Import a source video and mine it into clips. Body accepts source_url (a public/durable video URL), raw_s3_uri, or s3_key, plus an optional prompt describing which scenes to keep (e.g. 'people with food, no on-screen text, vertical, 5-10s')." },
3265
- { method: "GET", path: `${CLIPS_PREFIX}/scan/:scanId`, summary: "Poll clip-scan status for an in-progress import." },
3266
- { method: "GET", path: `${CLIPS_PREFIX}/presets`, summary: "List the caller's saved clip-search presets." }
3265
+ { method: "GET", path: `${RAWS_PREFIX}/feed`, summary: "List the caller's reusable clip library (optional ?q, ?source, ?limit)." },
3266
+ { method: "POST", path: `${RAWS_PREFIX}/search`, summary: "Hybrid structured + semantic clip search. Body is { query } (natural language) or { criteria } (structured filter)." },
3267
+ { method: "POST", path: `${RAWS_PREFIX}/scan`, summary: "Import a source video and mine it into clips. Body accepts source_url (a public/durable video URL), raw_s3_uri, or s3_key, plus an optional prompt describing which scenes to keep (e.g. 'people with food, no on-screen text, vertical, 5-10s')." },
3268
+ { method: "GET", path: `${RAWS_PREFIX}/scan/:scanId`, summary: "Poll clip-scan status for an in-progress import." },
3269
+ { method: "GET", path: `${RAWS_PREFIX}/presets`, summary: "List the caller's saved clip-search presets." }
3267
3270
  ];
3268
3271
  return {
3269
3272
  apiUrl: resolveEditorChatApiUrl(),
@@ -3627,7 +3630,7 @@ function normalizeEditorChatPath(input) {
3627
3630
  // and NEVER runs GhostCut on the long-form source — "no text" is a
3628
3631
  // scene-selection filter; caption removal stays a per-finished-clip
3629
3632
  // primitive (/api/v1/primitives/videos/remove-captions).
3630
- CLIPS_PREFIX,
3633
+ RAWS_PREFIX,
3631
3634
  // Public skill knowledge: the vendored skill packs (load_skill fetches
3632
3635
  // these too) and per-template SKILL.md manifests. Read-only markdown.
3633
3636
  "/skill/",
@@ -4911,27 +4914,268 @@ app.get("/", async (c) => {
4911
4914
  // Each page is a standalone document with its own CSS; renders sample data so
4912
4915
  // it's viewable without auth. Ported pages are wired here one at a time.
4913
4916
  app.get("/reskin", (c) => c.html(renderReskinIndex()));
4914
- app.get("/reskin/settings", (c) => c.html(renderReskinSettings()));
4917
+ app.get("/settings", async (c) => {
4918
+ const customer = await requireBrowserCustomer(c);
4919
+ if (!customer) {
4920
+ setLoginReturnPath(c, currentPathWithSearch(c));
4921
+ return redirect(c, "/login");
4922
+ }
4923
+ const flash = consumeSettingsFlash(c);
4924
+ const flockPosterApiKey = customer.flockposterApiKey?.trim() || null;
4925
+ const activeTab = flash?.tab === "wallet" || flash?.tab === "channels" || flash?.tab === "developer"
4926
+ ? flash.tab
4927
+ : c.req.query("tab") === "wallet" || c.req.query("tab") === "channels" || c.req.query("tab") === "developer"
4928
+ ? c.req.query("tab")
4929
+ : "profile";
4930
+ const directorSkill = readRootSkillFile("SKILL.director.md", "SKILL.user.md");
4931
+ const emailChannels = await getEmailPseudoChannels(customer.id, c);
4932
+ const [wallet, walletEvents] = await Promise.all([
4933
+ fetchBillingWalletSummary(customer.id).catch((error) => ({
4934
+ balanceUsd: 0, totalFundsAddedUsd: 0, totalChargeUsd: 0, totalBaseCostUsd: 0,
4935
+ totalEventCount: 0, updatedAtMs: 0,
4936
+ unavailableReason: error instanceof Error ? error.message : "Unable to load wallet."
4937
+ })),
4938
+ fetchBillingWalletEvents({ customerId: customer.id, limit: 50 }).catch((error) => ({
4939
+ events: [],
4940
+ nextCursor: null,
4941
+ unavailableReason: error instanceof Error ? error.message : "Unable to load billing history."
4942
+ }))
4943
+ ]);
4944
+ return c.html(renderReskinSettings({
4945
+ userId: customer.id,
4946
+ currentAccountId: customer.id,
4947
+ name: customer.name,
4948
+ notice: flash?.notice ?? null,
4949
+ error: flash?.error ?? null,
4950
+ activeTab,
4951
+ openFlockPosterDialog: c.req.query("connect") === "flockposter" || (activeTab === "channels" && !flockPosterApiKey),
4952
+ email: customer.email,
4953
+ isPaidPlan: customer.isPaidPlan,
4954
+ isDeveloper: customer.isDeveloper,
4955
+ about: customer.about,
4956
+ groupchatUrl: customer.groupchatUrl,
4957
+ flockposterApiKey: flockPosterApiKey,
4958
+ emailChannelIconUrl: await getEmailChannelIconUrl(c),
4959
+ emailChannels,
4960
+ vidfarmApiKey: await getVisibleApiKey(customer.id),
4961
+ billingUrl: config.VIDFARM_BILLING_URL?.trim() || null,
4962
+ directorSkill,
4963
+ wallet,
4964
+ walletEvents,
4965
+ flockposterChannels: [],
4966
+ flockposterChannelsError: null,
4967
+ providerKeys: (await listProviderKeysWithSecretsForCustomer(customer.id)).map((entry) => ({
4968
+ id: String(entry.id),
4969
+ provider: String(entry.provider),
4970
+ label: entry.label ? String(entry.label) : null,
4971
+ secret: readStoredSecret(String(entry.secret)),
4972
+ status: String(entry.status),
4973
+ created_at: String(entry.created_at),
4974
+ last_used_at: entry.last_used_at ? String(entry.last_used_at) : null
4975
+ })),
4976
+ attachments: (await serverlessRecords.listUserAttachments(customer.id)).map((attachment) => serializeUserAttachment(c, attachment))
4977
+ }));
4978
+ });
4915
4979
  // Library hosts two tabs at distinct URLs (approved projects + mined clips).
4916
- app.get("/reskin/library", (c) => redirect(c, "/reskin/library/approved"));
4917
- app.get("/reskin/library/approved", (c) => c.html(renderReskinLibrary("approved")));
4918
- app.get("/reskin/library/clips", (c) => c.html(renderReskinLibrary("clips")));
4919
- app.get("/reskin/clips", (c) => redirect(c, "/reskin/library/clips")); // merged into Library
4920
- // Discover has three modes at distinct URLs (feed / swipe / categories).
4921
- app.get("/reskin/discover", (c) => redirect(c, "/reskin/discover/feed"));
4980
+ app.get("/library", (c) => redirect(c, "/library/approved"));
4981
+ const reskinLibraryHandler = async (c, tab) => {
4982
+ const customer = await getPreferredBrowserCustomer(c);
4983
+ if (!customer) {
4984
+ setLoginReturnPath(c, currentPathWithSearch(c));
4985
+ return redirect(c, "/login");
4986
+ }
4987
+ const emailChannels = await getEmailPseudoChannels(customer.id, c);
4988
+ const flockPoster = await getFlockPosterChannels(customer.flockposterApiKey?.trim() || null);
4989
+ const scheduleChannels = [
4990
+ ...emailChannels.map((channel) => ({ id: channel.id, name: channel.name, title: channel.title, handle: channel.handle, platform: channel.platform, status: channel.status, avatarUrl: channel.avatarUrl })),
4991
+ ...flockPoster.channels.map((channel) => ({ id: channel.id, name: channel.title, title: channel.title, handle: channel.handle, platform: channel.platform, status: channel.status, avatarUrl: channel.avatarUrl }))
4992
+ ];
4993
+ const localPosts = await Promise.all((await serverlessRecords.listReadyPosts(customer.id))
4994
+ .filter((post) => (post.source ?? "legacy") === "hyperframes")
4995
+ .filter((post) => post.status === "ready" || post.status === "scheduled")
4996
+ .map(async (post) => ({ ...serializeReadyPost(c, post, { includePrivate: true }), template_id: post.compositionId ?? null })));
4997
+ const posts = await mergeUpstreamReadyPosts(localPosts);
4998
+ return c.html(renderReskinLibrary(tab, {
4999
+ userId: customer.id,
5000
+ currentAccountId: customer.id,
5001
+ name: customer.name,
5002
+ email: customer.email,
5003
+ posts,
5004
+ schedule: { channels: scheduleChannels, connectHref: "/settings?tab=channels&connect=flockposter" },
5005
+ editorChat: null
5006
+ }));
5007
+ };
5008
+ app.get("/library/approved", (c) => reskinLibraryHandler(c, "approved"));
5009
+ app.get("/library/raws", (c) => reskinLibraryHandler(c, "raws"));
5010
+ app.get("/library/logs", (c) => reskinLibraryHandler(c, "logs")); // job-runs, folded in
5011
+ app.get("/library/clips", (c) => redirect(c, "/library/raws")); // renamed Clips → Raws
5012
+ // Discover has three modes selected via ?view= (feed default / swipe / categories).
5013
+ // Path segments are avoided because /discover/feed, /discover/swipe/* are JSON APIs.
5014
+ app.get("/discover", async (c) => {
5015
+ const view = c.req.query("view");
5016
+ const mode = view === "swipe" || view === "categories" ? view : "feed";
5017
+ const customer = await getOptionalPreferredBrowserCustomer(c);
5018
+ return c.html(renderReskinDiscover(mode, { accountId: customer?.id, isLoggedIn: Boolean(customer) }));
5019
+ });
5020
+ app.get("/chat", async (c) => {
5021
+ const customer = await getPreferredBrowserCustomer(c);
5022
+ if (!customer) {
5023
+ setLoginReturnPath(c, currentPathWithSearch(c));
5024
+ return redirect(c, "/login");
5025
+ }
5026
+ return c.html(renderReskinChat({
5027
+ userId: customer.id,
5028
+ currentAccountId: customer.id,
5029
+ name: customer.name,
5030
+ email: customer.email,
5031
+ editorChat: await buildChatBrainstormEditorChatBoot({ customer })
5032
+ }));
5033
+ });
5034
+ // /reskin/inpaint — the "Create Image" masked-image editor (reached from the
5035
+ // chat page's studio-mode select). Auth-gated like the other tools.
5036
+ app.get("/inpaint", async (c) => {
5037
+ const customer = await getOptionalPreferredBrowserCustomer(c);
5038
+ if (!customer) {
5039
+ setLoginReturnPath(c, currentPathWithSearch(c));
5040
+ return redirect(c, "/login");
5041
+ }
5042
+ return c.html(renderReskinInpaint({ accountId: customer.id, isLoggedIn: true }));
5043
+ });
5044
+ app.get("/calendar", async (c) => {
5045
+ const customer = await requireBrowserCustomer(c);
5046
+ if (!customer) {
5047
+ setLoginReturnPath(c, currentPathWithSearch(c));
5048
+ return redirect(c, "/login");
5049
+ }
5050
+ const flockPosterApiKey = customer.flockposterApiKey?.trim() || null;
5051
+ const emailChannels = await getEmailPseudoChannels(customer.id, c);
5052
+ return c.html(renderReskinCalendar({
5053
+ userId: customer.id,
5054
+ currentAccountId: customer.id,
5055
+ name: customer.name,
5056
+ email: customer.email,
5057
+ channels: emailChannels.map((channel) => ({
5058
+ id: channel.id,
5059
+ name: channel.title,
5060
+ handle: channel.handle,
5061
+ platform: channel.platform,
5062
+ avatarUrl: channel.avatarUrl,
5063
+ platformIconUrl: channel.platformIconUrl
5064
+ })),
5065
+ posts: [],
5066
+ loadEndpoint: withAccountQuery("/calendar/feed", customer.id),
5067
+ hasFlockPosterKey: Boolean(flockPosterApiKey || emailChannels.length),
5068
+ connectChannelsHref: "/settings?tab=channels&connect=flockposter"
5069
+ }));
5070
+ });
5071
+ // Job runs were renamed and folded into the Library "Logs" tab.
5072
+ app.get("/job-runs", (c) => redirect(c, "/library/logs"));
5073
+ app.get("/agency", async (c) => {
5074
+ const customer = await requireBrowserCustomer(c);
5075
+ return c.html(renderReskinAgency({
5076
+ userId: customer?.id ?? null,
5077
+ currentAccountId: customer?.id ?? null,
5078
+ name: customer?.name ?? null,
5079
+ email: customer?.email ?? null,
5080
+ loggedIn: Boolean(customer)
5081
+ }));
5082
+ });
5083
+ app.get("/pricing", async (c) => {
5084
+ const customer = await getOptionalPreferredBrowserCustomer(c);
5085
+ return c.html(renderReskinPricing({ email: customer?.email ?? "" }));
5086
+ });
5087
+ app.get("/login", (c) => c.html(renderReskinLogin({ mode: getLoginMode(c.req.query("mode")), routePrefix: "/login" })));
5088
+ app.post("/login/password", async (c) => {
5089
+ const mode = getLoginMode(c.req.query("mode"));
5090
+ const routePrefix = "/login";
5091
+ const parsed = passwordLoginSchema.safeParse(await parseFormBody(c));
5092
+ if (!parsed.success)
5093
+ return c.html(renderReskinLogin({ mode, routePrefix, error: "Enter a valid email and password." }), 400);
5094
+ try {
5095
+ const result = await auth.authenticateWithPassword(parsed.data.email, parsed.data.password);
5096
+ if (result.status === "pricing")
5097
+ return redirect(c, "/pricing");
5098
+ setBrowserSession(c, result.customer);
5099
+ return redirect(c, resolvePostLoginRedirect(c, result.customer));
5100
+ }
5101
+ catch (error) {
5102
+ return c.html(renderReskinLogin({ mode, routePrefix, email: parsed.data.email, error: error instanceof Error ? error.message : "Unable to login." }), 401);
5103
+ }
5104
+ });
5105
+ app.post("/login/otp/request", async (c) => {
5106
+ const mode = getLoginMode(c.req.query("mode"));
5107
+ const routePrefix = "/login";
5108
+ const parsed = otpRequestSchema.safeParse(await parseFormBody(c));
5109
+ if (!parsed.success)
5110
+ return c.html(renderReskinLogin({ mode, routePrefix, error: "Enter a valid email address." }), 400);
5111
+ try {
5112
+ const result = await auth.requestOtp(parsed.data.email);
5113
+ const message = result.delivery === "console" && result.code
5114
+ ? `Local dev mode: OTP is ${result.code}.`
5115
+ : "OTP sent. Enter the code to continue.";
5116
+ return c.html(renderReskinLogin({ mode, routePrefix, email: parsed.data.email, otpSent: true, message }));
5117
+ }
5118
+ catch (error) {
5119
+ return c.html(renderReskinLogin({ mode, routePrefix, email: parsed.data.email, error: error instanceof Error ? error.message : "Unable to send OTP." }), 502);
5120
+ }
5121
+ });
5122
+ app.post("/login/otp/verify", async (c) => {
5123
+ const mode = getLoginMode(c.req.query("mode"));
5124
+ const routePrefix = "/login";
5125
+ const parsed = otpVerifySchema.safeParse(await parseFormBody(c));
5126
+ if (!parsed.success)
5127
+ return c.html(renderReskinLogin({ mode, routePrefix, error: "Enter the email and 6-digit OTP." }), 400);
5128
+ try {
5129
+ const result = await auth.verifyOtpForBrowserLogin(parsed.data.email, parsed.data.code, parsed.data.name);
5130
+ if (result.status === "pricing") {
5131
+ clearBrowserSession(c, result.customer.id);
5132
+ return redirect(c, "/pricing");
5133
+ }
5134
+ setBrowserSession(c, result.customer);
5135
+ return redirect(c, resolvePostLoginRedirect(c, result.customer));
5136
+ }
5137
+ catch (error) {
5138
+ return c.html(renderReskinLogin({ mode, routePrefix, email: parsed.data.email, otpSent: true, error: error instanceof Error ? error.message : "Unable to verify OTP." }), 401);
5139
+ }
5140
+ });
5141
+ app.post("/logout", async (c) => {
5142
+ const formBody = await parseFormBody(c);
5143
+ const customer = await requireBrowserCustomer(c, formBody);
5144
+ if (customer) {
5145
+ clearBrowserSession(c, customer.id);
5146
+ }
5147
+ return redirect(c, "/login");
5148
+ });
5149
+ app.get("/help", async (c) => {
5150
+ const customer = await getOptionalPreferredBrowserCustomer(c);
5151
+ return c.html(renderReskinHelp({
5152
+ account: { isLoggedIn: Boolean(customer), userId: customer?.id ?? null, displayName: customer?.name ?? null, email: customer?.email ?? null },
5153
+ expand: c.req.query("expand") ?? null
5154
+ }));
5155
+ });
5156
+ // The reskin IS the app now — every old /reskin/* URL 301/302s to its promoted
5157
+ // canonical route so existing bookmarks/links keep working. The /reskin gallery
5158
+ // (above) is kept as a dev reference; its cards link to the canonical routes.
5159
+ app.get("/reskin/settings", (c) => redirect(c, "/settings"));
5160
+ app.get("/reskin/library", (c) => redirect(c, "/library/approved"));
5161
+ app.get("/reskin/library/approved", (c) => redirect(c, "/library/approved"));
5162
+ app.get("/reskin/library/raws", (c) => redirect(c, "/library/raws"));
5163
+ app.get("/reskin/library/logs", (c) => redirect(c, "/library/logs"));
5164
+ app.get("/reskin/library/clips", (c) => redirect(c, "/library/raws"));
5165
+ app.get("/reskin/clips", (c) => redirect(c, "/library/raws"));
5166
+ app.get("/reskin/discover", (c) => redirect(c, "/discover"));
4922
5167
  app.get("/reskin/discover/:mode", (c) => {
4923
- const mode = c.req.param("mode");
4924
- if (mode !== "feed" && mode !== "swipe" && mode !== "categories")
4925
- return c.notFound();
4926
- return c.html(renderReskinDiscover(mode));
4927
- });
4928
- app.get("/reskin/chat", (c) => c.html(renderReskinChat()));
4929
- app.get("/reskin/calendar", (c) => c.html(renderReskinCalendar()));
4930
- app.get("/reskin/job-runs", (c) => c.html(renderReskinJobRuns()));
4931
- app.get("/reskin/agency", (c) => c.html(renderReskinAgency()));
4932
- app.get("/reskin/pricing", (c) => c.html(renderReskinPricing()));
4933
- app.get("/reskin/login", (c) => c.html(renderReskinLogin()));
4934
- app.get("/reskin/help", (c) => c.html(renderReskinHelp()));
5168
+ const m = c.req.param("mode");
5169
+ return redirect(c, m === "swipe" || m === "categories" ? `/discover?view=${m}` : "/discover");
5170
+ });
5171
+ app.get("/reskin/chat", (c) => redirect(c, "/chat"));
5172
+ app.get("/reskin/inpaint", (c) => redirect(c, "/inpaint"));
5173
+ app.get("/reskin/calendar", (c) => redirect(c, "/calendar"));
5174
+ app.get("/reskin/job-runs", (c) => redirect(c, "/library/logs"));
5175
+ app.get("/reskin/agency", (c) => redirect(c, "/agency"));
5176
+ app.get("/reskin/pricing", (c) => redirect(c, "/pricing"));
5177
+ app.get("/reskin/help", (c) => redirect(c, "/help"));
5178
+ app.get("/reskin/login", (c) => redirect(c, "/login"));
4935
5179
  app.get("/discover", async (c) => renderApprovedHomepage(c));
4936
5180
  app.get("/editor/:templateId", async (c) => {
4937
5181
  const templateId = c.req.param("templateId");
@@ -12134,9 +12378,21 @@ app.use(`${USER_PREFIX}/me/*`, requireAuth);
12134
12378
  // is the public HTML gallery page (like /discover vs /discover/feed). Hono's
12135
12379
  // `/clips/*` also matches the bare `/clips`, so let that one path through
12136
12380
  // unauthenticated.
12137
- const isClipsGalleryPage = (c) => c.req.path === CLIPS_PREFIX || c.req.path === `${CLIPS_PREFIX}/`;
12138
- app.use(`${CLIPS_PREFIX}/*`, (c, next) => (isClipsGalleryPage(c) ? next() : requireAuth(c, next)));
12139
- app.use(`${CLIPS_PREFIX}/*`, (c, next) => (isClipsGalleryPage(c) ? next() : captureApiCallHistory(c, next)));
12381
+ const isClipsGalleryPage = (c) => c.req.path === RAWS_PREFIX || c.req.path === `${RAWS_PREFIX}/`;
12382
+ app.use(`${RAWS_PREFIX}/*`, (c, next) => (isClipsGalleryPage(c) ? next() : requireAuth(c, next)));
12383
+ app.use(`${RAWS_PREFIX}/*`, (c, next) => (isClipsGalleryPage(c) ? next() : captureApiCallHistory(c, next)));
12384
+ // Deprecated /clips/* → /raws/* alias: transparently redispatch legacy paths so
12385
+ // existing skills, editor-chat prompts, and the devcli keep working during the
12386
+ // clips→raws rename. Remove once all callers emit /raws.
12387
+ const rawsCompatAlias = (c) => {
12388
+ const url = new URL(c.req.url);
12389
+ url.pathname = RAWS_PREFIX + url.pathname.slice(CLIPS_COMPAT_PREFIX.length);
12390
+ // Redispatch through the app with just the rewritten request — do NOT forward
12391
+ // c.executionCtx (its getter throws in the Node adapter when absent).
12392
+ return app.fetch(new Request(url.toString(), c.req.raw));
12393
+ };
12394
+ app.all(CLIPS_COMPAT_PREFIX, rawsCompatAlias);
12395
+ app.all(`${CLIPS_COMPAT_PREFIX}/*`, rawsCompatAlias);
12140
12396
  app.use(AGENCY_PREFIX, requireAuth);
12141
12397
  app.use(`${AGENCY_PREFIX}/*`, requireAuth);
12142
12398
  app.use(`${API_PREFIX}/rate-limit-status`, requireAuth);
@@ -17045,7 +17301,7 @@ function clipKeywordMatch(clip, q) {
17045
17301
  // GET /clips — the Clips gallery, rendered in the same shell as the rest of the
17046
17302
  // frontend and reached via the Library page's "Finished / Clips" toggle. The
17047
17303
  // client fetches /clips/feed with the session cookie (mirrors /discover vs /feed).
17048
- app.get(CLIPS_PREFIX, async (c) => {
17304
+ app.get(RAWS_PREFIX, async (c) => {
17049
17305
  const customer = await getPreferredBrowserCustomer(c);
17050
17306
  if (!customer) {
17051
17307
  return redirect(c, "/login");
@@ -17059,7 +17315,7 @@ app.get(CLIPS_PREFIX, async (c) => {
17059
17315
  }));
17060
17316
  });
17061
17317
  // GET /clips/feed — the caller's clip library as JSON (optional ?source, ?q, ?limit).
17062
- app.get(`${CLIPS_PREFIX}/feed`, async (c) => {
17318
+ app.get(`${RAWS_PREFIX}/feed`, async (c) => {
17063
17319
  const customer = requireCustomer(c);
17064
17320
  const limit = clampPageLimit(Number(c.req.query("limit") || "60"), 200);
17065
17321
  const q = c.req.query("q")?.trim();
@@ -17072,7 +17328,7 @@ app.get(`${CLIPS_PREFIX}/feed`, async (c) => {
17072
17328
  return c.json({ clips: await Promise.all(clips.map(serializeClip)) });
17073
17329
  });
17074
17330
  // POST /clips/search — hybrid structured + semantic search.
17075
- app.post(`${CLIPS_PREFIX}/search`, async (c) => {
17331
+ app.post(`${RAWS_PREFIX}/search`, async (c) => {
17076
17332
  const customer = requireCustomer(c);
17077
17333
  try {
17078
17334
  const body = (await c.req.json().catch(() => ({})));
@@ -17120,7 +17376,7 @@ function normalizeMatchScene(s, index) {
17120
17376
  }
17121
17377
  // POST /clips/match — find the top library clips that could REPLACE one scene.
17122
17378
  // Body: a scene annotation, or {criteria, embedding_text|text, limit}.
17123
- app.post(`${CLIPS_PREFIX}/match`, async (c) => {
17379
+ app.post(`${RAWS_PREFIX}/match`, async (c) => {
17124
17380
  const customer = requireCustomer(c);
17125
17381
  try {
17126
17382
  const body = (await c.req.json().catch(() => ({})));
@@ -17143,7 +17399,7 @@ app.post(`${CLIPS_PREFIX}/match`, async (c) => {
17143
17399
  // POST /clips/match-scenes — batch: take a decomposed video's scene annotations
17144
17400
  // and return the top replacement-clip matches per scene (one library read).
17145
17401
  // Body: { scenes | annotations: SceneAnnotation[], limit? }.
17146
- app.post(`${CLIPS_PREFIX}/match-scenes`, async (c) => {
17402
+ app.post(`${RAWS_PREFIX}/match-scenes`, async (c) => {
17147
17403
  const customer = requireCustomer(c);
17148
17404
  try {
17149
17405
  const body = (await c.req.json());
@@ -17184,7 +17440,7 @@ app.post(`${CLIPS_PREFIX}/match-scenes`, async (c) => {
17184
17440
  // pipeline runs the hunt and GET /clips/scan/:scanId polls it. Billing is AWS
17185
17441
  // compute only (clip_scan_lambda + step_functions_standard) — the AI spend is
17186
17442
  // the caller's own provider key (BYOK) and is never wallet-billed.
17187
- app.post(`${CLIPS_PREFIX}/scan`, async (c) => {
17443
+ app.post(`${RAWS_PREFIX}/scan`, async (c) => {
17188
17444
  const customer = requireCustomer(c);
17189
17445
  try {
17190
17446
  const body = (await c.req.json());
@@ -17503,7 +17759,7 @@ app.post(`${CLIPS_PREFIX}/scan`, async (c) => {
17503
17759
  // GET /clips/scan-options — what the Import Source modal can run a hunt with:
17504
17760
  // which BYOK providers have saved keys, whether a local agent CLI is available
17505
17761
  // (vidfarm serve boxes only), what "auto" resolves to, and where the hunt runs.
17506
- app.get(`${CLIPS_PREFIX}/scan-options`, async (c) => {
17762
+ app.get(`${RAWS_PREFIX}/scan-options`, async (c) => {
17507
17763
  const customer = requireCustomer(c);
17508
17764
  const keys = await loadCustomerVisionKeys(customer.id);
17509
17765
  const cloudPipeline = Boolean(config.VIDFARM_CLIP_SCAN_STATE_MACHINE_ARN);
@@ -17748,7 +18004,7 @@ async function mirrorUpstreamClipsToLocal(input) {
17748
18004
  await clipRecords.putScan({ ...(current ?? scan), status: "complete", updated_at: nowIso() });
17749
18005
  }
17750
18006
  // GET /clips/scan/:scanId — poll scan status.
17751
- app.get(`${CLIPS_PREFIX}/scan/:scanId`, async (c) => {
18007
+ app.get(`${RAWS_PREFIX}/scan/:scanId`, async (c) => {
17752
18008
  const customer = requireCustomer(c);
17753
18009
  let scan = await clipRecords.getScan(customer.id, c.req.param("scanId"));
17754
18010
  if (!scan)
@@ -17786,17 +18042,17 @@ app.get(`${CLIPS_PREFIX}/scan/:scanId`, async (c) => {
17786
18042
  return c.json({ scan, source });
17787
18043
  });
17788
18044
  // GET /clips/sources — scanned source videos.
17789
- app.get(`${CLIPS_PREFIX}/sources`, async (c) => {
18045
+ app.get(`${RAWS_PREFIX}/sources`, async (c) => {
17790
18046
  const customer = requireCustomer(c);
17791
18047
  return c.json({ sources: await clipRecords.listSources(customer.id) });
17792
18048
  });
17793
18049
  // GET /clips/presets — built-in + saved presets.
17794
- app.get(`${CLIPS_PREFIX}/presets`, async (c) => {
18050
+ app.get(`${RAWS_PREFIX}/presets`, async (c) => {
17795
18051
  const customer = requireCustomer(c);
17796
18052
  return c.json({ presets: await clipRecords.listPresets(customer.id) });
17797
18053
  });
17798
18054
  // POST /clips/presets — save a custom preset from criteria or a NL query.
17799
- app.post(`${CLIPS_PREFIX}/presets`, async (c) => {
18055
+ app.post(`${RAWS_PREFIX}/presets`, async (c) => {
17800
18056
  const customer = requireCustomer(c);
17801
18057
  try {
17802
18058
  const body = (await c.req.json());
@@ -17824,7 +18080,7 @@ app.post(`${CLIPS_PREFIX}/presets`, async (c) => {
17824
18080
  }
17825
18081
  });
17826
18082
  // DELETE /clips/presets/:presetId — delete a saved (non-builtin) preset.
17827
- app.delete(`${CLIPS_PREFIX}/presets/:presetId`, async (c) => {
18083
+ app.delete(`${RAWS_PREFIX}/presets/:presetId`, async (c) => {
17828
18084
  const customer = requireCustomer(c);
17829
18085
  const id = c.req.param("presetId");
17830
18086
  if (id.startsWith("preset_") && BUILTIN_CLIP_PRESET_IDS.has(id)) {
@@ -17834,7 +18090,7 @@ app.delete(`${CLIPS_PREFIX}/presets/:presetId`, async (c) => {
17834
18090
  return c.json({ ok: true });
17835
18091
  });
17836
18092
  // GET /clips/:clipId/download — presigned redirect to the clip mp4.
17837
- app.get(`${CLIPS_PREFIX}/:clipId/download`, async (c) => {
18093
+ app.get(`${RAWS_PREFIX}/:clipId/download`, async (c) => {
17838
18094
  const customer = requireCustomer(c);
17839
18095
  const clip = await clipRecords.getClip(customer.id, c.req.param("clipId"));
17840
18096
  if (!clip)
@@ -17845,7 +18101,7 @@ app.get(`${CLIPS_PREFIX}/:clipId/download`, async (c) => {
17845
18101
  return redirect(c, url, 302);
17846
18102
  });
17847
18103
  // GET /clips/:clipId — one clip (+ presigned view/thumbnail urls).
17848
- app.get(`${CLIPS_PREFIX}/:clipId`, async (c) => {
18104
+ app.get(`${RAWS_PREFIX}/:clipId`, async (c) => {
17849
18105
  const customer = requireCustomer(c);
17850
18106
  const clip = await clipRecords.getClip(customer.id, c.req.param("clipId"));
17851
18107
  if (!clip)
@@ -17853,7 +18109,7 @@ app.get(`${CLIPS_PREFIX}/:clipId`, async (c) => {
17853
18109
  return c.json({ clip: await serializeClip(clip) });
17854
18110
  });
17855
18111
  // DELETE /clips/:clipId — remove a clip record (+ vector).
17856
- app.delete(`${CLIPS_PREFIX}/:clipId`, async (c) => {
18112
+ app.delete(`${RAWS_PREFIX}/:clipId`, async (c) => {
17857
18113
  const customer = requireCustomer(c);
17858
18114
  await clipRecords.deleteClip(customer.id, c.req.param("clipId"));
17859
18115
  return c.json({ ok: true });
package/dist/src/cli.js CHANGED
@@ -168,22 +168,22 @@ Generate AI media and drop it on the timeline (for local coding agents):
168
168
  original vs decomposed (caption-free),
169
169
  non-billing (alias: ghostcut)
170
170
 
171
- Clip hunting (the third library — mine long-form video into a reusable clip store):
172
- clips scan <video-path> Hunt short clips out of a long video. LOCAL-FIRST: local ffmpeg +
171
+ Raws (the third library — mine long-form video into a reusable raws store):
172
+ raws scan <video-path> Hunt short clips out of a long video. LOCAL-FIRST: local ffmpeg +
173
173
  Detect → tag/transcribe → embed → persist your local claude/codex CLI
174
174
  to ~/.vidfarm/clips.db. (keys are fallback)
175
175
  --range "MM:SS-MM:SS" Only hunt these source windows (repeatable; big cost saver)
176
176
  --duration <sec> Target clip length as a SOFT band (10→5-20s, 30→20-40s)
177
177
  --aspect 9:16|16:9|4:3|1:1 Crop clips (also: vertical/horizontal/square) [--crop-focus <f>]
178
178
  --no-text Prefer scenes WITHOUT captions/on-screen text (selection only)
179
- --cloud [--url <url>] BACKUP: run on the deployed pipeline → POST /clips/scan (+ poll)
179
+ --cloud [--url <url>] BACKUP: run on the deployed pipeline → POST /raws/scan (+ poll)
180
180
  (uploads to your temp folder — 30-day TTL — bills AWS compute only)
181
181
  --dry-run Show the estimated cost and stop (over $1 needs --yes/confirm)
182
- clips list List the local clip library → GET /clips
183
- clips search "<text>" Hybrid structured + semantic search → POST /clips/search
184
- clips preset list|run <name>|save <name> --from-query '<...>' → /clips/presets
185
- clips export <ids...> --to <dir> Copy clip mp4s + metadata out
186
- (run 'vidfarm clips' for the full clips help)
182
+ raws list List the local raws library → GET /raws
183
+ raws search "<text>" Hybrid structured + semantic search → POST /raws/search
184
+ raws preset list|run <name>|save <name> --from-query '<...>' → /raws/presets
185
+ raws export <ids...> --to <dir> Copy raw mp4s + metadata out
186
+ (run 'vidfarm raws' for the full raws help)
187
187
 
188
188
  Speech (TTS/STT) — LOCAL-FIRST on your own AI key; --cloud is the explicit backup:
189
189
  tts "<text>" Text → narration audio file. LOCAL: your OPENAI/GEMINI/
@@ -528,6 +528,7 @@ async function main() {
528
528
  case "annotate-file":
529
529
  await runAnnotateFileCommand(rest);
530
530
  return;
531
+ case "raws":
531
532
  case "clips":
532
533
  await runClipsCommand(rest);
533
534
  return;
@@ -722,7 +723,7 @@ async function runServeCommand(argv) {
722
723
  // render_target="cloud" upstream handoff (the editor's "Render in Cloud").
723
724
  process.env.VIDFARM_JOB_STATE_MACHINE_ARN = "";
724
725
  // Same trap for clip hunts: a dev-shell .env carrying the staging clip-scan
725
- // ARN would route this box's /clips/scan to the cloud Step Functions. serve
726
+ // ARN would route this box's /raws/scan to the cloud Step Functions. serve
726
727
  // hunts are LOCAL-FIRST (local agent CLI / BYOK keys); the upstream cloud is
727
728
  // the explicit backup inside the scan route itself.
728
729
  process.env.VIDFARM_CLIP_SCAN_STATE_MACHINE_ARN = "";
@@ -2256,7 +2257,7 @@ async function runGenerateCommand(argv) {
2256
2257
  }
2257
2258
  }
2258
2259
  // ── Speech: tts / stt ────────────────────────────────────────────────────────
2259
- // LOCAL-FIRST like `clips scan`: both commands default to running on the
2260
+ // LOCAL-FIRST like `raws scan`: both commands default to running on the
2260
2261
  // user's OWN AI key (GEMINI_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY or
2261
2262
  // the --*-key flags) via src/services/speech.ts — local ffmpeg demux, direct
2262
2263
  // provider call, no cloud job, no wallet. The REST primitive routes