@easysociable/cli 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,10 +68,39 @@ indexes. Topics can cite either one without forcing a product connection.
68
68
 
69
69
  Default `--root` / `--engine` parent is `/workspace` when that directory exists, otherwise the current working directory.
70
70
 
71
+ ## MCP Server Configuration
72
+
73
+ EasySociable exposes native Model Context Protocol (MCP) tools for Claude Desktop, Claude Code, Cursor, and any MCP-compatible agent host.
74
+
75
+ ### Claude Desktop (`claude_desktop_config.json`)
76
+ ```json
77
+ {
78
+ "mcpServers": {
79
+ "easysociable": {
80
+ "command": "npx",
81
+ "args": ["-y", "@easysociable/cli", "mcp", "serve", "--transport", "stdio"]
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ ### Cursor (`~/.cursor/mcp.json`)
88
+ ```json
89
+ {
90
+ "mcpServers": {
91
+ "easysociable": {
92
+ "command": "easysociable",
93
+ "args": ["mcp", "serve", "--transport", "stdio"]
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
71
99
  ## Authentication
72
100
 
73
101
  ```bash
74
- easysociable auth login --api-key <key> --gateway-url https://gateway.easysociable.com
102
+ easysociable auth login # opens the browser; approve and the key is stored automatically
103
+ easysociable auth login --api-key <key> --gateway-url https://gateway.easysociable.com # headless / CI
75
104
  easysociable auth status
76
105
  easysociable templates list --json
77
106
  easysociable mcp serve --transport stdio
@@ -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(),
@@ -16638,7 +16645,7 @@ var init_client = __esm({
16638
16645
  code: "TEMPLATE_API_KEY_REQUIRED",
16639
16646
  message: "Template API key is required",
16640
16647
  retryable: false,
16641
- suggestedAction: "Run `easysociable auth login --api-key <key>`"
16648
+ suggestedAction: "Run `easysociable auth login` and approve in the browser"
16642
16649
  });
16643
16650
  }
16644
16651
  const url2 = `${this.options.baseUrl.replace(/\/$/, "")}/v1/slideshow/runs/${encodeURIComponent(runId)}/artifacts/${pageIndex}`;
@@ -16833,7 +16840,7 @@ var init_client = __esm({
16833
16840
  code: "TEMPLATE_API_KEY_REQUIRED",
16834
16841
  message: "Template API key is required",
16835
16842
  retryable: false,
16836
- 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"
16837
16844
  });
16838
16845
  }
16839
16846
  const maxRetries = this.options.maxRetries ?? 2;
@@ -32551,9 +32558,9 @@ var init_v1_mcp = __esm({
32551
32558
  // src/v1-cli.ts
32552
32559
  init_src2();
32553
32560
  import { cp, readFile, writeFile, mkdir, chmod, copyFile, rm } from "fs/promises";
32554
- import { existsSync as existsSync4, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
32555
- import { homedir as homedir3 } from "os";
32556
- 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";
32557
32564
  import { Command } from "commander";
32558
32565
 
32559
32566
  // src/runtime/mcp-hosts.ts
@@ -33233,7 +33240,7 @@ function validateRun(moduleUrl, args) {
33233
33240
 
33234
33241
  // src/studioIdentity.ts
33235
33242
  var FIXTURE_WORKSPACE_IDS = /* @__PURE__ */ new Set(["workspace_local"]);
33236
- 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";
33237
33244
  async function assertHumanStudioIdentity(client, options = {}) {
33238
33245
  if (options.allowLocalFixture === true || process.env.EASYSOCIABLE_ALLOW_LOCAL_FIXTURE === "1")
33239
33246
  return;
@@ -33246,25 +33253,329 @@ async function assertHumanStudioIdentity(client, options = {}) {
33246
33253
  }
33247
33254
  }
33248
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
+
33249
33560
  // src/v1-cli.ts
33250
33561
  var CLI_MODULE_URL = import.meta.url;
33251
33562
  function readCliVersion() {
33252
33563
  try {
33253
- 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"));
33254
33565
  return typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
33255
33566
  } catch {
33256
33567
  return "0.0.0";
33257
33568
  }
33258
33569
  }
33259
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.";
33260
- var credentialPath = join4(homedir3(), ".config", "easysociable", "credentials.json");
33571
+ var credentialPath = join6(homedir4(), ".config", "easysociable", "credentials.json");
33261
33572
  function resolveInstallSkillIds(options) {
33262
33573
  return parseInstallSkills(typeof options.skills === "string" ? options.skills : void 0);
33263
33574
  }
33264
33575
  function readStoredCredentials() {
33265
33576
  try {
33266
- if (!existsSync4(credentialPath)) return null;
33267
- const parsed = JSON.parse(readFileSync3(credentialPath, "utf8"));
33577
+ if (!existsSync6(credentialPath)) return null;
33578
+ const parsed = JSON.parse(readFileSync5(credentialPath, "utf8"));
33268
33579
  if (typeof parsed.apiKey !== "string" || typeof parsed.baseUrl !== "string") return null;
33269
33580
  return { apiKey: parsed.apiKey, baseUrl: parsed.baseUrl };
33270
33581
  } catch {
@@ -33272,7 +33583,7 @@ function readStoredCredentials() {
33272
33583
  }
33273
33584
  }
33274
33585
  async function writeStoredCredentials(credentials) {
33275
- await mkdir(join4(homedir3(), ".config", "easysociable"), { recursive: true, mode: 448 });
33586
+ await mkdir(join6(homedir4(), ".config", "easysociable"), { recursive: true, mode: 448 });
33276
33587
  await writeFile(credentialPath, `${JSON.stringify(credentials, null, 2)}
33277
33588
  `, { mode: 384 });
33278
33589
  await chmod(credentialPath, 384);
@@ -33295,10 +33606,13 @@ function commandWantsJson(command) {
33295
33606
  const withGlobals = typeof command.optsWithGlobals === "function" ? command.optsWithGlobals() : {};
33296
33607
  return withGlobals.json === true;
33297
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
+ }
33298
33612
  function clientFromEnv() {
33299
33613
  const stored = readStoredCredentials();
33300
33614
  const allowEnv = process.env.EASYSOCIABLE_ALLOW_ENV_CREDENTIALS === "true";
33301
- 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);
33302
33616
  const apiKey = stored?.apiKey ?? (allowEnv ? process.env.EASYSOCIABLE_API_KEY : void 0);
33303
33617
  if (!apiKey) throw new Error("EASYSOCIABLE_API_KEY is required. Run `easysociable auth login` first.");
33304
33618
  return new TemplateGatewayClient({
@@ -33313,14 +33627,14 @@ function jsonOption(command) {
33313
33627
  }
33314
33628
  function defaultEngineRoot() {
33315
33629
  try {
33316
- if (existsSync4("/workspace") && statSync2("/workspace").isDirectory())
33630
+ if (existsSync6("/workspace") && statSync2("/workspace").isDirectory())
33317
33631
  return "/workspace";
33318
33632
  } catch {
33319
33633
  }
33320
33634
  return process.cwd();
33321
33635
  }
33322
33636
  function installHome(options) {
33323
- return typeof options.home === "string" && options.home ? String(options.home) : homedir3();
33637
+ return typeof options.home === "string" && options.home ? String(options.home) : homedir4();
33324
33638
  }
33325
33639
  function installSource(options) {
33326
33640
  if (typeof options.source === "string" && options.source)
@@ -33331,17 +33645,17 @@ function installSource(options) {
33331
33645
  }
33332
33646
  function detectedHosts(home) {
33333
33647
  const candidates = [
33334
- { host: "codex", dir: join4(home, ".codex") },
33335
- { host: "claude", dir: join4(home, ".claude") }
33648
+ { host: "codex", dir: join6(home, ".codex") },
33649
+ { host: "claude", dir: join6(home, ".claude") }
33336
33650
  ];
33337
- 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") }));
33338
33652
  }
33339
33653
  async function planInstallation(options) {
33340
33654
  const home = installHome(options);
33341
33655
  const source = installSource(options);
33342
33656
  const hosts = detectedHosts(home);
33343
33657
  const skillNames = resolveInstallSkillIds(options);
33344
- const missingSkills = skillNames.filter((name) => !existsSync4(join4(source, name)));
33658
+ const missingSkills = skillNames.filter((name) => !existsSync6(join6(source, name)));
33345
33659
  return {
33346
33660
  source,
33347
33661
  home,
@@ -33350,7 +33664,7 @@ async function planInstallation(options) {
33350
33664
  missingSkills,
33351
33665
  changes: hosts.flatMap((host) => [
33352
33666
  `merge ${host.configPath} with one easysociable MCP registration`,
33353
- ...skillNames.map((name) => `install ${join4(host.skillsPath, name)}`)
33667
+ ...skillNames.map((name) => `install ${join6(host.skillsPath, name)}`)
33354
33668
  ])
33355
33669
  };
33356
33670
  }
@@ -33366,7 +33680,7 @@ async function applyInstallation(options) {
33366
33680
  const backupSuffix = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
33367
33681
  const backups = [];
33368
33682
  for (const host of detectedHosts(String(plan.home))) {
33369
- if (existsSync4(host.configPath)) {
33683
+ if (existsSync6(host.configPath)) {
33370
33684
  const backupPath = `${host.configPath}.bak.${backupSuffix}`;
33371
33685
  await copyFile(host.configPath, backupPath);
33372
33686
  backups.push(backupPath);
@@ -33374,7 +33688,7 @@ async function applyInstallation(options) {
33374
33688
  await installHostConfig({ host: host.host, dataDir: "", configPath: host.configPath });
33375
33689
  await mkdir(host.skillsPath, { recursive: true, mode: 448 });
33376
33690
  for (const skill of resolveInstallSkillIds(options))
33377
- 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 });
33378
33692
  }
33379
33693
  if (options.json !== true) {
33380
33694
  process.stdout.write(`Installed skills your Agent will use:
@@ -33521,12 +33835,23 @@ function buildV1Cli() {
33521
33835
  jsonOption(auth.command("status").description("Show authentication status")).action(async (...args) => {
33522
33836
  const command = args.at(-1);
33523
33837
  const stored = readStoredCredentials();
33524
- 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));
33525
33839
  });
33526
- 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) => {
33527
33841
  const command = args.at(-1);
33528
33842
  const opts = command.opts();
33529
- 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 });
33530
33855
  process.stdout.write(`Stored EasySociable credentials at ${credentialPath}.
33531
33856
  `);
33532
33857
  });
@@ -33629,6 +33954,100 @@ function buildV1Cli() {
33629
33954
  jsonOption(renders.command("export").description("Retired: PNG packs come from Studio Download").requiredOption("--id <id>").requiredOption("--output <path>")).action(withClient(async () => {
33630
33955
  throw new Error(HEADLESS_RENDER_DISABLED);
33631
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
+ });
33632
34051
  const mcp = program.command("mcp").description("Install and run the unified EasySociable MCP server");
33633
34052
  jsonOption(mcp.command("install").requiredOption("--host <host>", "codex, claude, hermes, or openclaw").option("--config-path <path>")).action((...args) => {
33634
34053
  const command = args.at(-1);