openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
88 lines (87 loc) • 4.11 kB
JavaScript
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import { n as sanitizeTerminalText } from "./safe-text-BGBqp1a4.js";
import "./model-ref-shared-Dz7QU0Lx.js";
import { t as modelKey } from "./model-key-CMdQNkZf.js";
import { i as readPromotionClaims, n as markPromotionSlugsNotified, r as maybeRefreshPromotionsFeed, t as listLivePromotionEntries } from "./promotions-feed-BPY4OXVb.js";
//#region src/commands/models/list.promotions.ts
/** Promotion decorations for `models list`: claim tags + passive discovery. */
const PROMOTIONS_SECTION_MAX_ENTRIES = 3;
/** Starts the passive feed refresh so callers can overlap it with model row construction. */
function startPromotionsFeedRefresh(nowMs = Date.now()) {
return {
nowMs,
statePromise: maybeRefreshPromotionsFeed({ nowMs }).catch(() => void 0)
};
}
/**
* Tag configured rows that were registered by `promos claim`, flipping to
* "promo ended" once the window passes so users learn why a model stopped
* serving. Reads only local provenance; never the network.
*/
function applyPromotionClaimTags(rows, nowMs = Date.now()) {
const claims = readPromotionClaims();
if (claims.length === 0) return;
const endsByKey = /* @__PURE__ */ new Map();
for (const claim of claims) for (const key of claim.modelKeys) {
const prev = endsByKey.get(key);
if (prev === void 0 || claim.endsAtMs > prev) endsByKey.set(key, claim.endsAtMs);
}
for (const row of rows) {
const endsAtMs = endsByKey.get(row.key);
if (endsAtMs === void 0) continue;
row.tags.push(endsAtMs < nowMs ? "promo ended" : "promo");
}
}
function canonicalPromotionModelKey(entry, modelRef) {
const provider = entry.provider?.trim();
const prefix = provider ? `${provider}/` : "";
if (!provider || !modelRef.startsWith(prefix) || modelRef.length <= prefix.length) return;
return modelKey(provider, modelRef.slice(prefix.length));
}
/**
* Passive discovery: cadence-gated feed refresh (fail-silent), an
* "Available via promotion" group for live offers whose models are not
* configured yet, and a one-time notice per newly seen offer. Callers gate
* machine outputs (`--json`/`--plain`) — this only ever writes human text.
* `configuredKeys` must be the user's configured model set, not the
* rendered rows — filtered or `--all` listings show a different set.
*/
async function printAvailablePromotionsSection(params) {
const { refresh } = params;
const nowMs = refresh.nowMs;
const state = await refresh.statePromise;
if (!state) return;
const live = listLivePromotionEntries(state, nowMs);
if (live.length === 0) return;
const unclaimed = live.filter((entry) => entry.models.some((model) => {
const key = canonicalPromotionModelKey(entry, model.modelRef);
return key !== void 0 && !params.configuredKeys.has(key);
}));
const { runtime } = params;
const safe = sanitizeTerminalText;
if (unclaimed.length > 0) {
runtime.log("");
runtime.log("Available via promotion:");
for (const entry of unclaimed.slice(0, PROMOTIONS_SECTION_MAX_ENTRIES)) {
const sponsor = entry.sponsor ? ` — ${safe(entry.sponsor)}` : "";
runtime.log(` ${safe(entry.title)}${sponsor} (ends ${new Date(entry.endsAt).toLocaleDateString()})`);
for (const model of entry.models) {
const alias = model.alias ? ` (${safe(model.alias)})` : "";
runtime.log(` · ${safe(model.modelRef)}${alias}`);
}
runtime.log(` Claim: ${formatCliCommand(`openclaw promos claim ${safe(entry.slug)}`)}`);
}
if (unclaimed.length > PROMOTIONS_SECTION_MAX_ENTRIES) {
const more = unclaimed.length - PROMOTIONS_SECTION_MAX_ENTRIES;
runtime.log(` …and ${more} more: ${formatCliCommand("openclaw promos list")}`);
}
}
const unseen = live.filter((entry) => !state.notifiedSlugs.has(entry.slug));
if (unseen.length > 0) {
runtime.log("");
runtime.log(`🎁 New promotional model offers available — ${formatCliCommand("openclaw promos list")} for details.`);
markPromotionSlugsNotified(unseen.map((entry) => entry.slug));
}
}
//#endregion
export { applyPromotionClaimTags, printAvailablePromotionsSection, startPromotionsFeedRefresh };