@easysociable/cli 0.1.11 → 0.1.13

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.
@@ -15545,7 +15545,8 @@ var init_src = __esm({
15545
15545
  previewImageUrl: external_exports.string().url().optional(),
15546
15546
  /** Layout Family id — catalog UIs group variants by this (ADR-0033). */
15547
15547
  familyId: external_exports.string().optional(),
15548
- familyRole: external_exports.enum(["master", "variant"]).optional()
15548
+ familyRole: external_exports.enum(["master", "variant"]).optional(),
15549
+ familyName: external_exports.string().optional()
15549
15550
  }).strict();
15550
15551
  publicTemplateSlotTypeSchema = external_exports.enum(["text", "number", "image"]);
15551
15552
  publicTemplateSlotSchema = external_exports.object({
@@ -16061,6 +16062,7 @@ var init_src = __esm({
16061
16062
  previewThemeId: external_exports.string().nullable().optional(),
16062
16063
  familyId: external_exports.string().optional(),
16063
16064
  familyRole: external_exports.enum(["master", "variant"]).optional(),
16065
+ familyName: external_exports.string().optional(),
16064
16066
  variants: external_exports.array(authoringDraftVariantSchema).optional(),
16065
16067
  intendedPlatforms: external_exports.array(external_exports.string()).optional(),
16066
16068
  name: external_exports.string(),
@@ -16115,10 +16117,14 @@ var init_src = __esm({
16115
16117
  ]);
16116
16118
  publicSlotValueSchema = external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean(), external_exports.null()]);
16117
16119
  contentPackPageSchema = external_exports.object({
16118
- pageTemplateId: external_exports.string().min(1),
16120
+ pageTemplateId: external_exports.string().min(1).optional(),
16121
+ layoutFamilyId: external_exports.string().min(1).optional(),
16119
16122
  role: pageRoleSchema,
16120
16123
  slots: external_exports.record(external_exports.string(), publicSlotValueSchema)
16121
- }).strict();
16124
+ }).strict().refine((page) => page.pageTemplateId || page.layoutFamilyId, {
16125
+ message: "page requires layoutFamilyId or pageTemplateId",
16126
+ path: ["layoutFamilyId"]
16127
+ });
16122
16128
  contentPackSchema = external_exports.object({
16123
16129
  schemaVersion: external_exports.literal("content-pack.v1"),
16124
16130
  platform: slideshowPlatformSchema,
@@ -16143,6 +16149,7 @@ var init_src = __esm({
16143
16149
  index: external_exports.number().int().nonnegative(),
16144
16150
  role: pageRoleSchema,
16145
16151
  pageTemplateId: external_exports.string().min(1).optional(),
16152
+ layoutFamilyId: external_exports.string().min(1).optional(),
16146
16153
  slots: external_exports.record(external_exports.string(), publicSlotValueSchema),
16147
16154
  layout: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
16148
16155
  style: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
@@ -16297,11 +16304,13 @@ function isRetryableStatus(status) {
16297
16304
  function sleep(ms) {
16298
16305
  return new Promise((resolve3) => setTimeout(resolve3, ms));
16299
16306
  }
16300
- var TemplateGatewayError, TemplateGatewayClient;
16307
+ var DEFAULT_TEMPLATE_PAGE_SIZE, MAX_TEMPLATE_PAGE_SIZE, TemplateGatewayError, TemplateGatewayClient;
16301
16308
  var init_client = __esm({
16302
16309
  "../../packages/template-gateway-client/src/client.ts"() {
16303
16310
  "use strict";
16304
16311
  init_src();
16312
+ DEFAULT_TEMPLATE_PAGE_SIZE = 20;
16313
+ MAX_TEMPLATE_PAGE_SIZE = 50;
16305
16314
  TemplateGatewayError = class extends Error {
16306
16315
  code;
16307
16316
  retryable;
@@ -16347,6 +16356,13 @@ var init_client = __esm({
16347
16356
  return response.json;
16348
16357
  }
16349
16358
  async listTemplates(query) {
16359
+ return (await this.listTemplatesPage(query)).templates;
16360
+ }
16361
+ /**
16362
+ * Same as listTemplates plus `nextCursor` so Agents know the catalog is paged.
16363
+ * Derived client-side (full page ⇒ maybe more) to keep the strict v1 envelope.
16364
+ */
16365
+ async listTemplatesPage(query) {
16350
16366
  const parsed = parseListTemplatesQuery(query ?? {});
16351
16367
  const params = new URLSearchParams();
16352
16368
  const appendList = (key, values) => {
@@ -16375,7 +16391,23 @@ var init_client = __esm({
16375
16391
  const qs = params.toString();
16376
16392
  const response = await this.request("GET", qs ? `/v1/templates?${qs}` : "/v1/templates");
16377
16393
  this.throwIfError(response.status, response.json);
16378
- return parseTemplateSummaryList(response.json);
16394
+ const templates = parseTemplateSummaryList(response.json);
16395
+ const limit = parsed.limit ?? DEFAULT_TEMPLATE_PAGE_SIZE;
16396
+ const offset = Number.parseInt(parsed.cursor ?? "0", 10) || 0;
16397
+ return { templates, nextCursor: templates.length >= limit ? String(offset + limit) : null };
16398
+ }
16399
+ /** Drain every page for a filtered query (platform catalogs are tens of templates). */
16400
+ async listAllTemplates(query) {
16401
+ const all = [];
16402
+ let cursor;
16403
+ for (let page = 0; page < 40; page += 1) {
16404
+ const result = await this.listTemplatesPage({ ...query, limit: MAX_TEMPLATE_PAGE_SIZE, cursor });
16405
+ all.push(...result.templates);
16406
+ if (!result.nextCursor)
16407
+ return all;
16408
+ cursor = result.nextCursor;
16409
+ }
16410
+ return all;
16379
16411
  }
16380
16412
  async getTemplate(templateId) {
16381
16413
  const response = await this.request("GET", `/v1/templates/${encodeURIComponent(templateId)}`);
@@ -16543,6 +16575,12 @@ var init_client = __esm({
16543
16575
  this.throwIfError(response.status, response.json);
16544
16576
  return parseSlideshowRun(response.json);
16545
16577
  }
16578
+ /** Per-page shortlist (hard filter → fit score → recent-family penalty → explore slot). Agent still picks. */
16579
+ async recommendSlideshowTemplates(input) {
16580
+ const response = await this.request("POST", "/v1/slideshow/template-recommendations", input);
16581
+ this.throwIfError(response.status, response.json);
16582
+ return response.json.data;
16583
+ }
16546
16584
  async listSlideshowRuns(query = {}) {
16547
16585
  const input = listSlideshowRunsQuerySchema.parse(query);
16548
16586
  const params = new URLSearchParams();
@@ -16607,7 +16645,7 @@ var init_client = __esm({
16607
16645
  code: "TEMPLATE_API_KEY_REQUIRED",
16608
16646
  message: "Template API key is required",
16609
16647
  retryable: false,
16610
- suggestedAction: "Run `easysociable auth login --api-key <key>`"
16648
+ suggestedAction: "Run `easysociable auth login` and approve in the browser"
16611
16649
  });
16612
16650
  }
16613
16651
  const url2 = `${this.options.baseUrl.replace(/\/$/, "")}/v1/slideshow/runs/${encodeURIComponent(runId)}/artifacts/${pageIndex}`;
@@ -16802,7 +16840,7 @@ var init_client = __esm({
16802
16840
  code: "TEMPLATE_API_KEY_REQUIRED",
16803
16841
  message: "Template API key is required",
16804
16842
  retryable: false,
16805
- suggestedAction: "Run `easysociable auth login --api-key <key>` or set EASYSOCIABLE_ALLOW_ENV_CREDENTIALS=true for local tests"
16843
+ suggestedAction: "Run `easysociable auth login` (browser approval) or set EASYSOCIABLE_ALLOW_ENV_CREDENTIALS=true for local tests"
16806
16844
  });
16807
16845
  }
16808
16846
  const maxRetries = this.options.maxRetries ?? 2;
@@ -31720,7 +31758,7 @@ var init_src4 = __esm({
31720
31758
  {
31721
31759
  name: "list_templates",
31722
31760
  title: "List templates",
31723
- description: "List public template catalog summaries from Gateway (filterable; no AST)",
31761
+ description: "List public template catalog summaries from Gateway (filterable; no AST). Paged: follow nextCursor, or pass all=true with platforms to get the whole platform catalog before choosing. For Slideshow page choice prefer recommend_slideshow_templates.",
31724
31762
  effect: "read-only",
31725
31763
  inputSchema: {
31726
31764
  type: "object",
@@ -31734,11 +31772,13 @@ var init_src4 = __esm({
31734
31772
  requiresImage: { type: "boolean" },
31735
31773
  locale: { type: "string" },
31736
31774
  q: { type: "string" },
31737
- limit: { type: "number" }
31775
+ limit: { type: "number" },
31776
+ cursor: { type: "string" },
31777
+ all: { type: "boolean" }
31738
31778
  },
31739
31779
  additionalProperties: false
31740
31780
  },
31741
- outputSchema: { type: "object", properties: { templates: { type: "array" } } }
31781
+ outputSchema: { type: "object", properties: { templates: { type: "array" }, nextCursor: { type: ["string", "null"] } } }
31742
31782
  },
31743
31783
  {
31744
31784
  name: "get_template",
@@ -32057,7 +32097,7 @@ var init_src4 = __esm({
32057
32097
  };
32058
32098
  try {
32059
32099
  if (toolName === "list_templates") {
32060
- const templates = await this.client.listTemplates({
32100
+ const query = {
32061
32101
  platforms: args.platforms,
32062
32102
  roles: args.roles,
32063
32103
  contentFamily: args.contentFamily,
@@ -32066,10 +32106,16 @@ var init_src4 = __esm({
32066
32106
  canvasPreset: args.canvasPreset,
32067
32107
  requiresImage: typeof args.requiresImage === "boolean" ? args.requiresImage : void 0,
32068
32108
  locale: args.locale,
32069
- q: typeof args.q === "string" ? args.q : void 0,
32070
- limit: typeof args.limit === "number" ? args.limit : void 0
32109
+ q: typeof args.q === "string" ? args.q : void 0
32110
+ };
32111
+ if (args.all === true)
32112
+ return { ok: true, data: { templates: await this.client.listAllTemplates(query), nextCursor: null }, meta: meta4 };
32113
+ const page = await this.client.listTemplatesPage({
32114
+ ...query,
32115
+ limit: typeof args.limit === "number" ? args.limit : void 0,
32116
+ cursor: typeof args.cursor === "string" ? args.cursor : void 0
32071
32117
  });
32072
- return { ok: true, data: { templates }, meta: meta4 };
32118
+ return { ok: true, data: page, meta: meta4 };
32073
32119
  }
32074
32120
  if (toolName === "get_template") {
32075
32121
  const template = await this.client.getTemplate(String(args.templateId));
@@ -32277,6 +32323,7 @@ var init_src5 = __esm({
32277
32323
  objectSchema = { type: "object", properties: {}, additionalProperties: true };
32278
32324
  slideshowToolRegistry = [
32279
32325
  { name: "list_runs", title: "List slideshow runs", description: "List owner-scoped Slideshow Runs with optional filters.", effect: "read-only", inputSchema: { ...objectSchema, properties: { q: { type: "string" }, platform: { type: "string" }, profileId: { type: "string" }, brandProfileId: { type: "string" }, contentSlug: { type: "string" }, status: { type: "string" } } }, outputSchema: objectSchema },
32326
+ { name: "recommend_templates", title: "Recommend slideshow page templates", description: "Before create_run: send the Page Contracts (role, density, slotSummary per page) and get a shortlist per page with reasons. Recently used Layout Families are demoted and one unused family is always kept. Pick one templateId per page yourself; gaps list pages needing more templates.", effect: "read-only", inputSchema: { ...objectSchema, properties: { platform: { type: "string" }, canvasPreset: { type: "string" }, pages: { type: "array", items: { type: "object", properties: { role: { type: "string" }, density: { type: "string" }, slotSummary: { type: "object" }, requiresImage: { type: "boolean" } }, required: ["role"] } }, k: { type: "integer", minimum: 2, maximum: 5 }, seed: { type: "string" } }, required: ["platform", "pages"] }, outputSchema: objectSchema },
32280
32327
  { name: "create_run", title: "Create slideshow run", description: "Create an ADR-0026 Slideshow Run from a ContentPack.", effect: "local-write-job", inputSchema: { ...objectSchema, properties: { pack: { type: "object" }, brandId: { type: "string" }, brandProfileId: { type: "string" }, contentSlug: { type: "string" } }, required: ["pack"] }, outputSchema: objectSchema },
32281
32328
  { name: "get_run", title: "Get slideshow run", description: "Get an owner-scoped Slideshow Run.", effect: "read-only", inputSchema: { ...objectSchema, properties: { runId: { type: "string" } }, required: ["runId"] }, outputSchema: objectSchema },
32282
32329
  { name: "update_run", title: "Update slideshow run", description: "Edit page-instance slots/layout/style/decorations or presentation preference with optimistic revision.", effect: "local-write-job", inputSchema: { ...objectSchema, properties: { runId: { type: "string" }, pack: { type: "object" }, brandId: { type: "string" }, brandProfileId: { type: "string" }, contentSlug: { type: "string" }, baseRevision: { type: "integer", minimum: 1 }, pageIndex: { type: "integer", minimum: 0 }, slots: { type: "object" }, layout: { type: "object" }, style: { type: "object" }, decorations: { type: "array" } }, required: ["runId"] }, outputSchema: objectSchema },
@@ -32305,6 +32352,8 @@ var init_src5 = __esm({
32305
32352
  contentSlug: args.contentSlug,
32306
32353
  status: args.status
32307
32354
  }), meta: m };
32355
+ case "recommend_templates":
32356
+ return { ok: true, data: await this.client.recommendSlideshowTemplates({ platform: String(args.platform), pages: args.pages, canvasPreset: args.canvasPreset, k: args.k, seed: args.seed }), meta: m };
32308
32357
  case "create_run":
32309
32358
  return { ok: true, data: await this.client.createSlideshowRun({ pack: args.pack, brandId: args.brandId, brandProfileId: args.brandProfileId, contentSlug: args.contentSlug }), meta: m };
32310
32359
  case "get_run":
@@ -32376,6 +32425,7 @@ var init_src6 = __esm({
32376
32425
  };
32377
32426
  slideshowNames = {
32378
32427
  list_runs: "list_slideshow_runs",
32428
+ recommend_templates: "recommend_slideshow_templates",
32379
32429
  create_run: "create_slideshow_run",
32380
32430
  get_run: "get_slideshow_run",
32381
32431
  update_run: "update_slideshow_run",
@@ -32508,9 +32558,9 @@ var init_v1_mcp = __esm({
32508
32558
  // src/v1-cli.ts
32509
32559
  init_src2();
32510
32560
  import { cp, readFile, writeFile, mkdir, chmod, copyFile, rm } from "fs/promises";
32511
- import { existsSync as existsSync4, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
32512
- import { homedir as homedir3 } from "os";
32513
- import { join as join4, resolve as resolve2 } from "path";
32561
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
32562
+ import { homedir as homedir4 } from "os";
32563
+ import { join as join6, resolve as resolve2 } from "path";
32514
32564
  import { Command } from "commander";
32515
32565
 
32516
32566
  // src/runtime/mcp-hosts.ts
@@ -33190,7 +33240,7 @@ function validateRun(moduleUrl, args) {
33190
33240
 
33191
33241
  // src/studioIdentity.ts
33192
33242
  var FIXTURE_WORKSPACE_IDS = /* @__PURE__ */ new Set(["workspace_local"]);
33193
- var IDENTITY_CHAIN_HINT = "Studio login \u2192 Dashboard create API key \u2192 easysociable auth login --api-key <key> \u2192 create Run \u2192 open studioUrl as the same account. See docs/operations/studio-identity-chain.md";
33243
+ var IDENTITY_CHAIN_HINT = "Run `easysociable auth login` (approve in the browser you use for Studio) \u2192 create Run \u2192 open studioUrl as the same account. See docs/operations/studio-identity-chain.md";
33194
33244
  async function assertHumanStudioIdentity(client, options = {}) {
33195
33245
  if (options.allowLocalFixture === true || process.env.EASYSOCIABLE_ALLOW_LOCAL_FIXTURE === "1")
33196
33246
  return;
@@ -33203,25 +33253,329 @@ async function assertHumanStudioIdentity(client, options = {}) {
33203
33253
  }
33204
33254
  }
33205
33255
 
33256
+ // src/browserLogin.ts
33257
+ import { spawn as spawn2 } from "child_process";
33258
+ import { randomBytes } from "crypto";
33259
+ import { createServer } from "http";
33260
+ var DONE_HTML = '<!doctype html><meta charset="utf-8"><title>EasySociable</title><body style="font-family:system-ui;text-align:center;padding:64px"><h1>Agent connected</h1><p>You can close this tab and return to your Agent.</p></body>';
33261
+ function openBrowser(url2) {
33262
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url2]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url2]] : ["xdg-open", [url2]];
33263
+ try {
33264
+ spawn2(cmd, args, { stdio: "ignore", detached: true }).on("error", () => {
33265
+ }).unref();
33266
+ } catch {
33267
+ }
33268
+ }
33269
+ function waitForBrowserLogin(options) {
33270
+ const state = randomBytes(16).toString("hex");
33271
+ return new Promise((resolve3, reject) => {
33272
+ const server = createServer((req, res) => {
33273
+ if (req.method !== "POST" || !req.url?.startsWith("/callback")) {
33274
+ res.writeHead(404).end();
33275
+ return;
33276
+ }
33277
+ let body = "";
33278
+ req.on("data", (chunk) => {
33279
+ body += chunk;
33280
+ });
33281
+ req.on("end", () => {
33282
+ const form = new URLSearchParams(body);
33283
+ const apiKey = form.get("apiKey") ?? "";
33284
+ if (form.get("state") !== state || !apiKey.startsWith("es_")) {
33285
+ res.writeHead(400, { "content-type": "text/plain" }).end("Invalid login callback");
33286
+ return;
33287
+ }
33288
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(DONE_HTML);
33289
+ finish(null, { apiKey, gatewayUrl: (form.get("gatewayUrl") ?? "").replace(/\/$/, "") });
33290
+ });
33291
+ });
33292
+ const timer = setTimeout(() => finish(new Error("Browser login timed out. Re-run `easysociable auth login`.")), options.timeoutMs ?? 5 * 6e4);
33293
+ function finish(error52, result) {
33294
+ clearTimeout(timer);
33295
+ server.close();
33296
+ if (error52) reject(error52);
33297
+ else resolve3(result);
33298
+ }
33299
+ server.on("error", (error52) => finish(error52));
33300
+ server.listen(0, "127.0.0.1", () => {
33301
+ const { port } = server.address();
33302
+ const url2 = new URL("/cli/authorize", options.siteUrl);
33303
+ url2.searchParams.set("port", String(port));
33304
+ url2.searchParams.set("state", state);
33305
+ options.onReady(url2.toString());
33306
+ });
33307
+ });
33308
+ }
33309
+
33310
+ // src/formulas.ts
33311
+ import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
33312
+ import { homedir as homedir3 } from "os";
33313
+ import { join as join4 } from "path";
33314
+ var DEFAULT_CANDIDATES = [
33315
+ process.env.EASYSOCIABLE_FORMULA_CATALOG,
33316
+ join4(homedir3(), "Documents", "Obsidian Vault", "TikTok Radar", "graph")
33317
+ ].filter((p) => Boolean(p));
33318
+ function resolveCatalogDir(explicit) {
33319
+ if (explicit) {
33320
+ if (!existsSync4(join4(explicit, "catalog.json"))) {
33321
+ throw new Error(`No catalog.json under ${explicit}`);
33322
+ }
33323
+ return explicit;
33324
+ }
33325
+ for (const candidate of DEFAULT_CANDIDATES) {
33326
+ if (existsSync4(join4(candidate, "catalog.json"))) return candidate;
33327
+ }
33328
+ throw new Error(
33329
+ `Formula catalog not found. Set EASYSOCIABLE_FORMULA_CATALOG to the graph/ dir (expected catalog.json). Tried: ${DEFAULT_CANDIDATES.join(", ")}`
33330
+ );
33331
+ }
33332
+ function readJson(path) {
33333
+ return JSON.parse(readFileSync3(path, "utf8"));
33334
+ }
33335
+ function loadCatalog(dir) {
33336
+ const root = resolveCatalogDir(dir);
33337
+ return readJson(join4(root, "catalog.json"));
33338
+ }
33339
+ function listFormulas(opts = {}) {
33340
+ const root = resolveCatalogDir(opts.dir);
33341
+ const formulasDir = join4(root, "formulas");
33342
+ if (!existsSync4(formulasDir)) return [];
33343
+ const cards = [];
33344
+ for (const name of readdirSync2(formulasDir)) {
33345
+ if (!name.startsWith("F-") || !name.endsWith(".json")) continue;
33346
+ const card = readJson(join4(formulasDir, name));
33347
+ if (opts.recipe && card.recipe !== opts.recipe) continue;
33348
+ if (opts.niche && card.niche !== opts.niche) continue;
33349
+ if (opts.status && card.status !== opts.status) continue;
33350
+ if (opts.platform && card.platform !== opts.platform) continue;
33351
+ cards.push(card);
33352
+ }
33353
+ return cards.sort((a, b) => {
33354
+ const statusRank = (s) => s === "published" ? 0 : 1;
33355
+ const d = statusRank(a.status) - statusRank(b.status);
33356
+ if (d !== 0) return d;
33357
+ return (b.evidence.account_count || 0) - (a.evidence.account_count || 0);
33358
+ });
33359
+ }
33360
+ function getFormula(id, dir) {
33361
+ const root = resolveCatalogDir(dir);
33362
+ const path = join4(root, "formulas", `${id}.json`);
33363
+ if (!existsSync4(path)) throw new Error(`Formula not found: ${id}`);
33364
+ const looksPath = join4(root, "looks.json");
33365
+ const looks = existsSync4(looksPath) ? readJson(looksPath)[id] : void 0;
33366
+ return { ...readJson(path), looks: looks ?? [] };
33367
+ }
33368
+ async function fetchHostedFormula(id, baseUrl, fetchImpl = fetch) {
33369
+ const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/v1/formulas/${encodeURIComponent(id)}`);
33370
+ const body = await response.json().catch(() => null);
33371
+ if (!response.ok || !body?.ok || !body.data) {
33372
+ throw new Error(`Formula not found: ${id} (${body?.error?.message ?? `HTTP ${response.status}`})`);
33373
+ }
33374
+ return body.data;
33375
+ }
33376
+ async function resolveFormula(id, opts) {
33377
+ if (opts.dir) return getFormula(id, opts.dir);
33378
+ try {
33379
+ return getFormula(id);
33380
+ } catch {
33381
+ return fetchHostedFormula(id, opts.baseUrl, opts.fetchImpl);
33382
+ }
33383
+ }
33384
+ function recommendFormulas(input) {
33385
+ const limit = input.limit ?? 5;
33386
+ const all = listFormulas({
33387
+ dir: input.dir,
33388
+ recipe: input.recipe,
33389
+ platform: input.platform
33390
+ });
33391
+ const scored = all.map((card) => {
33392
+ let score = 0;
33393
+ const reasons = [];
33394
+ if (card.recipe === input.recipe) {
33395
+ score += 5;
33396
+ reasons.push("recipe match");
33397
+ }
33398
+ if (input.niche && card.niche === input.niche) {
33399
+ score += 2;
33400
+ reasons.push("niche match");
33401
+ } else if (input.niche && card.fit?.length) {
33402
+ score += 0.5;
33403
+ reasons.push("cross-niche kept");
33404
+ }
33405
+ if (card.status === "published") {
33406
+ score += 2;
33407
+ reasons.push(`published (${card.evidence.account_count} accounts)`);
33408
+ } else {
33409
+ reasons.push("hypothesis \u2014 weak evidence");
33410
+ }
33411
+ const points = card.beats.filter((b) => b.role === "point").length;
33412
+ if (typeof input.factCount === "number" && input.factCount > 0) {
33413
+ if (input.factCount >= points) {
33414
+ score += 1.5;
33415
+ reasons.push("enough facts for point beats");
33416
+ } else {
33417
+ score -= 1;
33418
+ reasons.push(`need ~${points} facts, have ${input.factCount}`);
33419
+ }
33420
+ }
33421
+ const newest = card.evidence.newest_days_ago;
33422
+ if (typeof newest === "number" && newest <= 60) {
33423
+ score += 0.5;
33424
+ reasons.push("recent evidence");
33425
+ }
33426
+ score += Math.min(card.evidence.account_count, 5) * 0.3;
33427
+ return { ...card, score, reasons };
33428
+ });
33429
+ scored.sort((a, b) => b.score - a.score);
33430
+ const top = scored.slice(0, limit);
33431
+ const best = top[0]?.score ?? 0;
33432
+ if (!top.length || best < 4) {
33433
+ return {
33434
+ cards: top,
33435
+ stop: true,
33436
+ message: "Top score is low \u2014 change the angle or recipe rather than forcing a card."
33437
+ };
33438
+ }
33439
+ return { cards: top, stop: false };
33440
+ }
33441
+
33442
+ // src/hooks.ts
33443
+ import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
33444
+ import { join as join5 } from "path";
33445
+ function readJson2(path) {
33446
+ return JSON.parse(readFileSync4(path, "utf8"));
33447
+ }
33448
+ function loadHooksCatalog(dir) {
33449
+ const root = resolveCatalogDir(dir);
33450
+ const path = join5(root, "hooks_catalog.json");
33451
+ if (!existsSync5(path)) {
33452
+ throw new Error(`No hooks_catalog.json under ${root}. Run: python3 scripts/compile_hooks.py`);
33453
+ }
33454
+ return readJson2(path);
33455
+ }
33456
+ function listHooks(opts = {}) {
33457
+ const root = resolveCatalogDir(opts.dir);
33458
+ const hooksDir = join5(root, "hooks");
33459
+ if (!existsSync5(hooksDir))
33460
+ return [];
33461
+ const cards = [];
33462
+ for (const name of readdirSync3(hooksDir)) {
33463
+ if (!name.startsWith("H-") || !name.endsWith(".json"))
33464
+ continue;
33465
+ const card = readJson2(join5(hooksDir, name));
33466
+ if (opts.type && card.hook_type !== opts.type)
33467
+ continue;
33468
+ if (opts.niche && card.niche !== opts.niche)
33469
+ continue;
33470
+ if (opts.status && card.status !== opts.status)
33471
+ continue;
33472
+ if (opts.formula && !(card.formula_ids || []).includes(opts.formula))
33473
+ continue;
33474
+ cards.push(card);
33475
+ }
33476
+ return cards.sort((a, b) => {
33477
+ const statusRank = (s) => s === "published" ? 0 : 1;
33478
+ const d = statusRank(a.status) - statusRank(b.status);
33479
+ if (d !== 0)
33480
+ return d;
33481
+ return (b.evidence.account_count || 0) - (a.evidence.account_count || 0);
33482
+ });
33483
+ }
33484
+ function getHook(id, dir) {
33485
+ const root = resolveCatalogDir(dir);
33486
+ const path = join5(root, "hooks", `${id}.json`);
33487
+ if (!existsSync5(path))
33488
+ throw new Error(`Hook not found: ${id}`);
33489
+ return readJson2(path);
33490
+ }
33491
+ async function fetchHostedHook(id, baseUrl, fetchImpl = fetch) {
33492
+ const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/v1/hooks/${encodeURIComponent(id)}`);
33493
+ const body = await response.json().catch(() => null);
33494
+ if (!response.ok || !body?.ok || !body.data) {
33495
+ throw new Error(`Hook not found: ${id} (${body?.error?.message ?? `HTTP ${response.status}`})`);
33496
+ }
33497
+ return body.data;
33498
+ }
33499
+ async function resolveHook(id, opts) {
33500
+ if (opts.dir)
33501
+ return getHook(id, opts.dir);
33502
+ try {
33503
+ return getHook(id);
33504
+ } catch {
33505
+ return fetchHostedHook(id, opts.baseUrl, opts.fetchImpl);
33506
+ }
33507
+ }
33508
+ function recommendHooks(input) {
33509
+ const limit = input.limit ?? 5;
33510
+ const all = listHooks({
33511
+ dir: input.dir,
33512
+ type: input.type
33513
+ });
33514
+ const topic = (input.topic || "").toLowerCase();
33515
+ const scored = all.map((card) => {
33516
+ let score = 0;
33517
+ const reasons = [];
33518
+ if (input.type && card.hook_type === input.type) {
33519
+ score += 3;
33520
+ reasons.push("hook_type match");
33521
+ }
33522
+ if (input.niche && card.niche === input.niche) {
33523
+ score += 2;
33524
+ reasons.push("niche match");
33525
+ } else if (input.niche) {
33526
+ score += 0.25;
33527
+ reasons.push("cross-niche kept");
33528
+ }
33529
+ if (card.status === "published") {
33530
+ score += 2;
33531
+ reasons.push(`published (${card.evidence.account_count} accounts)`);
33532
+ } else {
33533
+ reasons.push("hypothesis \u2014 weak evidence");
33534
+ }
33535
+ const newest = card.evidence.newest_days_ago;
33536
+ if (typeof newest === "number" && newest <= 60) {
33537
+ score += 0.5;
33538
+ reasons.push("recent evidence");
33539
+ }
33540
+ score += Math.min(card.evidence.account_count, 5) * 0.3;
33541
+ if (topic && card.format.toLowerCase().includes("{x}")) {
33542
+ score += 0.25;
33543
+ reasons.push("format has {X} slot");
33544
+ }
33545
+ return { ...card, score, reasons };
33546
+ });
33547
+ scored.sort((a, b) => b.score - a.score);
33548
+ const top = scored.slice(0, limit);
33549
+ const best = top[0]?.score ?? 0;
33550
+ if (!top.length || best < 2) {
33551
+ return {
33552
+ cards: top,
33553
+ stop: true,
33554
+ message: "Top score is low \u2014 try another niche/type or gather more labeled hooks."
33555
+ };
33556
+ }
33557
+ return { cards: top, stop: false };
33558
+ }
33559
+
33206
33560
  // src/v1-cli.ts
33207
33561
  var CLI_MODULE_URL = import.meta.url;
33208
33562
  function readCliVersion() {
33209
33563
  try {
33210
- const pkg = JSON.parse(readFileSync3(join4(cliPackageRoot(CLI_MODULE_URL), "package.json"), "utf8"));
33564
+ const pkg = JSON.parse(readFileSync5(join6(cliPackageRoot(CLI_MODULE_URL), "package.json"), "utf8"));
33211
33565
  return typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
33212
33566
  } catch {
33213
33567
  return "0.0.0";
33214
33568
  }
33215
33569
  }
33216
33570
  var HEADLESS_RENDER_DISABLED = "PNG delivery is Studio Download (browser ZIP). CLI/MCP hosted PNG jobs are retired. Open the Run in Studio and use Download.";
33217
- var credentialPath = join4(homedir3(), ".config", "easysociable", "credentials.json");
33571
+ var credentialPath = join6(homedir4(), ".config", "easysociable", "credentials.json");
33218
33572
  function resolveInstallSkillIds(options) {
33219
33573
  return parseInstallSkills(typeof options.skills === "string" ? options.skills : void 0);
33220
33574
  }
33221
33575
  function readStoredCredentials() {
33222
33576
  try {
33223
- if (!existsSync4(credentialPath)) return null;
33224
- const parsed = JSON.parse(readFileSync3(credentialPath, "utf8"));
33577
+ if (!existsSync6(credentialPath)) return null;
33578
+ const parsed = JSON.parse(readFileSync5(credentialPath, "utf8"));
33225
33579
  if (typeof parsed.apiKey !== "string" || typeof parsed.baseUrl !== "string") return null;
33226
33580
  return { apiKey: parsed.apiKey, baseUrl: parsed.baseUrl };
33227
33581
  } catch {
@@ -33229,7 +33583,7 @@ function readStoredCredentials() {
33229
33583
  }
33230
33584
  }
33231
33585
  async function writeStoredCredentials(credentials) {
33232
- await mkdir(join4(homedir3(), ".config", "easysociable"), { recursive: true, mode: 448 });
33586
+ await mkdir(join6(homedir4(), ".config", "easysociable"), { recursive: true, mode: 448 });
33233
33587
  await writeFile(credentialPath, `${JSON.stringify(credentials, null, 2)}
33234
33588
  `, { mode: 384 });
33235
33589
  await chmod(credentialPath, 384);
@@ -33252,10 +33606,13 @@ function commandWantsJson(command) {
33252
33606
  const withGlobals = typeof command.optsWithGlobals === "function" ? command.optsWithGlobals() : {};
33253
33607
  return withGlobals.json === true;
33254
33608
  }
33609
+ function gatewayBaseUrl(stored = readStoredCredentials()) {
33610
+ return stored?.baseUrl ?? process.env.EASYSOCIABLE_GATEWAY_URL ?? process.env.EASYSOCIABLE_API_URL ?? "http://127.0.0.1:8787";
33611
+ }
33255
33612
  function clientFromEnv() {
33256
33613
  const stored = readStoredCredentials();
33257
33614
  const allowEnv = process.env.EASYSOCIABLE_ALLOW_ENV_CREDENTIALS === "true";
33258
- const baseUrl = stored?.baseUrl ?? process.env.EASYSOCIABLE_GATEWAY_URL ?? process.env.EASYSOCIABLE_API_URL ?? "http://127.0.0.1:8787";
33615
+ const baseUrl = gatewayBaseUrl(stored);
33259
33616
  const apiKey = stored?.apiKey ?? (allowEnv ? process.env.EASYSOCIABLE_API_KEY : void 0);
33260
33617
  if (!apiKey) throw new Error("EASYSOCIABLE_API_KEY is required. Run `easysociable auth login` first.");
33261
33618
  return new TemplateGatewayClient({
@@ -33270,14 +33627,14 @@ function jsonOption(command) {
33270
33627
  }
33271
33628
  function defaultEngineRoot() {
33272
33629
  try {
33273
- if (existsSync4("/workspace") && statSync2("/workspace").isDirectory())
33630
+ if (existsSync6("/workspace") && statSync2("/workspace").isDirectory())
33274
33631
  return "/workspace";
33275
33632
  } catch {
33276
33633
  }
33277
33634
  return process.cwd();
33278
33635
  }
33279
33636
  function installHome(options) {
33280
- return typeof options.home === "string" && options.home ? String(options.home) : homedir3();
33637
+ return typeof options.home === "string" && options.home ? String(options.home) : homedir4();
33281
33638
  }
33282
33639
  function installSource(options) {
33283
33640
  if (typeof options.source === "string" && options.source)
@@ -33288,17 +33645,17 @@ function installSource(options) {
33288
33645
  }
33289
33646
  function detectedHosts(home) {
33290
33647
  const candidates = [
33291
- { host: "codex", dir: join4(home, ".codex") },
33292
- { host: "claude", dir: join4(home, ".claude") }
33648
+ { host: "codex", dir: join6(home, ".codex") },
33649
+ { host: "claude", dir: join6(home, ".claude") }
33293
33650
  ];
33294
- return candidates.filter((item) => existsSync4(item.dir)).map((item) => ({ host: item.host, configPath: join4(item.dir, "mcp.json"), skillsPath: join4(item.dir, "skills") }));
33651
+ return candidates.filter((item) => existsSync6(item.dir)).map((item) => ({ host: item.host, configPath: join6(item.dir, "mcp.json"), skillsPath: join6(item.dir, "skills") }));
33295
33652
  }
33296
33653
  async function planInstallation(options) {
33297
33654
  const home = installHome(options);
33298
33655
  const source = installSource(options);
33299
33656
  const hosts = detectedHosts(home);
33300
33657
  const skillNames = resolveInstallSkillIds(options);
33301
- const missingSkills = skillNames.filter((name) => !existsSync4(join4(source, name)));
33658
+ const missingSkills = skillNames.filter((name) => !existsSync6(join6(source, name)));
33302
33659
  return {
33303
33660
  source,
33304
33661
  home,
@@ -33307,7 +33664,7 @@ async function planInstallation(options) {
33307
33664
  missingSkills,
33308
33665
  changes: hosts.flatMap((host) => [
33309
33666
  `merge ${host.configPath} with one easysociable MCP registration`,
33310
- ...skillNames.map((name) => `install ${join4(host.skillsPath, name)}`)
33667
+ ...skillNames.map((name) => `install ${join6(host.skillsPath, name)}`)
33311
33668
  ])
33312
33669
  };
33313
33670
  }
@@ -33323,7 +33680,7 @@ async function applyInstallation(options) {
33323
33680
  const backupSuffix = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
33324
33681
  const backups = [];
33325
33682
  for (const host of detectedHosts(String(plan.home))) {
33326
- if (existsSync4(host.configPath)) {
33683
+ if (existsSync6(host.configPath)) {
33327
33684
  const backupPath = `${host.configPath}.bak.${backupSuffix}`;
33328
33685
  await copyFile(host.configPath, backupPath);
33329
33686
  backups.push(backupPath);
@@ -33331,7 +33688,7 @@ async function applyInstallation(options) {
33331
33688
  await installHostConfig({ host: host.host, dataDir: "", configPath: host.configPath });
33332
33689
  await mkdir(host.skillsPath, { recursive: true, mode: 448 });
33333
33690
  for (const skill of resolveInstallSkillIds(options))
33334
- await cp(join4(String(plan.source), skill), join4(host.skillsPath, skill), { recursive: true, force: false, errorOnExist: false });
33691
+ await cp(join6(String(plan.source), skill), join6(host.skillsPath, skill), { recursive: true, force: false, errorOnExist: false });
33335
33692
  }
33336
33693
  if (options.json !== true) {
33337
33694
  process.stdout.write(`Installed skills your Agent will use:
@@ -33478,12 +33835,23 @@ function buildV1Cli() {
33478
33835
  jsonOption(auth.command("status").description("Show authentication status")).action(async (...args) => {
33479
33836
  const command = args.at(-1);
33480
33837
  const stored = readStoredCredentials();
33481
- print({ authenticated: Boolean(stored), baseUrl: stored?.baseUrl ?? process.env.EASYSOCIABLE_GATEWAY_URL ?? process.env.EASYSOCIABLE_API_URL ?? "http://127.0.0.1:8787", credentialPath }, Boolean(command.opts().json));
33838
+ print({ authenticated: Boolean(stored), baseUrl: gatewayBaseUrl(stored), credentialPath }, Boolean(command.opts().json));
33482
33839
  });
33483
- auth.command("login").description("Store an API key in the OS user credential file").requiredOption("--api-key <key>").option("--gateway-url <url>", "Gateway URL", "http://127.0.0.1:8787").action(async (...args) => {
33840
+ auth.command("login").description("Sign in via the browser (or store an existing API key with --api-key)").option("--api-key <key>").option("--gateway-url <url>", "Gateway URL").option("--site-url <url>", "EasySociable site URL", process.env.EASYSOCIABLE_SITE_URL ?? "https://easysociable.com").action(async (...args) => {
33484
33841
  const command = args.at(-1);
33485
33842
  const opts = command.opts();
33486
- await writeStoredCredentials({ apiKey: opts.apiKey, baseUrl: opts.gatewayUrl.replace(/\/$/, "") });
33843
+ const login = opts.apiKey ? { apiKey: opts.apiKey, gatewayUrl: "" } : await waitForBrowserLogin({
33844
+ siteUrl: opts.siteUrl,
33845
+ onReady: (url2) => {
33846
+ process.stdout.write(`Opening your browser to connect this Agent:
33847
+ ${url2}
33848
+ Waiting for approval\u2026
33849
+ `);
33850
+ openBrowser(url2);
33851
+ }
33852
+ });
33853
+ const baseUrl = (opts.gatewayUrl ?? (login.gatewayUrl || gatewayBaseUrl(null))).replace(/\/$/, "");
33854
+ await writeStoredCredentials({ apiKey: login.apiKey, baseUrl });
33487
33855
  process.stdout.write(`Stored EasySociable credentials at ${credentialPath}.
33488
33856
  `);
33489
33857
  });
@@ -33503,7 +33871,20 @@ function buildV1Cli() {
33503
33871
  jsonOption(images.command("search").requiredOption("--q <query>")).action(withClient(async (client, opts) => client.requestJson("GET", `/v1/images?q=${encodeURIComponent(String(opts.q))}`)));
33504
33872
  jsonOption(images.command("attest").requiredOption("--id <id>")).action(withClient(async (client, opts) => client.requestJson("POST", `/v1/images/${opts.id}/attest`, { accepted: true })));
33505
33873
  const templates = program.command("templates").description("Create and manage Page Template drafts");
33506
- jsonOption(templates.command("list")).action(withClient(async (client) => client.listTemplates()));
33874
+ jsonOption(templates.command("list").option("--platform <platform>", "tiktok | instagram | linkedin | threads | xiaohongshu").option("--canvas <preset>", "Canvas preset e.g. 9:16, 4:5").option("--role <role>", "Page role filter (weak tag)").option("--limit <n>", "Page size (max 50)").option("--cursor <cursor>", "Offset cursor from a previous page").option("--all", "Return every matching template (drains pages)")).action(withClient(async (client, opts) => {
33875
+ const query = {
33876
+ platforms: opts.platform ? [String(opts.platform)] : void 0,
33877
+ canvasPreset: opts.canvas ? String(opts.canvas) : void 0,
33878
+ roles: opts.role ? [String(opts.role)] : void 0
33879
+ };
33880
+ if (opts.all === true)
33881
+ return client.listAllTemplates(query);
33882
+ const page = await client.listTemplatesPage({ ...query, limit: opts.limit ? Number(opts.limit) : void 0, cursor: opts.cursor ? String(opts.cursor) : void 0 });
33883
+ if (page.nextCursor)
33884
+ process.stderr.write(`More templates: --cursor ${page.nextCursor} or --all
33885
+ `);
33886
+ return page.templates;
33887
+ }));
33507
33888
  jsonOption(templates.command("get").requiredOption("--id <id>")).action(withClient(async (client, opts) => client.getTemplate(String(opts.id))));
33508
33889
  jsonOption(templates.command("create-draft").requiredOption("--file <path>")).action(withClient(async (client, opts) => client.createDraft(JSON.parse(await readFile(String(opts.file), "utf8")))));
33509
33890
  jsonOption(templates.command("update-draft").requiredOption("--id <id>").requiredOption("--file <path>")).action(withClient(async (client, opts) => client.updateDraft(String(opts.id), JSON.parse(await readFile(String(opts.file), "utf8")))));
@@ -33531,6 +33912,10 @@ function buildV1Cli() {
33531
33912
  contentSlug: opts.contentSlug ? String(opts.contentSlug) : void 0,
33532
33913
  status: opts.status ? String(opts.status) : void 0
33533
33914
  })));
33915
+ jsonOption(slideshows.command("recommend").description("Shortlist Page Templates per page from Page Contracts JSON ({ platform, pages: [{ role, density?, slotSummary? }], seed? })").requiredOption("--file <path>").option("--seed <seed>", "Tie-break seed, e.g. the content slug")).action(withClient(async (client, opts) => {
33916
+ const input = JSON.parse(await readFile(String(opts.file), "utf8"));
33917
+ return client.recommendSlideshowTemplates(opts.seed ? { ...input, seed: String(opts.seed) } : input);
33918
+ }));
33534
33919
  jsonOption(slideshows.command("create").requiredOption("--file <path>").option("--allow-local-fixture", "Allow workspace_local fixture keys (dev only)")).action(withClient(async (client, opts) => {
33535
33920
  await assertHumanStudioIdentity(client, { allowLocalFixture: opts.allowLocalFixture === true });
33536
33921
  return client.createSlideshowRun({ pack: JSON.parse(await readFile(String(opts.file), "utf8")) });
@@ -33569,6 +33954,100 @@ function buildV1Cli() {
33569
33954
  jsonOption(renders.command("export").description("Retired: PNG packs come from Studio Download").requiredOption("--id <id>").requiredOption("--output <path>")).action(withClient(async () => {
33570
33955
  throw new Error(HEADLESS_RENDER_DISABLED);
33571
33956
  }));
33957
+ const formulas = program.command("formulas").description("Read Formula cards (local TikTok Radar graph/; `get` falls back to the hosted catalog)");
33958
+ formulas.option("--catalog <path>", "Path to graph/ dir containing catalog.json (or set EASYSOCIABLE_FORMULA_CATALOG)");
33959
+ jsonOption(formulas.command("list").option("--recipe <recipe>").option("--niche <niche>").option("--status <status>").option("--platform <platform>")).action((...args) => {
33960
+ const command = args.at(-1);
33961
+ const opts = command.optsWithGlobals();
33962
+ const cards = listFormulas({
33963
+ dir: opts.catalog ? String(opts.catalog) : void 0,
33964
+ recipe: opts.recipe ? String(opts.recipe) : void 0,
33965
+ niche: opts.niche ? String(opts.niche) : void 0,
33966
+ status: opts.status ? String(opts.status) : void 0,
33967
+ platform: opts.platform ? String(opts.platform) : void 0
33968
+ });
33969
+ print(cards.map((c) => ({
33970
+ id: c.id,
33971
+ recipe: c.recipe,
33972
+ niche: c.niche,
33973
+ swipe: c.swipe,
33974
+ status: c.status,
33975
+ accounts: c.evidence.account_count,
33976
+ posts: c.evidence.post_count,
33977
+ title_template: c.title_template
33978
+ })), commandWantsJson(command));
33979
+ });
33980
+ jsonOption(formulas.command("get").requiredOption("--id <id>")).action(async (...args) => {
33981
+ const command = args.at(-1);
33982
+ const opts = command.optsWithGlobals();
33983
+ const card = await resolveFormula(String(opts.id), { dir: opts.catalog ? String(opts.catalog) : void 0, baseUrl: gatewayBaseUrl() });
33984
+ print(card, commandWantsJson(command));
33985
+ });
33986
+ jsonOption(formulas.command("catalog")).action((...args) => {
33987
+ const command = args.at(-1);
33988
+ const opts = command.optsWithGlobals();
33989
+ print(loadCatalog(opts.catalog ? String(opts.catalog) : void 0), commandWantsJson(command));
33990
+ });
33991
+ jsonOption(formulas.command("recommend").description("Rank cards for a recipe (niche is a bonus). Pass --recipe from Jev theme\u2192recipe.").requiredOption("--recipe <recipe>").option("--niche <niche>").option("--platform <platform>", "tiktok").option("--fact-count <n>", "How many user facts are available to fill point beats").option("--limit <n>", "Max cards", "5")).action((...args) => {
33992
+ const command = args.at(-1);
33993
+ const opts = command.optsWithGlobals();
33994
+ const result = recommendFormulas({
33995
+ recipe: String(opts.recipe),
33996
+ niche: opts.niche ? String(opts.niche) : void 0,
33997
+ platform: opts.platform ? String(opts.platform) : void 0,
33998
+ factCount: opts.factCount != null ? Number(opts.factCount) : void 0,
33999
+ dir: opts.catalog ? String(opts.catalog) : void 0,
34000
+ limit: opts.limit != null ? Number(opts.limit) : 5
34001
+ });
34002
+ print(result, commandWantsJson(command));
34003
+ });
34004
+ const hooks = program.command("hooks").description("Read Hook format molds (local graph/hooks/; `get` falls back to hosted catalog)");
34005
+ hooks.option("--catalog <path>", "Path to graph/ dir (or set EASYSOCIABLE_FORMULA_CATALOG)");
34006
+ jsonOption(hooks.command("list").option("--type <hook_type>", "contrast | list-tease | identity | number | other").option("--niche <niche>").option("--status <status>").option("--formula <formula_id>", "Only hooks soft-linked to this formula")).action((...args) => {
34007
+ const command = args.at(-1);
34008
+ const opts = command.optsWithGlobals();
34009
+ const cards = listHooks({
34010
+ dir: opts.catalog ? String(opts.catalog) : void 0,
34011
+ type: opts.type ? String(opts.type) : void 0,
34012
+ niche: opts.niche ? String(opts.niche) : void 0,
34013
+ status: opts.status ? String(opts.status) : void 0,
34014
+ formula: opts.formula ? String(opts.formula) : void 0
34015
+ });
34016
+ print(cards.map((c) => ({
34017
+ id: c.id,
34018
+ hook_type: c.hook_type,
34019
+ niche: c.niche,
34020
+ status: c.status,
34021
+ accounts: c.evidence.account_count,
34022
+ posts: c.evidence.post_count,
34023
+ formula_ids: c.formula_ids,
34024
+ title: c.title,
34025
+ format: c.format
34026
+ })), commandWantsJson(command));
34027
+ });
34028
+ jsonOption(hooks.command("get").requiredOption("--id <id>")).action(async (...args) => {
34029
+ const command = args.at(-1);
34030
+ const opts = command.optsWithGlobals();
34031
+ const card = await resolveHook(String(opts.id), { dir: opts.catalog ? String(opts.catalog) : void 0, baseUrl: gatewayBaseUrl() });
34032
+ print(card, commandWantsJson(command));
34033
+ });
34034
+ jsonOption(hooks.command("catalog")).action((...args) => {
34035
+ const command = args.at(-1);
34036
+ const opts = command.optsWithGlobals();
34037
+ print(loadHooksCatalog(opts.catalog ? String(opts.catalog) : void 0), commandWantsJson(command));
34038
+ });
34039
+ jsonOption(hooks.command("recommend").description("Rank hook molds for a topic/niche. Agent then fills format with the topic (do not copy example).").option("--topic <topic>", "User topic for {X}").option("--niche <niche>").option("--type <hook_type>").option("--limit <n>", "Max hooks", "5")).action((...args) => {
34040
+ const command = args.at(-1);
34041
+ const opts = command.optsWithGlobals();
34042
+ const result = recommendHooks({
34043
+ topic: opts.topic ? String(opts.topic) : void 0,
34044
+ niche: opts.niche ? String(opts.niche) : void 0,
34045
+ type: opts.type ? String(opts.type) : void 0,
34046
+ dir: opts.catalog ? String(opts.catalog) : void 0,
34047
+ limit: opts.limit != null ? Number(opts.limit) : 5
34048
+ });
34049
+ print(result, commandWantsJson(command));
34050
+ });
33572
34051
  const mcp = program.command("mcp").description("Install and run the unified EasySociable MCP server");
33573
34052
  jsonOption(mcp.command("install").requiredOption("--host <host>", "codex, claude, hermes, or openclaw").option("--config-path <path>")).action((...args) => {
33574
34053
  const command = args.at(-1);
@@ -33598,5 +34077,5 @@ async function runV1Cli(argv = process.argv) {
33598
34077
 
33599
34078
  // src/bin.ts
33600
34079
  var exitCode = await runV1Cli();
33601
- process.exit(exitCode);
34080
+ process.stdout.write("", () => process.exit(exitCode));
33602
34081
  //# sourceMappingURL=easysociable.js.map