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,154 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
const TIME_RE = /^([01]?\d|2[0-3]):([0-5]\d)$/; // HH:MM, 24h
|
|
3
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; // YYYY-MM-DD
|
|
4
|
+
function validTimezone(tz) {
|
|
5
|
+
// Intl is the source of truth for zone names: constructing a formatter with an
|
|
6
|
+
// unknown timeZone throws a RangeError. "UTC" is itself a valid IANA zone.
|
|
7
|
+
try {
|
|
8
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// Build a validated Schedule from the raw string params the LLM passed to the
|
|
16
|
+
// skill. Returns `{ error }` with a message written FOR the model (it becomes the
|
|
17
|
+
// skill observation), so a bad request turns into a helpful reply to the user.
|
|
18
|
+
export function validateSchedule(raw) {
|
|
19
|
+
const type = raw.schedule;
|
|
20
|
+
if (type === "interval") {
|
|
21
|
+
const minutes = Number(raw.minutes);
|
|
22
|
+
if (!Number.isFinite(minutes) || minutes <= 0) {
|
|
23
|
+
return { error: `interval schedules need a positive "minutes" param (got "${raw.minutes}")` };
|
|
24
|
+
}
|
|
25
|
+
if (minutes < config.cronMinIntervalMin) {
|
|
26
|
+
return { error: `interval too short — the minimum is every ${config.cronMinIntervalMin} minute${config.cronMinIntervalMin === 1 ? "" : "s"}` };
|
|
27
|
+
}
|
|
28
|
+
return { type: "interval", minutes: Math.round(minutes) };
|
|
29
|
+
}
|
|
30
|
+
if (type === "once") {
|
|
31
|
+
if (raw.minutes !== undefined && raw.minutes !== "") {
|
|
32
|
+
const minutes = Number(raw.minutes);
|
|
33
|
+
if (!Number.isFinite(minutes) || minutes <= 0) {
|
|
34
|
+
return { error: `one-shot relative schedules need a positive "minutes" param (got "${raw.minutes}")` };
|
|
35
|
+
}
|
|
36
|
+
return { type: "once", minutes: Math.round(minutes) };
|
|
37
|
+
}
|
|
38
|
+
// Absolute one-shot: time (+ optional date) in an explicit timezone.
|
|
39
|
+
const abs = validateWallClock(raw, "a one-shot at a specific time");
|
|
40
|
+
if ("error" in abs)
|
|
41
|
+
return abs;
|
|
42
|
+
return { type: "once", date: raw.date || undefined, time: abs.time, timezone: abs.timezone };
|
|
43
|
+
}
|
|
44
|
+
if (type === "daily") {
|
|
45
|
+
const abs = validateWallClock(raw, "a daily schedule");
|
|
46
|
+
if ("error" in abs)
|
|
47
|
+
return abs;
|
|
48
|
+
return { type: "daily", time: abs.time, timezone: abs.timezone };
|
|
49
|
+
}
|
|
50
|
+
return { error: `unknown schedule type "${type}" — use "interval", "once" or "daily"` };
|
|
51
|
+
}
|
|
52
|
+
// Shared validation for wall-clock (time + timezone) schedules. The timezone is
|
|
53
|
+
// REQUIRED by design: if the user didn't state one, the agent must ask, not guess.
|
|
54
|
+
function validateWallClock(raw, what) {
|
|
55
|
+
if (!raw.time || !TIME_RE.test(raw.time)) {
|
|
56
|
+
return { error: `${what} needs a "time" param in 24h HH:MM format (got "${raw.time ?? ""}")` };
|
|
57
|
+
}
|
|
58
|
+
if (!raw.timezone) {
|
|
59
|
+
return {
|
|
60
|
+
error: `${what} needs an explicit timezone — ask the user which timezone they mean (an IANA name like Europe/Paris, America/New_York, or UTC)`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (!validTimezone(raw.timezone)) {
|
|
64
|
+
return { error: `unknown timezone "${raw.timezone}" — ask the user for an IANA timezone name like Europe/Paris or UTC` };
|
|
65
|
+
}
|
|
66
|
+
if (raw.date && !DATE_RE.test(raw.date)) {
|
|
67
|
+
return { error: `"date" must be YYYY-MM-DD (got "${raw.date}")` };
|
|
68
|
+
}
|
|
69
|
+
return { time: raw.time, timezone: raw.timezone };
|
|
70
|
+
}
|
|
71
|
+
// ── Wall-clock → instant conversion ─────────────────────────────────────────
|
|
72
|
+
//
|
|
73
|
+
// JS has no native "instant for 09:00 on 2026-06-12 in Europe/Paris". Intl can
|
|
74
|
+
// only go the other way (instant → zone-local wall time), so we invert it by
|
|
75
|
+
// fixed-point iteration:
|
|
76
|
+
//
|
|
77
|
+
// 1. guess: treat the wall time as if it were UTC → t0
|
|
78
|
+
// 2. format t0 in the target zone, measure how far the result is from the
|
|
79
|
+
// wanted wall time, and shift the guess by that difference
|
|
80
|
+
// 3. repeat once — the zone offset is piecewise-constant, so this converges
|
|
81
|
+
// in ≤2 steps even across DST transitions.
|
|
82
|
+
//
|
|
83
|
+
// DST edge cases (documented, accepted): a nonexistent local time (the
|
|
84
|
+
// spring-forward gap, e.g. 02:30 the night clocks jump) resolves ~1h shifted;
|
|
85
|
+
// an ambiguous time (fall-back hour) resolves to one of the two instants.
|
|
86
|
+
// Wall-clock fields of `t` in `timeZone`, read via Intl (en-CA gives YYYY-MM-DD).
|
|
87
|
+
function wallParts(t, timeZone) {
|
|
88
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
89
|
+
timeZone, year: "numeric", month: "2-digit", day: "2-digit",
|
|
90
|
+
hour: "2-digit", minute: "2-digit", hourCycle: "h23",
|
|
91
|
+
}).formatToParts(t);
|
|
92
|
+
const get = (type) => Number(parts.find((p) => p.type === type)?.value);
|
|
93
|
+
return { y: get("year"), mo: get("month"), d: get("day"), h: get("hour"), mi: get("minute") };
|
|
94
|
+
}
|
|
95
|
+
// Epoch ms of `y-mo-d hh:mm` wall time in `timeZone` (fixed-point, see above).
|
|
96
|
+
function zonedTimeToInstant(y, mo, d, hh, mm, timeZone) {
|
|
97
|
+
let t = Date.UTC(y, mo - 1, d, hh, mm);
|
|
98
|
+
for (let i = 0; i < 2; i++) {
|
|
99
|
+
const w = wallParts(t, timeZone);
|
|
100
|
+
const diff = Date.UTC(w.y, w.mo - 1, w.d, w.h, w.mi) - Date.UTC(y, mo - 1, d, hh, mm);
|
|
101
|
+
t -= diff;
|
|
102
|
+
}
|
|
103
|
+
return t;
|
|
104
|
+
}
|
|
105
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
106
|
+
// Next occurrence of `s` strictly after `after` (epoch ms). Returns null for a
|
|
107
|
+
// spent one-shot (absolute time already in the past — the runner treats overdue
|
|
108
|
+
// one-shots separately; at creation the store rejects null).
|
|
109
|
+
export function nextRunAt(s, after) {
|
|
110
|
+
if (s.type === "interval")
|
|
111
|
+
return after + s.minutes * 60_000;
|
|
112
|
+
if (s.type === "once") {
|
|
113
|
+
if (s.minutes !== undefined)
|
|
114
|
+
return after + s.minutes * 60_000;
|
|
115
|
+
const [hh, mm] = s.time.split(":").map(Number);
|
|
116
|
+
if (s.date) {
|
|
117
|
+
const [y, mo, d] = s.date.split("-").map(Number);
|
|
118
|
+
const t = zonedTimeToInstant(y, mo, d, hh, mm, s.timezone);
|
|
119
|
+
return t > after ? t : null;
|
|
120
|
+
}
|
|
121
|
+
// No date: the next time the wall clock reads HH:MM in that zone.
|
|
122
|
+
return nextWallClock(hh, mm, s.timezone, after);
|
|
123
|
+
}
|
|
124
|
+
// daily
|
|
125
|
+
const [hh, mm] = s.time.split(":").map(Number);
|
|
126
|
+
return nextWallClock(hh, mm, s.timezone, after);
|
|
127
|
+
}
|
|
128
|
+
// Next instant after `after` at which it is HH:MM in `timeZone`: try today (in
|
|
129
|
+
// the zone), then advance day by day. The +DAY_MS probe moves the wall-clock
|
|
130
|
+
// date forward; the exact instant is recomputed from the new date each step, so
|
|
131
|
+
// DST days (23h/25h long) can't drift the result.
|
|
132
|
+
function nextWallClock(hh, mm, timeZone, after) {
|
|
133
|
+
let probe = after;
|
|
134
|
+
for (let i = 0; i < 3; i++) {
|
|
135
|
+
const w = wallParts(probe, timeZone);
|
|
136
|
+
const t = zonedTimeToInstant(w.y, w.mo, w.d, hh, mm, timeZone);
|
|
137
|
+
if (t > after)
|
|
138
|
+
return t;
|
|
139
|
+
probe += DAY_MS;
|
|
140
|
+
}
|
|
141
|
+
// Unreachable (within 2 days there is always a next HH:MM), but never loop forever.
|
|
142
|
+
return after + DAY_MS;
|
|
143
|
+
}
|
|
144
|
+
// Human/one-line form for logs and the skill's `list` output.
|
|
145
|
+
export function describeSchedule(s) {
|
|
146
|
+
if (s.type === "interval")
|
|
147
|
+
return `every ${s.minutes} min`;
|
|
148
|
+
if (s.type === "once") {
|
|
149
|
+
if (s.minutes !== undefined)
|
|
150
|
+
return `once, ${s.minutes} min after creation`;
|
|
151
|
+
return `once at ${s.date ? `${s.date} ` : ""}${s.time} ${s.timezone}`;
|
|
152
|
+
}
|
|
153
|
+
return `daily at ${s.time} ${s.timezone}`;
|
|
154
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Tweet } from "../x/types.js";
|
|
2
|
+
import { type Schedule, describeSchedule } from "./schedule.js";
|
|
3
|
+
export type CronJob = {
|
|
4
|
+
id: number;
|
|
5
|
+
prompt: string;
|
|
6
|
+
schedule: Schedule;
|
|
7
|
+
creatorId: string;
|
|
8
|
+
creatorHandle: string;
|
|
9
|
+
sourceTweet: Tweet | null;
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
nextRunAt: number;
|
|
12
|
+
lastRunAt: number | null;
|
|
13
|
+
lastResult: string | null;
|
|
14
|
+
lastError: string | null;
|
|
15
|
+
runs: number;
|
|
16
|
+
consecutiveFailures: number;
|
|
17
|
+
createdAt: number;
|
|
18
|
+
};
|
|
19
|
+
export declare function addCronJob(input: {
|
|
20
|
+
prompt: string;
|
|
21
|
+
schedule: Schedule;
|
|
22
|
+
tweet: Tweet;
|
|
23
|
+
}): {
|
|
24
|
+
job: CronJob;
|
|
25
|
+
} | {
|
|
26
|
+
error: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function getCronJob(id: number): CronJob | null;
|
|
29
|
+
export declare function listCronJobs(opts?: {
|
|
30
|
+
includeDisabled?: boolean;
|
|
31
|
+
creatorId?: string;
|
|
32
|
+
}): CronJob[];
|
|
33
|
+
export declare function setCronJobEnabled(id: number, enabled: boolean): boolean;
|
|
34
|
+
export declare function resumeCronJob(id: number): {
|
|
35
|
+
job: CronJob;
|
|
36
|
+
} | {
|
|
37
|
+
error: string;
|
|
38
|
+
};
|
|
39
|
+
export declare function removeCronJob(id: number): boolean;
|
|
40
|
+
export declare function dueCronJobs(now: number): CronJob[];
|
|
41
|
+
export declare function armCronJob(id: number, nextRunAt: number | null): void;
|
|
42
|
+
export declare function markCronRun(id: number, outcome: {
|
|
43
|
+
result?: string;
|
|
44
|
+
error?: string;
|
|
45
|
+
}): void;
|
|
46
|
+
export { describeSchedule };
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { withSchema } from "../db.js";
|
|
2
|
+
import { config } from "../config.js";
|
|
3
|
+
import { nextRunAt, describeSchedule } from "./schedule.js";
|
|
4
|
+
// Persistent cron jobs in the shared SQLite DB (see db.ts) — same pattern as
|
|
5
|
+
// state.ts/stats.ts: the feature owns its table, the DB survives redeploys (it
|
|
6
|
+
// lives at DB_PATH outside the wiped project dir) and rides the dashboard's
|
|
7
|
+
// backup system. The DB row is the ONLY scheduler state: there is no in-memory
|
|
8
|
+
// timer registry to keep in sync — the runner just reads `next_run_at` (see
|
|
9
|
+
// runner.ts for why that design).
|
|
10
|
+
//
|
|
11
|
+
// Security model: the stored prompt is replayed later through an LLM that can
|
|
12
|
+
// call skills, so treat it like code — it is shown verbatim by `list`, logged on
|
|
13
|
+
// every run, and grants nothing by itself: the creator's privileges are
|
|
14
|
+
// re-derived from ADMIN_HANDLES at each run (runner.ts), never from this table.
|
|
15
|
+
const SCHEMA = `
|
|
16
|
+
CREATE TABLE IF NOT EXISTS cron_jobs (
|
|
17
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
18
|
+
prompt TEXT NOT NULL,
|
|
19
|
+
schedule TEXT NOT NULL,
|
|
20
|
+
creator_id TEXT NOT NULL,
|
|
21
|
+
creator_handle TEXT NOT NULL,
|
|
22
|
+
source_tweet TEXT,
|
|
23
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
24
|
+
next_run_at INTEGER NOT NULL,
|
|
25
|
+
last_run_at INTEGER,
|
|
26
|
+
last_result TEXT,
|
|
27
|
+
last_error TEXT,
|
|
28
|
+
runs INTEGER NOT NULL DEFAULT 0,
|
|
29
|
+
consecutive_failures INTEGER NOT NULL DEFAULT 0,
|
|
30
|
+
created_at INTEGER NOT NULL
|
|
31
|
+
);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_cron_due ON cron_jobs(enabled, next_run_at);
|
|
33
|
+
`;
|
|
34
|
+
const conn = () => withSchema(SCHEMA);
|
|
35
|
+
function rowToJob(r) {
|
|
36
|
+
let sourceTweet = null;
|
|
37
|
+
try {
|
|
38
|
+
sourceTweet = r.source_tweet ? JSON.parse(r.source_tweet) : null;
|
|
39
|
+
}
|
|
40
|
+
catch { /* keep null */ }
|
|
41
|
+
return {
|
|
42
|
+
id: r.id,
|
|
43
|
+
prompt: r.prompt,
|
|
44
|
+
schedule: JSON.parse(r.schedule),
|
|
45
|
+
creatorId: r.creator_id,
|
|
46
|
+
creatorHandle: r.creator_handle,
|
|
47
|
+
sourceTweet,
|
|
48
|
+
enabled: r.enabled === 1,
|
|
49
|
+
nextRunAt: r.next_run_at,
|
|
50
|
+
lastRunAt: r.last_run_at,
|
|
51
|
+
lastResult: r.last_result,
|
|
52
|
+
lastError: r.last_error,
|
|
53
|
+
runs: r.runs,
|
|
54
|
+
consecutiveFailures: r.consecutive_failures,
|
|
55
|
+
createdAt: r.created_at,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Create a job. Error strings are written for the LLM observation (the skill
|
|
59
|
+
// returns them verbatim), so they read as something the model can relay to the
|
|
60
|
+
// user. The creator is snapshotted from the CREATING tweet's author — params are
|
|
61
|
+
// model-controlled, the tweet author is not.
|
|
62
|
+
// Caps on ACTIVE (enabled = 1) jobs — every run costs money, so disabled jobs
|
|
63
|
+
// don't count. Checked at creation AND on resume (resumeCronJob): if resume
|
|
64
|
+
// skipped it, pause-create-resume cycles would bypass the limits. The per-user
|
|
65
|
+
// cap on top of the global one matters once the skill is opened to non-admins —
|
|
66
|
+
// a single user must not be able to exhaust the pool.
|
|
67
|
+
function checkCaps(d, creatorId) {
|
|
68
|
+
const active = d.prepare("SELECT COUNT(*) AS n FROM cron_jobs WHERE enabled = 1").get();
|
|
69
|
+
if (active.n >= config.cronMaxJobs) {
|
|
70
|
+
return { error: `cron job limit reached (${config.cronMaxJobs} active jobs) — remove one first` };
|
|
71
|
+
}
|
|
72
|
+
const own = d.prepare("SELECT COUNT(*) AS n FROM cron_jobs WHERE enabled = 1 AND creator_id = ?")
|
|
73
|
+
.get(creatorId);
|
|
74
|
+
if (own.n >= config.cronMaxJobsPerUser) {
|
|
75
|
+
return { error: `you already have ${own.n} active cron jobs (limit ${config.cronMaxJobsPerUser}) — remove one first` };
|
|
76
|
+
}
|
|
77
|
+
return { ok: true };
|
|
78
|
+
}
|
|
79
|
+
export function addCronJob(input) {
|
|
80
|
+
const d = conn();
|
|
81
|
+
if (!d)
|
|
82
|
+
return { error: "cron storage unavailable" };
|
|
83
|
+
const prompt = input.prompt?.trim();
|
|
84
|
+
if (!prompt)
|
|
85
|
+
return { error: 'missing "prompt" — the self-contained instruction to run on schedule' };
|
|
86
|
+
const author = input.tweet.author;
|
|
87
|
+
if (!author?.id || !author?.username) {
|
|
88
|
+
return { error: "could not identify the requesting user from the tweet" };
|
|
89
|
+
}
|
|
90
|
+
const caps = checkCaps(d, author.id);
|
|
91
|
+
if ("error" in caps)
|
|
92
|
+
return caps;
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
// Relative schedules ("in/every N minutes") anchor at the asking tweet's
|
|
95
|
+
// created_at, not at job creation: poll lag + the creating agent loop add
|
|
96
|
+
// ~15-40s before this row exists, and the user counts from when they tweeted.
|
|
97
|
+
// Falls back to `now` when created_at is missing/unparseable (or ahead of our
|
|
98
|
+
// clock). If processing was so slow that anchor+delay is already past, the
|
|
99
|
+
// job simply fires on the next tick, like any overdue job.
|
|
100
|
+
const isRelative = input.schedule.type === "interval" ||
|
|
101
|
+
(input.schedule.type === "once" && input.schedule.minutes !== undefined);
|
|
102
|
+
const tweetAt = Date.parse(input.tweet.created_at ?? "");
|
|
103
|
+
const anchor = isRelative && Number.isFinite(tweetAt) && tweetAt <= now ? tweetAt : now;
|
|
104
|
+
const next = nextRunAt(input.schedule, anchor);
|
|
105
|
+
// null = an absolute one-shot already in the past — refuse rather than store a
|
|
106
|
+
// job that would either never fire or fire "late" immediately.
|
|
107
|
+
if (next === null)
|
|
108
|
+
return { error: "that time is already in the past — pick a future time" };
|
|
109
|
+
const res = d.prepare(`
|
|
110
|
+
INSERT INTO cron_jobs (prompt, schedule, creator_id, creator_handle, source_tweet, next_run_at, created_at)
|
|
111
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
112
|
+
`).run(prompt, JSON.stringify(input.schedule), author.id, author.username.toLowerCase(), JSON.stringify(input.tweet), next, now);
|
|
113
|
+
const job = getCronJob(Number(res.lastInsertRowid));
|
|
114
|
+
return job ? { job } : { error: "failed to store the cron job" };
|
|
115
|
+
}
|
|
116
|
+
export function getCronJob(id) {
|
|
117
|
+
const d = conn();
|
|
118
|
+
if (!d)
|
|
119
|
+
return null;
|
|
120
|
+
const row = d.prepare("SELECT * FROM cron_jobs WHERE id = ?").get(id);
|
|
121
|
+
return row ? rowToJob(row) : null;
|
|
122
|
+
}
|
|
123
|
+
// Disabled jobs (spent one-shots, paused, auto-paused after repeated failures)
|
|
124
|
+
// stay listed until removed — `list` is the audit surface for stored prompts.
|
|
125
|
+
// `creatorId` filters to one user's jobs (matched on the stable X user id, not
|
|
126
|
+
// the handle, so renames don't detach a user from their jobs).
|
|
127
|
+
export function listCronJobs(opts = {}) {
|
|
128
|
+
const d = conn();
|
|
129
|
+
if (!d)
|
|
130
|
+
return [];
|
|
131
|
+
const { includeDisabled = true, creatorId } = opts;
|
|
132
|
+
const where = [];
|
|
133
|
+
const args = [];
|
|
134
|
+
if (!includeDisabled)
|
|
135
|
+
where.push("enabled = 1");
|
|
136
|
+
if (creatorId) {
|
|
137
|
+
where.push("creator_id = ?");
|
|
138
|
+
args.push(creatorId);
|
|
139
|
+
}
|
|
140
|
+
const sql = `SELECT * FROM cron_jobs${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY id`;
|
|
141
|
+
return d.prepare(sql).all(...args).map(rowToJob);
|
|
142
|
+
}
|
|
143
|
+
// Disable only — resuming goes through resumeCronJob (cap re-check + re-arm).
|
|
144
|
+
export function setCronJobEnabled(id, enabled) {
|
|
145
|
+
const d = conn();
|
|
146
|
+
if (!d)
|
|
147
|
+
return false;
|
|
148
|
+
if (enabled)
|
|
149
|
+
return "job" in resumeCronJob(id);
|
|
150
|
+
return d.prepare("UPDATE cron_jobs SET enabled = 0 WHERE id = ?").run(id).changes > 0;
|
|
151
|
+
}
|
|
152
|
+
// Re-arm a paused job. Subject to the same active-job caps as creation —
|
|
153
|
+
// otherwise pause-create-resume cycles would bypass the limits — and
|
|
154
|
+
// next_run_at is recomputed from now, otherwise a job paused past its slot
|
|
155
|
+
// would fire immediately on resume.
|
|
156
|
+
export function resumeCronJob(id) {
|
|
157
|
+
const d = conn();
|
|
158
|
+
if (!d)
|
|
159
|
+
return { error: "cron storage unavailable" };
|
|
160
|
+
const job = getCronJob(id);
|
|
161
|
+
if (!job)
|
|
162
|
+
return { error: `no cron job #${id}` };
|
|
163
|
+
if (job.enabled)
|
|
164
|
+
return { job }; // already running — nothing to do
|
|
165
|
+
const caps = checkCaps(d, job.creatorId);
|
|
166
|
+
if ("error" in caps)
|
|
167
|
+
return caps;
|
|
168
|
+
const next = nextRunAt(job.schedule, Date.now());
|
|
169
|
+
if (next === null)
|
|
170
|
+
return { error: `cron #${id} can't be resumed — its one-shot time is already spent` };
|
|
171
|
+
d.prepare("UPDATE cron_jobs SET enabled = 1, next_run_at = ?, consecutive_failures = 0 WHERE id = ?")
|
|
172
|
+
.run(next, id);
|
|
173
|
+
return { job: getCronJob(id) };
|
|
174
|
+
}
|
|
175
|
+
export function removeCronJob(id) {
|
|
176
|
+
const d = conn();
|
|
177
|
+
if (!d)
|
|
178
|
+
return false;
|
|
179
|
+
return d.prepare("DELETE FROM cron_jobs WHERE id = ?").run(id).changes > 0;
|
|
180
|
+
}
|
|
181
|
+
// Runner internals ───────────────────────────────────────────────────────────
|
|
182
|
+
export function dueCronJobs(now) {
|
|
183
|
+
const d = conn();
|
|
184
|
+
if (!d)
|
|
185
|
+
return [];
|
|
186
|
+
return d.prepare("SELECT * FROM cron_jobs WHERE enabled = 1 AND next_run_at <= ? ORDER BY next_run_at")
|
|
187
|
+
.all(now).map(rowToJob);
|
|
188
|
+
}
|
|
189
|
+
// Advance the job's clock — called by the runner BEFORE executing, so a crash
|
|
190
|
+
// mid-run skips the slot instead of double-firing on restart (at-most-once).
|
|
191
|
+
// `enabled = 0` here is how one-shots are spent.
|
|
192
|
+
export function armCronJob(id, nextRunAt) {
|
|
193
|
+
const d = conn();
|
|
194
|
+
if (!d)
|
|
195
|
+
return;
|
|
196
|
+
if (nextRunAt === null) {
|
|
197
|
+
d.prepare("UPDATE cron_jobs SET enabled = 0 WHERE id = ?").run(id);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
d.prepare("UPDATE cron_jobs SET next_run_at = ? WHERE id = ?").run(nextRunAt, id);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
export function markCronRun(id, outcome) {
|
|
204
|
+
const d = conn();
|
|
205
|
+
if (!d)
|
|
206
|
+
return;
|
|
207
|
+
if (outcome.error !== undefined) {
|
|
208
|
+
d.prepare(`
|
|
209
|
+
UPDATE cron_jobs SET last_run_at = ?, last_error = ?, runs = runs + 1,
|
|
210
|
+
consecutive_failures = consecutive_failures + 1 WHERE id = ?
|
|
211
|
+
`).run(Date.now(), outcome.error, id);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
d.prepare(`
|
|
215
|
+
UPDATE cron_jobs SET last_run_at = ?, last_result = ?, last_error = NULL, runs = runs + 1,
|
|
216
|
+
consecutive_failures = 0 WHERE id = ?
|
|
217
|
+
`).run(Date.now(), outcome.result ?? "", id);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export { describeSchedule };
|
package/dist/src/db.d.ts
ADDED
package/dist/src/db.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
// The single SQLite database for the whole app (yappr.db). One shared connection that
|
|
5
|
+
// every feature needing storage opens through getDb() — stats today, more tables
|
|
6
|
+
// later. Each feature owns its own `CREATE TABLE IF NOT EXISTS` (see stats.ts), so
|
|
7
|
+
// adding a table is local to that feature; this module just owns the connection and
|
|
8
|
+
// pragmas.
|
|
9
|
+
//
|
|
10
|
+
// Path comes from DB_PATH. On the server that points *outside* the redeploy-wiped
|
|
11
|
+
// /yappr dir (so data survives deploys); locally it defaults to ./yappr.db. Opening
|
|
12
|
+
// is best-effort: a DB that won't open returns null, and callers degrade to no-ops
|
|
13
|
+
// rather than crashing the agent.
|
|
14
|
+
const DB_PATH = process.env.DB_PATH || resolve(process.cwd(), "yappr.db");
|
|
15
|
+
let db = null;
|
|
16
|
+
let initFailed = false;
|
|
17
|
+
export function getDb() {
|
|
18
|
+
if (db)
|
|
19
|
+
return db;
|
|
20
|
+
if (initFailed)
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const handle = new Database(DB_PATH);
|
|
24
|
+
handle.pragma("journal_mode = WAL"); // concurrent readers (the CLI) alongside the writer
|
|
25
|
+
handle.pragma("busy_timeout = 5000"); // wait out a brief writer lock instead of erroring
|
|
26
|
+
db = handle;
|
|
27
|
+
return db;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
initFailed = true;
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const ensured = new Set();
|
|
35
|
+
// Get the shared connection with a feature's tables guaranteed to exist. Pass the
|
|
36
|
+
// feature's `CREATE TABLE IF NOT EXISTS …` DDL; it runs once per process (memoised on
|
|
37
|
+
// the DDL text). Returns null if the DB can't be opened, so callers stay best-effort.
|
|
38
|
+
// This is how each feature owns its own schema while sharing one connection.
|
|
39
|
+
export function withSchema(ddl) {
|
|
40
|
+
const d = getDb();
|
|
41
|
+
if (!d)
|
|
42
|
+
return null;
|
|
43
|
+
if (!ensured.has(ddl)) {
|
|
44
|
+
try {
|
|
45
|
+
d.exec(ddl);
|
|
46
|
+
ensured.add(ddl);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return d;
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loadHooks(): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { registerHooks } from "./registry.js";
|
|
2
|
+
import { log } from "../log.js";
|
|
3
|
+
import { listHooks, importConfigModule } from "../config-loader.js";
|
|
4
|
+
export async function loadHooks() {
|
|
5
|
+
for (const { name, modulePath } of await listHooks()) {
|
|
6
|
+
try {
|
|
7
|
+
const mod = await importConfigModule(modulePath);
|
|
8
|
+
if (mod.hooks && typeof mod.hooks === "object") {
|
|
9
|
+
registerHooks(mod.hooks);
|
|
10
|
+
log.info({ file: name }, `hook loaded: ${name}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
log.error({ file: name, err: err.message }, `hook load failed: ${name}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AgentHooks } from "./types.js";
|
|
2
|
+
import type { Tweet } from "../x/types.js";
|
|
3
|
+
import type { TreasuryBalances } from "../treasury/index.js";
|
|
4
|
+
import type { TreasuryCycleResult } from "../treasury/cycle.js";
|
|
5
|
+
export declare function registerHooks(hooks: AgentHooks): void;
|
|
6
|
+
export declare function runOnMention(tweet: Tweet): Promise<void>;
|
|
7
|
+
export declare function runShouldReply(tweet: Tweet): Promise<boolean>;
|
|
8
|
+
export declare function runOnBeforeInference(tweet: Tweet, question: string, context: string | undefined): Promise<{
|
|
9
|
+
question: string;
|
|
10
|
+
context: string | undefined;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function runOnAfterInference(question: string, output: string): Promise<string>;
|
|
13
|
+
export declare function runOnBeforeReply(tweet: Tweet, text: string): Promise<string | null>;
|
|
14
|
+
export declare function runOnAfterReply(tweet: Tweet, text: string): Promise<void>;
|
|
15
|
+
export declare function runOnBeforeClaim(balances: TreasuryBalances): Promise<void>;
|
|
16
|
+
export declare function runOnAfterClaim(result: TreasuryCycleResult): Promise<void>;
|
|
17
|
+
export declare function runOnSwap(kind: "burn" | "swap", amount: bigint): Promise<void>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Every loaded hook file registers its own AgentHooks set; they COMPOSE rather
|
|
2
|
+
// than overwrite (multiple files can implement the same hook — e.g. user-memory
|
|
3
|
+
// and holder both use onBeforeInference; a spread-merge here used to let the
|
|
4
|
+
// last file silently clobber the others). Sets run in registration order (the
|
|
5
|
+
// loader's alphabetical file order):
|
|
6
|
+
// - observers (onMention, onAfterReply, treasury hooks) all run;
|
|
7
|
+
// - shouldReply is a veto chain — any false skips the reply;
|
|
8
|
+
// - transformers (onBeforeInference, onAfterInference, onBeforeReply) thread
|
|
9
|
+
// their value through each set in turn; onBeforeReply short-circuits on null.
|
|
10
|
+
const _hookSets = [];
|
|
11
|
+
export function registerHooks(hooks) {
|
|
12
|
+
_hookSets.push(hooks);
|
|
13
|
+
}
|
|
14
|
+
export async function runOnMention(tweet) {
|
|
15
|
+
for (const h of _hookSets) {
|
|
16
|
+
if (h.onMention)
|
|
17
|
+
await h.onMention(tweet);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function runShouldReply(tweet) {
|
|
21
|
+
for (const h of _hookSets) {
|
|
22
|
+
if (h.shouldReply && !(await h.shouldReply(tweet)))
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
export async function runOnBeforeInference(tweet, question, context) {
|
|
28
|
+
let cur = { question, context };
|
|
29
|
+
for (const h of _hookSets) {
|
|
30
|
+
if (h.onBeforeInference)
|
|
31
|
+
cur = await h.onBeforeInference({ tweet, ...cur });
|
|
32
|
+
}
|
|
33
|
+
return cur;
|
|
34
|
+
}
|
|
35
|
+
export async function runOnAfterInference(question, output) {
|
|
36
|
+
let cur = output;
|
|
37
|
+
for (const h of _hookSets) {
|
|
38
|
+
if (h.onAfterInference)
|
|
39
|
+
cur = await h.onAfterInference({ question, output: cur });
|
|
40
|
+
}
|
|
41
|
+
return cur;
|
|
42
|
+
}
|
|
43
|
+
export async function runOnBeforeReply(tweet, text) {
|
|
44
|
+
let cur = text;
|
|
45
|
+
for (const h of _hookSets) {
|
|
46
|
+
if (!h.onBeforeReply)
|
|
47
|
+
continue;
|
|
48
|
+
const next = await h.onBeforeReply({ tweet, text: cur });
|
|
49
|
+
if (next === null)
|
|
50
|
+
return null; // vetoed — later sets don't resurrect it
|
|
51
|
+
cur = next;
|
|
52
|
+
}
|
|
53
|
+
return cur;
|
|
54
|
+
}
|
|
55
|
+
export async function runOnAfterReply(tweet, text) {
|
|
56
|
+
for (const h of _hookSets) {
|
|
57
|
+
if (h.onAfterReply)
|
|
58
|
+
await h.onAfterReply({ tweet, text });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function runOnBeforeClaim(balances) {
|
|
62
|
+
for (const h of _hookSets) {
|
|
63
|
+
if (h.onBeforeClaim)
|
|
64
|
+
await h.onBeforeClaim(balances);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export async function runOnAfterClaim(result) {
|
|
68
|
+
for (const h of _hookSets) {
|
|
69
|
+
if (h.onAfterClaim)
|
|
70
|
+
await h.onAfterClaim(result);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export async function runOnSwap(kind, amount) {
|
|
74
|
+
for (const h of _hookSets) {
|
|
75
|
+
if (h.onSwap)
|
|
76
|
+
await h.onSwap({ kind, amount });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Tweet } from "../x/types.js";
|
|
2
|
+
import type { TreasuryBalances } from "../treasury/index.js";
|
|
3
|
+
import type { TreasuryCycleResult } from "../treasury/cycle.js";
|
|
4
|
+
export type OnMentionHook = (tweet: Tweet) => Promise<void> | void;
|
|
5
|
+
export type ShouldReplyHook = (tweet: Tweet) => Promise<boolean> | boolean;
|
|
6
|
+
export type OnBeforeInferenceHook = (input: {
|
|
7
|
+
tweet: Tweet;
|
|
8
|
+
question: string;
|
|
9
|
+
context: string | undefined;
|
|
10
|
+
}) => Promise<{
|
|
11
|
+
question: string;
|
|
12
|
+
context: string | undefined;
|
|
13
|
+
}> | {
|
|
14
|
+
question: string;
|
|
15
|
+
context: string | undefined;
|
|
16
|
+
};
|
|
17
|
+
export type OnAfterInferenceHook = (input: {
|
|
18
|
+
question: string;
|
|
19
|
+
output: string;
|
|
20
|
+
}) => Promise<string> | string;
|
|
21
|
+
export type OnBeforeReplyHook = (input: {
|
|
22
|
+
tweet: Tweet;
|
|
23
|
+
text: string;
|
|
24
|
+
}) => Promise<string | null> | string | null;
|
|
25
|
+
export type OnAfterReplyHook = (input: {
|
|
26
|
+
tweet: Tweet;
|
|
27
|
+
text: string;
|
|
28
|
+
}) => Promise<void> | void;
|
|
29
|
+
export type OnBeforeClaimHook = (balances: TreasuryBalances) => Promise<void> | void;
|
|
30
|
+
export type OnAfterClaimHook = (result: TreasuryCycleResult) => Promise<void> | void;
|
|
31
|
+
export type OnSwapHook = (input: {
|
|
32
|
+
kind: "burn" | "swap";
|
|
33
|
+
amount: bigint;
|
|
34
|
+
}) => Promise<void> | void;
|
|
35
|
+
export type AgentHooks = {
|
|
36
|
+
onMention?: OnMentionHook;
|
|
37
|
+
shouldReply?: ShouldReplyHook;
|
|
38
|
+
onBeforeInference?: OnBeforeInferenceHook;
|
|
39
|
+
onAfterInference?: OnAfterInferenceHook;
|
|
40
|
+
onBeforeReply?: OnBeforeReplyHook;
|
|
41
|
+
onAfterReply?: OnAfterReplyHook;
|
|
42
|
+
onBeforeClaim?: OnBeforeClaimHook;
|
|
43
|
+
onAfterClaim?: OnAfterClaimHook;
|
|
44
|
+
onSwap?: OnSwapHook;
|
|
45
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type { SkillHandler, SkillResult, SkillDef, SkillAccess } from "./skills/types.js";
|
|
2
|
+
export type { AgentHooks } from "./hooks/types.js";
|
|
3
|
+
export type { Tweet, SearchResponse } from "./x/types.js";
|
|
4
|
+
export type { Treasury, TreasuryBalances } from "./treasury/index.js";
|
|
5
|
+
export type { TreasuryCycleResult } from "./treasury/cycle.js";
|
|
6
|
+
export type { CronJob } from "./cron/store.js";
|
|
7
|
+
export type { Schedule } from "./cron/schedule.js";
|
|
8
|
+
export type { SkillStore, SkillStoreEntry } from "./storage.js";
|
|
9
|
+
export type { Database } from "better-sqlite3";
|
|
10
|
+
export { agentPrompt } from "./agent-prompt.js";
|
|
11
|
+
export { getTreasury } from "./treasury/index.js";
|
|
12
|
+
export { log } from "./log.js";
|
|
13
|
+
export { config } from "./config.js";
|
|
14
|
+
export { payFetch, paidUsd, walletAddress } from "./wallet.js";
|
|
15
|
+
export { chat, llmCreditBalance } from "./llm/index.js";
|
|
16
|
+
export type { ChatMessage, ContentPart } from "./llm/index.js";
|
|
17
|
+
export { summary } from "./stats.js";
|
|
18
|
+
export type { Summary, SpendType } from "./stats.js";
|
|
19
|
+
export { checkHolderAccess } from "./skills/holder-access.js";
|
|
20
|
+
export { skillStore } from "./storage.js";
|
|
21
|
+
export { withSchema } from "./db.js";
|
|
22
|
+
export { addCronJob, listCronJobs, getCronJob, setCronJobEnabled, resumeCronJob, removeCronJob, describeSchedule } from "./cron/store.js";
|
|
23
|
+
export { validateSchedule } from "./cron/schedule.js";
|
|
24
|
+
export { checkCronCapability } from "./cron/capability.js";
|
|
25
|
+
export * from "./x/client.js";
|