Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ myna crawlproof login # paste an API token from Social → API t
myna post --to htmlblog < post.md # …and the new page gets a campaign
myna crawlproof ad https://example.com/launch --budget 300
myna crawlproof ads # campaigns, newest first
myna crawlproof ads show crawlproof-ad-144 # delivery, and the visits it sent
myna crawlproof ads pause crawlproof-ad-144 # or resume, budget <cents>, delete --yes
myna crawlproof auto off # stop the automatic ones
```

Expand Down
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/myna-api",
"version": "0.6.0",
"version": "0.7.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/myna",
"version": "0.6.0",
"version": "0.7.0",
"description": "A terminal social media manager. Log in, compose, schedule and post to every network from one TUI.",
"license": "MIT",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/myna-desktop",
"version": "0.6.0",
"version": "0.7.0",
"description": "The myna desktop app.",
"license": "MIT",
"private": true,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/myna-web",
"version": "0.6.0",
"version": "0.7.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
16 changes: 8 additions & 8 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/myna-core",
"version": "0.6.0",
"version": "0.7.0",
"description": "Network adapters, credential vault, scheduling and the AI writer behind myna.",
"license": "MIT",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
* binary reporting the wrong version, and nothing would fail. A test asserts
* this matches the package.json it is published under.
*/
export const VERSION = "0.6.0";
export const VERSION = "0.7.0";
2 changes: 1 addition & 1 deletion packages/mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/myna-mcp",
"version": "0.6.0",
"version": "0.7.0",
"description": "MCP server for myna. Lets an agent post, schedule and read across every connected social account.",
"license": "MIT",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-crawlproof/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/myna-plugin-crawlproof",
"version": "0.6.0",
"version": "0.7.0",
"description": "myna plugin: run a CrawlProof ad for every blog post myna publishes.",
"license": "MIT",
"type": "module",
Expand Down
64 changes: 63 additions & 1 deletion packages/plugin-crawlproof/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
* myna crawlproof login paste a CrawlProof API token (crp_…)
* myna crawlproof ad <url> [--name N] [--budget CENTS] [--draft true]
* myna crawlproof ads campaigns, newest first
* myna crawlproof ads show <ref> one campaign with its delivery
* myna crawlproof ads pause|resume|budget|delete <ref>
* myna crawlproof auto on|off run an ad for every blog post (on)
* myna crawlproof logout
*
Expand Down Expand Up @@ -109,6 +111,30 @@ export async function createAd(secrets: Secrets, url: string, options: AdOptions
});
}

export interface CampaignStats {
impressions: number;
clicks: number;
spent_cents: number;
free_impressions: number;
free_clicks: number;
visits: { total: number; days: { day: string; visits: number }[] };
}

/** One campaign by ref slug or id, with its delivery. */
export async function showAd(secrets: Secrets, ref: string): Promise<Campaign & { stats?: CampaignStats }> {
return call<Campaign & { stats?: CampaignStats }>(secrets, "GET", `/api/ads/v1/campaigns/${encodeURIComponent(ref)}`);
}

/** Change a campaign in place: status, budget, bid, name. */
export async function patchAd(secrets: Secrets, ref: string, patch: Record<string, unknown>): Promise<Campaign> {
return call<Campaign>(secrets, "PATCH", `/api/ads/v1/campaigns/${encodeURIComponent(ref)}`, patch);
}

/** Remove a campaign, metering included. Pausing keeps the history. */
export async function deleteAd(secrets: Secrets, ref: string): Promise<{ ok: boolean; deleted?: string }> {
return call<{ ok: boolean; deleted?: string }>(secrets, "DELETE", `/api/ads/v1/campaigns/${encodeURIComponent(ref)}`);
}

export async function listAds(secrets: Secrets, limit = 20): Promise<Campaign[]> {
const result = await call<{ campaigns?: Campaign[] }>(secrets, "GET", `/api/ads/v1/campaigns?limit=${limit}`);
return result.campaigns ?? [];
Expand Down Expand Up @@ -141,7 +167,7 @@ export function urlsToPromote(event: PostedEvent): string[] {
const plugin: MynaPlugin = {
id: "crawlproof",
name: "CrawlProof ads",
version: "0.6.0",
version: "0.7.0",
description: "Run a CrawlProof ad campaign for every blog post myna publishes, or for any URL by hand.",

commands: [
Expand All @@ -152,6 +178,10 @@ const plugin: MynaPlugin = {
"crawlproof login Paste a CrawlProof API token (Social → API tokens)",
"crawlproof ad <url> [--name N] [--budget CENTS] [--draft true]",
"crawlproof ads [--limit N] Campaigns, newest first",
"crawlproof ads show <ref> One campaign with its delivery and attributed visits",
"crawlproof ads pause|resume <ref> Stop or restart it; the history stays",
"crawlproof ads budget <ref> <cents> Daily budget",
"crawlproof ads delete <ref> --yes Remove it, metering included",
"crawlproof auto on|off Run an ad for every blog post myna publishes (on by default)",
"crawlproof budget <cents> Daily budget for automatic campaigns",
"crawlproof status | logout",
Expand Down Expand Up @@ -219,6 +249,38 @@ const plugin: MynaPlugin = {
case "ads": {
const secrets = secretsOf(ctx);
if (!secrets) throw new Error("Not connected. Run: myna crawlproof login");
const [verb, ref, value] = rest;
if (verb === "show" || verb === "pause" || verb === "resume" || verb === "budget" || verb === "delete") {
if (!ref) throw new Error(`Usage: myna crawlproof ads ${verb} <ref>${verb === "budget" ? " <cents>" : ""}`);
if (verb === "delete") {
if (!ctx.flags.yes) throw new Error("delete removes the campaign and its metering; pass --yes. Pause keeps the history.");
const gone = await deleteAd(secrets, ref);
ctx.out(`deleted ${gone.deleted ?? ref}`);
return 0;
}
if (verb === "budget") {
const cents = Number(value);
if (!Number.isInteger(cents) || cents < 0) throw new Error("Usage: myna crawlproof ads budget <ref> <cents per day>");
ctx.out(describe(await patchAd(secrets, ref, { daily_budget_cents: cents })));
return 0;
}
if (verb === "pause" || verb === "resume") {
ctx.out(describe(await patchAd(secrets, ref, { status: verb === "pause" ? "paused" : "active" })));
return 0;
}
const campaign = await showAd(secrets, ref);
if (ctx.flags.json) {
ctx.out(JSON.stringify(campaign, null, 2));
return 0;
}
ctx.out(`${describe(campaign)}\n ${campaign.daily_budget_cents ?? "?"}¢/day${campaign.dashboard_url ? ` ${campaign.dashboard_url}` : ""}`);
const s = campaign.stats;
if (s) {
ctx.out(` impressions ${s.impressions} (+${s.free_impressions} free) · clicks ${s.clicks} (+${s.free_clicks} free) · spent ${s.spent_cents}¢ · visits attributed ${s.visits?.total ?? 0}`);
for (const day of (s.visits?.days ?? []).slice(0, 7)) ctx.out(` ${day.day} ${day.visits} visit${day.visits === 1 ? "" : "s"}`);
}
return 0;
}
const limit = typeof ctx.flags.limit === "string" ? Number(ctx.flags.limit) || 20 : 20;
const campaigns = await listAds(secrets, limit);
if (ctx.flags.json) {
Expand Down
37 changes: 37 additions & 0 deletions packages/plugin-crawlproof/test/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,40 @@ test("the command refuses a token that is not a CrawlProof token, and stores a g
expect(good.ctx.secrets.get().budgetCents).toBe("300");
await expect(command.run(["auto", "maybe"], good.ctx)).rejects.toThrow(/on\|off/);
});

test("ads show, pause, budget and delete talk to the campaign route by ref", async () => {
const command = plugin.commands![0];
const calls: { method: string; url: string; body?: string }[] = [];
const fake = (url: string, init?: RequestInit) => {
calls.push({ method: init?.method ?? "GET", url: String(url), body: init?.body ? String(init.body) : undefined });
if (init?.method === "DELETE") return new Response(JSON.stringify({ ok: true, deleted: "crawlproof-ad-144" }), { status: 200 });
return new Response(
JSON.stringify({
id: "1",
ref_slug: "crawlproof-ad-144",
name: "Post",
status: init?.method === "PATCH" ? "paused" : "active",
destination_url: "https://x.y/blog/044-post.html",
daily_budget_cents: 500,
stats: { impressions: 12, clicks: 3, spent_cents: 30, free_impressions: 100, free_clicks: 2, visits: { total: 5, days: [{ day: "2026-09-05", visits: 5 }] } },
}),
{ status: 200 },
);
};
const { ctx, lines } = context({ token: "crp_x" });
await withFetch(fake, () => command.run(["ads", "show", "crawlproof-ad-144"], ctx));
expect(calls[0]).toMatchObject({ method: "GET", url: `${DEFAULT_URL}/api/ads/v1/campaigns/crawlproof-ad-144` });
expect(lines.join("\n")).toContain("impressions 12 (+100 free) · clicks 3 (+2 free) · spent 30¢ · visits attributed 5");

await withFetch(fake, () => command.run(["ads", "pause", "crawlproof-ad-144"], ctx));
expect(calls[1]).toMatchObject({ method: "PATCH", body: JSON.stringify({ status: "paused" }) });
await withFetch(fake, () => command.run(["ads", "budget", "crawlproof-ad-144", "300"], ctx));
expect(calls[2]).toMatchObject({ method: "PATCH", body: JSON.stringify({ daily_budget_cents: 300 }) });
await expect(command.run(["ads", "budget", "crawlproof-ad-144", "lots"], ctx)).rejects.toThrow(/cents per day/);

await expect(command.run(["ads", "delete", "crawlproof-ad-144"], ctx)).rejects.toThrow(/--yes/);
const yes = context({ token: "crp_x" }, { yes: true });
await withFetch(fake, () => command.run(["ads", "delete", "crawlproof-ad-144"], yes.ctx));
expect(calls[3]).toMatchObject({ method: "DELETE" });
expect(yes.lines).toEqual(["deleted crawlproof-ad-144"]);
});
2 changes: 1 addition & 1 deletion packages/plugin-outreachgraph/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/myna-plugin-outreachgraph",
"version": "0.6.0",
"version": "0.7.0",
"description": "myna plugin: pull the people OutreachGraph ranks for you into the follow graph as seeds.",
"license": "MIT",
"type": "module",
Expand Down