yappr 0.1.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/.env.example +115 -0
- package/config/context/personality.md +7 -0
- package/config/context/security.md +10 -0
- package/config/hooks/example.ts +47 -0
- package/config/hooks/holder.ts +154 -0
- package/config/hooks/user-memory.ts +102 -0
- package/config/skills/compute/handler.ts +6 -0
- package/config/skills/compute/skill.md +7 -0
- package/config/skills/cron/handler.ts +89 -0
- package/config/skills/cron/skill.md +36 -0
- package/config/skills/generate-image/handler.ts +133 -0
- package/config/skills/generate-image/skill.md +20 -0
- package/config/skills/generate-meme-prompt/handler.ts +40 -0
- package/config/skills/generate-meme-prompt/skill.md +23 -0
- package/config/skills/stats/handler.ts +76 -0
- package/config/skills/stats/skill.md +18 -0
- package/config/skills/wallet/handler.ts +56 -0
- package/config/skills/wallet/skill.md +17 -0
- package/config/skills/x/handler.ts +135 -0
- package/config/skills/x/skill.md +163 -0
- package/dist/config/hooks/example.d.ts +2 -0
- package/dist/config/hooks/example.js +37 -0
- package/dist/config/hooks/holder.d.ts +2 -0
- package/dist/config/hooks/holder.js +147 -0
- package/dist/config/hooks/user-memory.d.ts +2 -0
- package/dist/config/hooks/user-memory.js +79 -0
- package/dist/config/skills/compute/handler.d.ts +2 -0
- package/dist/config/skills/compute/handler.js +5 -0
- package/dist/config/skills/cron/handler.d.ts +2 -0
- package/dist/config/skills/cron/handler.js +84 -0
- package/dist/config/skills/generate-image/handler.d.ts +2 -0
- package/dist/config/skills/generate-image/handler.js +122 -0
- package/dist/config/skills/generate-meme/handler.d.ts +2 -0
- package/dist/config/skills/generate-meme/handler.js +121 -0
- package/dist/config/skills/generate-meme-prompt/handler.d.ts +2 -0
- package/dist/config/skills/generate-meme-prompt/handler.js +38 -0
- package/dist/config/skills/stats/handler.d.ts +2 -0
- package/dist/config/skills/stats/handler.js +71 -0
- package/dist/config/skills/wallet/handler.d.ts +2 -0
- package/dist/config/skills/wallet/handler.js +54 -0
- package/dist/config/skills/x/handler.d.ts +2 -0
- package/dist/config/skills/x/handler.js +115 -0
- package/dist/src/agent-prompt.d.ts +1 -0
- package/dist/src/agent-prompt.js +45 -0
- package/dist/src/bankr.d.ts +41 -0
- package/dist/src/bankr.js +76 -0
- package/dist/src/cli/backup.d.ts +7 -0
- package/dist/src/cli/backup.js +78 -0
- package/dist/src/cli/charts.d.ts +32 -0
- package/dist/src/cli/charts.js +222 -0
- package/dist/src/cli/config-sync.d.ts +7 -0
- package/dist/src/cli/config-sync.js +71 -0
- package/dist/src/cli/deploy.d.ts +2 -0
- package/dist/src/cli/deploy.js +1059 -0
- package/dist/src/cli/env.d.ts +4 -0
- package/dist/src/cli/env.js +50 -0
- package/dist/src/cli/host-key.d.ts +4 -0
- package/dist/src/cli/host-key.js +50 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +71 -0
- package/dist/src/cli/init.d.ts +1 -0
- package/dist/src/cli/init.js +51 -0
- package/dist/src/cli/ssh.d.ts +2 -0
- package/dist/src/cli/ssh.js +141 -0
- package/dist/src/cli/status.d.ts +7 -0
- package/dist/src/cli/status.js +1184 -0
- package/dist/src/cli/tui.d.ts +18 -0
- package/dist/src/cli/tui.js +115 -0
- package/dist/src/cli/ui.d.ts +30 -0
- package/dist/src/cli/ui.js +164 -0
- package/dist/src/cli/update.d.ts +1 -0
- package/dist/src/cli/update.js +263 -0
- package/dist/src/cli/x-login.d.ts +6 -0
- package/dist/src/cli/x-login.js +70 -0
- package/dist/src/compute.d.ts +11 -0
- package/dist/src/compute.js +109 -0
- package/dist/src/config-loader.d.ts +19 -0
- package/dist/src/config-loader.js +82 -0
- package/dist/src/config.d.ts +29 -0
- package/dist/src/config.js +68 -0
- package/dist/src/cron/capability.d.ts +6 -0
- package/dist/src/cron/capability.js +66 -0
- package/dist/src/cron/runner.d.ts +2 -0
- package/dist/src/cron/runner.js +113 -0
- package/dist/src/cron/schedule.d.ts +19 -0
- package/dist/src/cron/schedule.js +154 -0
- package/dist/src/cron/store.d.ts +46 -0
- package/dist/src/cron/store.js +220 -0
- package/dist/src/db.d.ts +4 -0
- package/dist/src/db.js +53 -0
- package/dist/src/hooks/loader.d.ts +1 -0
- package/dist/src/hooks/loader.js +17 -0
- package/dist/src/hooks/registry.d.ts +17 -0
- package/dist/src/hooks/registry.js +78 -0
- package/dist/src/hooks/types.d.ts +45 -0
- package/dist/src/hooks/types.js +1 -0
- package/dist/src/index.d.ts +25 -0
- package/dist/src/index.js +35 -0
- package/dist/src/llm/index.d.ts +23 -0
- package/dist/src/llm/index.js +213 -0
- package/dist/src/llm/prompts.d.ts +6 -0
- package/dist/src/llm/prompts.js +99 -0
- package/dist/src/log.d.ts +2 -0
- package/dist/src/log.js +30 -0
- package/dist/src/reply/agent.d.ts +20 -0
- package/dist/src/reply/agent.js +215 -0
- package/dist/src/reply/context-blocks.d.ts +12 -0
- package/dist/src/reply/context-blocks.js +22 -0
- package/dist/src/reply/gating.d.ts +3 -0
- package/dist/src/reply/gating.js +35 -0
- package/dist/src/reply/pipeline.d.ts +3 -0
- package/dist/src/reply/pipeline.js +144 -0
- package/dist/src/reply/poller.d.ts +5 -0
- package/dist/src/reply/poller.js +79 -0
- package/dist/src/skills/holder-access.d.ts +7 -0
- package/dist/src/skills/holder-access.js +53 -0
- package/dist/src/skills/loader.d.ts +2 -0
- package/dist/src/skills/loader.js +64 -0
- package/dist/src/skills/registry.d.ts +4 -0
- package/dist/src/skills/registry.js +10 -0
- package/dist/src/skills/types.d.ts +16 -0
- package/dist/src/skills/types.js +1 -0
- package/dist/src/state.d.ts +5 -0
- package/dist/src/state.js +26 -0
- package/dist/src/stats-cli.d.ts +1 -0
- package/dist/src/stats-cli.js +82 -0
- package/dist/src/stats.d.ts +41 -0
- package/dist/src/stats.js +236 -0
- package/dist/src/storage.d.ts +16 -0
- package/dist/src/storage.js +107 -0
- package/dist/src/treasury/abi.d.ts +99 -0
- package/dist/src/treasury/abi.js +71 -0
- package/dist/src/treasury/cycle.d.ts +16 -0
- package/dist/src/treasury/cycle.js +154 -0
- package/dist/src/treasury/index.d.ts +28 -0
- package/dist/src/treasury/index.js +222 -0
- package/dist/src/util.d.ts +3 -0
- package/dist/src/util.js +18 -0
- package/dist/src/wallet.d.ts +5 -0
- package/dist/src/wallet.js +241 -0
- package/dist/src/x/client.d.ts +74 -0
- package/dist/src/x/client.js +323 -0
- package/dist/src/x/types.d.ts +61 -0
- package/dist/src/x/types.js +1 -0
- package/dist/src/x402.d.ts +6 -0
- package/dist/src/x402.js +11 -0
- package/dist/src/yappr.d.ts +1 -0
- package/dist/src/yappr.js +85 -0
- package/package.json +52 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { chat, payFetch, log } from "yappr";
|
|
2
|
+
// generate-meme: turn a tweet into a funny, Crypto-Twitter-flavored meme. Two steps —
|
|
3
|
+
// (1) ask the LLM to write a vivid, CT-aware image prompt from the tweet, then (2) render
|
|
4
|
+
// it with BlockRun's gpt-image-1 over x402. Returns the image as `mediaUrl` so the reply
|
|
5
|
+
// pipeline uploads and attaches it (the model just writes a caption). The render half
|
|
6
|
+
// mirrors the generate-image skill; kept self-contained so either skill can be deleted.
|
|
7
|
+
const ORIGIN = "https://blockrun.ai";
|
|
8
|
+
const ENDPOINT = `${ORIGIN}/api/v1/images/generations`;
|
|
9
|
+
const MODEL = "openai/gpt-image-1"; // $0.021 / image (1024x1024) — see generate-image for the full price list
|
|
10
|
+
// gpt-image-1 only accepts these three sizes (1024x1024, 1536x1024, 1024x1536). Square default.
|
|
11
|
+
const SIZES = {
|
|
12
|
+
square: "1024x1024",
|
|
13
|
+
landscape: "1536x1024",
|
|
14
|
+
portrait: "1024x1536",
|
|
15
|
+
};
|
|
16
|
+
const DEFAULT_SIZE = "square";
|
|
17
|
+
function resolveSize(raw) {
|
|
18
|
+
const key = (raw ?? "").trim().toLowerCase();
|
|
19
|
+
if (key in SIZES)
|
|
20
|
+
return SIZES[key];
|
|
21
|
+
if (/^\d{3,4}x\d{3,4}$/.test(key))
|
|
22
|
+
return key;
|
|
23
|
+
return SIZES[DEFAULT_SIZE];
|
|
24
|
+
}
|
|
25
|
+
// Poll tuning — gpt-image-1 routinely exceeds the server's 30s inline window.
|
|
26
|
+
const POST_TIMEOUT_MS = 60_000;
|
|
27
|
+
const POLL_TIMEOUT_MS = 30_000;
|
|
28
|
+
const POLL_INTERVAL_MS = 5_000;
|
|
29
|
+
const POLL_MAX_ATTEMPTS = 24;
|
|
30
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
31
|
+
const imageUrl = (body) => body?.data?.[0]?.url;
|
|
32
|
+
const GENERATED_NOTE = "Meme generated — it will be attached to your reply automatically. Write a short, funny caption that fits the meme described below. Do NOT paste any URL, link, or the prompt text itself.";
|
|
33
|
+
// ── Step 1: craft a funny, CT-aware image prompt from the tweet subject ──
|
|
34
|
+
const MEME_SYSTEM = `You are a Crypto Twitter (CT) meme director. Given the subject of a tweet, write ONE image-generation prompt for a single funny meme image.
|
|
35
|
+
|
|
36
|
+
- Lean into CT humor and culture where it fits — degens, leverage and liquidations, rugs and exit liquidity, gm/wagmi/ngmi, "ser"/"anon", diamond vs paper hands, copium, "few understand this", and wojak/pepe/chad/virgin-vs-chad visual archetypes. Use what suits the subject; never force every trope in.
|
|
37
|
+
- Describe a clear, vivid visual scene, and specify the EXACT short bold meme caption text to render in the image (impact-font style, just a few words).
|
|
38
|
+
- Funny and a little edgy is good; never hateful, sexual, or harassing toward private individuals.
|
|
39
|
+
- Output ONLY the image prompt itself — no preamble, no quotes, no explanation, no markdown.`;
|
|
40
|
+
async function craftMemePrompt(subject, angle) {
|
|
41
|
+
const user = angle ? `Tweet subject: ${subject}\n\nExtra angle to play on: ${angle}` : `Tweet subject: ${subject}`;
|
|
42
|
+
const messages = [
|
|
43
|
+
{ role: "system", content: MEME_SYSTEM },
|
|
44
|
+
{ role: "user", content: user },
|
|
45
|
+
];
|
|
46
|
+
return (await chat(messages)).trim();
|
|
47
|
+
}
|
|
48
|
+
// ── Step 2: render the crafted prompt with gpt-image-1 (POST, then poll if it goes async) ──
|
|
49
|
+
async function generateImage(prompt, size) {
|
|
50
|
+
const res = await payFetch(ENDPOINT, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "Content-Type": "application/json" },
|
|
53
|
+
body: JSON.stringify({ prompt, model: MODEL, size }),
|
|
54
|
+
signal: AbortSignal.timeout(POST_TIMEOUT_MS),
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const detail = await res.text().catch(() => "");
|
|
58
|
+
log.warn({ status: res.status, detail }, "generate-meme: POST failed");
|
|
59
|
+
throw new Error(`image generation failed (HTTP ${res.status})`);
|
|
60
|
+
}
|
|
61
|
+
const body = await res.json();
|
|
62
|
+
const inline = imageUrl(body);
|
|
63
|
+
if (inline) {
|
|
64
|
+
log.info({ model: MODEL, size, url: inline }, "generate-meme: inline result");
|
|
65
|
+
return inline;
|
|
66
|
+
}
|
|
67
|
+
const pollPath = body?.poll_url;
|
|
68
|
+
if (!pollPath)
|
|
69
|
+
throw new Error("no image and no job to poll");
|
|
70
|
+
const pollUrl = new URL(pollPath, ORIGIN).toString();
|
|
71
|
+
log.info({ jobId: body?.id, status: body?.status, size, pollUrl }, "generate-meme: job queued, polling");
|
|
72
|
+
for (let attempt = 1; attempt <= POLL_MAX_ATTEMPTS; attempt++) {
|
|
73
|
+
await sleep(POLL_INTERVAL_MS);
|
|
74
|
+
let pb = null;
|
|
75
|
+
try {
|
|
76
|
+
const pr = await payFetch(pollUrl, { method: "GET", signal: AbortSignal.timeout(POLL_TIMEOUT_MS) });
|
|
77
|
+
pb = await pr.json().catch(() => null);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
// Transient poll error — the job may still finish, so log and try the next tick.
|
|
81
|
+
log.warn({ attempt, err: err instanceof Error ? err.message : String(err) }, "generate-meme: poll errored — retrying");
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const url = imageUrl(pb);
|
|
85
|
+
if (url) {
|
|
86
|
+
log.info({ attempt, url }, "generate-meme: completed");
|
|
87
|
+
return url;
|
|
88
|
+
}
|
|
89
|
+
if (pb?.status === "failed")
|
|
90
|
+
throw new Error("job reported failed");
|
|
91
|
+
log.info({ attempt, status: pb?.status }, "generate-meme: still generating");
|
|
92
|
+
}
|
|
93
|
+
throw new Error("timed out before the job finished");
|
|
94
|
+
}
|
|
95
|
+
export const handler = async (params) => {
|
|
96
|
+
const subject = (params.subject ?? params.prompt ?? "").trim();
|
|
97
|
+
if (!subject)
|
|
98
|
+
return { text: "missing subject — pass the tweet's content/topic to meme about" };
|
|
99
|
+
const size = resolveSize(params.size ?? params.orientation);
|
|
100
|
+
// 1) Craft the funny, CT-aware image prompt.
|
|
101
|
+
let memePrompt;
|
|
102
|
+
try {
|
|
103
|
+
memePrompt = await craftMemePrompt(subject, params.angle?.trim() || undefined);
|
|
104
|
+
if (!memePrompt)
|
|
105
|
+
throw new Error("empty prompt");
|
|
106
|
+
log.info({ subject, size, memePrompt }, "generate-meme: crafted prompt");
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
log.warn({ err: err instanceof Error ? err.message : String(err) }, "generate-meme: prompt crafting failed");
|
|
110
|
+
return { text: "couldn't come up with a meme for that — try again" };
|
|
111
|
+
}
|
|
112
|
+
// 2) Render it.
|
|
113
|
+
try {
|
|
114
|
+
const url = await generateImage(memePrompt, size);
|
|
115
|
+
return { text: `${GENERATED_NOTE}\n\nThe meme depicts: ${memePrompt}`, mediaUrl: url };
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
log.warn({ err: err instanceof Error ? err.message : String(err) }, "generate-meme: generation failed");
|
|
119
|
+
return { text: "the meme image failed to generate — try again" };
|
|
120
|
+
}
|
|
121
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { chat, log } from "yappr";
|
|
2
|
+
// generate-meme-prompt: craft a funny, Crypto-Twitter-flavored image PROMPT from a tweet.
|
|
3
|
+
// It does NOT render anything — it returns the prompt for the model to hand to the
|
|
4
|
+
// generate-image skill, which does the actual (paid) rendering and attaches the result.
|
|
5
|
+
// Keeping the two apart means one render path (generate-image) and no duplicated image-gen
|
|
6
|
+
// code here. Use it when the user wants a meme but hasn't described the visual themselves.
|
|
7
|
+
// The prompt-crafting brief. Bakes in Crypto Twitter context so the meme lands.
|
|
8
|
+
const MEME_SYSTEM = `You are a Crypto Twitter (CT) meme director. Given the subject of a tweet, write ONE image-generation prompt for a single funny meme image.
|
|
9
|
+
|
|
10
|
+
- Lean into CT humor and culture where it fits — degens, leverage and liquidations, rugs and exit liquidity, gm/wagmi/ngmi, "ser"/"anon", diamond vs paper hands, copium, "few understand this", and wojak/pepe/chad/virgin-vs-chad visual archetypes. Use what suits the subject; never force every trope in.
|
|
11
|
+
- Describe a clear, vivid visual scene, and specify the EXACT short bold meme caption text to render in the image (impact-font style, just a few words).
|
|
12
|
+
- Funny and a little edgy is good; never hateful, sexual, or harassing toward private individuals.
|
|
13
|
+
- Output ONLY the image prompt itself — no preamble, no quotes, no explanation, no markdown.`;
|
|
14
|
+
export const handler = async (params) => {
|
|
15
|
+
const subject = (params.subject ?? params.prompt ?? "").trim();
|
|
16
|
+
if (!subject)
|
|
17
|
+
return { text: "missing subject — pass the tweet's content/topic to meme about" };
|
|
18
|
+
const angle = params.angle?.trim() || undefined;
|
|
19
|
+
const user = angle ? `Tweet subject: ${subject}\n\nExtra angle to play on: ${angle}` : `Tweet subject: ${subject}`;
|
|
20
|
+
const messages = [
|
|
21
|
+
{ role: "system", content: MEME_SYSTEM },
|
|
22
|
+
{ role: "user", content: user },
|
|
23
|
+
];
|
|
24
|
+
try {
|
|
25
|
+
const memePrompt = (await chat(messages)).trim();
|
|
26
|
+
if (!memePrompt)
|
|
27
|
+
throw new Error("empty prompt");
|
|
28
|
+
log.info({ subject, memePrompt }, "generate-meme-prompt: crafted prompt");
|
|
29
|
+
// Return just the prompt (as data). The chaining — "now call generate-image with this"
|
|
30
|
+
// — is driven by skill.md (a trusted instruction), since observations are fed to the
|
|
31
|
+
// model as data, not instructions.
|
|
32
|
+
return { text: `Meme image prompt (pass verbatim to the generate-image skill's "prompt"):\n\n${memePrompt}` };
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
log.warn({ err: err instanceof Error ? err.message : String(err) }, "generate-meme-prompt: prompt crafting failed");
|
|
36
|
+
return { text: "couldn't come up with a meme for that — try again" };
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { summary, getTreasury, llmCreditBalance, log } from "yappr";
|
|
2
|
+
// stats: the agent's all-time metrics from its own ledger, plus a runway estimate (how
|
|
3
|
+
// long the treasury lasts at the current burn). The runway mirrors the status dashboard's
|
|
4
|
+
// two-tank model: USDC pays x-api + compute + x402, LLM credits pay inference — runway is
|
|
5
|
+
// whichever tank empties first. (Burn math kept in sync with cli/status.ts by hand.)
|
|
6
|
+
const RUNWAY_MIN_DATA_HOURS = 1; // trust the measured burn only after this much recorded window
|
|
7
|
+
const X_API_POLL_COST_USD = 0.005; // always-on mentions-poll cost — the cold-start/floor burn
|
|
8
|
+
function fmtDuration(hours) {
|
|
9
|
+
if (!Number.isFinite(hours))
|
|
10
|
+
return "∞";
|
|
11
|
+
if (hours < 1)
|
|
12
|
+
return `${Math.round(hours * 60)}m`;
|
|
13
|
+
if (hours < 48)
|
|
14
|
+
return `${hours.toFixed(1)}h`;
|
|
15
|
+
return `${(hours / 24).toFixed(1)}d`;
|
|
16
|
+
}
|
|
17
|
+
export const handler = async () => {
|
|
18
|
+
const s = summary();
|
|
19
|
+
// Burn rates from the trailing window. usdcBurn = x-api+compute+x402 (total minus the
|
|
20
|
+
// inference slice), floored at the always-on poll cost so downtime can't inflate runway;
|
|
21
|
+
// llmBurn = inference. Before there's enough recorded window, fall back to the poll-cost
|
|
22
|
+
// estimate for USDC and treat inference as not-yet-binding.
|
|
23
|
+
const pollSeconds = Math.round((Number(process.env.POLL_INTERVAL_MS) || 20_000) / 1000);
|
|
24
|
+
const predictedUsdcBurn = pollSeconds > 0 ? (3600 / pollSeconds) * X_API_POLL_COST_USD : 0;
|
|
25
|
+
const hasRate = s.rateWindowHours >= RUNWAY_MIN_DATA_HOURS && s.spentUsdWindow > 0;
|
|
26
|
+
const usdcBurn = hasRate
|
|
27
|
+
? Math.max((s.spentUsdWindow - s.inferenceUsdWindow) / s.rateWindowHours, predictedUsdcBurn)
|
|
28
|
+
: predictedUsdcBurn;
|
|
29
|
+
const llmBurn = hasRate ? s.inferenceUsdWindow / s.rateWindowHours : 0;
|
|
30
|
+
// Treasury value per tank: the USDC balance (live, on-chain) and the LLM credit balance.
|
|
31
|
+
let usdcUsd = null;
|
|
32
|
+
try {
|
|
33
|
+
usdcUsd = Number((await getTreasury().balances()).usdc) / 1e6;
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
log.warn({ err: err instanceof Error ? err.message : String(err) }, "stats: balances fetch failed");
|
|
37
|
+
}
|
|
38
|
+
const creditUsd = await llmCreditBalance();
|
|
39
|
+
const usdcRunwayH = usdcBurn > 0 ? (usdcUsd != null ? usdcUsd / usdcBurn : Infinity) : Infinity;
|
|
40
|
+
const llmRunwayH = llmBurn > 0 ? (creditUsd != null ? creditUsd / llmBurn : Infinity) : Infinity;
|
|
41
|
+
const runwayHours = Math.min(usdcRunwayH, llmRunwayH);
|
|
42
|
+
const runwayKnown = usdcUsd != null || creditUsd != null;
|
|
43
|
+
const runway = !runwayKnown
|
|
44
|
+
? { available: false }
|
|
45
|
+
: {
|
|
46
|
+
available: true,
|
|
47
|
+
human: fmtDuration(runwayHours),
|
|
48
|
+
hours: Number.isFinite(runwayHours) ? Number(runwayHours.toFixed(1)) : null, // null ≈ effectively infinite
|
|
49
|
+
days: Number.isFinite(runwayHours) ? Number((runwayHours / 24).toFixed(2)) : null,
|
|
50
|
+
estimated: !hasRate, // cold-start estimate — not enough recorded burn yet
|
|
51
|
+
limitedBy: usdcRunwayH <= llmRunwayH ? "usdc" : "llm-credits",
|
|
52
|
+
usdcBalanceUsd: usdcUsd,
|
|
53
|
+
llmCreditUsd: creditUsd,
|
|
54
|
+
usdcBurnUsdPerHour: Number(usdcBurn.toFixed(4)),
|
|
55
|
+
llmBurnUsdPerHour: Number(llmBurn.toFixed(4)),
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
data: {
|
|
59
|
+
mentions: s.mentions,
|
|
60
|
+
replies: s.replies,
|
|
61
|
+
llmCalls: s.llm,
|
|
62
|
+
// warns/errors are deliberately omitted — internal health metrics, not user-facing
|
|
63
|
+
// (and this skill is public). They stay in summary() for the admin status dashboard.
|
|
64
|
+
spentUsd: Number(s.spentUsd.toFixed(4)),
|
|
65
|
+
spentByType: s.spentByType, // { "x-api", inference, compute, x402 }
|
|
66
|
+
earnedWeth: s.earnedWeth, // lifetime gross creator fees, in ETH/WETH
|
|
67
|
+
devWeth: s.devWeth, // dev cut within earnedWeth
|
|
68
|
+
runway,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { agentPrompt } from "yappr";
|
|
2
|
+
// Bankr returns a natural-language response, so we pull transaction hashes out of
|
|
3
|
+
// it and append the right block-explorer link. Hash format tells us the chain:
|
|
4
|
+
// EVM: 0x + 64 hex chars -> basescan.org (our EVM actions run on Base)
|
|
5
|
+
// Solana: base58 signature ~88 chars -> solscan.io
|
|
6
|
+
// (EVM addresses are 0x+40 hex, so they never match the 64-hex tx pattern; Solana
|
|
7
|
+
// addresses are <=44 base58 chars, so they never match the long-signature pattern.)
|
|
8
|
+
const EVM_TX = /0x[0-9a-fA-F]{64}/g;
|
|
9
|
+
const SOL_TX = /\b[1-9A-HJ-NP-Za-km-z]{80,90}\b/g;
|
|
10
|
+
function appendTxLinks(text) {
|
|
11
|
+
const links = [];
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const add = (hash, base) => {
|
|
14
|
+
if (seen.has(hash))
|
|
15
|
+
return;
|
|
16
|
+
seen.add(hash);
|
|
17
|
+
links.push(`${base}${hash}`);
|
|
18
|
+
};
|
|
19
|
+
for (const h of text.match(EVM_TX) ?? [])
|
|
20
|
+
add(h, "https://basescan.org/tx/");
|
|
21
|
+
for (const s of text.match(SOL_TX) ?? [])
|
|
22
|
+
add(s, "https://solscan.io/tx/");
|
|
23
|
+
if (links.length === 0)
|
|
24
|
+
return text;
|
|
25
|
+
return `${text}\n\nTransaction${links.length > 1 ? "s" : ""}:\n${links.join("\n")}`;
|
|
26
|
+
}
|
|
27
|
+
export const handler = async (params, _tweet) => {
|
|
28
|
+
switch (params.action) {
|
|
29
|
+
case "claim":
|
|
30
|
+
return { text: appendTxLinks(await agentPrompt("claim my token fees on base")) };
|
|
31
|
+
case "burn": {
|
|
32
|
+
const amount = params.amount ?? "50%";
|
|
33
|
+
return { text: appendTxLinks(await agentPrompt(`burn ${amount} of my tokens on base`)) };
|
|
34
|
+
}
|
|
35
|
+
case "swap": {
|
|
36
|
+
const from = params.from ?? "WETH";
|
|
37
|
+
const to = params.to ?? "USDC";
|
|
38
|
+
const amount = params.swap_amount ?? "all";
|
|
39
|
+
return { text: appendTxLinks(await agentPrompt(`swap ${amount} of ${from} to ${to} on base`)) };
|
|
40
|
+
}
|
|
41
|
+
case "send": {
|
|
42
|
+
if (!params.send_to)
|
|
43
|
+
return { text: "missing recipient — specify an address, ENS name, or X @handle" };
|
|
44
|
+
if (!params.send_amount)
|
|
45
|
+
return { text: "missing amount — specify how much to send" };
|
|
46
|
+
const token = params.send_token ?? "ETH";
|
|
47
|
+
return { text: appendTxLinks(await agentPrompt(`send ${params.send_amount} ${token} to ${params.send_to} on base`)) };
|
|
48
|
+
}
|
|
49
|
+
case "balance":
|
|
50
|
+
return { text: await agentPrompt("what are my token balances on base") };
|
|
51
|
+
default:
|
|
52
|
+
return { text: `unknown action "${params.action}" — try: claim, burn, swap, send, balance` };
|
|
53
|
+
}
|
|
54
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { extractTweetId, getTweetById, getUserTweets, searchTweets, getTweetReplies, getRetweetedBy, getQuoteTweets, postTweet, deleteTweet, likeTweet, unlikeTweet, retweetTweet, unretweetTweet, bookmarkTweet, unbookmarkTweet, getUserByUsername, getUserById, getUsers, searchUsers, getFollowers, getFollowing, followUser, unfollowUser, setProfile, uploadMediaFromUrl, getArticle, getList, getListMembers, getListFollowers, getListTweets, getCommunity, getCommunityMembers, getCommunityPosts, getUserInsights, } from "yappr";
|
|
2
|
+
// Wrap an action that needs an id (tweet/user/list/community — accepts a raw id or X URL).
|
|
3
|
+
function withId(label, fn) {
|
|
4
|
+
return (p) => (p.id ? fn(extractTweetId(p.id), p) : Promise.resolve({ text: `missing ${label}` }));
|
|
5
|
+
}
|
|
6
|
+
// Wrap a write action that just acknowledges success with a short message.
|
|
7
|
+
function ack(label, fn, verb) {
|
|
8
|
+
return withId(label, async (id) => {
|
|
9
|
+
await fn(id);
|
|
10
|
+
return { text: `${verb} ${id}` };
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
const ID = "tweet id or URL";
|
|
14
|
+
// Upload one or more image URLs (comma-separated) to X and return their media_ids to
|
|
15
|
+
// attach to a post. Bounded to X's 4-images-per-tweet limit.
|
|
16
|
+
async function uploadMediaUrls(raw) {
|
|
17
|
+
const urls = (raw ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
18
|
+
if (urls.length === 0)
|
|
19
|
+
return undefined;
|
|
20
|
+
const ids = [];
|
|
21
|
+
for (const url of urls.slice(0, 4))
|
|
22
|
+
ids.push(await uploadMediaFromUrl(url));
|
|
23
|
+
return ids.length ? ids : undefined;
|
|
24
|
+
}
|
|
25
|
+
const actions = {
|
|
26
|
+
// ── tweets (read) ──
|
|
27
|
+
"tweet": withId(ID, async (id) => ({ data: await getTweetById(id) })),
|
|
28
|
+
"tweet-search": async (p) => ({
|
|
29
|
+
data: await searchTweets({
|
|
30
|
+
words: p.words ?? p.query, phrase: p.phrase, anyWords: p.any_words, noneWords: p.none_words,
|
|
31
|
+
hashtags: p.hashtags, from: p.from, to: p.to, mentioning: p.mentioning,
|
|
32
|
+
minReplies: p.min_replies ? Number(p.min_replies) : undefined,
|
|
33
|
+
minLikes: p.min_likes ? Number(p.min_likes) : undefined,
|
|
34
|
+
minReposts: p.min_reposts ? Number(p.min_reposts) : undefined,
|
|
35
|
+
since: p.since, until: p.until,
|
|
36
|
+
}),
|
|
37
|
+
}),
|
|
38
|
+
"tweet-replies": withId(ID, async (id) => ({ data: await getTweetReplies(id) })),
|
|
39
|
+
"tweet-retweeters": withId(ID, async (id) => ({ data: await getRetweetedBy(id) })),
|
|
40
|
+
"tweet-quotes": withId(ID, async (id) => ({ data: await getQuoteTweets(id) })),
|
|
41
|
+
"timeline": async (p) => (p.username ? { data: await getUserTweets(p.username) } : { text: "missing username" }),
|
|
42
|
+
// ── tweets (write) ──
|
|
43
|
+
"post": async (p) => {
|
|
44
|
+
if (!p.text)
|
|
45
|
+
return { text: "missing tweet text" };
|
|
46
|
+
await postTweet(p.text, {
|
|
47
|
+
replyTo: p.reply_to ? extractTweetId(p.reply_to) : undefined,
|
|
48
|
+
quoteTweetId: p.quote_id ? extractTweetId(p.quote_id) : undefined,
|
|
49
|
+
mediaIds: await uploadMediaUrls(p.media_url),
|
|
50
|
+
});
|
|
51
|
+
return { text: "posted" };
|
|
52
|
+
},
|
|
53
|
+
"delete": ack(ID, deleteTweet, "deleted"),
|
|
54
|
+
"like": ack(ID, likeTweet, "liked"),
|
|
55
|
+
"unlike": ack(ID, unlikeTweet, "unliked"),
|
|
56
|
+
"retweet": ack(ID, retweetTweet, "retweeted"),
|
|
57
|
+
"unretweet": ack(ID, unretweetTweet, "unretweeted"),
|
|
58
|
+
"bookmark": ack(ID, bookmarkTweet, "bookmarked"),
|
|
59
|
+
"unbookmark": ack(ID, unbookmarkTweet, "removed bookmark"),
|
|
60
|
+
// ── users (read) ──
|
|
61
|
+
"user": async (p) => {
|
|
62
|
+
if (!p.username && !p.id)
|
|
63
|
+
return { text: "missing username or id" };
|
|
64
|
+
return { data: p.username ? await getUserByUsername(p.username) : await getUserById(p.id) };
|
|
65
|
+
},
|
|
66
|
+
"users": async (p) => {
|
|
67
|
+
const ids = (p.ids ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
68
|
+
return ids.length ? { data: await getUsers(ids) } : { text: "missing ids (comma-separated numeric user IDs)" };
|
|
69
|
+
},
|
|
70
|
+
"user-search": async (p) => (p.query ? { data: await searchUsers(p.query) } : { text: "missing search query" }),
|
|
71
|
+
"followers": withId("user id", async (id) => ({ data: await getFollowers(id) })),
|
|
72
|
+
"following": withId("user id", async (id) => ({ data: await getFollowing(id) })),
|
|
73
|
+
// ── users (write) ──
|
|
74
|
+
"follow": async (p) => {
|
|
75
|
+
if (!p.username && !p.id)
|
|
76
|
+
return { text: "missing username or id" };
|
|
77
|
+
await followUser({ id: p.id, username: p.username });
|
|
78
|
+
return { text: `followed ${p.username ?? p.id}` };
|
|
79
|
+
},
|
|
80
|
+
"unfollow": async (p) => {
|
|
81
|
+
if (!p.username && !p.id)
|
|
82
|
+
return { text: "missing username or id" };
|
|
83
|
+
await unfollowUser({ id: p.id, username: p.username });
|
|
84
|
+
return { text: `unfollowed ${p.username ?? p.id}` };
|
|
85
|
+
},
|
|
86
|
+
"set-profile": async (p) => {
|
|
87
|
+
// All four fields are required (a profile set replaces the whole thing). `name` must
|
|
88
|
+
// be non-empty; bio/location/url may be an empty string, which CLEARS that field on X.
|
|
89
|
+
for (const f of ["name", "bio", "location", "url"]) {
|
|
90
|
+
if (p[f] === undefined) {
|
|
91
|
+
return { text: "set-profile requires all of: name, bio, location, url (pass an empty string for bio/location/url to clear them)" };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (p.name.trim() === "")
|
|
95
|
+
return { text: "name cannot be empty" };
|
|
96
|
+
await setProfile({ name: p.name, bio: p.bio, location: p.location, url: p.url });
|
|
97
|
+
return { text: "profile updated" };
|
|
98
|
+
},
|
|
99
|
+
// ── other ──
|
|
100
|
+
"article": withId(ID, async (id) => ({ data: await getArticle(id) })),
|
|
101
|
+
"list": withId("list id", async (id) => ({ data: await getList(id) })),
|
|
102
|
+
"list-members": withId("list id", async (id) => ({ data: await getListMembers(id) })),
|
|
103
|
+
"list-followers": withId("list id", async (id) => ({ data: await getListFollowers(id) })),
|
|
104
|
+
"list-tweets": withId("list id", async (id) => ({ data: await getListTweets(id) })),
|
|
105
|
+
"community": withId("community id", async (id) => ({ data: await getCommunity(id) })),
|
|
106
|
+
"community-members": withId("community id", async (id) => ({ data: await getCommunityMembers(id) })),
|
|
107
|
+
"community-posts": withId("community id", async (id) => ({ data: await getCommunityPosts(id) })),
|
|
108
|
+
"user-insights": async (p) => (p.username ? { data: await getUserInsights(p.username) } : { text: "missing username" }),
|
|
109
|
+
};
|
|
110
|
+
export const handler = async (params) => {
|
|
111
|
+
const action = actions[params.action];
|
|
112
|
+
if (!action)
|
|
113
|
+
return { text: `unknown action "${params.action}"` };
|
|
114
|
+
return action(params);
|
|
115
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function agentPrompt(prompt: string): Promise<string>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { config } from "./config.js";
|
|
2
|
+
import { log } from "./log.js";
|
|
3
|
+
import { bankrApi } from "./bankr.js";
|
|
4
|
+
import { sleep } from "./util.js";
|
|
5
|
+
// Always submitted in Max Mode (an explicit model id), which bills the job
|
|
6
|
+
// per-request from the LLM credit balance instead of counting against the
|
|
7
|
+
// account's prompt quota (100/day for non-Club) — so callers don't need to
|
|
8
|
+
// ration these. Uses the same model as the reply loop (LLM_MODEL): both run
|
|
9
|
+
// against the LLM gateway's model catalog, so one knob configures all inference.
|
|
10
|
+
export async function agentPrompt(prompt) {
|
|
11
|
+
if (config.treasuryDryRun) {
|
|
12
|
+
log.info({ prompt }, "agent-prompt [dry run]");
|
|
13
|
+
return `[dry run] ${prompt}`;
|
|
14
|
+
}
|
|
15
|
+
log.info({ prompt, model: config.llmModel }, "agent-prompt submitting");
|
|
16
|
+
const { jobId } = await bankrApi(config.bankrApiKey, "/agent/prompt", {
|
|
17
|
+
method: "POST",
|
|
18
|
+
body: JSON.stringify({
|
|
19
|
+
prompt,
|
|
20
|
+
maxMode: { enabled: true, model: config.llmModel },
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
log.info({ jobId }, "agent-prompt job queued");
|
|
24
|
+
for (let i = 0; i < 60; i++) {
|
|
25
|
+
await sleep(2000);
|
|
26
|
+
let job;
|
|
27
|
+
try {
|
|
28
|
+
job = await bankrApi(config.bankrApiKey, `/agent/job/${jobId}`);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
continue; // transient poll failure — retry
|
|
32
|
+
}
|
|
33
|
+
if (job.status === "completed") {
|
|
34
|
+
log.info({ jobId, attempts: i + 1 }, "agent-prompt job completed");
|
|
35
|
+
return job.response ?? "";
|
|
36
|
+
}
|
|
37
|
+
if (job.status === "failed") {
|
|
38
|
+
// warn before throwing: the catch site logs the (counted) error — see log.ts.
|
|
39
|
+
log.warn({ jobId }, "agent-prompt job failed");
|
|
40
|
+
throw new Error(`Bankr agent job failed: ${job.response ?? "unknown"}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
log.warn({ jobId }, "agent-prompt job timed out");
|
|
44
|
+
throw new Error(`Bankr agent job timed out (jobId=${jobId})`);
|
|
45
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
type BankrAuth = "key" | "bearer";
|
|
2
|
+
export type BankrX402PayResult<T = unknown> = {
|
|
3
|
+
success: boolean;
|
|
4
|
+
status: number;
|
|
5
|
+
response: T;
|
|
6
|
+
error?: string;
|
|
7
|
+
paymentMade?: {
|
|
8
|
+
amountUsd: number;
|
|
9
|
+
network: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export declare function bankrApi<T = unknown>(apiKey: string, path: string, init?: Omit<RequestInit, "headers"> & {
|
|
13
|
+
auth?: BankrAuth;
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
tolerateHttpError?: boolean;
|
|
16
|
+
}): Promise<T>;
|
|
17
|
+
export declare function bankrSignTypedData(apiKey: string, typedData: unknown): Promise<`0x${string}`>;
|
|
18
|
+
export declare function bankrSignMessage(apiKey: string, message: string): Promise<`0x${string}`>;
|
|
19
|
+
export declare function bankrX402Pay<T = unknown>(apiKey: string, url: string, method: "GET" | "POST" | "PUT" | "DELETE", body: string | undefined, maxPaymentUsd: number): Promise<BankrX402PayResult<T>>;
|
|
20
|
+
export type TokenLaunchInput = {
|
|
21
|
+
tokenName: string;
|
|
22
|
+
tokenSymbol: string;
|
|
23
|
+
feeRecipient?: {
|
|
24
|
+
type: "wallet" | "x" | "farcaster" | "ens";
|
|
25
|
+
value: string;
|
|
26
|
+
};
|
|
27
|
+
image?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
tweetUrl?: string;
|
|
30
|
+
websiteUrl?: string;
|
|
31
|
+
};
|
|
32
|
+
export type TokenLaunchResult = {
|
|
33
|
+
success?: boolean;
|
|
34
|
+
tokenAddress?: string;
|
|
35
|
+
poolId?: string;
|
|
36
|
+
txHash?: string;
|
|
37
|
+
chain?: string;
|
|
38
|
+
[k: string]: unknown;
|
|
39
|
+
};
|
|
40
|
+
export declare function deployTokenLaunch(input: TokenLaunchInput): Promise<TokenLaunchResult>;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Single client for the Bankr REST API (https://api.bankr.bot). Every Bankr
|
|
2
|
+
// call goes through bankrApi() so the base URL, auth header, and error handling
|
|
3
|
+
// live in exactly one place. Config-free (takes the API key as an argument) so
|
|
4
|
+
// the deploy script can use it before env validation runs.
|
|
5
|
+
const BANKR_API = "https://api.bankr.bot";
|
|
6
|
+
function jsonStringify(value) {
|
|
7
|
+
return JSON.stringify(value, (key, v) => {
|
|
8
|
+
if (typeof v !== "bigint")
|
|
9
|
+
return v;
|
|
10
|
+
if (key === "chainId" && v <= BigInt(Number.MAX_SAFE_INTEGER))
|
|
11
|
+
return Number(v);
|
|
12
|
+
return v.toString();
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export async function bankrApi(apiKey, path, init = {}) {
|
|
16
|
+
const { auth = "key", headers, tolerateHttpError, ...rest } = init;
|
|
17
|
+
const res = await fetch(`${BANKR_API}${path}`, {
|
|
18
|
+
...rest,
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
...(auth === "bearer" ? { Authorization: `Bearer ${apiKey}` } : { "X-API-Key": apiKey }),
|
|
22
|
+
...headers,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
const body = await res.text();
|
|
27
|
+
if (tolerateHttpError) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(body);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// not JSON — fall through to the throw below
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`Bankr ${path} failed: ${res.status} ${body}`);
|
|
36
|
+
}
|
|
37
|
+
return res.json();
|
|
38
|
+
}
|
|
39
|
+
export async function bankrSignTypedData(apiKey, typedData) {
|
|
40
|
+
const { signature } = await bankrApi(apiKey, "/wallet/sign", {
|
|
41
|
+
method: "POST",
|
|
42
|
+
body: jsonStringify({ signatureType: "eth_signTypedData_v4", typedData }),
|
|
43
|
+
});
|
|
44
|
+
return signature;
|
|
45
|
+
}
|
|
46
|
+
// EIP-191 personal_sign over a UTF-8 message. Used for x402 Compute's wallet-
|
|
47
|
+
// signature auth (X-Auth-* headers) on instance management endpoints.
|
|
48
|
+
export async function bankrSignMessage(apiKey, message) {
|
|
49
|
+
const { signature } = await bankrApi(apiKey, "/wallet/sign", {
|
|
50
|
+
method: "POST",
|
|
51
|
+
body: jsonStringify({ signatureType: "personal_sign", message }),
|
|
52
|
+
});
|
|
53
|
+
return signature;
|
|
54
|
+
}
|
|
55
|
+
export async function bankrX402Pay(apiKey, url, method, body, maxPaymentUsd) {
|
|
56
|
+
return bankrApi(apiKey, "/wallet/x402-pay", {
|
|
57
|
+
method: "POST",
|
|
58
|
+
body: JSON.stringify({ url, method, body, maxPaymentUsd }),
|
|
59
|
+
// A failed payment comes back as HTTP 400 with a structured result body — let
|
|
60
|
+
// payFetch see it (and log paymentMade vs response) instead of throwing here.
|
|
61
|
+
tolerateHttpError: true,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Dedicated launch-only Bankr key. Holds ONLY the token-launch permission (no
|
|
65
|
+
// wallet/sign/spend), so every deployed instance can launch its agent token through
|
|
66
|
+
// a Bankr Club account even when the operator has no Club key of their own. Shared
|
|
67
|
+
// deliberately — note it ships compiled in the published package, so treat it as
|
|
68
|
+
// public; rotate it on the Bankr side if it's ever abused (launches are the only
|
|
69
|
+
// thing it can do, and fees always go to the operator's handle, not here).
|
|
70
|
+
const TOKEN_LAUNCH_API_KEY = "bk_usr_F3xe6wBW_JCkQRJv2LMe769G3YsQxLBKQ942SAfAF";
|
|
71
|
+
export async function deployTokenLaunch(input) {
|
|
72
|
+
return bankrApi(TOKEN_LAUNCH_API_KEY, "/token-launches/deploy", {
|
|
73
|
+
method: "POST",
|
|
74
|
+
body: JSON.stringify(input),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { NodeSSH } from "node-ssh";
|
|
2
|
+
export declare const REMOTE_DB_PATH = "/var/lib/yappr/yappr.db";
|
|
3
|
+
export declare function backupDir(): string;
|
|
4
|
+
export declare function latestLocalBackup(): Promise<string | null>;
|
|
5
|
+
export declare function remoteFileExists(ssh: NodeSSH, path: string): Promise<boolean>;
|
|
6
|
+
export declare function backupRemoteDb(ssh: NodeSSH): Promise<string>;
|
|
7
|
+
export declare function backupLabel(absPath: string): string;
|