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,78 @@
|
|
|
1
|
+
// Stats DB backup/restore helpers shared by the CLI.
|
|
2
|
+
//
|
|
3
|
+
// The agent's stats live in a SQLite DB on the server at /var/lib/yappr/yappr.db
|
|
4
|
+
// (outside the redeploy-wiped /yappr, so it survives redeploys to the SAME instance).
|
|
5
|
+
// To survive *switching* instances, the status dashboard periodically pulls a
|
|
6
|
+
// consistent snapshot down into instance/backups/, and a fresh deploy offers to
|
|
7
|
+
// restore the latest snapshot onto the new box. Everything here is best-effort.
|
|
8
|
+
import { resolve, join, basename } from "node:path";
|
|
9
|
+
import { mkdir, readdir, unlink } from "node:fs/promises";
|
|
10
|
+
// Where the server keeps the live DB (matches DB_PATH set by deploy).
|
|
11
|
+
export const REMOTE_DB_PATH = "/var/lib/yappr/yappr.db";
|
|
12
|
+
// Keep at most this many daily snapshots (one file per day) — ~one week of coverage.
|
|
13
|
+
const MAX_LOCAL_BACKUPS = 7;
|
|
14
|
+
export function backupDir() {
|
|
15
|
+
return resolve(process.cwd(), "backups");
|
|
16
|
+
}
|
|
17
|
+
// Day stamp (YYYY-MM-DD) for the local filename — lexically sortable (= chronological).
|
|
18
|
+
// Repeated backups within a day overwrite the same file, so we keep one snapshot per day.
|
|
19
|
+
function dayStamp() {
|
|
20
|
+
return new Date().toISOString().slice(0, 10);
|
|
21
|
+
}
|
|
22
|
+
async function listBackups() {
|
|
23
|
+
try {
|
|
24
|
+
return (await readdir(backupDir()))
|
|
25
|
+
.filter((f) => f.startsWith("yappr-") && f.endsWith(".db"))
|
|
26
|
+
.sort();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Path to the newest local backup, or null if there are none.
|
|
33
|
+
export async function latestLocalBackup() {
|
|
34
|
+
const files = await listBackups();
|
|
35
|
+
return files.length ? join(backupDir(), files[files.length - 1]) : null;
|
|
36
|
+
}
|
|
37
|
+
// Single-quote a path for safe interpolation into a remote shell command.
|
|
38
|
+
const shq = (p) => `'${p.split("'").join(`'\\''`)}'`;
|
|
39
|
+
// True if `path` exists on the remote host. Decided by `test -f`'s exit code —
|
|
40
|
+
// never by parsing stdout, which channel noise could corrupt.
|
|
41
|
+
export async function remoteFileExists(ssh, path) {
|
|
42
|
+
const r = await ssh.execCommand(`test -f ${shq(path)}`, { cwd: "/" });
|
|
43
|
+
return r.code === 0;
|
|
44
|
+
}
|
|
45
|
+
// Snapshot the server DB (consistent, via SQLite VACUUM INTO in stats-cli) and pull it
|
|
46
|
+
// into instance/backups/. Returns the local file path. Throws on failure (callers are
|
|
47
|
+
// best-effort and catch).
|
|
48
|
+
export async function backupRemoteDb(ssh) {
|
|
49
|
+
// Remote temp is uniquely named (VACUUM INTO requires a non-existent dest); the local
|
|
50
|
+
// file is named per-day, so a later backup the same day overwrites it.
|
|
51
|
+
const remoteTmp = `/tmp/yappr-backup-${Date.now()}.db`;
|
|
52
|
+
// `cd /yappr` so stats-cli's dotenv picks up DB_PATH; the engine lives in node_modules.
|
|
53
|
+
const snap = await ssh.execCommand(`cd /yappr && node node_modules/yappr/dist/src/stats-cli.js backup ${shq(remoteTmp)}`, { cwd: "/" });
|
|
54
|
+
if (snap.code !== 0) {
|
|
55
|
+
throw new Error((snap.stderr || snap.stdout || "remote snapshot failed").trim());
|
|
56
|
+
}
|
|
57
|
+
await mkdir(backupDir(), { recursive: true });
|
|
58
|
+
const local = join(backupDir(), `yappr-${dayStamp()}.db`);
|
|
59
|
+
try {
|
|
60
|
+
await ssh.getFile(local, remoteTmp);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
// Remove the remote snapshot even when the download fails — each one is a
|
|
64
|
+
// full DB copy, and stranded ones would pile up in the server's /tmp.
|
|
65
|
+
await ssh.execCommand(`rm -f ${shq(remoteTmp)}`, { cwd: "/" }).catch(() => { });
|
|
66
|
+
}
|
|
67
|
+
await pruneOldBackups();
|
|
68
|
+
return local;
|
|
69
|
+
}
|
|
70
|
+
async function pruneOldBackups() {
|
|
71
|
+
const files = await listBackups();
|
|
72
|
+
const excess = files.slice(0, Math.max(0, files.length - MAX_LOCAL_BACKUPS));
|
|
73
|
+
await Promise.all(excess.map((f) => unlink(join(backupDir(), f)).catch(() => { })));
|
|
74
|
+
}
|
|
75
|
+
// Short, cwd-relative label for a backup path (e.g. "backups/yappr-….db").
|
|
76
|
+
export function backupLabel(absPath) {
|
|
77
|
+
return `backups/${basename(absPath)}`;
|
|
78
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const SPENT_RGB: () => string;
|
|
2
|
+
export declare const EARN_RGB: () => string;
|
|
3
|
+
export declare const CAT_RGB: () => {
|
|
4
|
+
xapi: string;
|
|
5
|
+
inference: string;
|
|
6
|
+
compute: string;
|
|
7
|
+
x402: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const catColor: (rgb: string) => (s: string) => string;
|
|
10
|
+
export type ChartSeries = {
|
|
11
|
+
spendUsd: number[];
|
|
12
|
+
earnedWeth: number[];
|
|
13
|
+
startMs: number;
|
|
14
|
+
endMs: number;
|
|
15
|
+
};
|
|
16
|
+
export declare const HOUR_MS = 3600000;
|
|
17
|
+
export declare function renderLineChart(cols: number, series: ChartSeries, ethUsd: number | null, windowStart: number, windowEnd: number): string[];
|
|
18
|
+
export declare function renderHourlyBars(cols: number, byType: {
|
|
19
|
+
startMs: number;
|
|
20
|
+
xapi: number[];
|
|
21
|
+
inference: number[];
|
|
22
|
+
compute: number[];
|
|
23
|
+
x402: number[];
|
|
24
|
+
}): string[];
|
|
25
|
+
export declare function renderHourlySpentEarned(cols: number, d: {
|
|
26
|
+
startMs: number;
|
|
27
|
+
xapi: number[];
|
|
28
|
+
inference: number[];
|
|
29
|
+
compute: number[];
|
|
30
|
+
x402: number[];
|
|
31
|
+
earned: number[];
|
|
32
|
+
}, ethUsd: number | null): string[];
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Chart rendering for the status dashboard (`cli/status.ts`): the cumulative
|
|
2
|
+
// spent/earned line charts (asciichart) and the hand-rolled hourly stacked bar
|
|
3
|
+
// charts, plus their shared colors, scales and the adaptive time axis. Pure
|
|
4
|
+
// string-building — no I/O, no dashboard state — so it's testable in isolation
|
|
5
|
+
// (see scripts/chart-equivalence.ts for the bar-renderer fuzz check).
|
|
6
|
+
import * as asciichart from "asciichart";
|
|
7
|
+
import { dim, chartRgb } from "./ui.js";
|
|
8
|
+
import { envNumber } from "../util.js";
|
|
9
|
+
// Chart colors come from the current ui.ts theme (chartRgb()) as truecolor RGB
|
|
10
|
+
// triples — used by the stacked bar charts (the boundary cell needs an fg AND a
|
|
11
|
+
// bg color), the legends, and (escape-wrapped) the asciichart line charts.
|
|
12
|
+
// Resolved lazily per render, so the TUI's live theme toggle applies here too.
|
|
13
|
+
export const SPENT_RGB = () => chartRgb().spent;
|
|
14
|
+
export const EARN_RGB = () => chartRgb().earn;
|
|
15
|
+
// Plot rows for the spent/earned line charts. asciichart draws height+1 rows, so 8 → 9
|
|
16
|
+
// rows + 1 axis = 10 content lines, matching the hourly bar chart (H=9 bars + 1 axis).
|
|
17
|
+
// Override with STATUS_CHART_HEIGHT. (1-decimal y labels keep the rows distinct.)
|
|
18
|
+
const LINE_CHART_HEIGHT = Math.max(3, envNumber("STATUS_CHART_HEIGHT", 8));
|
|
19
|
+
// Expense-category colors as truecolor RGB triples, from the current theme.
|
|
20
|
+
// Shared by the bar chart and its legend.
|
|
21
|
+
export const CAT_RGB = () => ({ xapi: chartRgb().xapi, inference: chartRgb().inference, compute: chartRgb().compute, x402: chartRgb().x402 });
|
|
22
|
+
export const catColor = (rgb) => (s) => `\x1b[38;2;${rgb}m${s}\x1b[0m`;
|
|
23
|
+
export const HOUR_MS = 3_600_000;
|
|
24
|
+
const DAY_MS = 86_400_000;
|
|
25
|
+
const LABEL_OFFSET = 9; // asciichart's y-axis label column width (7-char label + " " + axis)
|
|
26
|
+
// Resample a series to exactly `width` points (down- or up-sampling) with linear
|
|
27
|
+
// interpolation, keeping the endpoints — so the chart fills the panel width regardless
|
|
28
|
+
// of how many points the server sent.
|
|
29
|
+
function fitSeries(arr, width) {
|
|
30
|
+
if (arr.length === 0 || width < 1)
|
|
31
|
+
return [];
|
|
32
|
+
if (arr.length === width)
|
|
33
|
+
return arr;
|
|
34
|
+
if (width === 1)
|
|
35
|
+
return [arr[arr.length - 1]];
|
|
36
|
+
const out = [];
|
|
37
|
+
for (let i = 0; i < width; i++) {
|
|
38
|
+
const pos = (i * (arr.length - 1)) / (width - 1);
|
|
39
|
+
const lo = Math.floor(pos);
|
|
40
|
+
const hi = Math.min(arr.length - 1, lo + 1);
|
|
41
|
+
out.push(arr[lo] + (arr[hi] - arr[lo]) * (pos - lo));
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
// Compact money label, e.g. $0.3 / $4.7 / $45 / $1.2k / $3.4m (negative-zero stripped).
|
|
46
|
+
// Keeps 1 decimal under $100 so closely-spaced axis rows don't collapse to the same label.
|
|
47
|
+
function fmtMoney(x) {
|
|
48
|
+
const a = Math.abs(x);
|
|
49
|
+
let t = a >= 1e6 ? (x / 1e6).toFixed(1) + "m" : a >= 1000 ? (x / 1000).toFixed(1) + "k" : a >= 100 ? x.toFixed(0) : x.toFixed(1);
|
|
50
|
+
if (parseFloat(t) === 0)
|
|
51
|
+
t = t.replace(/^-/, "");
|
|
52
|
+
return "$" + t;
|
|
53
|
+
}
|
|
54
|
+
// Adaptive x-axis row aligned to the plot columns over [startMs, endMs]: hourly ticks for
|
|
55
|
+
// spans ≤2 days, daily up to ~75 days, else monthly. Labels are placed left→right and any
|
|
56
|
+
// that would touch the previous one is skipped, so they thin to fit and never merge.
|
|
57
|
+
function adaptiveTimeAxis(labelOffset, plotWidth, startMs, endMs) {
|
|
58
|
+
const total = labelOffset + plotWidth;
|
|
59
|
+
const cells = new Array(total).fill(" ");
|
|
60
|
+
const span = endMs - startMs;
|
|
61
|
+
if (span <= 0)
|
|
62
|
+
return cells.join("");
|
|
63
|
+
const ticks = [];
|
|
64
|
+
if (span <= 2 * DAY_MS) {
|
|
65
|
+
const d = new Date(startMs);
|
|
66
|
+
d.setMinutes(0, 0, 0);
|
|
67
|
+
for (let t = d.getTime(); t <= endMs; t += HOUR_MS)
|
|
68
|
+
ticks.push({ t, label: `${String(new Date(t).getHours()).padStart(2, "0")}:00` });
|
|
69
|
+
}
|
|
70
|
+
else if (span <= 75 * DAY_MS) {
|
|
71
|
+
const d = new Date(startMs);
|
|
72
|
+
d.setHours(0, 0, 0, 0);
|
|
73
|
+
for (let t = d.getTime(); t <= endMs; t += DAY_MS)
|
|
74
|
+
ticks.push({ t, label: new Date(t).toLocaleDateString(undefined, { month: "short", day: "numeric" }) });
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const multiYear = span > 330 * DAY_MS;
|
|
78
|
+
const m = new Date(startMs);
|
|
79
|
+
m.setDate(1);
|
|
80
|
+
m.setHours(0, 0, 0, 0);
|
|
81
|
+
while (m.getTime() <= endMs) {
|
|
82
|
+
ticks.push({ t: m.getTime(), label: m.toLocaleDateString(undefined, multiYear ? { month: "short", year: "2-digit" } : { month: "short" }) });
|
|
83
|
+
m.setMonth(m.getMonth() + 1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!ticks.length)
|
|
87
|
+
return cells.join("");
|
|
88
|
+
// Thin uniformly: show every `step`-th tick so labels are evenly spaced and don't collide
|
|
89
|
+
// (greedy per-tick skipping looked irregular with many same-width hour labels).
|
|
90
|
+
const maxLen = Math.max(...ticks.map((t) => t.label.length));
|
|
91
|
+
const colsPerTick = plotWidth / ticks.length;
|
|
92
|
+
const step = Math.max(1, Math.ceil((maxLen + 2) / Math.max(colsPerTick, 0.001)));
|
|
93
|
+
let lastEnd = labelOffset - 1;
|
|
94
|
+
for (let i = 0; i < ticks.length; i += step) {
|
|
95
|
+
const { t, label } = ticks[i];
|
|
96
|
+
const plotCol = Math.round(((t - startMs) / span) * (plotWidth - 1));
|
|
97
|
+
let pos = labelOffset + plotCol - Math.floor(label.length / 2); // center under the tick
|
|
98
|
+
if (pos < labelOffset)
|
|
99
|
+
pos = labelOffset;
|
|
100
|
+
if (pos + label.length > total)
|
|
101
|
+
pos = total - label.length;
|
|
102
|
+
if (pos <= lastEnd)
|
|
103
|
+
continue; // safety: never overlap the previous label
|
|
104
|
+
for (let j = 0; j < label.length; j++)
|
|
105
|
+
cells[pos + j] = label[j];
|
|
106
|
+
lastEnd = pos + label.length;
|
|
107
|
+
}
|
|
108
|
+
return cells.join("");
|
|
109
|
+
}
|
|
110
|
+
// Cumulative spend/earn line chart → panel lines. The series covers [windowStart, endMs];
|
|
111
|
+
// it's drawn across the fraction of the plot that the data occupies within [windowStart,
|
|
112
|
+
// windowEnd], leaving the rest blank on the right (so a partial 24h shows empty space).
|
|
113
|
+
export function renderLineChart(cols, series, ethUsd, windowStart, windowEnd) {
|
|
114
|
+
const w = Math.max(8, cols - 13);
|
|
115
|
+
const winSpan = windowEnd - windowStart;
|
|
116
|
+
const dataCols = winSpan > 0 ? Math.max(2, Math.min(w, Math.round((w * (series.endMs - windowStart)) / winSpan))) : w;
|
|
117
|
+
const spend = fitSeries(series.spendUsd, dataCols);
|
|
118
|
+
const earn = ethUsd != null ? fitSeries(series.earnedWeth.map((v) => v * ethUsd), dataCols) : null;
|
|
119
|
+
const seriesArr = earn && earn.length === spend.length ? [spend, earn] : [spend];
|
|
120
|
+
const spendEsc = `\x1b[38;2;${SPENT_RGB()}m`, earnEsc = `\x1b[38;2;${EARN_RGB()}m`;
|
|
121
|
+
const colors = seriesArr.length === 2 ? [spendEsc, earnEsc] : [spendEsc];
|
|
122
|
+
const lines = asciichart.plot(seriesArr, { height: LINE_CHART_HEIGHT, colors, format: (x) => fmtMoney(x).padEnd(7) }).split("\n");
|
|
123
|
+
lines.push(dim(adaptiveTimeAxis(LABEL_OFFSET, w, windowStart, windowEnd))); // x-axis spans the full window
|
|
124
|
+
return lines;
|
|
125
|
+
}
|
|
126
|
+
// Generic stacked vertical bar chart over the 24 hourly buckets → panel lines.
|
|
127
|
+
// One discrete bar per hour, its segments stacked bottom-up in layer order
|
|
128
|
+
// (`layers`, each a values series + RGB). Half-block characters double the
|
|
129
|
+
// vertical resolution (each text row = 8 sub-levels via ▁..█ + fg/bg), so even
|
|
130
|
+
// thin segments show. A y-axis shows the max; an x-axis shows the hours below.
|
|
131
|
+
// Backs both the per-category expenses chart and the spent-vs-earned chart.
|
|
132
|
+
function renderStackedBars(cols, startMs, layers) {
|
|
133
|
+
const N = 24, H = 9, EIGHTH = 8, SUB = H * EIGHTH, labelW = 8, HOUR = 3_600_000;
|
|
134
|
+
const LOWER = [" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]; // lower-block fills, 0..8 eighths
|
|
135
|
+
const plotW = Math.max(N, cols - 4 - labelW);
|
|
136
|
+
const slot = plotW / N;
|
|
137
|
+
const totals = Array.from({ length: N }, (_, i) => layers.reduce((sum, l) => sum + (l.values[i] ?? 0), 0));
|
|
138
|
+
const max = Math.max(...totals, 1e-9);
|
|
139
|
+
// Per-hour segment heights in SUB-levels (8 per row), bottom→top in layer order.
|
|
140
|
+
// Rounding is reconciled so the parts sum to the bar's total height: the last
|
|
141
|
+
// layer takes the remainder, and any negative remainder eats into lower layers.
|
|
142
|
+
const seg = totals.map((tot, i) => {
|
|
143
|
+
const tr = Math.round((tot / max) * SUB);
|
|
144
|
+
const parts = layers.map((l, k) => k === layers.length - 1 ? 0 : tot > 0 ? Math.round(((l.values[i] ?? 0) / tot) * tr) : 0);
|
|
145
|
+
parts[parts.length - 1] = tr - parts.reduce((a, b) => a + b, 0);
|
|
146
|
+
for (let k = parts.length - 1; k > 0; k--) {
|
|
147
|
+
if (parts[k] < 0) {
|
|
148
|
+
parts[k - 1] += parts[k];
|
|
149
|
+
parts[k] = 0;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Cumulative top boundary of each layer, so subColor is a simple scan.
|
|
153
|
+
const bounds = [];
|
|
154
|
+
parts.reduce((acc, p) => { bounds.push(acc + p); return acc + p; }, 0);
|
|
155
|
+
return { bounds, tr };
|
|
156
|
+
});
|
|
157
|
+
// RGB of a bar's segment at a given sub-level, or null above the bar.
|
|
158
|
+
const subColor = (sg, sub) => {
|
|
159
|
+
if (sub >= sg.tr)
|
|
160
|
+
return null;
|
|
161
|
+
for (let k = 0; k < sg.bounds.length; k++)
|
|
162
|
+
if (sub < sg.bounds[k])
|
|
163
|
+
return layers[k].rgb;
|
|
164
|
+
return null;
|
|
165
|
+
};
|
|
166
|
+
// One text cell covering 8 sub-levels [base, base+8). Filled bottom-up: the bottom color
|
|
167
|
+
// fills `h1` eighths (foreground of a lower-block glyph); whatever's above (next segment
|
|
168
|
+
// color, or empty) is the cell background. Resolves single-color, a color boundary, and
|
|
169
|
+
// the bar's partial top at 1/8 precision.
|
|
170
|
+
const cellAt = (sg, base) => {
|
|
171
|
+
const c1 = subColor(sg, base);
|
|
172
|
+
if (!c1)
|
|
173
|
+
return " ";
|
|
174
|
+
let h1 = 1;
|
|
175
|
+
while (h1 < EIGHTH && subColor(sg, base + h1) === c1)
|
|
176
|
+
h1++;
|
|
177
|
+
const c2 = h1 < EIGHTH ? subColor(sg, base + h1) : null;
|
|
178
|
+
return c2 ? `\x1b[38;2;${c1};48;2;${c2}m${LOWER[h1]}\x1b[0m` : `\x1b[38;2;${c1}m${LOWER[h1]}\x1b[0m`;
|
|
179
|
+
};
|
|
180
|
+
// Each hour is a discrete bar `barW` columns wide, centered in its slot, with a clear gap.
|
|
181
|
+
const barW = Math.max(1, Math.round(slot * 0.5));
|
|
182
|
+
const lines = [];
|
|
183
|
+
for (let r = 0; r < H; r++) {
|
|
184
|
+
const base = (H - 1 - r) * EIGHTH; // sub-level at the bottom of this row (0 = chart floor)
|
|
185
|
+
const row = new Array(plotW).fill(" ");
|
|
186
|
+
for (let hour = 0; hour < N; hour++) {
|
|
187
|
+
const ch = cellAt(seg[hour], base);
|
|
188
|
+
if (ch === " ")
|
|
189
|
+
continue;
|
|
190
|
+
const colStart = Math.round(hour * slot + (slot - barW) / 2);
|
|
191
|
+
for (let w = 0; w < barW && colStart + w < plotW; w++)
|
|
192
|
+
row[colStart + w] = ch;
|
|
193
|
+
}
|
|
194
|
+
// Label every row at its top-edge value (bottom row = $0), flush-left like the line
|
|
195
|
+
// charts. The scale tracks `max`, so it adapts as spend grows ($1.2k / $3.4m).
|
|
196
|
+
const ylab = r === H - 1 ? "$0" : fmtMoney((max * (H - r)) / H);
|
|
197
|
+
lines.push(ylab.padEnd(labelW - 1) + "│" + row.join(""));
|
|
198
|
+
}
|
|
199
|
+
lines.push(dim(adaptiveTimeAxis(labelW, plotW, startMs, startMs + N * HOUR)));
|
|
200
|
+
return lines;
|
|
201
|
+
}
|
|
202
|
+
// Per-hour spend stacked by type: x-api / inference / compute / x402, bottom-up.
|
|
203
|
+
export function renderHourlyBars(cols, byType) {
|
|
204
|
+
const cat = CAT_RGB();
|
|
205
|
+
return renderStackedBars(cols, byType.startMs, [
|
|
206
|
+
{ values: byType.xapi, rgb: cat.xapi },
|
|
207
|
+
{ values: byType.inference, rgb: cat.inference },
|
|
208
|
+
{ values: byType.compute, rgb: cat.compute },
|
|
209
|
+
{ values: byType.x402, rgb: cat.x402 },
|
|
210
|
+
]);
|
|
211
|
+
}
|
|
212
|
+
// Per-hour spent (red, bottom) vs earned (cyan, stacked on top), both in USD —
|
|
213
|
+
// earned is the WETH series converted at the live ETH price.
|
|
214
|
+
export function renderHourlySpentEarned(cols, d, ethUsd) {
|
|
215
|
+
const N = 24;
|
|
216
|
+
const spent = Array.from({ length: N }, (_, i) => (d.xapi[i] ?? 0) + (d.inference[i] ?? 0) + (d.compute[i] ?? 0) + (d.x402[i] ?? 0));
|
|
217
|
+
const earned = Array.from({ length: N }, (_, i) => (d.earned[i] ?? 0) * (ethUsd ?? 0));
|
|
218
|
+
return renderStackedBars(cols, d.startMs, [
|
|
219
|
+
{ values: spent, rgb: SPENT_RGB() },
|
|
220
|
+
{ values: earned, rgb: EARN_RGB() },
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const MANIFEST_NAME = ".yappr-sync.json";
|
|
2
|
+
export declare function hashFile(path: string): Promise<string | null>;
|
|
3
|
+
export declare function relFiles(dir: string): Promise<string[]>;
|
|
4
|
+
export type Manifest = Record<string, string>;
|
|
5
|
+
export declare function loadManifest(configDir: string): Promise<Manifest>;
|
|
6
|
+
export declare function saveManifest(configDir: string, files: Manifest): Promise<void>;
|
|
7
|
+
export declare function manifestForDir(configDir: string): Promise<Manifest>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Provenance tracking for the scaffolded config/, so `yappr update` can tell which
|
|
2
|
+
// skill/hook/context files the user has edited from those still pristine.
|
|
3
|
+
//
|
|
4
|
+
// `init` records a manifest — the hash of every file it scaffolded — at
|
|
5
|
+
// config/.yappr-sync.json. On `update`, for each file the new package ships we compare
|
|
6
|
+
// the user's copy against (a) the recorded hash (what we last installed) and (b) the
|
|
7
|
+
// new shipped hash, to decide: fast-forward an untouched file, leave/ask on an edited
|
|
8
|
+
// one, or create a brand-new one. The manifest is dot-prefixed so the engine's config
|
|
9
|
+
// loader (which skips dotfiles) never treats it as a skill.
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { readFile, writeFile, readdir } from "node:fs/promises";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { join, relative, sep } from "node:path";
|
|
14
|
+
export const MANIFEST_NAME = ".yappr-sync.json";
|
|
15
|
+
// sha256 of a file's bytes, or null if it doesn't exist / can't be read.
|
|
16
|
+
export async function hashFile(path) {
|
|
17
|
+
try {
|
|
18
|
+
return createHash("sha256").update(await readFile(path)).digest("hex");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// All files under `dir`, as forward-slash relative paths, skipping any dot-prefixed
|
|
25
|
+
// file or directory (matches init's scaffold filter and the engine's loader). Returns
|
|
26
|
+
// [] if `dir` is absent.
|
|
27
|
+
export async function relFiles(dir) {
|
|
28
|
+
if (!existsSync(dir))
|
|
29
|
+
return [];
|
|
30
|
+
const out = [];
|
|
31
|
+
async function walk(abs) {
|
|
32
|
+
const entries = await readdir(abs, { withFileTypes: true }).catch(() => []);
|
|
33
|
+
for (const e of entries) {
|
|
34
|
+
if (e.name.startsWith("."))
|
|
35
|
+
continue;
|
|
36
|
+
const child = join(abs, e.name);
|
|
37
|
+
if (e.isDirectory())
|
|
38
|
+
await walk(child);
|
|
39
|
+
else if (e.isFile())
|
|
40
|
+
out.push(relative(dir, child).split(sep).join("/"));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
await walk(dir);
|
|
44
|
+
return out.sort();
|
|
45
|
+
}
|
|
46
|
+
export async function loadManifest(configDir) {
|
|
47
|
+
try {
|
|
48
|
+
const raw = await readFile(join(configDir, MANIFEST_NAME), "utf8");
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
return parsed && typeof parsed === "object" ? (parsed.files ?? parsed) : {};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export async function saveManifest(configDir, files) {
|
|
57
|
+
const body = { version: 1, updatedAt: new Date().toISOString(), files };
|
|
58
|
+
await writeFile(join(configDir, MANIFEST_NAME), JSON.stringify(body, null, 2) + "\n");
|
|
59
|
+
}
|
|
60
|
+
// Build a manifest by hashing every (non-dot) file in a config dir — used by init to
|
|
61
|
+
// record exactly what it scaffolded.
|
|
62
|
+
export async function manifestForDir(configDir) {
|
|
63
|
+
const files = await relFiles(configDir);
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const rel of files) {
|
|
66
|
+
const h = await hashFile(join(configDir, rel));
|
|
67
|
+
if (h)
|
|
68
|
+
out[rel] = h;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|