marketing-cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -17,10 +17,16 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
17
17
  var isTTY = () => typeof process.stdout.isTTY === "boolean" && process.stdout.isTTY, isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v), getNestedValue = (obj, path) => {
18
18
  const parts = path.split(".");
19
19
  let current = obj;
20
- for (const part of parts) {
21
- if (current === null || current === undefined || typeof current !== "object")
20
+ for (let i = 0;i < parts.length; i++) {
21
+ if (current === null || current === undefined)
22
22
  return;
23
- current = current[part];
23
+ if (Array.isArray(current)) {
24
+ const remaining = parts.slice(i).join(".");
25
+ return current.map((item) => item !== null && typeof item === "object" && !Array.isArray(item) ? getNestedValue(item, remaining) : undefined);
26
+ }
27
+ if (typeof current !== "object")
28
+ return;
29
+ current = current[parts[i]];
24
30
  }
25
31
  return current;
26
32
  }, setNestedValue = (obj, path, value) => {
@@ -214,8 +220,8 @@ var package_default;
214
220
  var init_package = __esm(() => {
215
221
  package_default = {
216
222
  name: "marketing-cli",
217
- version: "0.1.0",
218
- description: "Agent-native marketing playbook CLI — one install gives AI agents a full CMO brain with 49 skills, brand memory, and parallel research.",
223
+ version: "0.2.0",
224
+ description: "Agent-native marketing playbook CLI — one install gives AI agents a full CMO brain with 50 skills, 5 research/review agents, 1 upstream catalog (postiz), brand memory, and parallel research.",
219
225
  type: "module",
220
226
  bin: {
221
227
  mktg: "./dist/cli.js"
@@ -226,6 +232,7 @@ var init_package = __esm(() => {
226
232
  "skills-manifest.json",
227
233
  "agents",
228
234
  "agents-manifest.json",
235
+ "catalogs-manifest.json",
229
236
  "LICENSE",
230
237
  "README.md"
231
238
  ],
@@ -256,11 +263,11 @@ var init_package = __esm(() => {
256
263
  license: "MIT",
257
264
  repository: {
258
265
  type: "git",
259
- url: "git+https://github.com/MoizIbnYousaf/mktg.git"
266
+ url: "git+https://github.com/MoizIbnYousaf/marketing-cli.git"
260
267
  },
261
- homepage: "https://github.com/MoizIbnYousaf/mktg#readme",
268
+ homepage: "https://github.com/MoizIbnYousaf/marketing-cli#readme",
262
269
  bugs: {
263
- url: "https://github.com/MoizIbnYousaf/mktg/issues"
270
+ url: "https://github.com/MoizIbnYousaf/marketing-cli/issues"
264
271
  },
265
272
  engines: {
266
273
  bun: ">=1.1.0",
@@ -302,10 +309,17 @@ var init_types = __esm(() => {
302
309
  ];
303
310
  });
304
311
 
312
+ // src/constants.ts
313
+ var GITHUB_REPO_URL = "https://github.com/MoizIbnYousaf/marketing-cli", GITHUB_REPO_ISSUES_URL, GITHUB_REPO_BLOB_MAIN;
314
+ var init_constants = __esm(() => {
315
+ GITHUB_REPO_ISSUES_URL = `${GITHUB_REPO_URL}/issues`;
316
+ GITHUB_REPO_BLOB_MAIN = `${GITHUB_REPO_URL}/blob/main`;
317
+ });
318
+
305
319
  // src/core/errors.ts
306
320
  import { resolve, relative, isAbsolute } from "node:path";
307
321
  import { lstatSync } from "node:fs";
308
- var DOCS, notFound = (what, suggestions = [], docs) => err("NOT_FOUND", `${what} not found`, suggestions, 1, docs), invalidArgs = (message, suggestions = [], docs) => err("INVALID_ARGS", message, suggestions, 2, docs), missingDep = (dep, suggestions = [], docs) => err("MISSING_DEPENDENCY", `Required dependency not found: ${dep}`, suggestions, 3, docs), networkError = (message, docs) => err("NETWORK_ERROR", message, ["Check your internet connection"], 5, docs), missingInput = (example) => err("MISSING_INPUT", "Non-interactive mode requires --json input", [`Example: mktg init --json '${example}'`], 2), sandboxPath = (root, untrusted) => {
322
+ var DOCS, notFound = (what, suggestions = [], docs) => err("NOT_FOUND", `${what} not found`, suggestions, 1, docs), invalidArgs = (message, suggestions = [], docs) => err("INVALID_ARGS", message, suggestions, 2, docs), missingDep = (dep, suggestions = [], docs) => err("MISSING_DEPENDENCY", `Required dependency not found: ${dep}`, suggestions, 3, docs), networkError = (message, docs) => err("NETWORK_ERROR", message, ["Check your internet connection"], 5, docs), sandboxPath = (root, untrusted) => {
309
323
  if (isAbsolute(untrusted)) {
310
324
  return { ok: false, message: "Absolute paths are not allowed" };
311
325
  }
@@ -382,10 +396,11 @@ var DOCS, notFound = (what, suggestions = [], docs) => err("NOT_FOUND", `${what}
382
396
  };
383
397
  var init_errors = __esm(() => {
384
398
  init_types();
399
+ init_constants();
385
400
  DOCS = {
386
- skills: "https://github.com/moizibnyousaf/mktg#skills",
387
- brand: "https://github.com/moizibnyousaf/mktg#brand-files",
388
- commands: "https://github.com/moizibnyousaf/mktg#commands"
401
+ skills: `${GITHUB_REPO_BLOB_MAIN}/docs/skill-contract.md`,
402
+ brand: `${GITHUB_REPO_BLOB_MAIN}/brand/SCHEMA.md`,
403
+ commands: `${GITHUB_REPO_URL}#commands`
389
404
  };
390
405
  CONTROL_CHAR_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/;
391
406
  VALID_RESOURCE_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
@@ -983,6 +998,10 @@ Be specific about metrics when available (CTR, views, signups).
983
998
  });
984
999
 
985
1000
  // src/core/paths.ts
1001
+ var exports_paths = {};
1002
+ __export(exports_paths, {
1003
+ getPackageRoot: () => getPackageRoot
1004
+ });
986
1005
  import { dirname as dirname2, join as join3 } from "node:path";
987
1006
  import { fileURLToPath } from "node:url";
988
1007
  var moduleDir = () => dirname2(fileURLToPath(import.meta.url)), getPackageRoot = () => {
@@ -995,6 +1014,7 @@ var moduleDir = () => dirname2(fileURLToPath(import.meta.url)), getPackageRoot =
995
1014
  var init_paths = () => {};
996
1015
 
997
1016
  // src/core/skills.ts
1017
+ import { spawnSync } from "node:child_process";
998
1018
  import { join as join4, dirname as dirname3 } from "node:path";
999
1019
  import { mkdir as mkdir3, chmod, stat } from "node:fs/promises";
1000
1020
  import { homedir } from "node:os";
@@ -1145,7 +1165,56 @@ var SKILLS_INSTALL_DIR, loadManifest = async () => {
1145
1165
  }
1146
1166
  }
1147
1167
  return { updated, unchanged, notBundled, versionChanges };
1148
- }, getSkillsInstallDir = () => SKILLS_INSTALL_DIR, findExternalSkill = async (nameOrPath) => {
1168
+ }, getSkillsInstallDir = () => SKILLS_INSTALL_DIR, findAiAgentSkillsBinary = () => {
1169
+ const pathValue = process.env.PATH ?? "";
1170
+ for (const segment of pathValue.split(":")) {
1171
+ if (!segment)
1172
+ continue;
1173
+ const candidate = join4(segment, "ai-agent-skills");
1174
+ try {
1175
+ const result = spawnSync(candidate, ["--version"], {
1176
+ encoding: "utf8",
1177
+ stdio: ["ignore", "pipe", "pipe"]
1178
+ });
1179
+ if (result.status === 0) {
1180
+ return candidate;
1181
+ }
1182
+ } catch {}
1183
+ }
1184
+ return null;
1185
+ }, installSkillsWithAiAgentSkills = async (manifest, dryRun = false) => {
1186
+ const binary = findAiAgentSkillsBinary();
1187
+ if (!binary) {
1188
+ return { installed: [], skipped: [], failed: [], delegated: false, binary: null };
1189
+ }
1190
+ const args = ["mktg"];
1191
+ if (dryRun) {
1192
+ args.push("--dry-run");
1193
+ }
1194
+ args.push("--format", "json");
1195
+ const result = spawnSync(binary, args, {
1196
+ encoding: "utf8",
1197
+ stdio: ["ignore", "pipe", "pipe"],
1198
+ env: process.env
1199
+ });
1200
+ if (result.status !== 0) {
1201
+ const detail = (result.stderr || result.stdout || `exit ${result.status ?? 1}`).trim();
1202
+ return {
1203
+ installed: [],
1204
+ skipped: [],
1205
+ failed: [{ name: "_delegate", reason: detail }],
1206
+ delegated: false,
1207
+ binary
1208
+ };
1209
+ }
1210
+ return {
1211
+ installed: getSkillNames(manifest),
1212
+ skipped: [],
1213
+ failed: [],
1214
+ delegated: true,
1215
+ binary
1216
+ };
1217
+ }, findExternalSkill = async (nameOrPath) => {
1149
1218
  const home = homedir();
1150
1219
  if (nameOrPath.includes("/")) {
1151
1220
  const resolved = nameOrPath.startsWith("~") ? nameOrPath.replace("~", home) : nameOrPath;
@@ -2208,13 +2277,263 @@ var init_doctor_fix = __esm(() => {
2208
2277
  };
2209
2278
  });
2210
2279
 
2280
+ // src/core/catalogs.ts
2281
+ var exports_catalogs = {};
2282
+ __export(exports_catalogs, {
2283
+ loadCatalogManifest: () => loadCatalogManifest,
2284
+ isLinkSafeLicense: () => isLinkSafeLicense,
2285
+ isCopyleftLicense: () => isCopyleftLicense,
2286
+ getCatalogNames: () => getCatalogNames,
2287
+ getCatalog: () => getCatalog,
2288
+ detectLicenseDenials: () => detectLicenseDenials,
2289
+ detectAdapterCollisions: () => detectAdapterCollisions,
2290
+ computeConfiguredStatus: () => computeConfiguredStatus,
2291
+ _testing: () => _testing,
2292
+ DEFAULT_BUILTIN_PUBLISH_ADAPTERS: () => DEFAULT_BUILTIN_PUBLISH_ADAPTERS
2293
+ });
2294
+ import { join as join7 } from "node:path";
2295
+ var LINK_SAFE_LICENSES, COPYLEFT_LICENSES, isLinkSafeLicense = (license) => LINK_SAFE_LICENSES.includes(license), isCopyleftLicense = (license) => COPYLEFT_LICENSES.includes(license), VALID_CATALOG_NAME_RE, AUTH_STYLES, TRANSPORTS, expectedCredentialEnvCount = (style) => {
2296
+ switch (style) {
2297
+ case "bearer":
2298
+ return { min: 1, max: 1 };
2299
+ case "basic":
2300
+ return { min: 2, max: 2 };
2301
+ case "oauth2":
2302
+ return { min: 3, max: Number.POSITIVE_INFINITY };
2303
+ case "none":
2304
+ return { min: 0, max: 0 };
2305
+ default:
2306
+ return { min: 0, max: Number.POSITIVE_INFINITY };
2307
+ }
2308
+ }, detectAdapterCollisions = (manifest, hardcoded = { publish_adapters: [] }) => {
2309
+ const seen = new Map;
2310
+ for (const name of hardcoded.publish_adapters) {
2311
+ seen.set(`publish_adapters:${name}`, ["<builtin>"]);
2312
+ }
2313
+ const kinds = ["publish_adapters", "scheduling_adapters", "email_adapters"];
2314
+ for (const [catName, cat] of Object.entries(manifest.catalogs)) {
2315
+ for (const kind of kinds) {
2316
+ const adapters = cat.capabilities[kind] ?? [];
2317
+ for (const adapter of adapters) {
2318
+ const key = `${kind}:${adapter}`;
2319
+ const prior = seen.get(key);
2320
+ if (prior)
2321
+ seen.set(key, [...prior, catName]);
2322
+ else
2323
+ seen.set(key, [catName]);
2324
+ }
2325
+ }
2326
+ }
2327
+ const collisions = [];
2328
+ for (const [key, declaredBy] of seen) {
2329
+ if (declaredBy.length < 2)
2330
+ continue;
2331
+ const colonIdx = key.indexOf(":");
2332
+ const kind = key.slice(0, colonIdx);
2333
+ const adapter = key.slice(colonIdx + 1);
2334
+ collisions.push({ kind, adapter, declaredBy });
2335
+ }
2336
+ return collisions;
2337
+ }, detectLicenseDenials = (manifest) => {
2338
+ const denials = [];
2339
+ for (const [name, cat] of Object.entries(manifest.catalogs)) {
2340
+ if (!isCopyleftLicense(cat.license))
2341
+ continue;
2342
+ if (cat.transport !== "http" || cat.sdk_reference !== null) {
2343
+ denials.push({
2344
+ catalog: name,
2345
+ license: cat.license,
2346
+ transport: cat.transport,
2347
+ sdk_reference: cat.sdk_reference,
2348
+ reason: "sdk-link-on-copyleft-license"
2349
+ });
2350
+ }
2351
+ }
2352
+ return denials;
2353
+ }, validateShape = (raw) => {
2354
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
2355
+ return { ok: false, detail: "manifest must be a JSON object", path: "" };
2356
+ }
2357
+ const m = raw;
2358
+ if (typeof m.version !== "number") {
2359
+ return { ok: false, detail: "missing or non-number 'version' field", path: "version" };
2360
+ }
2361
+ if (m.catalogs === null || typeof m.catalogs !== "object" || Array.isArray(m.catalogs)) {
2362
+ return { ok: false, detail: "'catalogs' must be an object", path: "catalogs" };
2363
+ }
2364
+ const catalogs = m.catalogs;
2365
+ for (const [name, entry] of Object.entries(catalogs)) {
2366
+ if (!VALID_CATALOG_NAME_RE.test(name)) {
2367
+ return { ok: false, detail: `catalog key '${name}' is not a valid resource id`, path: `catalogs.${name}` };
2368
+ }
2369
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
2370
+ return { ok: false, detail: "catalog entry must be an object", path: `catalogs.${name}` };
2371
+ }
2372
+ const e = entry;
2373
+ for (const required of ["name", "repo_url", "docs_url", "license", "version_pinned", "capabilities", "transport", "auth", "skills"]) {
2374
+ if (!(required in e)) {
2375
+ return { ok: false, detail: `missing required field '${required}'`, path: `catalogs.${name}.${required}` };
2376
+ }
2377
+ }
2378
+ if (typeof e.name !== "string" || e.name !== name) {
2379
+ return { ok: false, detail: "entry 'name' must equal the object key", path: `catalogs.${name}.name` };
2380
+ }
2381
+ if (typeof e.repo_url !== "string")
2382
+ return { ok: false, detail: "'repo_url' must be a string", path: `catalogs.${name}.repo_url` };
2383
+ if (typeof e.docs_url !== "string")
2384
+ return { ok: false, detail: "'docs_url' must be a string", path: `catalogs.${name}.docs_url` };
2385
+ if (typeof e.license !== "string")
2386
+ return { ok: false, detail: "'license' must be a string", path: `catalogs.${name}.license` };
2387
+ if (typeof e.version_pinned !== "string")
2388
+ return { ok: false, detail: "'version_pinned' must be a string", path: `catalogs.${name}.version_pinned` };
2389
+ if (typeof e.transport !== "string" || !TRANSPORTS.has(e.transport)) {
2390
+ return { ok: false, detail: "'transport' must be 'sdk' or 'http'", path: `catalogs.${name}.transport` };
2391
+ }
2392
+ if (e.transport === "sdk") {
2393
+ if (typeof e.sdk_reference !== "string" || e.sdk_reference.length === 0) {
2394
+ return { ok: false, detail: "'sdk_reference' must be a non-empty string when transport='sdk'", path: `catalogs.${name}.sdk_reference` };
2395
+ }
2396
+ } else {
2397
+ if (e.sdk_reference !== null) {
2398
+ return { ok: false, detail: "'sdk_reference' must be null when transport='http'", path: `catalogs.${name}.sdk_reference` };
2399
+ }
2400
+ }
2401
+ if (e.capabilities === null || typeof e.capabilities !== "object" || Array.isArray(e.capabilities)) {
2402
+ return { ok: false, detail: "'capabilities' must be an object", path: `catalogs.${name}.capabilities` };
2403
+ }
2404
+ const caps = e.capabilities;
2405
+ let hasAnyAdapter = false;
2406
+ for (const kind of ["publish_adapters", "scheduling_adapters", "email_adapters"]) {
2407
+ const arr = caps[kind];
2408
+ if (arr === undefined)
2409
+ continue;
2410
+ if (!Array.isArray(arr)) {
2411
+ return { ok: false, detail: `'capabilities.${kind}' must be an array of strings`, path: `catalogs.${name}.capabilities.${kind}` };
2412
+ }
2413
+ for (const item of arr) {
2414
+ if (typeof item !== "string" || !VALID_CATALOG_NAME_RE.test(item)) {
2415
+ return { ok: false, detail: `'capabilities.${kind}' contains invalid adapter name`, path: `catalogs.${name}.capabilities.${kind}` };
2416
+ }
2417
+ }
2418
+ if (arr.length > 0)
2419
+ hasAnyAdapter = true;
2420
+ }
2421
+ if (!Array.isArray(e.skills)) {
2422
+ return { ok: false, detail: "'skills' must be an array of strings", path: `catalogs.${name}.skills` };
2423
+ }
2424
+ for (const skill of e.skills) {
2425
+ if (typeof skill !== "string" || !VALID_CATALOG_NAME_RE.test(skill)) {
2426
+ return { ok: false, detail: "'skills' contains invalid skill name", path: `catalogs.${name}.skills` };
2427
+ }
2428
+ }
2429
+ if (!hasAnyAdapter && e.skills.length === 0) {
2430
+ return { ok: false, detail: "catalog must declare at least one adapter OR one skill", path: `catalogs.${name}` };
2431
+ }
2432
+ if (e.auth === null || typeof e.auth !== "object" || Array.isArray(e.auth)) {
2433
+ return { ok: false, detail: "'auth' must be an object", path: `catalogs.${name}.auth` };
2434
+ }
2435
+ const auth = e.auth;
2436
+ if (typeof auth.style !== "string" || !AUTH_STYLES.has(auth.style)) {
2437
+ return { ok: false, detail: "'auth.style' must be 'bearer' | 'basic' | 'oauth2' | 'none'", path: `catalogs.${name}.auth.style` };
2438
+ }
2439
+ if (typeof auth.base_env !== "string") {
2440
+ return { ok: false, detail: "'auth.base_env' must be a string", path: `catalogs.${name}.auth.base_env` };
2441
+ }
2442
+ if (!Array.isArray(auth.credential_envs) || !auth.credential_envs.every((x) => typeof x === "string")) {
2443
+ return { ok: false, detail: "'auth.credential_envs' must be a string array", path: `catalogs.${name}.auth.credential_envs` };
2444
+ }
2445
+ const credCount = auth.credential_envs.length;
2446
+ const { min, max } = expectedCredentialEnvCount(auth.style);
2447
+ if (credCount < min || credCount > max) {
2448
+ return {
2449
+ ok: false,
2450
+ detail: `auth.style '${auth.style}' requires ${min === max ? min : `${min}+`} credential_envs, got ${credCount}`,
2451
+ path: `catalogs.${name}.auth.credential_envs`
2452
+ };
2453
+ }
2454
+ if (auth.header_format !== undefined && auth.header_format !== "bearer" && auth.header_format !== "bare") {
2455
+ return { ok: false, detail: "'auth.header_format' must be 'bearer' or 'bare' when present", path: `catalogs.${name}.auth.header_format` };
2456
+ }
2457
+ }
2458
+ return { ok: true, manifest: raw };
2459
+ }, DEFAULT_BUILTIN_PUBLISH_ADAPTERS, loadCatalogManifest = async (hardcoded = { publish_adapters: DEFAULT_BUILTIN_PUBLISH_ADAPTERS }) => {
2460
+ const manifestPath = join7(getPackageRoot(), "catalogs-manifest.json");
2461
+ const file = Bun.file(manifestPath);
2462
+ const exists = await file.exists();
2463
+ if (!exists) {
2464
+ return { ok: false, reason: "manifest-missing", path: manifestPath };
2465
+ }
2466
+ const raw = await file.text();
2467
+ const parsed = parseJsonInput(raw);
2468
+ if (!parsed.ok) {
2469
+ return { ok: false, reason: "manifest-invalid", detail: parsed.message, path: manifestPath };
2470
+ }
2471
+ const shape = validateShape(parsed.data);
2472
+ if (!shape.ok) {
2473
+ return { ok: false, reason: "manifest-invalid", detail: shape.detail, path: shape.path };
2474
+ }
2475
+ const denials = detectLicenseDenials(shape.manifest);
2476
+ if (denials.length > 0) {
2477
+ return { ok: false, reason: "license-denied", denials };
2478
+ }
2479
+ const collisions = detectAdapterCollisions(shape.manifest, hardcoded);
2480
+ if (collisions.length > 0) {
2481
+ return { ok: false, reason: "collision", collisions };
2482
+ }
2483
+ return { ok: true, manifest: shape.manifest };
2484
+ }, getCatalogNames = (manifest) => Object.keys(manifest.catalogs), getCatalog = (manifest, name) => manifest.catalogs[name] ?? null, computeConfiguredStatus = (entry, env = process.env) => {
2485
+ const missing = [];
2486
+ if (!env[entry.auth.base_env])
2487
+ missing.push(entry.auth.base_env);
2488
+ for (const cred of entry.auth.credential_envs) {
2489
+ if (!env[cred])
2490
+ missing.push(cred);
2491
+ }
2492
+ return {
2493
+ configured: missing.length === 0,
2494
+ missingEnvs: missing,
2495
+ resolvedBase: env[entry.auth.base_env] ?? null
2496
+ };
2497
+ }, _testing;
2498
+ var init_catalogs = __esm(() => {
2499
+ init_paths();
2500
+ init_errors();
2501
+ LINK_SAFE_LICENSES = [
2502
+ "MIT",
2503
+ "Apache-2.0",
2504
+ "BSD-2-Clause",
2505
+ "BSD-3-Clause",
2506
+ "ISC"
2507
+ ];
2508
+ COPYLEFT_LICENSES = [
2509
+ "AGPL-3.0",
2510
+ "AGPL-3.0-or-later",
2511
+ "AGPL-3.0-only",
2512
+ "GPL-3.0",
2513
+ "GPL-3.0-or-later",
2514
+ "GPL-3.0-only",
2515
+ "LGPL-3.0",
2516
+ "LGPL-3.0-or-later",
2517
+ "LGPL-3.0-only"
2518
+ ];
2519
+ VALID_CATALOG_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
2520
+ AUTH_STYLES = new Set(["bearer", "basic", "oauth2", "none"]);
2521
+ TRANSPORTS = new Set(["sdk", "http"]);
2522
+ DEFAULT_BUILTIN_PUBLISH_ADAPTERS = ["typefully", "resend", "file"];
2523
+ _testing = {
2524
+ validateShape,
2525
+ LINK_SAFE_LICENSES,
2526
+ COPYLEFT_LICENSES
2527
+ };
2528
+ });
2529
+
2211
2530
  // src/commands/doctor.ts
2212
2531
  var exports_doctor = {};
2213
2532
  __export(exports_doctor, {
2214
2533
  schema: () => schema,
2215
2534
  handler: () => handler
2216
2535
  });
2217
- import { join as join7 } from "node:path";
2536
+ import { join as join8 } from "node:path";
2218
2537
  var schema, indicator = (s) => s === "pass" ? green("●") : s === "fail" ? red("●") : yellow("●"), fixIndicator = (r) => r === "fixed" ? green("✓") : r === "skipped" ? yellow("−") : red("✗"), checkBrand = async (cwd) => {
2219
2538
  const checks = [];
2220
2539
  const statuses = await getBrandStatus(cwd);
@@ -2233,7 +2552,7 @@ var schema, indicator = (s) => s === "pass" ? green("●") : s === "fail" ? red(
2233
2552
  const templateFiles = [];
2234
2553
  for (const s of existingProfiles) {
2235
2554
  try {
2236
- const content = await Bun.file(join7(cwd, "brand", s.file)).text();
2555
+ const content = await Bun.file(join8(cwd, "brand", s.file)).text();
2237
2556
  if (isTemplateContent(s.file, content))
2238
2557
  templateFiles.push(s.file);
2239
2558
  } catch {}
@@ -2339,7 +2658,7 @@ var schema, indicator = (s) => s === "pass" ? green("●") : s === "fail" ? red(
2339
2658
  const checks = [];
2340
2659
  let healthy = 0;
2341
2660
  for (const ext of entries) {
2342
- const skillMdPath = join7(ext.source_path, "SKILL.md");
2661
+ const skillMdPath = join8(ext.source_path, "SKILL.md");
2343
2662
  const exists = await Bun.file(skillMdPath).exists();
2344
2663
  if (!exists) {
2345
2664
  checks.push({
@@ -2377,17 +2696,55 @@ var schema, indicator = (s) => s === "pass" ? green("●") : s === "fail" ? red(
2377
2696
  });
2378
2697
  }
2379
2698
  return checks;
2699
+ }, checkCatalogs = async () => {
2700
+ try {
2701
+ const { loadCatalogManifest: loadCatalogManifest2, computeConfiguredStatus: computeConfiguredStatus2 } = await Promise.resolve().then(() => (init_catalogs(), exports_catalogs));
2702
+ const result = await loadCatalogManifest2();
2703
+ if (!result.ok) {
2704
+ if (result.reason === "manifest-missing")
2705
+ return [];
2706
+ const detail = result.reason === "manifest-invalid" ? `catalogs-manifest.json is malformed: ${result.detail}` : result.reason === "collision" ? `catalogs-manifest.json has adapter-name collisions (${result.collisions.length})` : `catalogs-manifest.json has ${result.denials.length} license denial(s)`;
2707
+ return [{
2708
+ name: "catalogs-manifest",
2709
+ status: "fail",
2710
+ detail,
2711
+ fix: "mktg catalog list --json (shows structured error envelope)"
2712
+ }];
2713
+ }
2714
+ const checks = [];
2715
+ for (const cat of Object.values(result.manifest.catalogs)) {
2716
+ const status = computeConfiguredStatus2(cat);
2717
+ if (status.configured) {
2718
+ checks.push({
2719
+ name: `catalog-${cat.name}`,
2720
+ status: "pass",
2721
+ detail: `${cat.name} configured (${cat.transport} → ${status.resolvedBase})`
2722
+ });
2723
+ } else {
2724
+ checks.push({
2725
+ name: `catalog-${cat.name}`,
2726
+ status: "warn",
2727
+ detail: `${cat.name} unconfigured — missing: ${status.missingEnvs.join(", ")}`,
2728
+ fix: `mktg catalog info ${cat.name} --json (shows required env vars + docs_url)`
2729
+ });
2730
+ }
2731
+ }
2732
+ return checks;
2733
+ } catch {
2734
+ return [];
2735
+ }
2380
2736
  }, runAllChecks = async (cwd) => {
2381
- const [brand, skills, agents, graph, clis, integrations, externalSkills] = await Promise.all([
2737
+ const [brand, skills, agents, graph, clis, integrations, externalSkills, catalogs] = await Promise.all([
2382
2738
  checkBrand(cwd),
2383
2739
  checkSkills(),
2384
2740
  checkAgents(),
2385
2741
  checkGraph(),
2386
2742
  checkCLIs(),
2387
2743
  checkIntegrations(),
2388
- checkExternalSkills(cwd)
2744
+ checkExternalSkills(cwd),
2745
+ checkCatalogs()
2389
2746
  ]);
2390
- return { brand, skills, agents, graph, clis, integrations, externalSkills };
2747
+ return { brand, skills, agents, graph, clis, integrations, externalSkills, catalogs };
2391
2748
  }, printChecks = (sections, fixes) => {
2392
2749
  writeStderr("");
2393
2750
  writeStderr(` ${bold("mktg doctor")}`);
@@ -2408,6 +2765,8 @@ var schema, indicator = (s) => s === "pass" ? green("●") : s === "fail" ? red(
2408
2765
  printSection("External Skills", sections.externalSkills);
2409
2766
  if (sections.integrations.length > 0)
2410
2767
  printSection("Integrations", sections.integrations);
2768
+ if (sections.catalogs.length > 0)
2769
+ printSection("Catalogs", sections.catalogs);
2411
2770
  if (fixes && fixes.length > 0) {
2412
2771
  writeStderr(` ${dim("Fixes")}`);
2413
2772
  for (const f of fixes) {
@@ -2415,7 +2774,7 @@ var schema, indicator = (s) => s === "pass" ? green("●") : s === "fail" ? red(
2415
2774
  }
2416
2775
  writeStderr("");
2417
2776
  }
2418
- const all = [...sections.brand, ...sections.skills, ...sections.agents, ...sections.graph, ...sections.clis, ...sections.externalSkills, ...sections.integrations];
2777
+ const all = [...sections.brand, ...sections.skills, ...sections.agents, ...sections.graph, ...sections.clis, ...sections.externalSkills, ...sections.integrations, ...sections.catalogs];
2419
2778
  const passed = !all.some((c) => c.status === "fail");
2420
2779
  if (passed) {
2421
2780
  writeStderr(` ${green(bold("All checks pass"))}`);
@@ -2461,13 +2820,13 @@ var schema, indicator = (s) => s === "pass" ? green("●") : s === "fail" ? red(
2461
2820
  }
2462
2821
  const wantsFix = args.includes("--fix");
2463
2822
  let sections = await runAllChecks(flags.cwd);
2464
- let allChecks = [...sections.brand, ...sections.skills, ...sections.agents, ...sections.graph, ...sections.clis, ...sections.externalSkills, ...sections.integrations];
2823
+ let allChecks = [...sections.brand, ...sections.skills, ...sections.agents, ...sections.graph, ...sections.clis, ...sections.externalSkills, ...sections.integrations, ...sections.catalogs];
2465
2824
  let fixes;
2466
2825
  if (wantsFix) {
2467
2826
  fixes = await executeDoctor(allChecks, flags.cwd, flags.dryRun);
2468
2827
  if (!flags.dryRun && fixes.some((f) => f.result === "fixed")) {
2469
2828
  sections = await runAllChecks(flags.cwd);
2470
- allChecks = [...sections.brand, ...sections.skills, ...sections.agents, ...sections.graph, ...sections.clis, ...sections.integrations];
2829
+ allChecks = [...sections.brand, ...sections.skills, ...sections.agents, ...sections.graph, ...sections.clis, ...sections.integrations, ...sections.catalogs];
2471
2830
  }
2472
2831
  }
2473
2832
  const passed = !allChecks.some((c) => c.status === "fail");
@@ -2514,7 +2873,7 @@ __export(exports_init, {
2514
2873
  schema: () => schema2,
2515
2874
  handler: () => handler2
2516
2875
  });
2517
- import { join as join8 } from "node:path";
2876
+ import { join as join9 } from "node:path";
2518
2877
  import { mkdir as mkdir6 } from "node:fs/promises";
2519
2878
  var schema2, parseInitFlags = (args) => {
2520
2879
  let skipBrand = false;
@@ -2582,7 +2941,7 @@ var schema2, parseInitFlags = (args) => {
2582
2941
  return null;
2583
2942
  }
2584
2943
  }, populateFromScrape = async (cwd, scraped, dryRun) => {
2585
- const brandDir = join8(cwd, "brand");
2944
+ const brandDir = join9(cwd, "brand");
2586
2945
  if (!dryRun)
2587
2946
  await mkdir6(brandDir, { recursive: true });
2588
2947
  const populated = [];
@@ -2665,18 +3024,18 @@ ${scraped.url}
2665
3024
  for (const [file, content] of Object.entries(files)) {
2666
3025
  populated.push(file);
2667
3026
  if (!dryRun) {
2668
- writes.push(Bun.write(join8(brandDir, file), content).then(() => {}));
3027
+ writes.push(Bun.write(join9(brandDir, file), content).then(() => {}));
2669
3028
  }
2670
3029
  }
2671
3030
  await Promise.all(writes);
2672
3031
  return populated;
2673
3032
  }, detectProject = async (cwd) => {
2674
- const pkgFile = Bun.file(join8(cwd, "package.json"));
2675
- const readmeFile = Bun.file(join8(cwd, "README.md"));
3033
+ const pkgFile = Bun.file(join9(cwd, "package.json"));
3034
+ const readmeFile = Bun.file(join9(cwd, "README.md"));
2676
3035
  const checkBrandDir = async () => {
2677
3036
  try {
2678
3037
  const { stat: stat3 } = await import("node:fs/promises");
2679
- const s = await stat3(join8(cwd, "brand"));
3038
+ const s = await stat3(join9(cwd, "brand"));
2680
3039
  return s.isDirectory();
2681
3040
  } catch {
2682
3041
  return false;
@@ -2703,12 +3062,9 @@ ${scraped.url}
2703
3062
  return invalidArgs(parsed.message);
2704
3063
  return ok(parsed.data);
2705
3064
  }
2706
- if (flags.yes) {
3065
+ if (flags.yes || !isTTY()) {
2707
3066
  return ok({ business: projectName, goal: "launch" });
2708
3067
  }
2709
- if (!isTTY()) {
2710
- return missingInput('{"business":"My App","goal":"launch"}');
2711
- }
2712
3068
  const readline = await import("node:readline");
2713
3069
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
2714
3070
  const ask = (question, defaultVal) => new Promise((resolve2) => {
@@ -2788,17 +3144,32 @@ ${scraped.url}
2788
3144
  }
2789
3145
  try {
2790
3146
  const manifest = await loadManifest();
2791
- const installedSkills = await installSkills(manifest, flags.dryRun, flags.cwd);
2792
- skillsResult = {
2793
- installed: installedSkills.installed,
2794
- skipped: installedSkills.skipped,
2795
- failed: installedSkills.failed.map(({ name, reason }) => `${name}:${reason}`)
2796
- };
2797
- if (isTTY() && !flags.json) {
2798
- const total = skillsResult.installed.length;
2799
- writeStderr(` ${green("✓")} ${total} skills installed to ~/.claude/skills/`);
2800
- if (skillsResult.skipped.length > 0) {
2801
- writeStderr(` ${dim(`${skillsResult.skipped.length} skills not yet bundled (skipped)`)}`);
3147
+ const delegated = await installSkillsWithAiAgentSkills(manifest, flags.dryRun);
3148
+ if (delegated.delegated) {
3149
+ skillsResult = {
3150
+ installed: delegated.installed,
3151
+ skipped: delegated.skipped,
3152
+ failed: delegated.failed.map(({ name, reason }) => `${name}:${reason}`)
3153
+ };
3154
+ if (isTTY() && !flags.json) {
3155
+ writeStderr(` ${green("✓")} ${skillsResult.installed.length} skills installed via ai-agent-skills`);
3156
+ }
3157
+ } else {
3158
+ if (delegated.binary && delegated.failed.length > 0 && isTTY() && !flags.json) {
3159
+ writeStderr(` ${yellow("●")} ai-agent-skills delegation failed — falling back to direct install`);
3160
+ }
3161
+ const installedSkills = await installSkills(manifest, flags.dryRun, flags.cwd);
3162
+ skillsResult = {
3163
+ installed: installedSkills.installed,
3164
+ skipped: installedSkills.skipped,
3165
+ failed: installedSkills.failed.map(({ name, reason }) => `${name}:${reason}`)
3166
+ };
3167
+ if (isTTY() && !flags.json) {
3168
+ const total = skillsResult.installed.length;
3169
+ writeStderr(` ${green("✓")} ${total} skills installed to ~/.claude/skills/`);
3170
+ if (skillsResult.skipped.length > 0) {
3171
+ writeStderr(` ${dim(`${skillsResult.skipped.length} skills not yet bundled (skipped)`)}`);
3172
+ }
2802
3173
  }
2803
3174
  }
2804
3175
  } catch (e) {
@@ -2856,8 +3227,8 @@ ${scraped.url}
2856
3227
  RESEND_WEBHOOK_SECRET: "https://resend.com/webhooks",
2857
3228
  GEMINI_API_KEY: "https://aistudio.google.com/apikey",
2858
3229
  FIRECRAWL_API_KEY: "https://firecrawl.dev",
2859
- MKTG_X_AUTH_TOKEN: "https://github.com/MoizIbnYousaf/mktg#mktg-x-setup",
2860
- MKTG_X_CT0: "https://github.com/MoizIbnYousaf/mktg#mktg-x-setup"
3230
+ MKTG_X_AUTH_TOKEN: "https://github.com/MoizIbnYousaf/marketing-cli/blob/main/skills/mktg-x/SKILL.md",
3231
+ MKTG_X_CT0: "https://github.com/MoizIbnYousaf/marketing-cli/blob/main/skills/mktg-x/SKILL.md"
2861
3232
  };
2862
3233
  const configuredIntegrations = integrationChecks.filter((c) => c.status === "pass").map((c) => c.name.replace("integration-", ""));
2863
3234
  const missingIntegrations = integrationChecks.filter((c) => c.status !== "pass").map((c) => {
@@ -2973,11 +3344,11 @@ var init_init = __esm(() => {
2973
3344
  });
2974
3345
 
2975
3346
  // src/core/run-log.ts
2976
- import { join as join9 } from "node:path";
3347
+ import { join as join10 } from "node:path";
2977
3348
  import { mkdir as mkdir7, appendFile as appendFile2, readFile as readFile3 } from "node:fs/promises";
2978
3349
  var LOG_FILE = ".mktg/runs.jsonl", logRun = async (cwd, record) => {
2979
- const logPath = join9(cwd, LOG_FILE);
2980
- await mkdir7(join9(cwd, ".mktg"), { recursive: true });
3350
+ const logPath = join10(cwd, LOG_FILE);
3351
+ await mkdir7(join10(cwd, ".mktg"), { recursive: true });
2981
3352
  await appendFile2(logPath, JSON.stringify(record) + `
2982
3353
  `);
2983
3354
  }, parseJsonl = (content) => {
@@ -2991,7 +3362,7 @@ var LOG_FILE = ".mktg/runs.jsonl", logRun = async (cwd, record) => {
2991
3362
  }
2992
3363
  return records;
2993
3364
  }, getRunHistory = async (cwd, skillName, limit = 50) => {
2994
- const logPath = join9(cwd, LOG_FILE);
3365
+ const logPath = join10(cwd, LOG_FILE);
2995
3366
  try {
2996
3367
  const content = await readFile3(logPath, "utf-8");
2997
3368
  let records = parseJsonl(content);
@@ -3028,13 +3399,13 @@ __export(exports_status, {
3028
3399
  schema: () => schema3,
3029
3400
  handler: () => handler3
3030
3401
  });
3031
- import { join as join10 } from "node:path";
3402
+ import { join as join11 } from "node:path";
3032
3403
  var schema3, countContentFiles = async (cwd) => {
3033
3404
  const contentDirs = ["marketing", "campaigns", "content"];
3034
3405
  const byDir = {};
3035
3406
  let totalFiles = 0;
3036
3407
  for (const dir of contentDirs) {
3037
- const dirPath = join10(cwd, dir);
3408
+ const dirPath = join11(cwd, dir);
3038
3409
  let count = 0;
3039
3410
  try {
3040
3411
  const glob = new Bun.Glob("**/*.{md,mdx,txt,html}");
@@ -3049,7 +3420,7 @@ var schema3, countContentFiles = async (cwd) => {
3049
3420
  return { totalFiles, byDir };
3050
3421
  }, getProjectName = async (cwd) => {
3051
3422
  try {
3052
- const pkgPath = join10(cwd, "package.json");
3423
+ const pkgPath = join11(cwd, "package.json");
3053
3424
  const file = Bun.file(pkgPath);
3054
3425
  if (await file.exists()) {
3055
3426
  const pkg = await file.json();
@@ -3121,7 +3492,7 @@ var schema3, countContentFiles = async (cwd) => {
3121
3492
  const brandEntries = await Promise.all(brandStatuses.map(async (status) => {
3122
3493
  if (!status.exists)
3123
3494
  return [status.file, { exists: false, freshness: "missing" }];
3124
- const filePath = join10(cwd, "brand", status.file);
3495
+ const filePath = join11(cwd, "brand", status.file);
3125
3496
  try {
3126
3497
  const content = await Bun.file(filePath).text();
3127
3498
  const isTemplate = isTemplateContent(status.file, content);
@@ -3269,7 +3640,7 @@ __export(exports_list, {
3269
3640
  schema: () => schema4,
3270
3641
  handler: () => handler4
3271
3642
  });
3272
- import { join as join11 } from "node:path";
3643
+ import { join as join12 } from "node:path";
3273
3644
  var schema4, CATEGORY_LABELS, CATEGORY_ORDER, handler4 = async (args, flags) => {
3274
3645
  const wantsNdjson = args.includes("--ndjson");
3275
3646
  const wantsRouting = args.includes("--routing");
@@ -3307,7 +3678,7 @@ var schema4, CATEGORY_LABELS, CATEGORY_ORDER, handler4 = async (args, flags) =>
3307
3678
  source_path: ext.source_path,
3308
3679
  triggers: ext.triggers,
3309
3680
  added: ext.added,
3310
- source_exists: await Bun.file(join11(ext.source_path, "SKILL.md")).exists()
3681
+ source_exists: await Bun.file(join12(ext.source_path, "SKILL.md")).exists()
3311
3682
  })));
3312
3683
  const result = {
3313
3684
  skills,
@@ -3527,14 +3898,14 @@ var init_update = __esm(() => {
3527
3898
  var isKeyOf = (obj, key) => (key in obj);
3528
3899
 
3529
3900
  // src/core/skill-add.ts
3530
- import { join as join12 } from "node:path";
3901
+ import { join as join13 } from "node:path";
3531
3902
  import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir8 } from "node:fs/promises";
3532
3903
  var addExternalSkill = async (opts) => {
3533
3904
  const { source, cwd, dryRun, manifest } = opts;
3534
3905
  const foundPath = await findExternalSkill(source);
3535
3906
  if (!foundPath)
3536
3907
  return { kind: "not-found", source };
3537
- const skillMdPath = join12(foundPath, "SKILL.md");
3908
+ const skillMdPath = join13(foundPath, "SKILL.md");
3538
3909
  let content;
3539
3910
  try {
3540
3911
  content = await readFile4(skillMdPath, "utf-8");
@@ -3552,7 +3923,7 @@ var addExternalSkill = async (opts) => {
3552
3923
  if (dryRun) {
3553
3924
  return { name: fm.name, mode: "chain", source_path: foundPath, action: "dry-run", conflicts };
3554
3925
  }
3555
- const projectManifestPath = join12(cwd, "skills-manifest.json");
3926
+ const projectManifestPath = join13(cwd, "skills-manifest.json");
3556
3927
  let projectManifest;
3557
3928
  try {
3558
3929
  const raw = await readFile4(projectManifestPath, "utf-8");
@@ -3588,7 +3959,7 @@ __export(exports_skill, {
3588
3959
  handler: () => handler6
3589
3960
  });
3590
3961
  import { readFile as readFile5, stat as stat3 } from "node:fs/promises";
3591
- import { join as join13 } from "node:path";
3962
+ import { join as join14 } from "node:path";
3592
3963
  var SUBCOMMANDS, schema6, handleInfo = async (args, flags) => {
3593
3964
  const name = args[0];
3594
3965
  if (!name)
@@ -3611,8 +3982,8 @@ var SUBCOMMANDS, schema6, handleInfo = async (args, flags) => {
3611
3982
  const encodingCheck = detectDoubleEncoding(path);
3612
3983
  if (!encodingCheck.ok)
3613
3984
  return invalidArgs(encodingCheck.message, ["Use plain paths, not URL-encoded components"]);
3614
- const fullPath = path.startsWith("/") ? path : join13(flags.cwd, path);
3615
- const skillMdPath = fullPath.endsWith("SKILL.md") ? fullPath : join13(fullPath, "SKILL.md");
3985
+ const fullPath = path.startsWith("/") ? path : join14(flags.cwd, path);
3986
+ const skillMdPath = fullPath.endsWith("SKILL.md") ? fullPath : join14(fullPath, "SKILL.md");
3616
3987
  try {
3617
3988
  const fileStat = await stat3(skillMdPath);
3618
3989
  if (fileStat.size > 256 * 1024) {
@@ -3654,14 +4025,14 @@ var SUBCOMMANDS, schema6, handleInfo = async (args, flags) => {
3654
4025
  return invalidArgs(encodingCheck.message, ["Use plain paths, not URL-encoded components"]);
3655
4026
  const manifest = await resolveManifest(flags.cwd);
3656
4027
  if (flags.dryRun) {
3657
- const fullPath = path.startsWith("/") ? path : join13(flags.cwd, path);
3658
- const skillMdPath = fullPath.endsWith("SKILL.md") ? fullPath : join13(fullPath, "SKILL.md");
4028
+ const fullPath = path.startsWith("/") ? path : join14(flags.cwd, path);
4029
+ const skillMdPath = fullPath.endsWith("SKILL.md") ? fullPath : join14(fullPath, "SKILL.md");
3659
4030
  try {
3660
4031
  const content = await readFile5(skillMdPath, "utf-8");
3661
4032
  const validated = validateSkill(content, manifest);
3662
- return ok({ name: validated.valid ? "(valid, would register)" : "(invalid, would fail)", action: "dry-run", manifestPath: join13(flags.cwd, "skills-manifest.json"), validation: validated });
4033
+ return ok({ name: validated.valid ? "(valid, would register)" : "(invalid, would fail)", action: "dry-run", manifestPath: join14(flags.cwd, "skills-manifest.json"), validation: validated });
3663
4034
  } catch {
3664
- return ok({ name: "(dry-run)", action: "dry-run", manifestPath: join13(flags.cwd, "skills-manifest.json"), validation: null });
4035
+ return ok({ name: "(dry-run)", action: "dry-run", manifestPath: join14(flags.cwd, "skills-manifest.json"), validation: null });
3665
4036
  }
3666
4037
  }
3667
4038
  const result = await registerSkill(path, flags.cwd, manifest);
@@ -3679,8 +4050,8 @@ var SUBCOMMANDS, schema6, handleInfo = async (args, flags) => {
3679
4050
  const encodingCheck = detectDoubleEncoding(path);
3680
4051
  if (!encodingCheck.ok)
3681
4052
  return invalidArgs(encodingCheck.message, ["Use plain paths, not URL-encoded components"]);
3682
- const fullPath = path.startsWith("/") ? path : join13(flags.cwd, path);
3683
- const skillMdPath = fullPath.endsWith("SKILL.md") ? fullPath : join13(fullPath, "SKILL.md");
4053
+ const fullPath = path.startsWith("/") ? path : join14(flags.cwd, path);
4054
+ const skillMdPath = fullPath.endsWith("SKILL.md") ? fullPath : join14(fullPath, "SKILL.md");
3684
4055
  try {
3685
4056
  const fileStat = await stat3(skillMdPath);
3686
4057
  if (fileStat.size > 256 * 1024) {
@@ -3702,7 +4073,7 @@ var SUBCOMMANDS, schema6, handleInfo = async (args, flags) => {
3702
4073
  if (!name)
3703
4074
  return invalidArgs("Missing skill name", ["Usage: mktg skill unregister <name> --confirm"]);
3704
4075
  if (flags.dryRun) {
3705
- return ok({ name, action: "would-remove", manifestPath: join13(flags.cwd, "skills-manifest.json") });
4076
+ return ok({ name, action: "would-remove", manifestPath: join14(flags.cwd, "skills-manifest.json") });
3706
4077
  }
3707
4078
  if (!confirm) {
3708
4079
  return invalidArgs(`skill unregister removes '${name}' from the project manifest — pass --confirm to proceed`, [
@@ -4029,7 +4400,7 @@ __export(exports_brand, {
4029
4400
  schema: () => schema7,
4030
4401
  handler: () => handler7
4031
4402
  });
4032
- import { join as join14 } from "node:path";
4403
+ import { join as join15 } from "node:path";
4033
4404
  import { mkdir as mkdir9, writeFile as writeFile4, rm, unlink } from "node:fs/promises";
4034
4405
  var SUBCOMMANDS2, schema7, handleExport = async (_args, flags) => {
4035
4406
  const bundle = await exportBrand(flags.cwd);
@@ -4067,7 +4438,7 @@ var SUBCOMMANDS2, schema7, handleExport = async (_args, flags) => {
4067
4438
  "mktg brand import --file bundle.json --dry-run"
4068
4439
  ]);
4069
4440
  }
4070
- const resolvedPath = filePath.startsWith("/") ? filePath : join14(flags.cwd, filePath);
4441
+ const resolvedPath = filePath.startsWith("/") ? filePath : join15(flags.cwd, filePath);
4071
4442
  const file = Bun.file(resolvedPath);
4072
4443
  if (!await file.exists()) {
4073
4444
  return notFound(`Bundle file: ${filePath}`, [
@@ -4149,7 +4520,7 @@ var SUBCOMMANDS2, schema7, handleExport = async (_args, flags) => {
4149
4520
  const brandFileSet = new Set(BRAND_FILES);
4150
4521
  const written = [];
4151
4522
  const rejected = [];
4152
- const brandDir = join14(flags.cwd, "brand");
4523
+ const brandDir = join15(flags.cwd, "brand");
4153
4524
  if (!flags.dryRun) {
4154
4525
  await mkdir9(brandDir, { recursive: true });
4155
4526
  }
@@ -4163,7 +4534,7 @@ var SUBCOMMANDS2, schema7, handleExport = async (_args, flags) => {
4163
4534
  continue;
4164
4535
  }
4165
4536
  if (!flags.dryRun) {
4166
- await writeFile4(join14(brandDir, fileName), content);
4537
+ await writeFile4(join15(brandDir, fileName), content);
4167
4538
  }
4168
4539
  written.push(fileName);
4169
4540
  }
@@ -4224,11 +4595,11 @@ var SUBCOMMANDS2, schema7, handleExport = async (_args, flags) => {
4224
4595
  }
4225
4596
  return claims;
4226
4597
  }, handleClaims = async (_args, flags) => {
4227
- const brandDir = join14(flags.cwd, "brand");
4228
- const landscapePath = join14(brandDir, "landscape.md");
4598
+ const brandDir = join15(flags.cwd, "brand");
4599
+ const landscapePath = join15(brandDir, "landscape.md");
4229
4600
  const bunFile = Bun.file(landscapePath);
4230
4601
  if (!await bunFile.exists()) {
4231
- const brandDirFile = Bun.file(join14(brandDir, "voice-profile.md"));
4602
+ const brandDirFile = Bun.file(join15(brandDir, "voice-profile.md"));
4232
4603
  if (!await brandDirFile.exists()) {
4233
4604
  return notFound("brand/ directory", [
4234
4605
  "Run: mktg init",
@@ -4303,11 +4674,11 @@ var SUBCOMMANDS2, schema7, handleExport = async (_args, flags) => {
4303
4674
  }
4304
4675
  };
4305
4676
  }, KIT_SECTIONS, handleKit = async (args, flags) => {
4306
- const brandDir = join14(flags.cwd, "brand");
4307
- const kitPath = join14(brandDir, "creative-kit.md");
4677
+ const brandDir = join15(flags.cwd, "brand");
4678
+ const kitPath = join15(brandDir, "creative-kit.md");
4308
4679
  const bunFile = Bun.file(kitPath);
4309
4680
  if (!await bunFile.exists()) {
4310
- const brandDirFile = Bun.file(join14(brandDir, "voice-profile.md"));
4681
+ const brandDirFile = Bun.file(join15(brandDir, "voice-profile.md"));
4311
4682
  if (!await brandDirFile.exists()) {
4312
4683
  return notFound("brand/ directory", [
4313
4684
  "Run: mktg init",
@@ -4399,8 +4770,8 @@ var SUBCOMMANDS2, schema7, handleExport = async (_args, flags) => {
4399
4770
  `Valid files: ${BRAND_FILES.join(", ")}`
4400
4771
  ]);
4401
4772
  }
4402
- const brandDir = join14(flags.cwd, "brand");
4403
- const filePath = join14(brandDir, fileName);
4773
+ const brandDir = join15(flags.cwd, "brand");
4774
+ const filePath = join15(brandDir, fileName);
4404
4775
  const fileExists = await Bun.file(filePath).exists();
4405
4776
  if (!fileExists) {
4406
4777
  return notFound(`brand/${fileName}`, [
@@ -4444,7 +4815,7 @@ var SUBCOMMANDS2, schema7, handleExport = async (_args, flags) => {
4444
4815
  "mktg brand reset --confirm --include-learnings (also clears learnings.md entries)"
4445
4816
  ]);
4446
4817
  }
4447
- const mktgDir = join14(flags.cwd, ".mktg");
4818
+ const mktgDir = join15(flags.cwd, ".mktg");
4448
4819
  const cleared = [];
4449
4820
  const { stat: stat4 } = await import("node:fs/promises");
4450
4821
  let mktgDirExists = false;
@@ -4461,7 +4832,7 @@ var SUBCOMMANDS2, schema7, handleExport = async (_args, flags) => {
4461
4832
  }
4462
4833
  }
4463
4834
  if (includeLearnings) {
4464
- const learningsPath = join14(flags.cwd, "brand", "learnings.md");
4835
+ const learningsPath = join15(flags.cwd, "brand", "learnings.md");
4465
4836
  const learningsExists = await Bun.file(learningsPath).exists();
4466
4837
  if (learningsExists) {
4467
4838
  cleared.push("brand/learnings.md");
@@ -4575,7 +4946,7 @@ __export(exports_run, {
4575
4946
  schema: () => schema8,
4576
4947
  handler: () => handler8
4577
4948
  });
4578
- import { join as join15 } from "node:path";
4949
+ import { join as join16 } from "node:path";
4579
4950
  var schema8, handler8 = async (args, flags) => {
4580
4951
  const positionalArgs = args.filter((a) => !a.startsWith("--"));
4581
4952
  const skillName = positionalArgs[0];
@@ -4602,7 +4973,7 @@ var schema8, handler8 = async (args, flags) => {
4602
4973
  }
4603
4974
  }));
4604
4975
  }
4605
- const skillMdPath = join15(getSkillsInstallDir(), resolved.name, "SKILL.md");
4976
+ const skillMdPath = join16(getSkillsInstallDir(), resolved.name, "SKILL.md");
4606
4977
  const skillFile = Bun.file(skillMdPath);
4607
4978
  if (!await skillFile.exists()) {
4608
4979
  return notFound(`Installed skill '${resolved.name}'`, [
@@ -4717,7 +5088,7 @@ var init_run = __esm(() => {
4717
5088
  // src/core/transcribe.ts
4718
5089
  import { existsSync } from "node:fs";
4719
5090
  import { unlink as unlink2 } from "node:fs/promises";
4720
- import { basename, extname, join as join16, resolve as resolve2 } from "node:path";
5091
+ import { basename, extname, join as join17, resolve as resolve2 } from "node:path";
4721
5092
  import { homedir as homedir4 } from "node:os";
4722
5093
  var WHISPER_MODELS, DEFAULT_MODEL = "small", AUDIO_EXTENSIONS, VIDEO_EXTENSIONS, YOUTUBE_HOSTS, isUrl = (s) => /^https?:\/\//i.test(s), classifyUrl = (url) => {
4723
5094
  try {
@@ -4766,7 +5137,7 @@ var WHISPER_MODELS, DEFAULT_MODEL = "small", AUDIO_EXTENSIONS, VIDEO_EXTENSIONS,
4766
5137
  return { ok: false, message: `File not found: ${source}` };
4767
5138
  }
4768
5139
  return { ok: true, detection: { kind: "file", sourceType, path: pathCheck.path } };
4769
- }, getModelPath = (model) => join16(homedir4(), ".mktg", "transcribe", "models", `ggml-${model}.bin`), srtTimeToSeconds = (ts) => {
5140
+ }, getModelPath = (model) => join17(homedir4(), ".mktg", "transcribe", "models", `ggml-${model}.bin`), srtTimeToSeconds = (ts) => {
4770
5141
  const match = ts.match(/(\d+):(\d+):(\d+)[,.](\d+)/);
4771
5142
  if (!match)
4772
5143
  return 0;
@@ -5240,7 +5611,7 @@ __export(exports_context, {
5240
5611
  schema: () => schema10,
5241
5612
  handler: () => handler10
5242
5613
  });
5243
- import { join as join17 } from "node:path";
5614
+ import { join as join18 } from "node:path";
5244
5615
  import { mkdir as mkdir11 } from "node:fs/promises";
5245
5616
  var estimateTokens = (text) => Math.ceil(text.length / 4), truncateToTokens = (content, maxTokens) => {
5246
5617
  if (estimateTokens(content) <= maxTokens)
@@ -5255,7 +5626,7 @@ var estimateTokens = (text) => Math.ceil(text.length / 4), truncateToTokens = (c
5255
5626
  return { text, truncated: true };
5256
5627
  }, VALID_LAYERS, schema10, FILE_PRIORITY, getProjectName2 = async (cwd) => {
5257
5628
  try {
5258
- const file = Bun.file(join17(cwd, "package.json"));
5629
+ const file = Bun.file(join18(cwd, "package.json"));
5259
5630
  if (await file.exists()) {
5260
5631
  const pkg = await file.json();
5261
5632
  if (pkg.name)
@@ -5312,7 +5683,7 @@ var estimateTokens = (text) => Math.ceil(text.length / 4), truncateToTokens = (c
5312
5683
  const targetFiles = layer ? CONTEXT_MATRIX[layer] : BRAND_FILES;
5313
5684
  const brandStatuses = await getBrandStatus(cwd);
5314
5685
  const statusMap = new Map(brandStatuses.map((s) => [s.file, s]));
5315
- const brandDir = join17(cwd, "brand");
5686
+ const brandDir = join18(cwd, "brand");
5316
5687
  const fileEntries = [];
5317
5688
  let totalPopulated = 0;
5318
5689
  let totalTemplate = 0;
@@ -5321,7 +5692,7 @@ var estimateTokens = (text) => Math.ceil(text.length / 4), truncateToTokens = (c
5321
5692
  const status = statusMap.get(file);
5322
5693
  if (!status || !status.exists)
5323
5694
  continue;
5324
- const filePath = join17(brandDir, file);
5695
+ const filePath = join18(brandDir, file);
5325
5696
  try {
5326
5697
  const content = await Bun.file(filePath).text();
5327
5698
  const isTemplate = isTemplateContent(file, content);
@@ -5383,9 +5754,9 @@ var estimateTokens = (text) => Math.ceil(text.length / 4), truncateToTokens = (c
5383
5754
  if (flags.dryRun) {
5384
5755
  writeStderr("dry-run: would write .mktg/context.json");
5385
5756
  } else {
5386
- const mktgDir = join17(cwd, ".mktg");
5757
+ const mktgDir = join18(cwd, ".mktg");
5387
5758
  await mkdir11(mktgDir, { recursive: true });
5388
- await Bun.write(join17(mktgDir, "context.json"), JSON.stringify(result, null, 2));
5759
+ await Bun.write(join18(mktgDir, "context.json"), JSON.stringify(result, null, 2));
5389
5760
  writeStderr("Saved to .mktg/context.json");
5390
5761
  }
5391
5762
  }
@@ -5466,10 +5837,10 @@ __export(exports_plan, {
5466
5837
  schema: () => schema11,
5467
5838
  handler: () => handler11
5468
5839
  });
5469
- import { join as join18 } from "node:path";
5840
+ import { join as join19 } from "node:path";
5470
5841
  import { mkdir as mkdir12 } from "node:fs/promises";
5471
5842
  var schema11, PLAN_FILE = ".mktg/plan.json", loadPersisted = async (cwd) => {
5472
- const file = Bun.file(join18(cwd, PLAN_FILE));
5843
+ const file = Bun.file(join19(cwd, PLAN_FILE));
5473
5844
  if (!await file.exists())
5474
5845
  return null;
5475
5846
  try {
@@ -5478,8 +5849,8 @@ var schema11, PLAN_FILE = ".mktg/plan.json", loadPersisted = async (cwd) => {
5478
5849
  return null;
5479
5850
  }
5480
5851
  }, savePersisted = async (cwd, plan) => {
5481
- await mkdir12(join18(cwd, ".mktg"), { recursive: true });
5482
- await Bun.write(join18(cwd, PLAN_FILE), JSON.stringify(plan, null, 2));
5852
+ await mkdir12(join19(cwd, ".mktg"), { recursive: true });
5853
+ await Bun.write(join19(cwd, PLAN_FILE), JSON.stringify(plan, null, 2));
5483
5854
  }, buildTasks = async (cwd, runSummary, completed, ndjson = false) => {
5484
5855
  const tasks = [];
5485
5856
  const completedSet = new Set(completed);
@@ -5520,7 +5891,7 @@ var schema11, PLAN_FILE = ".mktg/plan.json", loadPersisted = async (cwd) => {
5520
5891
  continue;
5521
5892
  }
5522
5893
  try {
5523
- const content = await Bun.file(join18(cwd, "brand", file)).text();
5894
+ const content = await Bun.file(join19(cwd, "brand", file)).text();
5524
5895
  if (isTemplateContent(file, content)) {
5525
5896
  const skillMap = {
5526
5897
  "voice-profile.md": "brand-voice",
@@ -5536,7 +5907,7 @@ var schema11, PLAN_FILE = ".mktg/plan.json", loadPersisted = async (cwd) => {
5536
5907
  const needsVoice = file !== "voice-profile.md" && foundationOrder.includes(file);
5537
5908
  const voicePopulated = !brandStatuses.find((s) => s.file === "voice-profile.md")?.exists ? false : !await (async () => {
5538
5909
  try {
5539
- const vc = await Bun.file(join18(cwd, "brand", "voice-profile.md")).text();
5910
+ const vc = await Bun.file(join19(cwd, "brand", "voice-profile.md")).text();
5540
5911
  return isTemplateContent("voice-profile.md", vc);
5541
5912
  } catch {
5542
5913
  return true;
@@ -5725,10 +6096,17 @@ var init_plan = __esm(() => {
5725
6096
  // src/commands/publish.ts
5726
6097
  var exports_publish = {};
5727
6098
  __export(exports_publish, {
6099
+ sentMarkerKey: () => sentMarkerKey,
5728
6100
  schema: () => schema12,
5729
- handler: () => handler12
6101
+ postizFetch: () => postizFetch,
6102
+ persistSentMarker: () => persistSentMarker,
6103
+ loadSentMarker: () => loadSentMarker,
6104
+ handler: () => handler12,
6105
+ BUILTIN_PUBLISH_ADAPTERS: () => BUILTIN_PUBLISH_ADAPTERS
5730
6106
  });
5731
- import { join as join19 } from "node:path";
6107
+ import { join as join20 } from "node:path";
6108
+ import { createHash as createHash2 } from "node:crypto";
6109
+ import { mkdir as mkdir13, rename, writeFile as writeFile5 } from "node:fs/promises";
5732
6110
  var schema12, publishTypefully = async (items, confirm, ndjson) => {
5733
6111
  const apiKey = process.env.TYPEFULLY_API_KEY;
5734
6112
  const results = [];
@@ -5831,7 +6209,7 @@ var schema12, publishTypefully = async (items, confirm, ndjson) => {
5831
6209
  };
5832
6210
  }, publishFile = async (items, confirm, cwd, ndjson) => {
5833
6211
  const results = [];
5834
- const outDir = join19(cwd, ".mktg", "published");
6212
+ const outDir = join20(cwd, ".mktg", "published");
5835
6213
  for (let i = 0;i < items.length; i++) {
5836
6214
  const item = items[i];
5837
6215
  const rawFilename = item.metadata?.filename ?? `item-${i}.txt`;
@@ -5845,7 +6223,7 @@ var schema12, publishTypefully = async (items, confirm, ndjson) => {
5845
6223
  try {
5846
6224
  const { mkdir: mkdirFs } = await import("node:fs/promises");
5847
6225
  await mkdirFs(outDir, { recursive: true });
5848
- await Bun.write(join19(outDir, filename), item.content);
6226
+ await Bun.write(join20(outDir, filename), item.content);
5849
6227
  results.push({ item: i, status: "published", detail: `Written to ${filename}` });
5850
6228
  } catch (e) {
5851
6229
  results.push({ item: i, status: "failed", detail: e instanceof Error ? e.message : "Unknown error" });
@@ -5861,7 +6239,269 @@ var schema12, publishTypefully = async (items, confirm, ndjson) => {
5861
6239
  errors: results.filter((r) => r.status === "failed").map((r) => r.detail),
5862
6240
  results
5863
6241
  };
5864
- }, ADAPTERS, ADAPTER_ENV_VARS, handler12 = async (args, flags) => {
6242
+ }, POSTIZ_DEFAULT_BASE = "https://api.postiz.com", postizFetch = async (path, init) => {
6243
+ const apiKey = process.env.POSTIZ_API_KEY;
6244
+ const base = process.env.POSTIZ_API_BASE ?? POSTIZ_DEFAULT_BASE;
6245
+ if (!apiKey) {
6246
+ return { ok: false, error: { kind: "auth-missing" }, status: null };
6247
+ }
6248
+ const headers = {
6249
+ ...init.headers ?? {},
6250
+ Authorization: apiKey
6251
+ };
6252
+ let body;
6253
+ if (init.body instanceof FormData) {
6254
+ body = init.body;
6255
+ } else if (init.body !== undefined) {
6256
+ body = JSON.stringify(init.body);
6257
+ headers["Content-Type"] = "application/json";
6258
+ }
6259
+ let res;
6260
+ try {
6261
+ const fetchInit = {
6262
+ method: init.method,
6263
+ headers,
6264
+ signal: AbortSignal.timeout(15000),
6265
+ ...body !== undefined ? { body } : {}
6266
+ };
6267
+ res = await fetch(`${base}${path}`, fetchInit);
6268
+ } catch (e) {
6269
+ return {
6270
+ ok: false,
6271
+ error: { kind: "network", detail: e instanceof Error ? e.message : String(e) },
6272
+ status: null
6273
+ };
6274
+ }
6275
+ if (res.ok) {
6276
+ const data = await res.json().catch(() => ({}));
6277
+ return { ok: true, data, status: res.status };
6278
+ }
6279
+ const errBody = await res.json().catch(() => ({}));
6280
+ const msg = typeof errBody.msg === "string" ? errBody.msg : `HTTP ${res.status}`;
6281
+ if (res.status === 401) {
6282
+ if (msg === "No subscription found") {
6283
+ return { ok: false, error: { kind: "subscription-required", msg }, status: 401 };
6284
+ }
6285
+ return { ok: false, error: { kind: "auth-invalid", msg }, status: 401 };
6286
+ }
6287
+ if (res.status === 429) {
6288
+ const retryAfter = res.headers.get("retry-after");
6289
+ const retryAfterSeconds = retryAfter && /^\d+$/.test(retryAfter) ? Number(retryAfter) : null;
6290
+ return { ok: false, error: { kind: "rate-limited", retryAfterSeconds, msg }, status: 429 };
6291
+ }
6292
+ if (res.status >= 400 && res.status < 500) {
6293
+ return { ok: false, error: { kind: "bad-request", msg, status: res.status }, status: res.status };
6294
+ }
6295
+ return { ok: false, error: { kind: "server-error", status: res.status, msg }, status: res.status };
6296
+ }, mapPostizError = (e) => {
6297
+ switch (e.kind) {
6298
+ case "auth-missing":
6299
+ return "POSTIZ_API_KEY is not set. Run: mktg catalog info postiz";
6300
+ case "auth-invalid":
6301
+ return `Invalid POSTIZ_API_KEY (${e.msg}). Verify the key in the postiz UI → Settings → API.`;
6302
+ case "subscription-required":
6303
+ return `Hosted postiz requires an active subscription (${e.msg}). Upgrade at https://postiz.com/pricing, or self-host (mktg catalog info postiz).`;
6304
+ case "rate-limited":
6305
+ return e.retryAfterSeconds !== null ? `Postiz rate limit (30 posts/hour per org) — retry in ${e.retryAfterSeconds}s. On self-host, raise API_LIMIT env var.` : "Postiz rate limit (30 posts/hour per org). Retry later or raise API_LIMIT on self-host.";
6306
+ case "bad-request":
6307
+ return `Postiz rejected request (HTTP ${e.status}): ${e.msg}. Check CreatePostDto body shape — see docs/integration/postiz-api-reference.md §4.`;
6308
+ case "server-error":
6309
+ return `Postiz server error (HTTP ${e.status}): ${e.msg}. Retry; if persistent, check postiz health at POSTIZ_API_BASE.`;
6310
+ case "network":
6311
+ return `Network error contacting postiz: ${e.detail}. Verify POSTIZ_API_BASE and connectivity.`;
6312
+ }
6313
+ }, sentMarkerKey = (campaign, content, integrationIds) => {
6314
+ const ids = [...integrationIds].sort().join("|");
6315
+ const buf = `${campaign}||${content}||${ids}`;
6316
+ return createHash2("sha256").update(buf).digest("hex");
6317
+ }, emptySentMarker = (campaign) => ({
6318
+ version: 1,
6319
+ campaign,
6320
+ catalog: "postiz",
6321
+ sent: {}
6322
+ }), isPostizSentMarker = (v) => {
6323
+ if (!v || typeof v !== "object")
6324
+ return false;
6325
+ const m = v;
6326
+ if (m.version !== 1)
6327
+ return false;
6328
+ if (typeof m.campaign !== "string")
6329
+ return false;
6330
+ if (m.catalog !== "postiz")
6331
+ return false;
6332
+ if (!m.sent || typeof m.sent !== "object" || Array.isArray(m.sent))
6333
+ return false;
6334
+ for (const entry of Object.values(m.sent)) {
6335
+ if (!entry || typeof entry !== "object")
6336
+ return false;
6337
+ const e = entry;
6338
+ if (typeof e.postedAt !== "string")
6339
+ return false;
6340
+ if (!Array.isArray(e.providers) || !e.providers.every((p) => typeof p === "string"))
6341
+ return false;
6342
+ }
6343
+ return true;
6344
+ }, archiveCorrupt = async (path) => {
6345
+ const iso = new Date().toISOString().replace(/:/g, "-");
6346
+ const corruptPath = path.replace(/\.json$/, `.corrupt.${iso}.json`);
6347
+ try {
6348
+ await rename(path, corruptPath);
6349
+ } catch {}
6350
+ }, loadSentMarker = async (path, campaign) => {
6351
+ try {
6352
+ const file = Bun.file(path);
6353
+ if (!await file.exists())
6354
+ return emptySentMarker(campaign);
6355
+ const raw = await file.text();
6356
+ const parsed = JSON.parse(raw);
6357
+ if (!isPostizSentMarker(parsed)) {
6358
+ await archiveCorrupt(path);
6359
+ writeStderr(JSON.stringify({ type: "postiz-sent-marker-corrupt", path, detail: "shape mismatch" }));
6360
+ return emptySentMarker(campaign);
6361
+ }
6362
+ if (parsed.campaign !== campaign)
6363
+ return emptySentMarker(campaign);
6364
+ return parsed;
6365
+ } catch (e) {
6366
+ await archiveCorrupt(path);
6367
+ writeStderr(JSON.stringify({ type: "postiz-sent-marker-corrupt", path, detail: e instanceof Error ? e.message : String(e) }));
6368
+ return emptySentMarker(campaign);
6369
+ }
6370
+ }, persistSentMarker = async (path, marker) => {
6371
+ await mkdir13(join20(path, ".."), { recursive: true });
6372
+ const tmp = `${path}.tmp`;
6373
+ await writeFile5(tmp, JSON.stringify(marker, null, 2));
6374
+ await rename(tmp, path);
6375
+ }, buildCreatePostDraft = (content, resolved) => ({
6376
+ type: "draft",
6377
+ shortLink: false,
6378
+ date: new Date().toISOString(),
6379
+ tags: [],
6380
+ posts: resolved.map(({ id }) => ({
6381
+ integration: { id },
6382
+ value: [{ content, image: [] }]
6383
+ }))
6384
+ }), extractProviders = (item) => {
6385
+ const meta = item.metadata;
6386
+ if (!meta || typeof meta !== "object") {
6387
+ return { ok: false, detail: 'Missing item.metadata.providers[] — add at least one postiz identifier (e.g., "linkedin", "bluesky")' };
6388
+ }
6389
+ const providers = meta.providers;
6390
+ if (!Array.isArray(providers) || providers.length === 0) {
6391
+ return { ok: false, detail: 'Missing item.metadata.providers[] — add at least one postiz identifier (e.g., "linkedin", "bluesky")' };
6392
+ }
6393
+ if (!providers.every((p) => typeof p === "string")) {
6394
+ return { ok: false, detail: "item.metadata.providers[] must contain only strings (postiz identifiers)" };
6395
+ }
6396
+ return { ok: true, providers };
6397
+ }, publishPostiz = async (inp) => {
6398
+ const results = [];
6399
+ const buildFailAll = (detail) => ({
6400
+ adapter: "postiz",
6401
+ items: inp.items.length,
6402
+ published: 0,
6403
+ failed: inp.items.length,
6404
+ errors: Array(inp.items.length).fill(detail),
6405
+ results: inp.items.map((_, i) => ({ item: i, status: "failed", detail }))
6406
+ });
6407
+ const markerPath = join20(inp.cwd, ".mktg", "publish", `${inp.campaign}-postiz.json`);
6408
+ const marker = await loadSentMarker(markerPath, inp.campaign);
6409
+ const listRes = await postizFetch("/public/v1/integrations", { method: "GET" });
6410
+ if (!listRes.ok) {
6411
+ return buildFailAll(mapPostizError(listRes.error));
6412
+ }
6413
+ const identifierToId = new Map;
6414
+ for (const int of listRes.data) {
6415
+ if (!int.disabled)
6416
+ identifierToId.set(int.identifier, int.id);
6417
+ }
6418
+ let published = 0;
6419
+ let failed = 0;
6420
+ let hardStop = false;
6421
+ for (let i = 0;i < inp.items.length; i++) {
6422
+ if (hardStop) {
6423
+ results.push({ item: i, status: "skipped", detail: "Skipped — prior item failed with hard-stop error" });
6424
+ continue;
6425
+ }
6426
+ const item = inp.items[i];
6427
+ const extracted = extractProviders(item);
6428
+ if (!extracted.ok) {
6429
+ results.push({ item: i, status: "failed", detail: extracted.detail });
6430
+ failed++;
6431
+ continue;
6432
+ }
6433
+ const providers = extracted.providers;
6434
+ const resolved = [];
6435
+ const unconnected = [];
6436
+ for (const provider of providers) {
6437
+ const id = identifierToId.get(provider);
6438
+ if (id)
6439
+ resolved.push({ provider, id });
6440
+ else
6441
+ unconnected.push(provider);
6442
+ }
6443
+ if (unconnected.length > 0) {
6444
+ const connected = Array.from(identifierToId.keys()).sort();
6445
+ results.push({
6446
+ item: i,
6447
+ status: "failed",
6448
+ detail: `Unconnected provider(s): ${unconnected.join(", ")}. Connected: ${connected.join(", ") || "(none)"}. Connect in the postiz UI first.`
6449
+ });
6450
+ failed++;
6451
+ continue;
6452
+ }
6453
+ if (!inp.confirm) {
6454
+ results.push({ item: i, status: "skipped", detail: `[dry-run] would draft to: ${providers.join(", ")}` });
6455
+ if (inp.ndjson)
6456
+ writeStderr(JSON.stringify({ adapter: "postiz", item: i, status: "skipped" }));
6457
+ continue;
6458
+ }
6459
+ const key = sentMarkerKey(inp.campaign, item.content, resolved.map((r) => r.id));
6460
+ if (marker.sent[key]) {
6461
+ results.push({ item: i, status: "skipped", detail: "already-sent (sent-marker hit)" });
6462
+ if (inp.ndjson)
6463
+ writeStderr(JSON.stringify({ adapter: "postiz", item: i, status: "skipped", reason: "already-sent" }));
6464
+ continue;
6465
+ }
6466
+ const body = buildCreatePostDraft(item.content, resolved);
6467
+ const postRes = await postizFetch("/public/v1/posts", { method: "POST", body });
6468
+ if (!postRes.ok) {
6469
+ const detail = mapPostizError(postRes.error);
6470
+ results.push({ item: i, status: "failed", detail });
6471
+ failed++;
6472
+ if (inp.ndjson)
6473
+ writeStderr(JSON.stringify({ adapter: "postiz", item: i, status: "failed", detail }));
6474
+ if (postRes.error.kind === "rate-limited" || postRes.error.kind === "subscription-required" || postRes.error.kind === "auth-missing" || postRes.error.kind === "auth-invalid") {
6475
+ hardStop = true;
6476
+ }
6477
+ continue;
6478
+ }
6479
+ marker.sent[key] = { postedAt: new Date().toISOString(), providers: [...providers] };
6480
+ if (inp.ndjson)
6481
+ writeStderr(JSON.stringify({ adapter: "postiz", item: i, status: "published", providers }));
6482
+ results.push({ item: i, status: "published", detail: `draft → ${providers.join(", ")}` });
6483
+ published++;
6484
+ }
6485
+ await persistSentMarker(markerPath, marker).catch((e) => {
6486
+ writeStderr(JSON.stringify({ type: "postiz-sent-marker-write-failed", path: markerPath, detail: e instanceof Error ? e.message : String(e) }));
6487
+ });
6488
+ return {
6489
+ adapter: "postiz",
6490
+ items: inp.items.length,
6491
+ published,
6492
+ failed,
6493
+ errors: results.filter((r) => r.status === "failed").map((r) => r.detail),
6494
+ results
6495
+ };
6496
+ }, listPostizIntegrations = async () => {
6497
+ const res = await postizFetch("/public/v1/integrations", { method: "GET" });
6498
+ if (!res.ok) {
6499
+ const detail = mapPostizError(res.error);
6500
+ const exitCode = res.error.kind === "auth-missing" || res.error.kind === "auth-invalid" || res.error.kind === "subscription-required" ? 3 : res.error.kind === "rate-limited" || res.error.kind === "network" || res.error.kind === "server-error" ? 5 : 2;
6501
+ return { ok: false, detail, exitCode };
6502
+ }
6503
+ return { ok: true, data: { adapter: "postiz", integrations: res.data } };
6504
+ }, BUILTIN_PUBLISH_ADAPTERS, ADAPTERS, ADAPTER_ENV_VARS, handler12 = async (args, flags) => {
5865
6505
  const confirm = args.includes("--confirm");
5866
6506
  const ndjson = args.includes("--ndjson");
5867
6507
  const isDryRun = flags.dryRun || !confirm;
@@ -5886,12 +6526,35 @@ var schema12, publishTypefully = async (items, confirm, ndjson) => {
5886
6526
  break;
5887
6527
  }
5888
6528
  }
6529
+ if (args.includes("--list-integrations")) {
6530
+ if (!adapterFilter) {
6531
+ return err("INVALID_ARGS", "--list-integrations requires --adapter <name>", [
6532
+ "Try: mktg publish --adapter postiz --list-integrations --json"
6533
+ ], 2);
6534
+ }
6535
+ if (adapterFilter !== "postiz") {
6536
+ return err("NOT_IMPLEMENTED", `Adapter '${adapterFilter}' does not expose integrations.`, [
6537
+ "This flag is only supported by adapters that back their own provider registry.",
6538
+ "Try --adapter postiz."
6539
+ ], 6);
6540
+ }
6541
+ const res = await listPostizIntegrations();
6542
+ if (!res.ok) {
6543
+ const suggestions = [
6544
+ "Verify POSTIZ_API_KEY and POSTIZ_API_BASE (defaults to https://api.postiz.com)",
6545
+ "Run: mktg catalog info postiz --json"
6546
+ ];
6547
+ const code = res.exitCode === 3 ? "POSTIZ_AUTH" : res.exitCode === 5 ? "POSTIZ_NETWORK" : "POSTIZ_BAD_REQUEST";
6548
+ return err(code, res.detail, suggestions, res.exitCode);
6549
+ }
6550
+ return ok(res.data);
6551
+ }
5889
6552
  const positionalArgs = args.filter((a, i) => !a.startsWith("--") && !flagValues.has(i));
5890
6553
  const campaignPath = positionalArgs[0] ?? ".";
5891
6554
  const pathCheck = validatePathInput(flags.cwd, campaignPath);
5892
6555
  if (!pathCheck.ok)
5893
6556
  return err("INVALID_ARGS", pathCheck.message, [], 2);
5894
- const manifestPath = campaignPath.endsWith("publish.json") ? join19(flags.cwd, campaignPath) : join19(flags.cwd, campaignPath, "publish.json");
6557
+ const manifestPath = campaignPath.endsWith("publish.json") ? join20(flags.cwd, campaignPath) : join20(flags.cwd, campaignPath, "publish.json");
5895
6558
  const manifestFile = Bun.file(manifestPath);
5896
6559
  let manifest;
5897
6560
  if (await manifestFile.exists()) {
@@ -5933,12 +6596,13 @@ var schema12, publishTypefully = async (items, confirm, ndjson) => {
5933
6596
  existing.push(item);
5934
6597
  grouped.set(adapter, existing);
5935
6598
  }
6599
+ const campaignName = manifest.name ?? "unnamed";
5936
6600
  const adapterResults = [];
5937
6601
  for (const [adapter, items] of grouped) {
5938
6602
  const fn = ADAPTERS[adapter];
5939
6603
  if (!fn)
5940
6604
  continue;
5941
- const result2 = await fn(items, confirm && !flags.dryRun, flags.cwd, ndjson);
6605
+ const result2 = await fn(items, confirm && !flags.dryRun, flags.cwd, ndjson, campaignName);
5942
6606
  adapterResults.push(result2);
5943
6607
  }
5944
6608
  const totalItems = adapterResults.reduce((sum, r) => sum + r.items, 0);
@@ -5981,9 +6645,10 @@ var init_publish = __esm(() => {
5981
6645
  positional: { name: "path", description: "Campaign directory or publish.json path", required: false },
5982
6646
  flags: [
5983
6647
  { name: "--confirm", type: "boolean", required: false, description: "Execute publishing (without this, publish is dry-run by default)" },
5984
- { name: "--adapter", type: "string", required: false, description: "Run only a specific adapter (typefully, resend, file)" },
6648
+ { name: "--adapter", type: "string", required: false, description: "Run only a specific adapter (typefully, resend, file, postiz)" },
5985
6649
  { name: "--ndjson", type: "boolean", required: false, description: "Stream progress as NDJSON lines" },
5986
- { name: "--list-adapters", type: "boolean", required: false, description: "List available adapters with env var requirements and configured status" }
6650
+ { name: "--list-adapters", type: "boolean", required: false, description: "List available adapters with env var requirements and configured status" },
6651
+ { name: "--list-integrations", type: "boolean", required: false, description: "For adapters backed by a provider registry (postiz), list connected providers. Returns NOT_IMPLEMENTED for adapters without one." }
5987
6652
  ],
5988
6653
  output: {
5989
6654
  campaign: "string — campaign name from manifest",
@@ -6002,15 +6667,18 @@ var init_publish = __esm(() => {
6002
6667
  ],
6003
6668
  vocabulary: ["publish", "distribute", "push", "ship", "deploy content"]
6004
6669
  };
6670
+ BUILTIN_PUBLISH_ADAPTERS = ["typefully", "resend", "file"];
6005
6671
  ADAPTERS = {
6006
- typefully: (items, confirm, _cwd, ndjson) => publishTypefully(items, confirm, ndjson),
6007
- resend: (items, confirm, _cwd, ndjson) => publishResend(items, confirm, ndjson),
6008
- file: publishFile
6672
+ typefully: (items, confirm, _cwd, ndjson, _campaign) => publishTypefully(items, confirm, ndjson),
6673
+ resend: (items, confirm, _cwd, ndjson, _campaign) => publishResend(items, confirm, ndjson),
6674
+ file: (items, confirm, cwd, ndjson, _campaign) => publishFile(items, confirm, cwd, ndjson),
6675
+ postiz: (items, confirm, cwd, ndjson, campaign) => publishPostiz({ items, confirm, cwd, ndjson, campaign })
6009
6676
  };
6010
6677
  ADAPTER_ENV_VARS = {
6011
6678
  typefully: "TYPEFULLY_API_KEY",
6012
6679
  resend: "RESEND_API_KEY",
6013
- file: null
6680
+ file: null,
6681
+ postiz: "POSTIZ_API_KEY"
6014
6682
  };
6015
6683
  });
6016
6684
 
@@ -6020,10 +6688,10 @@ __export(exports_compete, {
6020
6688
  schema: () => schema13,
6021
6689
  handler: () => handler13
6022
6690
  });
6023
- import { join as join20 } from "node:path";
6024
- import { mkdir as mkdir13 } from "node:fs/promises";
6025
- var schema13, COMPETE_DIR = ".mktg/compete", WATCHLIST_FILE = "watchlist.json", COMPETE_ROUTING, getCompeteDir = (cwd) => join20(cwd, COMPETE_DIR), loadWatchlist = async (cwd) => {
6026
- const file = Bun.file(join20(getCompeteDir(cwd), WATCHLIST_FILE));
6691
+ import { join as join21 } from "node:path";
6692
+ import { mkdir as mkdir14 } from "node:fs/promises";
6693
+ var schema13, COMPETE_DIR = ".mktg/compete", WATCHLIST_FILE = "watchlist.json", COMPETE_ROUTING, getCompeteDir = (cwd) => join21(cwd, COMPETE_DIR), loadWatchlist = async (cwd) => {
6694
+ const file = Bun.file(join21(getCompeteDir(cwd), WATCHLIST_FILE));
6027
6695
  if (!await file.exists())
6028
6696
  return { version: 1, entries: [] };
6029
6697
  try {
@@ -6032,12 +6700,12 @@ var schema13, COMPETE_DIR = ".mktg/compete", WATCHLIST_FILE = "watchlist.json",
6032
6700
  return { version: 1, entries: [] };
6033
6701
  }
6034
6702
  }, saveWatchlist = async (cwd, list) => {
6035
- await mkdir13(getCompeteDir(cwd), { recursive: true });
6036
- await Bun.write(join20(getCompeteDir(cwd), WATCHLIST_FILE), JSON.stringify(list, null, 2));
6703
+ await mkdir14(getCompeteDir(cwd), { recursive: true });
6704
+ await Bun.write(join21(getCompeteDir(cwd), WATCHLIST_FILE), JSON.stringify(list, null, 2));
6037
6705
  }, urlToFilename = (url) => {
6038
6706
  return url.replace(/https?:\/\//, "").replace(/[^a-z0-9.-]/gi, "_").slice(0, 100);
6039
6707
  }, loadSnapshot = async (cwd, url) => {
6040
- const file = Bun.file(join20(getCompeteDir(cwd), `${urlToFilename(url)}.json`));
6708
+ const file = Bun.file(join21(getCompeteDir(cwd), `${urlToFilename(url)}.json`));
6041
6709
  if (!await file.exists())
6042
6710
  return null;
6043
6711
  try {
@@ -6046,8 +6714,8 @@ var schema13, COMPETE_DIR = ".mktg/compete", WATCHLIST_FILE = "watchlist.json",
6046
6714
  return null;
6047
6715
  }
6048
6716
  }, saveSnapshot = async (cwd, snapshot) => {
6049
- await mkdir13(getCompeteDir(cwd), { recursive: true });
6050
- await Bun.write(join20(getCompeteDir(cwd), `${urlToFilename(snapshot.url)}.json`), JSON.stringify(snapshot, null, 2));
6717
+ await mkdir14(getCompeteDir(cwd), { recursive: true });
6718
+ await Bun.write(join21(getCompeteDir(cwd), `${urlToFilename(snapshot.url)}.json`), JSON.stringify(snapshot, null, 2));
6051
6719
  }, fetchPage = async (url) => {
6052
6720
  const validated = validatePublicUrl(url);
6053
6721
  if (!validated.ok) {
@@ -6352,9 +7020,9 @@ __export(exports_dashboard, {
6352
7020
  });
6353
7021
  import { randomUUID } from "node:crypto";
6354
7022
  import { existsSync as existsSync3 } from "node:fs";
6355
- import { mkdir as mkdir14, writeFile as writeFile5 } from "node:fs/promises";
7023
+ import { mkdir as mkdir15, writeFile as writeFile6 } from "node:fs/promises";
6356
7024
  import { spawn } from "node:child_process";
6357
- import { join as join21, basename as basename2, dirname as dirname5 } from "node:path";
7025
+ import { join as join22, basename as basename2, dirname as dirname5 } from "node:path";
6358
7026
  import { fileURLToPath as fileURLToPath2 } from "node:url";
6359
7027
  var SUBCOMMANDS3, DASHBOARD_PORT = 4311, DASHBOARD_HOST = "127.0.0.1", DASHBOARD_SESSION_FILE = ".mktg/dashboard-session.json", OUTPUT_DIRS, CONTENT_SKILLS, DISTRIBUTION_SKILLS, schema14, parseDashboardArgs = (args) => {
6360
7028
  const first = args[0];
@@ -6490,8 +7158,8 @@ var SUBCOMMANDS3, DASHBOARD_PORT = 4311, DASHBOARD_HOST = "127.0.0.1", DASHBOARD
6490
7158
  const passes = doctor.checks.filter((check) => check.status === "pass").length;
6491
7159
  const warns = doctor.checks.filter((check) => check.status === "warn").length;
6492
7160
  const fails = doctor.checks.filter((check) => check.status === "fail").length;
6493
- const creativeKitPath = join21(flags.cwd, "brand", "creative-kit.md");
6494
- const playgroundPath = join21(flags.cwd, "brand-playground.html");
7161
+ const creativeKitPath = join22(flags.cwd, "brand", "creative-kit.md");
7162
+ const playgroundPath = join22(flags.cwd, "brand-playground.html");
6495
7163
  return {
6496
7164
  version: DASHBOARD_SNAPSHOT_VERSION,
6497
7165
  project: {
@@ -6594,7 +7262,7 @@ var SUBCOMMANDS3, DASHBOARD_PORT = 4311, DASHBOARD_HOST = "127.0.0.1", DASHBOARD
6594
7262
  }, discoverOutputs = async (cwd) => {
6595
7263
  const outputs = [];
6596
7264
  for (const dir of OUTPUT_DIRS) {
6597
- const dirPath = join21(cwd, dir);
7265
+ const dirPath = join22(cwd, dir);
6598
7266
  if (!existsSync3(dirPath))
6599
7267
  continue;
6600
7268
  const glob = new Bun.Glob("**/*.{md,mdx,txt,html,json,png,jpg,jpeg,gif,svg}");
@@ -6610,7 +7278,7 @@ var SUBCOMMANDS3, DASHBOARD_PORT = 4311, DASHBOARD_HOST = "127.0.0.1", DASHBOARD
6610
7278
  });
6611
7279
  }
6612
7280
  }
6613
- const playgroundPath = join21(cwd, "brand-playground.html");
7281
+ const playgroundPath = join22(cwd, "brand-playground.html");
6614
7282
  if (existsSync3(playgroundPath)) {
6615
7283
  outputs.push({
6616
7284
  id: "output-brand-playground",
@@ -6681,7 +7349,7 @@ var SUBCOMMANDS3, DASHBOARD_PORT = 4311, DASHBOARD_HOST = "127.0.0.1", DASHBOARD
6681
7349
  for await (const relPath of glob.scan({ cwd })) {
6682
7350
  if (relPath.startsWith("node_modules/") || relPath.startsWith(".git/"))
6683
7351
  continue;
6684
- const absolutePath = join21(cwd, relPath);
7352
+ const absolutePath = join22(cwd, relPath);
6685
7353
  try {
6686
7354
  const parsed = await Bun.file(absolutePath).json();
6687
7355
  manifests.push({
@@ -6763,7 +7431,7 @@ var SUBCOMMANDS3, DASHBOARD_PORT = 4311, DASHBOARD_HOST = "127.0.0.1", DASHBOARD
6763
7431
  capabilityIndex: CAPABILITY_INDEX
6764
7432
  };
6765
7433
  }, loadCompeteWatchList = async (cwd) => {
6766
- const file = Bun.file(join21(cwd, ".mktg", "compete", "watchlist.json"));
7434
+ const file = Bun.file(join22(cwd, ".mktg", "compete", "watchlist.json"));
6767
7435
  if (!await file.exists())
6768
7436
  return { version: 1, entries: [] };
6769
7437
  try {
@@ -6887,7 +7555,7 @@ var SUBCOMMANDS3, DASHBOARD_PORT = 4311, DASHBOARD_HOST = "127.0.0.1", DASHBOARD
6887
7555
  }, handleLaunch = async (flags) => {
6888
7556
  const token = randomUUID();
6889
7557
  const url = `http://${DASHBOARD_HOST}:${DASHBOARD_PORT}/dashboard`;
6890
- const sessionPath = join21(flags.cwd, DASHBOARD_SESSION_FILE);
7558
+ const sessionPath = join22(flags.cwd, DASHBOARD_SESSION_FILE);
6891
7559
  const payload = {
6892
7560
  launched: !flags.dryRun,
6893
7561
  projectRoot: flags.cwd,
@@ -6900,11 +7568,11 @@ var SUBCOMMANDS3, DASHBOARD_PORT = 4311, DASHBOARD_HOST = "127.0.0.1", DASHBOARD
6900
7568
  };
6901
7569
  if (flags.dryRun)
6902
7570
  return ok(payload);
6903
- await mkdir14(join21(flags.cwd, ".mktg"), { recursive: true });
6904
- await writeFile5(sessionPath, JSON.stringify({ token, projectRoot: flags.cwd, url, createdAt: new Date().toISOString() }, null, 2), { mode: 384 });
7571
+ await mkdir15(join22(flags.cwd, ".mktg"), { recursive: true });
7572
+ await writeFile6(sessionPath, JSON.stringify({ token, projectRoot: flags.cwd, url, createdAt: new Date().toISOString() }, null, 2), { mode: 384 });
6905
7573
  const moduleDir2 = dirname5(fileURLToPath2(import.meta.url));
6906
- const rootWebsiteDir = join21(moduleDir2, "..", "..", "website");
6907
- const localWebsiteDir = join21(flags.cwd, "website");
7574
+ const rootWebsiteDir = join22(moduleDir2, "..", "..", "website");
7575
+ const localWebsiteDir = join22(flags.cwd, "website");
6908
7576
  const spawnCwd = existsSync3(localWebsiteDir) ? localWebsiteDir : rootWebsiteDir;
6909
7577
  const proc = spawn("bun", ["x", "next", "dev", "--hostname", DASHBOARD_HOST, "--port", String(DASHBOARD_PORT)], {
6910
7578
  cwd: spawnCwd,
@@ -7054,11 +7722,373 @@ var init_dashboard = __esm(() => {
7054
7722
  ];
7055
7723
  });
7056
7724
 
7725
+ // src/commands/catalog.ts
7726
+ var exports_catalog = {};
7727
+ __export(exports_catalog, {
7728
+ syncSchema: () => syncSchema,
7729
+ statusSchema: () => statusSchema,
7730
+ schema: () => schema15,
7731
+ listSchema: () => listSchema,
7732
+ infoSchema: () => infoSchema,
7733
+ handler: () => handler15,
7734
+ addSchema: () => addSchema
7735
+ });
7736
+ var loadErrorToResult = (result) => {
7737
+ if (result.reason === "manifest-missing") {
7738
+ return err("CATALOG_MANIFEST_INVALID", `catalogs-manifest.json not found at ${result.path}`, ["Run `mktg catalog add <name> --input '{...}' --confirm` to create the manifest", "Or ship catalogs-manifest.json in the package root"], 2);
7739
+ }
7740
+ if (result.reason === "manifest-invalid") {
7741
+ return err("CATALOG_MANIFEST_INVALID", `catalogs-manifest.json is malformed: ${result.detail}`, result.path ? [`Offending path: ${result.path}`, "Compare against docs/integration/catalog-design.md §1 (CatalogEntry type)"] : ["Compare against docs/integration/catalog-design.md §1 (CatalogEntry type)"], 2);
7742
+ }
7743
+ if (result.reason === "collision") {
7744
+ const lines2 = result.collisions.map((c) => `- ${c.kind}:${c.adapter} declared by: ${c.declaredBy.join(", ")}`);
7745
+ return err("CATALOG_COLLISION", `Two or more catalogs declare the same adapter name`, ["Adapter names must be globally unique across all catalogs + built-in adapters:", ...lines2, "Rename one or remove the duplicate"], 2);
7746
+ }
7747
+ const lines = result.denials.map((d) => `- '${d.catalog}' (license: ${d.license}) declares transport: '${d.transport}'${d.sdk_reference ? `, sdk_reference: '${d.sdk_reference}'` : ""}`);
7748
+ return err("CATALOG_LICENSE_DENIED", "Copyleft-licensed catalog cannot declare transport: 'sdk' or a non-null sdk_reference", [...lines, "Copyleft catalogs must use transport: 'http' AND sdk_reference: null (raw-fetch path)"], 2);
7749
+ }, parseNamePositional = (args, subcommand) => {
7750
+ const positional = args.filter((a) => !a.startsWith("--"));
7751
+ const name = positional[1];
7752
+ if (!name) {
7753
+ return {
7754
+ ok: false,
7755
+ result: invalidArgs(`Missing catalog name for 'catalog ${subcommand}'`, [`Usage: mktg catalog ${subcommand} <name> [--json]`, "mktg catalog list --json to see registered catalogs"])
7756
+ };
7757
+ }
7758
+ const idCheck = validateResourceId(name, "catalog");
7759
+ if (!idCheck.ok)
7760
+ return { ok: false, result: invalidArgs(idCheck.message) };
7761
+ const ctrlCheck = rejectControlChars(name, "catalog");
7762
+ if (!ctrlCheck.ok)
7763
+ return { ok: false, result: invalidArgs(ctrlCheck.message) };
7764
+ return { ok: true, name };
7765
+ }, listSchema, infoSchema, syncSchema, statusSchema, addSchema, schema15, handleList = async () => {
7766
+ const result = await loadCatalogManifest();
7767
+ if (!result.ok)
7768
+ return loadErrorToResult(result);
7769
+ const catalogs = Object.values(result.manifest.catalogs);
7770
+ let installed = 0;
7771
+ for (const c of catalogs) {
7772
+ const { configured } = computeConfiguredStatus(c);
7773
+ if (configured)
7774
+ installed++;
7775
+ }
7776
+ return ok({ catalogs, installed, total: catalogs.length });
7777
+ }, handleInfo2 = async (args) => {
7778
+ const parsed = parseNamePositional(args, "info");
7779
+ if (!parsed.ok)
7780
+ return parsed.result;
7781
+ const result = await loadCatalogManifest();
7782
+ if (!result.ok)
7783
+ return loadErrorToResult(result);
7784
+ const entry = getCatalog(result.manifest, parsed.name);
7785
+ if (!entry) {
7786
+ return err("CATALOG_NOT_FOUND", `Catalog '${parsed.name}' not found in catalogs-manifest.json`, [`Registered catalogs: ${Object.keys(result.manifest.catalogs).join(", ") || "(none)"}`, "mktg catalog list --json"], 1);
7787
+ }
7788
+ const status = computeConfiguredStatus(entry);
7789
+ return ok({
7790
+ ...entry,
7791
+ configured: status.configured,
7792
+ missing_envs: status.missingEnvs,
7793
+ resolved_base: status.resolvedBase
7794
+ });
7795
+ }, parseSyncFlags = (args) => {
7796
+ for (let i = 0;i < args.length; i++) {
7797
+ const next = args[i + 1];
7798
+ if (args[i] === "--catalog" && next !== undefined)
7799
+ return { catalogFilter: next };
7800
+ const a = args[i];
7801
+ if (a && a.startsWith("--catalog="))
7802
+ return { catalogFilter: a.slice(10) };
7803
+ }
7804
+ return {};
7805
+ }, handleSync = async (args) => {
7806
+ const result = await loadCatalogManifest();
7807
+ if (!result.ok)
7808
+ return loadErrorToResult(result);
7809
+ const { catalogFilter } = parseSyncFlags(args);
7810
+ const entries = Object.values(result.manifest.catalogs).filter((c) => !catalogFilter || c.name === catalogFilter);
7811
+ const items = entries.map((c) => ({
7812
+ name: c.name,
7813
+ from_version: c.version_pinned,
7814
+ to_version: null,
7815
+ changed: false,
7816
+ error: "upstream version check not yet implemented — see plan v2 §Phase A-prime"
7817
+ }));
7818
+ return ok({
7819
+ catalogs: items,
7820
+ summary: {
7821
+ total: items.length,
7822
+ changed: items.filter((i) => i.changed).length,
7823
+ errors: items.filter((i) => i.error !== undefined).length
7824
+ },
7825
+ dryRun: true
7826
+ });
7827
+ }, handleStatus = async () => {
7828
+ const result = await loadCatalogManifest();
7829
+ if (!result.ok)
7830
+ return loadErrorToResult(result);
7831
+ const items = Object.values(result.manifest.catalogs).map((c) => {
7832
+ const s = computeConfiguredStatus(c);
7833
+ const detail = s.configured ? `configured — ${c.transport} transport to ${s.resolvedBase ?? "<no base>"}` : `unconfigured — missing: ${s.missingEnvs.join(", ")}`;
7834
+ return {
7835
+ name: c.name,
7836
+ configured: s.configured,
7837
+ healthy: null,
7838
+ detail
7839
+ };
7840
+ });
7841
+ return ok({
7842
+ catalogs: items,
7843
+ summary: {
7844
+ total: items.length,
7845
+ configured: items.filter((i) => i.configured).length,
7846
+ healthy: items.filter((i) => i.healthy === true).length
7847
+ }
7848
+ });
7849
+ }, validateEntryShape = async (entry, name) => {
7850
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
7851
+ return { ok: false, detail: "entry must be an object", path: "" };
7852
+ }
7853
+ const withName = { ...entry, name };
7854
+ const syntheticManifest = { version: 1, catalogs: { [name]: withName } };
7855
+ const { _testing: _testing2 } = await Promise.resolve().then(() => (init_catalogs(), exports_catalogs));
7856
+ const check = _testing2.validateShape(syntheticManifest);
7857
+ if (!check.ok) {
7858
+ return { ok: false, detail: check.detail, path: check.path };
7859
+ }
7860
+ return { ok: true, entry: check.manifest.catalogs[name] };
7861
+ }, handleAdd2 = async (args, flags) => {
7862
+ const parsed = parseNamePositional(args, "add");
7863
+ if (!parsed.ok)
7864
+ return parsed.result;
7865
+ const raw = flags.jsonInput;
7866
+ if (!raw) {
7867
+ return invalidArgs("Missing --input flag with CatalogEntry JSON payload", [
7868
+ "Usage: mktg catalog add <name> --input '<json>' --confirm --json",
7869
+ `Example: mktg catalog add postiz --input '{"name":"postiz",...}' --dry-run --json`
7870
+ ]);
7871
+ }
7872
+ const payload = parseJsonInput(raw);
7873
+ if (!payload.ok)
7874
+ return invalidArgs(`Invalid catalog JSON: ${payload.message}`);
7875
+ const shapeCheck = await validateEntryShape(payload.data, parsed.name);
7876
+ if (!shapeCheck.ok) {
7877
+ return err("CATALOG_MANIFEST_INVALID", `Payload fails CatalogEntry shape check: ${shapeCheck.detail}`, shapeCheck.path ? [`Offending path: ${shapeCheck.path}`, "See docs/integration/catalog-design.md §1"] : ["See docs/integration/catalog-design.md §1"], 2);
7878
+ }
7879
+ const confirm = args.includes("--confirm");
7880
+ const explicitDryRun = args.includes("--dry-run") || flags.dryRun;
7881
+ const effectiveDryRun = explicitDryRun || !confirm;
7882
+ const { loadCatalogManifest: loader } = await Promise.resolve().then(() => (init_catalogs(), exports_catalogs));
7883
+ const current = await loader();
7884
+ const existingCatalogs = current.ok ? current.manifest.catalogs : {};
7885
+ if (!current.ok && current.reason !== "manifest-missing") {
7886
+ return loadErrorToResult(current);
7887
+ }
7888
+ const before = existingCatalogs[parsed.name] ?? null;
7889
+ const mergedCatalogs = { ...existingCatalogs, [parsed.name]: shapeCheck.entry };
7890
+ const mergedManifest = { version: 1, catalogs: mergedCatalogs };
7891
+ const { detectAdapterCollisions: detectAdapterCollisions2, detectLicenseDenials: detectLicenseDenials2 } = await Promise.resolve().then(() => (init_catalogs(), exports_catalogs));
7892
+ const collisions = detectAdapterCollisions2(mergedManifest, { publish_adapters: DEFAULT_BUILTIN_PUBLISH_ADAPTERS });
7893
+ if (collisions.length > 0) {
7894
+ return loadErrorToResult({ ok: false, reason: "collision", collisions });
7895
+ }
7896
+ const denials = detectLicenseDenials2(mergedManifest);
7897
+ if (denials.length > 0) {
7898
+ return loadErrorToResult({ ok: false, reason: "license-denied", denials });
7899
+ }
7900
+ const { getPackageRoot: getPackageRoot2 } = await Promise.resolve().then(() => (init_paths(), exports_paths));
7901
+ const { join: join23 } = await import("node:path");
7902
+ const manifestPath = join23(getPackageRoot2(), "catalogs-manifest.json");
7903
+ if (!effectiveDryRun) {
7904
+ await Bun.write(manifestPath, JSON.stringify(mergedManifest, null, 2) + `
7905
+ `);
7906
+ }
7907
+ return ok({
7908
+ name: parsed.name,
7909
+ added: !effectiveDryRun,
7910
+ location: manifestPath,
7911
+ before,
7912
+ after: shapeCheck.entry,
7913
+ dryRun: effectiveDryRun
7914
+ });
7915
+ }, handler15 = async (args, flags) => {
7916
+ const positional = args.filter((a) => !a.startsWith("--"));
7917
+ const sub = positional[0];
7918
+ if (!sub) {
7919
+ return invalidArgs("Missing subcommand", [
7920
+ "Usage: mktg catalog <list | info | sync | status | add> [...args]",
7921
+ "mktg catalog list --json"
7922
+ ]);
7923
+ }
7924
+ switch (sub) {
7925
+ case "list":
7926
+ return handleList();
7927
+ case "info":
7928
+ return handleInfo2(args);
7929
+ case "sync":
7930
+ return handleSync(args);
7931
+ case "status":
7932
+ return handleStatus();
7933
+ case "add":
7934
+ return handleAdd2(args, flags);
7935
+ default:
7936
+ return invalidArgs(`Unknown catalog subcommand: '${sub}'`, ["Available: list, info, sync, status, add"]);
7937
+ }
7938
+ };
7939
+ var init_catalog = __esm(() => {
7940
+ init_types();
7941
+ init_errors();
7942
+ init_catalogs();
7943
+ listSchema = {
7944
+ name: "list",
7945
+ description: "List registered catalogs from catalogs-manifest.json",
7946
+ flags: [],
7947
+ output: {
7948
+ catalogs: "CatalogEntry[] — registered catalogs in manifest order",
7949
+ "catalogs.*.name": "string — catalog identifier",
7950
+ "catalogs.*.repo_url": "string — upstream GitHub repo URL",
7951
+ "catalogs.*.docs_url": "string — upstream documentation URL",
7952
+ "catalogs.*.license": "string — SPDX license identifier",
7953
+ "catalogs.*.version_pinned": "string — upstream release tag",
7954
+ "catalogs.*.transport": "'sdk' | 'http' — how this catalog talks to upstream",
7955
+ "catalogs.*.sdk_reference": "string | null — npm package when transport='sdk'; null when transport='http'",
7956
+ "catalogs.*.auth.style": "'bearer' | 'basic' | 'oauth2' | 'none'",
7957
+ "catalogs.*.auth.base_env": "string — env var name for API base URL",
7958
+ "catalogs.*.auth.credential_envs": "string[] — env var names for credentials",
7959
+ "catalogs.*.auth.header_format": "'bearer' | 'bare' — HTTP Authorization header format (optional)",
7960
+ "catalogs.*.capabilities.publish_adapters": "string[] — publish adapter names this catalog contributes",
7961
+ "catalogs.*.capabilities.scheduling_adapters": "string[] — scheduling adapter names",
7962
+ "catalogs.*.capabilities.email_adapters": "string[] — email adapter names",
7963
+ "catalogs.*.skills": "string[] — SKILL.md names this catalog contributes",
7964
+ installed: "number — catalogs whose auth envs are all set in process.env",
7965
+ total: "number — total catalogs registered"
7966
+ },
7967
+ examples: [
7968
+ { args: "mktg catalog list --json", description: "Full catalog list" },
7969
+ { args: "mktg catalog list --fields catalogs.name --json", description: "Just catalog names" }
7970
+ ],
7971
+ vocabulary: ["catalog list", "list catalogs", "upstream"]
7972
+ };
7973
+ infoSchema = {
7974
+ name: "info",
7975
+ description: "Show full metadata for a single catalog, with computed env-configured state",
7976
+ flags: [],
7977
+ positional: { name: "name", description: "Catalog identifier", required: true },
7978
+ output: {
7979
+ name: "string — catalog identifier",
7980
+ repo_url: "string",
7981
+ docs_url: "string",
7982
+ license: "string — SPDX identifier",
7983
+ version_pinned: "string — upstream release tag",
7984
+ transport: "'sdk' | 'http'",
7985
+ sdk_reference: "string | null",
7986
+ "auth.style": "'bearer' | 'basic' | 'oauth2' | 'none'",
7987
+ "auth.base_env": "string",
7988
+ "auth.credential_envs": "string[]",
7989
+ "auth.header_format": "'bearer' | 'bare'",
7990
+ "capabilities.publish_adapters": "string[]",
7991
+ "capabilities.scheduling_adapters": "string[]",
7992
+ "capabilities.email_adapters": "string[]",
7993
+ skills: "string[]",
7994
+ configured: "boolean — true iff every auth env var is set in process.env",
7995
+ missing_envs: "string[] — auth env var names that are currently unset",
7996
+ resolved_base: "string | null — process.env[auth.base_env] if set, else null"
7997
+ },
7998
+ examples: [
7999
+ { args: "mktg catalog info postiz --json", description: "Full postiz metadata + configured status" },
8000
+ { args: "mktg catalog info postiz --fields configured,missing_envs --json", description: "Just runtime readiness" }
8001
+ ],
8002
+ vocabulary: ["catalog info", "catalog metadata"]
8003
+ };
8004
+ syncSchema = {
8005
+ name: "sync",
8006
+ description: "Check each catalog's pinned version against upstream (v1: read-only, reports drift without mutating)",
8007
+ flags: [
8008
+ { name: "--dry-run", type: "boolean", required: false, default: false, description: "Preview version diffs without writing (v1 is always read-only — flag accepted for forward-compat)" },
8009
+ { name: "--catalog", type: "string", required: false, description: "Limit sync to a single catalog" }
8010
+ ],
8011
+ output: {
8012
+ catalogs: "Array<{name, from_version, to_version, changed}> — per-catalog diff",
8013
+ "catalogs.*.name": "string",
8014
+ "catalogs.*.from_version": "string — currently pinned version",
8015
+ "catalogs.*.to_version": "string | null — latest upstream tag, or null on network error",
8016
+ "catalogs.*.changed": "boolean — true if from_version !== to_version",
8017
+ "catalogs.*.error": "string | undefined — present when upstream check failed",
8018
+ "summary.total": "number",
8019
+ "summary.changed": "number — catalogs with pending bumps",
8020
+ "summary.errors": "number — upstream check failures",
8021
+ dryRun: "boolean"
8022
+ },
8023
+ examples: [
8024
+ { args: "mktg catalog sync --json", description: "Check all catalogs for upstream version drift" },
8025
+ { args: "mktg catalog sync --catalog postiz --json", description: "Check one catalog only" }
8026
+ ],
8027
+ vocabulary: ["catalog sync", "upstream version"]
8028
+ };
8029
+ statusSchema = {
8030
+ name: "status",
8031
+ description: "Health snapshot across all catalogs: configured + reachable",
8032
+ flags: [],
8033
+ output: {
8034
+ catalogs: "Array<{name, configured, healthy, detail}>",
8035
+ "catalogs.*.name": "string",
8036
+ "catalogs.*.configured": "boolean — all auth env vars set",
8037
+ "catalogs.*.healthy": "boolean | null — reachable + auth ok (null when unconfigured or read-only path)",
8038
+ "catalogs.*.detail": "string — human-readable status message",
8039
+ "summary.total": "number",
8040
+ "summary.configured": "number",
8041
+ "summary.healthy": "number"
8042
+ },
8043
+ examples: [
8044
+ { args: "mktg catalog status --json", description: "All catalog health at a glance" },
8045
+ { args: "mktg catalog status --fields summary --json", description: "Summary counts only" }
8046
+ ],
8047
+ vocabulary: ["catalog status", "catalog health"]
8048
+ };
8049
+ addSchema = {
8050
+ name: "add",
8051
+ description: "Register a new catalog in catalogs-manifest.json (mutating)",
8052
+ flags: [
8053
+ { name: "--dry-run", type: "boolean", required: false, default: false, description: "Preview the manifest change without writing" },
8054
+ { name: "--confirm", type: "boolean", required: false, default: false, description: "Required to actually write the manifest" }
8055
+ ],
8056
+ positional: { name: "name", description: "Catalog identifier (must match resource-id regex)", required: true },
8057
+ output: {
8058
+ name: "string — catalog identifier that was added",
8059
+ added: "boolean — true when manifest was written; false on --dry-run or confirm-missing",
8060
+ location: "string — absolute path to catalogs-manifest.json",
8061
+ before: "CatalogEntry | null — previous entry when this was an overwrite",
8062
+ after: "CatalogEntry — final entry",
8063
+ dryRun: "boolean"
8064
+ },
8065
+ examples: [
8066
+ { args: `mktg catalog add postiz --input '{...}' --dry-run --json`, description: "Preview adding postiz via inline JSON" },
8067
+ { args: `mktg catalog add postiz --input '{...}' --confirm --json`, description: "Actually write postiz to manifest" }
8068
+ ],
8069
+ vocabulary: ["catalog add", "register catalog", "new catalog"]
8070
+ };
8071
+ schema15 = {
8072
+ name: "catalog",
8073
+ description: "Manage upstream-catalog registry (list, info, sync, status, add). Catalogs are external OSS projects mktg integrates with via SDK or raw HTTP.",
8074
+ flags: [],
8075
+ positional: { name: "subcommand", description: "Subcommand to run", required: true },
8076
+ subcommands: [listSchema, infoSchema, syncSchema, statusSchema, addSchema],
8077
+ output: {},
8078
+ examples: [
8079
+ { args: "mktg catalog list --json", description: "List registered catalogs" },
8080
+ { args: "mktg catalog info postiz --json", description: "Show postiz metadata" },
8081
+ { args: "mktg catalog status --json", description: "Health check across all catalogs" }
8082
+ ],
8083
+ vocabulary: ["catalog", "catalogs", "upstream", "integration"]
8084
+ };
8085
+ });
8086
+
7057
8087
  // src/commands/schema.ts
7058
8088
  var exports_schema = {};
7059
8089
  __export(exports_schema, {
7060
- schema: () => schema15,
7061
- handler: () => handler15
8090
+ schema: () => schema16,
8091
+ handler: () => handler16
7062
8092
  });
7063
8093
  var VERSION, GLOBAL_FLAGS, EXIT_CODES, loadSchemas = async () => {
7064
8094
  const modules = await Promise.all([
@@ -7075,15 +8105,16 @@ var VERSION, GLOBAL_FLAGS, EXIT_CODES, loadSchemas = async () => {
7075
8105
  Promise.resolve().then(() => (init_plan(), exports_plan)).then((m) => m.schema).catch(() => null),
7076
8106
  Promise.resolve().then(() => (init_publish(), exports_publish)).then((m) => m.schema).catch(() => null),
7077
8107
  Promise.resolve().then(() => (init_compete(), exports_compete)).then((m) => m.schema).catch(() => null),
7078
- Promise.resolve().then(() => (init_dashboard(), exports_dashboard)).then((m) => m.schema).catch(() => null)
8108
+ Promise.resolve().then(() => (init_dashboard(), exports_dashboard)).then((m) => m.schema).catch(() => null),
8109
+ Promise.resolve().then(() => (init_catalog(), exports_catalog)).then((m) => m.schema).catch(() => null)
7079
8110
  ]);
7080
- const schemas = { schema: schema15 };
8111
+ const schemas = { schema: schema16 };
7081
8112
  for (const s of modules) {
7082
8113
  if (s)
7083
8114
  schemas[s.name] = s;
7084
8115
  }
7085
8116
  return schemas;
7086
- }, schema15, extractEnumValues = (typeStr) => {
8117
+ }, schema16, extractEnumValues = (typeStr) => {
7087
8118
  const matches = typeStr.match(/'([^']+)'/g);
7088
8119
  if (matches && matches.length >= 2) {
7089
8120
  return matches.map((s) => s.replace(/'/g, ""));
@@ -7132,7 +8163,7 @@ var VERSION, GLOBAL_FLAGS, EXIT_CODES, loadSchemas = async () => {
7132
8163
  responseSchema: parseOutputToResponseSchema(sub.output)
7133
8164
  }))
7134
8165
  }
7135
- }), handler15 = async (args, _flags) => {
8166
+ }), handler16 = async (args, _flags) => {
7136
8167
  const schemas = await loadSchemas();
7137
8168
  const positionalArgs = args.filter((a) => !a.startsWith("--"));
7138
8169
  if (positionalArgs.length === 0) {
@@ -7176,7 +8207,7 @@ var init_schema = __esm(() => {
7176
8207
  5: "Network error (web research, API call)",
7177
8208
  6: "Not implemented (temporary, for stub commands)"
7178
8209
  };
7179
- schema15 = {
8210
+ schema16 = {
7180
8211
  name: "schema",
7181
8212
  description: "Introspect CLI commands, flags, and output shapes — the single source of truth for any agent using this CLI",
7182
8213
  flags: [],
@@ -7394,6 +8425,7 @@ Commands:
7394
8425
  plan Execution loop — prioritized task queue from project state
7395
8426
  status Project marketing state snapshot
7396
8427
  dashboard Local command center — snapshot, actions, launch
8428
+ catalog Upstream catalog registry (list, info, sync, status, add)
7397
8429
  list Show available skills
7398
8430
  update Force-update skills
7399
8431
  schema Introspect CLI commands and output shapes
@@ -7472,27 +8504,28 @@ var COMMANDS = {
7472
8504
  plan: () => Promise.resolve().then(() => (init_plan(), exports_plan)),
7473
8505
  publish: () => Promise.resolve().then(() => (init_publish(), exports_publish)),
7474
8506
  compete: () => Promise.resolve().then(() => (init_compete(), exports_compete)),
7475
- dashboard: () => Promise.resolve().then(() => (init_dashboard(), exports_dashboard))
8507
+ dashboard: () => Promise.resolve().then(() => (init_dashboard(), exports_dashboard)),
8508
+ catalog: () => Promise.resolve().then(() => (init_catalog(), exports_catalog))
7476
8509
  };
7477
- var formatSchemaAsHelp = (schema16) => {
7478
- const lines = [`mktg ${schema16.name} — ${schema16.description}`];
7479
- if (schema16.subcommands && schema16.subcommands.length > 0) {
8510
+ var formatSchemaAsHelp = (schema17) => {
8511
+ const lines = [`mktg ${schema17.name} — ${schema17.description}`];
8512
+ if (schema17.subcommands && schema17.subcommands.length > 0) {
7480
8513
  lines.push("", "Subcommands:");
7481
- for (const sub of schema16.subcommands) {
8514
+ for (const sub of schema17.subcommands) {
7482
8515
  lines.push(` ${sub.name.padEnd(14)} ${sub.description}`);
7483
8516
  }
7484
8517
  }
7485
- if (schema16.flags.length > 0) {
8518
+ if (schema17.flags.length > 0) {
7486
8519
  lines.push("", "Flags:");
7487
- for (const flag of schema16.flags) {
8520
+ for (const flag of schema17.flags) {
7488
8521
  const req = flag.required ? " (required)" : "";
7489
8522
  const name = flag.name.startsWith("--") ? flag.name : `--${flag.name}`;
7490
8523
  lines.push(` ${name.padEnd(16)} ${flag.description}${req}`);
7491
8524
  }
7492
8525
  }
7493
- if (schema16.examples.length > 0) {
8526
+ if (schema17.examples.length > 0) {
7494
8527
  lines.push("", "Examples:");
7495
- for (const ex of schema16.examples) {
8528
+ for (const ex of schema17.examples) {
7496
8529
  lines.push(` ${ex.args}`);
7497
8530
  lines.push(` ${ex.description}`);
7498
8531
  }
@@ -7557,12 +8590,12 @@ var run = async () => {
7557
8590
  try {
7558
8591
  const mod = await loader();
7559
8592
  if (wantsHelp) {
7560
- const schema16 = mod.schema;
7561
- if (schema16) {
8593
+ const schema17 = mod.schema;
8594
+ if (schema17) {
7562
8595
  if (flags.json) {
7563
- writeStdout(JSON.stringify(schema16, null, 2));
8596
+ writeStdout(JSON.stringify(schema17, null, 2));
7564
8597
  } else {
7565
- writeStdout(formatSchemaAsHelp(schema16));
8598
+ writeStdout(formatSchemaAsHelp(schema17));
7566
8599
  }
7567
8600
  process.exit(0);
7568
8601
  }