@easysociable/cli 0.1.12 → 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.
- package/README.md +2 -1
- package/dist/easysociable.js +446 -27
- package/dist/easysociable.js.map +1 -1
- package/package.json +13 -14
- package/scripts/engine/contracts.mjs +4 -3
- package/skills/ONBOARDING.md +5 -9
- package/skills/easysociable-slideshow/SKILL.md +57 -20
- package/skills/easysociable-slideshow/references/CONTENT_PACK.md +13 -10
- package/skills/easysociable-slideshow/workflows/CREATE_RUN.md +14 -12
- package/skills/easysociable-template/scripts/generate-theme-from-hex.mjs +0 -0
package/README.md
CHANGED
|
@@ -71,7 +71,8 @@ Default `--root` / `--engine` parent is `/workspace` when that directory exists,
|
|
|
71
71
|
## Authentication
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
|
-
easysociable auth login
|
|
74
|
+
easysociable auth login # opens the browser; approve and the key is stored automatically
|
|
75
|
+
easysociable auth login --api-key <key> --gateway-url https://gateway.easysociable.com # headless / CI
|
|
75
76
|
easysociable auth status
|
|
76
77
|
easysociable templates list --json
|
|
77
78
|
easysociable mcp serve --transport stdio
|
package/dist/easysociable.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
32555
|
-
import { homedir as
|
|
32556
|
-
import { join as
|
|
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 = "
|
|
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(
|
|
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 =
|
|
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 (!
|
|
33267
|
-
const parsed = JSON.parse(
|
|
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(
|
|
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
|
|
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 (
|
|
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) :
|
|
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:
|
|
33335
|
-
{ host: "claude", dir:
|
|
33648
|
+
{ host: "codex", dir: join6(home, ".codex") },
|
|
33649
|
+
{ host: "claude", dir: join6(home, ".claude") }
|
|
33336
33650
|
];
|
|
33337
|
-
return candidates.filter((item) =>
|
|
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) => !
|
|
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 ${
|
|
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 (
|
|
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(
|
|
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
|
|
33838
|
+
print({ authenticated: Boolean(stored), baseUrl: gatewayBaseUrl(stored), credentialPath }, Boolean(command.opts().json));
|
|
33525
33839
|
});
|
|
33526
|
-
auth.command("login").description("
|
|
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
|
-
|
|
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);
|