bunbase 0.0.9

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.
@@ -0,0 +1,1530 @@
1
+ #!/usr/bin/env bun
2
+ import {
3
+ BadRequestError,
4
+ ForbiddenError,
5
+ UnauthorizedError,
6
+ __commonJS,
7
+ __toESM,
8
+ loadConfig
9
+ } from "../shared/chunk-k195ahh5.js";
10
+
11
+ // node_modules/picocolors/picocolors.js
12
+ var require_picocolors = __commonJS((exports, module) => {
13
+ var p = process || {};
14
+ var argv = p.argv || [];
15
+ var env = p.env || {};
16
+ var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
17
+ var formatter = (open, close, replace = open) => (input) => {
18
+ let string = "" + input, index = string.indexOf(close, open.length);
19
+ return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
20
+ };
21
+ var replaceClose = (string, close, replace, index) => {
22
+ let result = "", cursor = 0;
23
+ do {
24
+ result += string.substring(cursor, index) + replace;
25
+ cursor = index + close.length;
26
+ index = string.indexOf(close, cursor);
27
+ } while (~index);
28
+ return result + string.substring(cursor);
29
+ };
30
+ var createColors = (enabled = isColorSupported) => {
31
+ let f = enabled ? formatter : () => String;
32
+ return {
33
+ isColorSupported: enabled,
34
+ reset: f("\x1B[0m", "\x1B[0m"),
35
+ bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
36
+ dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
37
+ italic: f("\x1B[3m", "\x1B[23m"),
38
+ underline: f("\x1B[4m", "\x1B[24m"),
39
+ inverse: f("\x1B[7m", "\x1B[27m"),
40
+ hidden: f("\x1B[8m", "\x1B[28m"),
41
+ strikethrough: f("\x1B[9m", "\x1B[29m"),
42
+ black: f("\x1B[30m", "\x1B[39m"),
43
+ red: f("\x1B[31m", "\x1B[39m"),
44
+ green: f("\x1B[32m", "\x1B[39m"),
45
+ yellow: f("\x1B[33m", "\x1B[39m"),
46
+ blue: f("\x1B[34m", "\x1B[39m"),
47
+ magenta: f("\x1B[35m", "\x1B[39m"),
48
+ cyan: f("\x1B[36m", "\x1B[39m"),
49
+ white: f("\x1B[37m", "\x1B[39m"),
50
+ gray: f("\x1B[90m", "\x1B[39m"),
51
+ bgBlack: f("\x1B[40m", "\x1B[49m"),
52
+ bgRed: f("\x1B[41m", "\x1B[49m"),
53
+ bgGreen: f("\x1B[42m", "\x1B[49m"),
54
+ bgYellow: f("\x1B[43m", "\x1B[49m"),
55
+ bgBlue: f("\x1B[44m", "\x1B[49m"),
56
+ bgMagenta: f("\x1B[45m", "\x1B[49m"),
57
+ bgCyan: f("\x1B[46m", "\x1B[49m"),
58
+ bgWhite: f("\x1B[47m", "\x1B[49m"),
59
+ blackBright: f("\x1B[90m", "\x1B[39m"),
60
+ redBright: f("\x1B[91m", "\x1B[39m"),
61
+ greenBright: f("\x1B[92m", "\x1B[39m"),
62
+ yellowBright: f("\x1B[93m", "\x1B[39m"),
63
+ blueBright: f("\x1B[94m", "\x1B[39m"),
64
+ magentaBright: f("\x1B[95m", "\x1B[39m"),
65
+ cyanBright: f("\x1B[96m", "\x1B[39m"),
66
+ whiteBright: f("\x1B[97m", "\x1B[39m"),
67
+ bgBlackBright: f("\x1B[100m", "\x1B[49m"),
68
+ bgRedBright: f("\x1B[101m", "\x1B[49m"),
69
+ bgGreenBright: f("\x1B[102m", "\x1B[49m"),
70
+ bgYellowBright: f("\x1B[103m", "\x1B[49m"),
71
+ bgBlueBright: f("\x1B[104m", "\x1B[49m"),
72
+ bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
73
+ bgCyanBright: f("\x1B[106m", "\x1B[49m"),
74
+ bgWhiteBright: f("\x1B[107m", "\x1B[49m")
75
+ };
76
+ };
77
+ module.exports = createColors();
78
+ module.exports.createColors = createColors;
79
+ });
80
+
81
+ // src/cli/index.ts
82
+ import { Command as Command5 } from "commander";
83
+
84
+ // src/cli/commands/build.ts
85
+ import { Command } from "commander";
86
+ var buildCommand = new Command("build").description("Build the project for production").action(async () => {
87
+ console.log("Building project...");
88
+ const proc = Bun.spawn([
89
+ "bun",
90
+ "build",
91
+ "./src/index.ts",
92
+ "--outdir",
93
+ "./dist",
94
+ "--target",
95
+ "bun"
96
+ ], {
97
+ stdout: "inherit",
98
+ stderr: "inherit"
99
+ });
100
+ await proc.exited;
101
+ console.log("Build complete.");
102
+ });
103
+
104
+ // src/cli/commands/create-action.ts
105
+ import { mkdir, writeFile } from "node:fs/promises";
106
+ import { join } from "node:path";
107
+ import { Command as Command2 } from "commander";
108
+ import inquirer from "inquirer";
109
+ var createActionCommand = new Command2("create:action").description("Create a new action").action(async () => {
110
+ const answers = await inquirer.prompt([
111
+ {
112
+ type: "input",
113
+ name: "name",
114
+ message: "Action name (e.g. user/create):",
115
+ validate: (input) => input.length > 0
116
+ },
117
+ {
118
+ type: "checkbox",
119
+ name: "triggers",
120
+ message: "Select triggers:",
121
+ choices: ["api", "cron", "event", "agent"]
122
+ }
123
+ ]);
124
+ const name = answers.name;
125
+ const parts = name.split("/");
126
+ const fileName = parts.pop() || "index";
127
+ const dir = parts.join("/");
128
+ const baseDir = "./src/actions";
129
+ const targetDir = join(baseDir, dir);
130
+ const targetFile = join(targetDir, `${fileName}.ts`);
131
+ await mkdir(targetDir, { recursive: true });
132
+ const content = `import { action } from 'basalt'
133
+ import { Type } from '@sinclair/typebox'
134
+
135
+ export default action({
136
+ name: '${name}',
137
+ description: 'Auto-generated action',
138
+ input: Type.Object({
139
+ // Define input schema
140
+ name: Type.String(),
141
+ }),
142
+ output: Type.Object({
143
+ result: Type.String(),
144
+ }),
145
+ triggers: [
146
+ ${answers.triggers.map((t) => {
147
+ if (t === "api")
148
+ return ` { kind: 'api', method: 'POST', path: '/${name}' },`;
149
+ if (t === "cron")
150
+ return ` { kind: 'cron', schedule: '0 * * * *' },`;
151
+ if (t === "event")
152
+ return ` { kind: 'event', event: '${name.replace("/", ".")}' },`;
153
+ if (t === "agent")
154
+ return ` { kind: 'agent', tool: '${name.replace("/", "_")}', description: 'Execute ${name}' },`;
155
+ return "";
156
+ }).join(`
157
+ `)}
158
+ ],
159
+ handler: async (input, ctx) => {
160
+ ctx.log.info('Executing ${name}', { input })
161
+ return {
162
+ result: \`Hello \${input.name}\`
163
+ }
164
+ }
165
+ })
166
+ `;
167
+ await writeFile(targetFile, content);
168
+ console.log(`✓ Created action at ${targetFile}`);
169
+ });
170
+
171
+ // src/cli/commands/dev.ts
172
+ import { Command as Command3 } from "commander";
173
+
174
+ // src/runtime/dev.ts
175
+ import { watch } from "node:fs";
176
+
177
+ // src/codegen/types.ts
178
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
179
+ import { join as join2 } from "node:path";
180
+ async function generateActionTypes(actions, outputDir = ".gravity") {
181
+ const typeMap = {};
182
+ for (const action of actions) {
183
+ const inputType = schemaToTypeString(action.config.input);
184
+ typeMap[action.config.name] = inputType;
185
+ }
186
+ const dts = `
187
+ // Auto-generated by Gravity - DO NOT EDIT
188
+ // Generated at ${new Date().toISOString()}
189
+
190
+ declare module 'gravity-run/types' {
191
+ export interface ActionRegistry {
192
+ ${Object.entries(typeMap).map(([name, type]) => ` '${name}': ${type}`).join(`
193
+ `)}
194
+ }
195
+ }
196
+
197
+ export {}
198
+ `.trim();
199
+ await mkdir2(outputDir, { recursive: true });
200
+ await writeFile2(join2(outputDir, "types.d.ts"), dts, "utf-8");
201
+ }
202
+ function schemaToTypeString(schema) {
203
+ if (!schema)
204
+ return "any";
205
+ const kind = schema.type;
206
+ if (kind === "object") {
207
+ const props = schema.properties || {};
208
+ const entries = Object.entries(props).map(([key, val]) => {
209
+ return `${key}: ${schemaToTypeString(val)}`;
210
+ });
211
+ return `{ ${entries.join("; ")} }`;
212
+ }
213
+ if (kind === "string")
214
+ return "string";
215
+ if (kind === "number")
216
+ return "number";
217
+ if (kind === "boolean")
218
+ return "boolean";
219
+ if (kind === "array") {
220
+ const items = schema.items;
221
+ return `Array<${schemaToTypeString(items)}>`;
222
+ }
223
+ return "any";
224
+ }
225
+
226
+ // src/logger/action-logger.ts
227
+ var import_picocolors2 = __toESM(require_picocolors(), 1);
228
+
229
+ // src/logger/symbols.ts
230
+ var import_picocolors = __toESM(require_picocolors(), 1);
231
+ import isUnicodeSupported from "is-unicode-supported";
232
+ var unicode = isUnicodeSupported();
233
+ var u = (c, fallback) => unicode ? c : fallback;
234
+ var S_BAR_START = u("┌", "T");
235
+ var S_BAR = u("│", "|");
236
+ var S_BAR_END = u("└", "-");
237
+ var S_STEP_SUBMIT = u("◇", "o");
238
+ var S_RADIO_ACTIVE = u("●", ">");
239
+ var S_SUCCESS = u("✔", "√");
240
+ var S_ERROR = u("✖", "x");
241
+ var S_WARN = u("▲", "!");
242
+ var S_INFO = u("●", "•");
243
+ var bar = () => import_picocolors.default.gray(S_BAR);
244
+ var barStart = (text) => `${import_picocolors.default.gray(S_BAR_START)} ${text}`;
245
+ var barEnd = (text) => `${import_picocolors.default.gray(S_BAR_END)} ${text}`;
246
+ var stepInfo = (label, detail) => `${import_picocolors.default.green(S_STEP_SUBMIT)} ${label}${detail ? `: ${detail}` : ""}`;
247
+ var stepActive = (label, detail) => `${import_picocolors.default.cyan(S_RADIO_ACTIVE)} ${label}${detail ? `: ${detail}` : ""}`;
248
+ var stepSuccess = (label, detail) => `${import_picocolors.default.green(S_SUCCESS)} ${label}${detail ? `: ${detail}` : ""}`;
249
+ var stepError = (label, detail) => `${import_picocolors.default.red(S_ERROR)} ${label}${detail ? `: ${detail}` : ""}`;
250
+ var stepWarn = (label, detail) => `${import_picocolors.default.yellow(S_WARN)} ${label}${detail ? `: ${detail}` : ""}`;
251
+
252
+ // src/logger/action-logger.ts
253
+ class LoggerSession {
254
+ output;
255
+ startTime;
256
+ constructor(title, output = process.stdout) {
257
+ this.output = output;
258
+ this.startTime = Date.now();
259
+ this.write(barStart(title));
260
+ }
261
+ write(line) {
262
+ this.output.write(`${line}
263
+ `);
264
+ }
265
+ info(label, detail) {
266
+ this.write(stepInfo(label, detail));
267
+ return this;
268
+ }
269
+ step(label, detail) {
270
+ this.write(stepActive(label, detail));
271
+ return this;
272
+ }
273
+ success(label, detail) {
274
+ this.write(stepSuccess(label, detail));
275
+ return this;
276
+ }
277
+ warn(label, detail) {
278
+ this.write(stepWarn(label, detail));
279
+ return this;
280
+ }
281
+ error(label, detail) {
282
+ this.write(stepError(label, detail));
283
+ return this;
284
+ }
285
+ bar() {
286
+ this.write(bar());
287
+ return this;
288
+ }
289
+ end(message) {
290
+ const elapsed = Date.now() - this.startTime;
291
+ const msg = message ?? "Done";
292
+ this.write(bar());
293
+ this.write(barEnd(import_picocolors2.default.green(`${msg}`) + import_picocolors2.default.gray(` (${elapsed}ms)`)));
294
+ this.write("");
295
+ }
296
+ fail(message) {
297
+ const elapsed = Date.now() - this.startTime;
298
+ const msg = message ?? "Failed";
299
+ this.write(bar());
300
+ this.write(barEnd(import_picocolors2.default.red(`${msg}`) + import_picocolors2.default.gray(` (${elapsed}ms)`)));
301
+ this.write("");
302
+ }
303
+ }
304
+ // src/logger/pretty-print.ts
305
+ var import_picocolors3 = __toESM(require_picocolors(), 1);
306
+ var stepTag = (step) => step ? import_picocolors3.default.bold(import_picocolors3.default.cyan(step)) : "";
307
+ var timestampTag = (timestamp) => import_picocolors3.default.gray(timestamp);
308
+ var traceIdTag = (traceId) => traceId ? import_picocolors3.default.gray(traceId) : "";
309
+ var levelTags = {
310
+ error: import_picocolors3.default.red("[ERROR]"),
311
+ info: import_picocolors3.default.blue("[INFO]"),
312
+ warn: import_picocolors3.default.yellow("[WARN]"),
313
+ debug: import_picocolors3.default.gray("[DEBUG]")
314
+ };
315
+ var numericTag = (value) => import_picocolors3.default.green(value);
316
+ var stringTag = (value) => import_picocolors3.default.cyan(value);
317
+ var booleanTag = (value) => import_picocolors3.default.blue(value);
318
+ var arrayBrackets = ["[", "]"].map((s) => import_picocolors3.default.gray(s));
319
+ var objectBrackets = ["{", "}"].map((s) => import_picocolors3.default.gray(s));
320
+ var prettyPrintObject = (obj, depth = 0, parentIsLast = false, prefix = "") => {
321
+ const tab = prefix + (depth === 0 ? "" : parentIsLast ? "│ " : "│ ");
322
+ if (depth > 2)
323
+ return `${tab} └ ${import_picocolors3.default.gray("[...]")}`;
324
+ const entries = Object.entries(obj);
325
+ return entries.map(([key, value], index) => {
326
+ const isLast = index === entries.length - 1;
327
+ const isObject = typeof value === "object" && value !== null;
328
+ const branch = isLast ? "└" : "├";
329
+ if (isObject) {
330
+ const subObject = prettyPrintObject(value, depth + 1, isLast, tab);
331
+ const [start, end] = Array.isArray(value) ? arrayBrackets : objectBrackets;
332
+ return `${tab}${branch} ${key}: ${start}
333
+ ${subObject}
334
+ ${tab}${isLast ? " " : "│"} ${end}`;
335
+ }
336
+ let printedValue = value;
337
+ if (typeof value === "number")
338
+ printedValue = numericTag(String(value));
339
+ else if (typeof value === "boolean")
340
+ printedValue = booleanTag(String(value));
341
+ else if (typeof value === "string")
342
+ printedValue = stringTag(value);
343
+ return `${tab}${branch} ${key}: ${printedValue}`;
344
+ }).join(`
345
+ `);
346
+ };
347
+ var prettyPrint = (json, excludeDetails = false) => {
348
+ const { time, traceId, msg, flows, level, step, ...details } = json;
349
+ const levelTag = levelTags[level?.toLowerCase?.()] ?? levelTags.info;
350
+ const timestamp = timestampTag(`[${new Date(time).toLocaleTimeString()}]`);
351
+ const objectHasKeys = Object.keys(details).length > 0;
352
+ process.stdout.write(`${timestamp} ${traceIdTag(traceId)} ${levelTag} ${stepTag(step)} ${msg}
353
+ `);
354
+ if (objectHasKeys && !excludeDetails) {
355
+ process.stdout.write(`${prettyPrintObject(details)}
356
+ `);
357
+ }
358
+ };
359
+
360
+ // src/logger/logger.ts
361
+ var LEVELS = {
362
+ DEBUG: 10,
363
+ INFO: 20,
364
+ WARNING: 30,
365
+ ERROR: 40,
366
+ CRITICAL: 50
367
+ };
368
+ var levelMap = {
369
+ debug: LEVELS.DEBUG,
370
+ info: LEVELS.INFO,
371
+ warn: LEVELS.WARNING,
372
+ warning: LEVELS.WARNING,
373
+ error: LEVELS.ERROR,
374
+ critical: LEVELS.CRITICAL
375
+ };
376
+
377
+ class Logger {
378
+ options;
379
+ meta;
380
+ coreListeners;
381
+ listeners = [];
382
+ minLevel;
383
+ store;
384
+ constructor(options = {}, meta = {}, coreListeners = [], store) {
385
+ this.options = options;
386
+ this.meta = meta;
387
+ this.coreListeners = coreListeners;
388
+ this.minLevel = options.level ? levelMap[options.level] ?? LEVELS.INFO : LEVELS.INFO;
389
+ this.store = store;
390
+ }
391
+ setStore(store) {
392
+ this.store = store;
393
+ }
394
+ child(meta) {
395
+ return new Logger(this.options, { ...this.meta, ...meta }, this.coreListeners, this.store);
396
+ }
397
+ session(title) {
398
+ return new LoggerSession(title);
399
+ }
400
+ shouldLog(messageLevel) {
401
+ return messageLevel >= this.minLevel;
402
+ }
403
+ _log(level, msg, args) {
404
+ const time = Date.now();
405
+ const meta = {
406
+ ...this.meta,
407
+ ...args && typeof args === "object" ? args : {}
408
+ };
409
+ const isVerbose = this.options.verbose ?? false;
410
+ prettyPrint({ level, time, msg, ...meta }, !isVerbose);
411
+ if (this.store) {
412
+ try {
413
+ this.store.logs.insert({
414
+ level,
415
+ message: msg,
416
+ meta: Object.keys(meta).length > 0 ? JSON.stringify(meta) : null,
417
+ action_name: meta.action ?? null,
418
+ trace_id: meta.traceId ?? null
419
+ });
420
+ } catch {}
421
+ }
422
+ this.coreListeners.forEach((listener) => {
423
+ listener(level, msg, meta);
424
+ });
425
+ this.listeners.forEach((listener) => {
426
+ listener(level, msg, meta);
427
+ });
428
+ }
429
+ info(message, args) {
430
+ if (this.shouldLog(LEVELS.INFO))
431
+ this._log("INFO", message, args);
432
+ }
433
+ error(message, args) {
434
+ if (this.shouldLog(LEVELS.ERROR))
435
+ this._log("ERROR", message, args);
436
+ }
437
+ debug(message, args) {
438
+ if (this.shouldLog(LEVELS.DEBUG))
439
+ this._log("DEBUG", message, args);
440
+ }
441
+ warn(message, args) {
442
+ if (this.shouldLog(LEVELS.WARNING))
443
+ this._log("WARNING", message, args);
444
+ }
445
+ addListener(listener) {
446
+ this.listeners.push(listener);
447
+ }
448
+ }
449
+ // src/runtime/loader.ts
450
+ async function loadActions(dir, logger = new Logger) {
451
+ const glob = new Bun.Glob(`${dir}/**/*.ts`);
452
+ const files = glob.scanSync();
453
+ logger.debug("Loading actions from", { files });
454
+ const actions = [];
455
+ for (const file of files) {
456
+ const module = await import(Bun.pathToFileURL(file).href);
457
+ logger.debug("Loading action", { file });
458
+ for (const exported of Object.values(module)) {
459
+ if (isAction(exported)) {
460
+ actions.push(exported);
461
+ }
462
+ }
463
+ }
464
+ return actions;
465
+ }
466
+ function isAction(obj) {
467
+ return typeof obj === "function" && "config" in obj && "handler" in obj && typeof obj.config === "object" && "name" in obj.config;
468
+ }
469
+
470
+ // src/store/job-store.ts
471
+ class JobStore {
472
+ db;
473
+ constructor(db) {
474
+ this.db = db;
475
+ }
476
+ upsert(job) {
477
+ this.db.run(`INSERT INTO jobs (action_name, trigger_name, schedule, status)
478
+ VALUES (?, ?, ?, 'active')
479
+ ON CONFLICT(action_name, trigger_name) DO UPDATE SET
480
+ schedule = excluded.schedule,
481
+ status = 'active'`, [job.action_name, job.trigger_name, job.schedule]);
482
+ }
483
+ recordRun(actionName, triggerName, error) {
484
+ if (error) {
485
+ this.db.run(`UPDATE jobs
486
+ SET last_run_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
487
+ run_count = run_count + 1,
488
+ last_error = ?
489
+ WHERE action_name = ? AND trigger_name = ?`, [error, actionName, triggerName]);
490
+ } else {
491
+ this.db.run(`UPDATE jobs
492
+ SET last_run_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
493
+ run_count = run_count + 1,
494
+ last_error = NULL
495
+ WHERE action_name = ? AND trigger_name = ?`, [actionName, triggerName]);
496
+ }
497
+ }
498
+ list(status) {
499
+ if (status) {
500
+ return this.db.query("SELECT * FROM jobs WHERE status = ? ORDER BY created_at DESC").all(status);
501
+ }
502
+ return this.db.query("SELECT * FROM jobs ORDER BY created_at DESC").all();
503
+ }
504
+ get(actionName, triggerName) {
505
+ return this.db.query("SELECT * FROM jobs WHERE action_name = ? AND trigger_name = ?").get(actionName, triggerName) ?? null;
506
+ }
507
+ pause(actionName, triggerName) {
508
+ this.db.run(`UPDATE jobs SET status = 'paused' WHERE action_name = ? AND trigger_name = ?`, [actionName, triggerName]);
509
+ }
510
+ resume(actionName, triggerName) {
511
+ this.db.run(`UPDATE jobs SET status = 'active' WHERE action_name = ? AND trigger_name = ?`, [actionName, triggerName]);
512
+ }
513
+ }
514
+ // src/store/log-store.ts
515
+ class LogStore {
516
+ db;
517
+ constructor(db) {
518
+ this.db = db;
519
+ }
520
+ insert(entry) {
521
+ this.db.run(`INSERT INTO logs (level, message, meta, action_name, trace_id) VALUES (?, ?, ?, ?, ?)`, [
522
+ entry.level,
523
+ entry.message,
524
+ entry.meta ?? null,
525
+ entry.action_name ?? null,
526
+ entry.trace_id ?? null
527
+ ]);
528
+ }
529
+ query(filter = {}) {
530
+ const conditions = [];
531
+ const params = [];
532
+ if (filter.level) {
533
+ conditions.push("level = ?");
534
+ params.push(filter.level);
535
+ }
536
+ if (filter.action_name) {
537
+ conditions.push("action_name = ?");
538
+ params.push(filter.action_name);
539
+ }
540
+ if (filter.trace_id) {
541
+ conditions.push("trace_id = ?");
542
+ params.push(filter.trace_id);
543
+ }
544
+ if (filter.since) {
545
+ conditions.push("created_at >= ?");
546
+ params.push(filter.since);
547
+ }
548
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
549
+ const limit = filter.limit ?? 100;
550
+ const offset = filter.offset ?? 0;
551
+ return this.db.query(`SELECT * FROM logs ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
552
+ }
553
+ cleanup(retentionDays) {
554
+ const result = this.db.run(`DELETE FROM logs WHERE created_at < datetime('now', '-' || ? || ' days')`, [retentionDays]);
555
+ return result.changes;
556
+ }
557
+ count(filter = {}) {
558
+ const conditions = [];
559
+ const params = [];
560
+ if (filter.level) {
561
+ conditions.push("level = ?");
562
+ params.push(filter.level);
563
+ }
564
+ if (filter.action_name) {
565
+ conditions.push("action_name = ?");
566
+ params.push(filter.action_name);
567
+ }
568
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
569
+ const row = this.db.query(`SELECT COUNT(*) as count FROM logs ${where}`).get(...params);
570
+ return row?.count ?? 0;
571
+ }
572
+ }
573
+ // src/store/run-store.ts
574
+ class RunStore {
575
+ db;
576
+ constructor(db) {
577
+ this.db = db;
578
+ }
579
+ start(run) {
580
+ this.db.run(`INSERT INTO action_runs (trace_id, action_name, trigger_name, trigger_type, actor_id, actor_type, input, status)
581
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')`, [
582
+ run.trace_id,
583
+ run.action_name,
584
+ run.trigger_name,
585
+ run.trigger_type,
586
+ run.actor_id ?? null,
587
+ run.actor_type ?? null,
588
+ run.input !== undefined ? JSON.stringify(run.input) : null
589
+ ]);
590
+ }
591
+ complete(traceId, output, durationMs) {
592
+ this.db.run(`UPDATE action_runs
593
+ SET status = 'success', output = ?, duration_ms = ?, completed_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
594
+ WHERE trace_id = ?`, [
595
+ output !== undefined ? JSON.stringify(output) : null,
596
+ durationMs,
597
+ traceId
598
+ ]);
599
+ }
600
+ fail(traceId, errorMessage, durationMs) {
601
+ this.db.run(`UPDATE action_runs
602
+ SET status = 'error', error_message = ?, duration_ms = ?, completed_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
603
+ WHERE trace_id = ?`, [errorMessage, durationMs, traceId]);
604
+ }
605
+ query(filter = {}) {
606
+ const conditions = [];
607
+ const params = [];
608
+ if (filter.action_name) {
609
+ conditions.push("action_name = ?");
610
+ params.push(filter.action_name);
611
+ }
612
+ if (filter.trigger_type) {
613
+ conditions.push("trigger_type = ?");
614
+ params.push(filter.trigger_type);
615
+ }
616
+ if (filter.status) {
617
+ conditions.push("status = ?");
618
+ params.push(filter.status);
619
+ }
620
+ if (filter.since) {
621
+ conditions.push("started_at >= ?");
622
+ params.push(filter.since);
623
+ }
624
+ if (filter.until) {
625
+ conditions.push("started_at <= ?");
626
+ params.push(filter.until);
627
+ }
628
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
629
+ const limit = filter.limit ?? 50;
630
+ const offset = filter.offset ?? 0;
631
+ return this.db.query(`SELECT * FROM action_runs ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
632
+ }
633
+ getByTraceId(traceId) {
634
+ return this.db.query("SELECT * FROM action_runs WHERE trace_id = ?").get(traceId) ?? null;
635
+ }
636
+ stats(since) {
637
+ const where = since ? "WHERE started_at >= ?" : "";
638
+ const params = since ? [since] : [];
639
+ const row = this.db.query(`
640
+ SELECT
641
+ COUNT(*) as total,
642
+ SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success,
643
+ SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error,
644
+ COALESCE(AVG(duration_ms), 0) as avg_duration_ms
645
+ FROM action_runs ${where}
646
+ `).get(...params);
647
+ return {
648
+ total: row.total ?? 0,
649
+ success: row.success ?? 0,
650
+ error: row.error ?? 0,
651
+ avg_duration_ms: Math.round(row.avg_duration_ms ?? 0)
652
+ };
653
+ }
654
+ cleanup(retentionDays) {
655
+ const result = this.db.run(`DELETE FROM action_runs WHERE started_at < datetime('now', '-' || ? || ' days')`, [retentionDays]);
656
+ return result.changes;
657
+ }
658
+ }
659
+ // src/store/store.ts
660
+ import { Database } from "bun:sqlite";
661
+ import { mkdirSync } from "node:fs";
662
+ import { dirname } from "node:path";
663
+
664
+ // src/store/schema.ts
665
+ var CREATE_LOGS_TABLE = `
666
+ CREATE TABLE IF NOT EXISTS logs (
667
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
668
+ level TEXT NOT NULL,
669
+ message TEXT NOT NULL,
670
+ meta TEXT,
671
+ action_name TEXT,
672
+ trace_id TEXT,
673
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
674
+ );
675
+ `;
676
+ var CREATE_ACTION_RUNS_TABLE = `
677
+ CREATE TABLE IF NOT EXISTS action_runs (
678
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
679
+ trace_id TEXT NOT NULL UNIQUE,
680
+ action_name TEXT NOT NULL,
681
+ trigger_name TEXT NOT NULL,
682
+ trigger_type TEXT NOT NULL,
683
+ actor_id TEXT,
684
+ actor_type TEXT,
685
+ input TEXT,
686
+ output TEXT,
687
+ status TEXT NOT NULL DEFAULT 'pending',
688
+ error_message TEXT,
689
+ duration_ms INTEGER,
690
+ started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
691
+ completed_at TEXT
692
+ );
693
+ `;
694
+ var CREATE_JOBS_TABLE = `
695
+ CREATE TABLE IF NOT EXISTS jobs (
696
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
697
+ action_name TEXT NOT NULL,
698
+ trigger_name TEXT NOT NULL,
699
+ schedule TEXT NOT NULL,
700
+ status TEXT NOT NULL DEFAULT 'active',
701
+ last_run_at TEXT,
702
+ next_run_at TEXT,
703
+ run_count INTEGER NOT NULL DEFAULT 0,
704
+ last_error TEXT,
705
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
706
+ UNIQUE(action_name, trigger_name)
707
+ );
708
+ `;
709
+ var CREATE_INDEXES = `
710
+ CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
711
+ CREATE INDEX IF NOT EXISTS idx_logs_action ON logs(action_name);
712
+ CREATE INDEX IF NOT EXISTS idx_logs_trace ON logs(trace_id);
713
+ CREATE INDEX IF NOT EXISTS idx_logs_created ON logs(created_at);
714
+
715
+ CREATE INDEX IF NOT EXISTS idx_runs_action ON action_runs(action_name);
716
+ CREATE INDEX IF NOT EXISTS idx_runs_status ON action_runs(status);
717
+ CREATE INDEX IF NOT EXISTS idx_runs_trigger ON action_runs(trigger_type);
718
+ CREATE INDEX IF NOT EXISTS idx_runs_started ON action_runs(started_at);
719
+
720
+ CREATE INDEX IF NOT EXISTS idx_jobs_action ON jobs(action_name);
721
+ CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
722
+ `;
723
+
724
+ // src/store/store.ts
725
+ class GravityStore {
726
+ db;
727
+ logs;
728
+ runs;
729
+ jobs;
730
+ retention;
731
+ constructor(config = {}) {
732
+ const dbPath = config.path ?? ".gravity/data.db";
733
+ this.retention = config.retention ?? 30;
734
+ mkdirSync(dirname(dbPath), { recursive: true });
735
+ this.db = new Database(dbPath, { create: true });
736
+ this.db.run("PRAGMA journal_mode = WAL;");
737
+ this.db.run("PRAGMA synchronous = NORMAL;");
738
+ this.db.run("PRAGMA foreign_keys = ON;");
739
+ this.db.run(CREATE_LOGS_TABLE);
740
+ this.db.run(CREATE_ACTION_RUNS_TABLE);
741
+ this.db.run(CREATE_JOBS_TABLE);
742
+ this.db.exec(CREATE_INDEXES);
743
+ this.logs = new LogStore(this.db);
744
+ this.runs = new RunStore(this.db);
745
+ this.jobs = new JobStore(this.db);
746
+ }
747
+ cleanup() {
748
+ return {
749
+ logs: this.logs.cleanup(this.retention),
750
+ runs: this.runs.cleanup(this.retention)
751
+ };
752
+ }
753
+ close() {
754
+ this.db.close();
755
+ }
756
+ }
757
+ // src/runtime/executor.ts
758
+ import { Value } from "typebox/value";
759
+
760
+ // src/runtime/tracer.ts
761
+ class Tracer {
762
+ logger;
763
+ store;
764
+ traceId;
765
+ session;
766
+ startTime;
767
+ actionName;
768
+ triggerName;
769
+ triggerType;
770
+ constructor(title, logger, store) {
771
+ this.logger = logger;
772
+ this.store = store;
773
+ this.traceId = Bun.randomUUIDv7();
774
+ this.startTime = Date.now();
775
+ this.session = new LoggerSession(title);
776
+ }
777
+ info(label, detail) {
778
+ this.session.info(label, detail);
779
+ return this;
780
+ }
781
+ step(label, detail) {
782
+ this.session.step(label, detail);
783
+ return this;
784
+ }
785
+ success(label, detail) {
786
+ this.session.success(label, detail);
787
+ return this;
788
+ }
789
+ warn(label, detail) {
790
+ this.session.warn(label, detail);
791
+ return this;
792
+ }
793
+ error(label, detail) {
794
+ this.session.error(label, detail);
795
+ return this;
796
+ }
797
+ bar() {
798
+ this.session.bar();
799
+ return this;
800
+ }
801
+ recordStart(opts) {
802
+ this.actionName = opts.actionName;
803
+ this.triggerName = opts.triggerName;
804
+ this.triggerType = opts.triggerType;
805
+ this.store?.runs.start({
806
+ trace_id: this.traceId,
807
+ action_name: opts.actionName,
808
+ trigger_name: opts.triggerName,
809
+ trigger_type: opts.triggerType,
810
+ actor_id: opts.actor.id,
811
+ actor_type: opts.actor.type,
812
+ input: opts.input
813
+ });
814
+ }
815
+ end(message, output) {
816
+ const durationMs = Date.now() - this.startTime;
817
+ this.session.end(message);
818
+ this.store?.runs.complete(this.traceId, output, durationMs);
819
+ this.persistLog("INFO", `${this.actionName ?? "request"} completed`, {
820
+ durationMs,
821
+ status: "success"
822
+ });
823
+ }
824
+ fail(message, errorMsg) {
825
+ const durationMs = Date.now() - this.startTime;
826
+ this.session.fail(message);
827
+ this.store?.runs.fail(this.traceId, errorMsg ?? message ?? "Unknown error", durationMs);
828
+ this.persistLog("ERROR", `${this.actionName ?? "request"} failed`, {
829
+ durationMs,
830
+ error: errorMsg,
831
+ status: "error"
832
+ });
833
+ }
834
+ get elapsed() {
835
+ return Date.now() - this.startTime;
836
+ }
837
+ persistLog(level, message, meta) {
838
+ try {
839
+ this.store?.logs.insert({
840
+ level,
841
+ message,
842
+ meta: meta ? JSON.stringify(meta) : null,
843
+ action_name: this.actionName ?? null,
844
+ trace_id: this.traceId
845
+ });
846
+ } catch {}
847
+ }
848
+ }
849
+
850
+ // src/runtime/executor.ts
851
+ class ActionExecutor {
852
+ baseContext;
853
+ store;
854
+ constructor(baseContext) {
855
+ this.baseContext = baseContext;
856
+ }
857
+ setStore(store) {
858
+ this.store = store;
859
+ }
860
+ async execute(action, triggerName, sourceData, tracer) {
861
+ const config = action.config;
862
+ const baseLogger = this.baseContext.logger ?? new Logger;
863
+ const logger = baseLogger.child({
864
+ action: config.name,
865
+ trigger: triggerName
866
+ });
867
+ const trace = tracer ?? new Tracer(`Action: ${config.name}`, logger, this.store);
868
+ const trigger = config.triggers?.find((t) => t.name === triggerName);
869
+ if (!trigger) {
870
+ trace.error("Trigger", `'${triggerName}' not found`);
871
+ trace.fail("Bad Request", `Trigger '${triggerName}' not found`);
872
+ throw new BadRequestError(`Trigger '${triggerName}' not found for action '${config.name}'`);
873
+ }
874
+ const actor = await this.resolveActor(trigger, sourceData);
875
+ trace.info("Trigger", `${trigger.kind} (${triggerName})`).info("Identity", `${actor.id ?? "anonymous"} (${actor.type})`).bar();
876
+ trace.recordStart({
877
+ actionName: config.name,
878
+ triggerName,
879
+ triggerType: trigger.kind,
880
+ actor,
881
+ input: sourceData
882
+ });
883
+ const ctx = {
884
+ ...this.baseContext,
885
+ traceId: trace.traceId,
886
+ triggerName,
887
+ triggerType: trigger.kind,
888
+ actor,
889
+ logger
890
+ };
891
+ try {
892
+ let input = sourceData;
893
+ if (trigger.map) {
894
+ input = await trigger.map(sourceData);
895
+ }
896
+ if (config.input && !Value.Check(config.input, input)) {
897
+ const errors = [...Value.Errors(config.input, input)];
898
+ throw new BadRequestError(`Validation Error: ${JSON.stringify(errors)}`);
899
+ }
900
+ await this.checkRateLimit(ctx, config);
901
+ if (config.access) {
902
+ trace.step("Access", "checking...");
903
+ await this.checkAccess(ctx, config.access);
904
+ trace.success("Access", "passed");
905
+ }
906
+ if (config.guard) {
907
+ trace.step("Guard", "checking...");
908
+ await this.checkGuard(ctx, input, config);
909
+ trace.success("Guard", "passed");
910
+ }
911
+ const result = await action(input, ctx);
912
+ trace.end("Success", result);
913
+ return result;
914
+ } catch (error) {
915
+ trace.error("Error", error.message);
916
+ trace.fail("Failed", error.message);
917
+ throw error;
918
+ }
919
+ }
920
+ async resolveActor(trigger, sourceData) {
921
+ if (typeof trigger.actor === "function") {
922
+ return await trigger.actor(sourceData);
923
+ }
924
+ if (trigger.actor) {
925
+ return trigger.actor;
926
+ }
927
+ return { type: "public", id: "guest", roles: [] };
928
+ }
929
+ async checkRateLimit(ctx, config) {
930
+ if (!config.rateLimit)
931
+ return;
932
+ const rules = this.resolveRateLimitRules(config.rateLimit, ctx.triggerType);
933
+ if (rules.length === 0)
934
+ return;
935
+ for (const rule of rules) {}
936
+ }
937
+ resolveRateLimitRules(config, triggerType) {
938
+ if (!config)
939
+ return [];
940
+ let rules;
941
+ if ("limit" in config || Array.isArray(config)) {
942
+ rules = config;
943
+ } else {
944
+ const structured = config;
945
+ const specific = structured[triggerType];
946
+ const common = structured.common;
947
+ if (specific && common) {
948
+ const sRules = Array.isArray(specific) ? specific : [specific];
949
+ const cRules = Array.isArray(common) ? common : [common];
950
+ return [...cRules, ...sRules];
951
+ }
952
+ rules = specific || common;
953
+ }
954
+ if (!rules)
955
+ return [];
956
+ return Array.isArray(rules) ? rules : [rules];
957
+ }
958
+ async checkAccess(ctx, access) {
959
+ if (ctx.actor.type === "system")
960
+ return;
961
+ let config = { ...access.common };
962
+ if (ctx.triggerType === "api" && access.api) {
963
+ config = { ...config, ...access.api };
964
+ } else if (ctx.triggerType === "agent" && access.agent) {
965
+ config = { ...config, ...access.agent };
966
+ }
967
+ if (Object.keys(config).length === 0 && (access.authenticated !== undefined || access.roles)) {
968
+ config = {
969
+ authenticated: access.authenticated,
970
+ roles: access.roles
971
+ };
972
+ }
973
+ if (config.authenticated) {
974
+ if (ctx.actor.type === "public") {
975
+ throw new UnauthorizedError("Authentication required");
976
+ }
977
+ }
978
+ if (config.roles && config.roles.length > 0) {
979
+ if (ctx.actor.type === "public") {
980
+ throw new UnauthorizedError("Authentication required");
981
+ }
982
+ const userRoles = ctx.actor.roles || [];
983
+ const hasRole = config.roles.some((role) => userRoles.includes(role));
984
+ if (!hasRole) {
985
+ throw new ForbiddenError(`User lacks required role(s): ${config.roles.join(", ")}`);
986
+ }
987
+ }
988
+ }
989
+ async checkGuard(ctx, input, config) {
990
+ if (!config.guard)
991
+ return;
992
+ const guardFn = config.guard[ctx.triggerName] || config.guard[ctx.triggerType];
993
+ if (guardFn) {
994
+ const allowed = await guardFn(input, ctx);
995
+ if (!allowed) {
996
+ throw new ForbiddenError("Guard check failed");
997
+ }
998
+ }
999
+ }
1000
+ }
1001
+
1002
+ // src/runtime/event-bus.ts
1003
+ class EventBus {
1004
+ subscriptions = [];
1005
+ executor;
1006
+ logger;
1007
+ store;
1008
+ constructor(baseContext, store) {
1009
+ this.executor = new ActionExecutor(baseContext);
1010
+ this.logger = baseContext.logger;
1011
+ this.store = store;
1012
+ }
1013
+ subscribe(pattern, action, triggerName) {
1014
+ this.subscriptions.push({ pattern, action, triggerName });
1015
+ }
1016
+ async publish(event) {
1017
+ const matching = this.subscriptions.filter((sub) => this.matchPattern(sub.pattern, event.type));
1018
+ if (matching.length === 0) {
1019
+ this.logger.debug("No subscribers for event", { eventType: event.type });
1020
+ return;
1021
+ }
1022
+ const results = await Promise.allSettled(matching.map(async (sub) => {
1023
+ const tracer = new Tracer(`Event: ${event.type}`, this.logger, this.store);
1024
+ tracer.bar();
1025
+ tracer.info("Handler", sub.action.config.name);
1026
+ return await this.executor.execute(sub.action, sub.triggerName, event, tracer);
1027
+ }));
1028
+ const failed = results.filter((r) => r.status === "rejected").length;
1029
+ const succeeded = results.filter((r) => r.status === "fulfilled").length;
1030
+ this.logger.info("Event published", {
1031
+ event: event.type,
1032
+ handlers: matching.length,
1033
+ succeeded,
1034
+ failed
1035
+ });
1036
+ }
1037
+ matchPattern(pattern, eventType) {
1038
+ const regex = new RegExp(`^${pattern.replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
1039
+ return regex.test(eventType);
1040
+ }
1041
+ }
1042
+
1043
+ // src/runtime/http.ts
1044
+ class HttpServer {
1045
+ config;
1046
+ logger;
1047
+ server;
1048
+ router;
1049
+ constructor(config, router, logger = new Logger) {
1050
+ this.config = config;
1051
+ this.logger = logger;
1052
+ this.router = router;
1053
+ }
1054
+ async start() {
1055
+ const port = this.config.server?.port || 3000;
1056
+ const host = this.config.server?.host || "localhost";
1057
+ const corsConfig = this.config.server?.cors;
1058
+ this.server = Bun.serve({
1059
+ port,
1060
+ hostname: host,
1061
+ fetch: async (req) => {
1062
+ const url = new URL(req.url);
1063
+ if (req.method === "OPTIONS") {
1064
+ return new Response(null, {
1065
+ status: 204,
1066
+ headers: getCorsHeaders(corsConfig)
1067
+ });
1068
+ }
1069
+ try {
1070
+ const path = url.pathname;
1071
+ const method = req.method;
1072
+ let body;
1073
+ if (["POST", "PUT", "PATCH"].includes(method)) {
1074
+ const contentType = req.headers.get("content-type") || "";
1075
+ if (contentType.includes("application/json")) {
1076
+ body = await req.json();
1077
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
1078
+ const formData = await req.formData();
1079
+ body = Object.fromEntries(formData.entries());
1080
+ } else if (contentType.includes("multipart/form-data")) {
1081
+ const formData = await req.formData();
1082
+ body = Object.fromEntries(formData.entries());
1083
+ } else {
1084
+ body = await req.text();
1085
+ }
1086
+ }
1087
+ const query = {};
1088
+ url.searchParams.forEach((value, key) => {
1089
+ query[key] = value;
1090
+ });
1091
+ const headers = {};
1092
+ req.headers.forEach((value, key) => {
1093
+ headers[key] = value;
1094
+ });
1095
+ const apiReq = {
1096
+ method,
1097
+ path,
1098
+ params: {},
1099
+ query,
1100
+ body,
1101
+ headers,
1102
+ raw: req
1103
+ };
1104
+ const result = await this.router.handle(apiReq);
1105
+ return new Response(JSON.stringify(result), {
1106
+ status: 200,
1107
+ headers: {
1108
+ "Content-Type": "application/json",
1109
+ ...getCorsHeaders(corsConfig)
1110
+ }
1111
+ });
1112
+ } catch (error) {
1113
+ let status = 500;
1114
+ let message = "Internal Server Error";
1115
+ if (error.name === "BadRequestError") {
1116
+ status = 400;
1117
+ message = error.message;
1118
+ } else if (error.name === "UnauthorizedError") {
1119
+ status = 401;
1120
+ message = error.message;
1121
+ } else if (error.name === "ForbiddenError") {
1122
+ status = 403;
1123
+ message = error.message;
1124
+ } else if (error.message.includes("Route not found")) {
1125
+ status = 404;
1126
+ message = error.message;
1127
+ }
1128
+ return new Response(JSON.stringify({ error: message }), {
1129
+ status,
1130
+ headers: {
1131
+ "Content-Type": "application/json",
1132
+ ...getCorsHeaders(corsConfig)
1133
+ }
1134
+ });
1135
+ }
1136
+ },
1137
+ error: (error) => {
1138
+ this.logger.error("HTTP server error", {
1139
+ error: error.message,
1140
+ stack: error.stack
1141
+ });
1142
+ return new Response(JSON.stringify({ error: "Internal Server Error" }), {
1143
+ status: 500,
1144
+ headers: {
1145
+ "Content-Type": "application/json"
1146
+ }
1147
+ });
1148
+ }
1149
+ });
1150
+ this.logger.info("HTTP server started", {
1151
+ url: `http://${host}:${port}`
1152
+ });
1153
+ }
1154
+ async stop() {
1155
+ if (this.server) {
1156
+ this.server.stop();
1157
+ this.logger.info("HTTP server stopped");
1158
+ }
1159
+ }
1160
+ getUrl() {
1161
+ if (!this.server)
1162
+ return null;
1163
+ return `http://${this.server.hostname}:${this.server.port}`;
1164
+ }
1165
+ }
1166
+ function getCorsHeaders(corsConfig) {
1167
+ if (!corsConfig)
1168
+ return {};
1169
+ const headers = {};
1170
+ if (corsConfig.origin) {
1171
+ const origin = Array.isArray(corsConfig.origin) ? corsConfig.origin.join(", ") : corsConfig.origin;
1172
+ headers["Access-Control-Allow-Origin"] = origin;
1173
+ }
1174
+ if (corsConfig.credentials) {
1175
+ headers["Access-Control-Allow-Credentials"] = "true";
1176
+ }
1177
+ headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS";
1178
+ headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization";
1179
+ return headers;
1180
+ }
1181
+
1182
+ // src/runtime/mcp-server.ts
1183
+ class McpServer {
1184
+ context;
1185
+ logger;
1186
+ constructor(context, logger = new Logger) {
1187
+ this.context = context;
1188
+ this.logger = logger;
1189
+ }
1190
+ register(action, trigger) {
1191
+ this.logger.info("Registering MCP tool", {
1192
+ tool: trigger.tool,
1193
+ action: action.config.name,
1194
+ context: this.context
1195
+ });
1196
+ }
1197
+ async start() {
1198
+ this.logger.info("MCP Server started");
1199
+ }
1200
+ async stop() {
1201
+ this.logger.info("MCP Server stopped");
1202
+ }
1203
+ }
1204
+
1205
+ // src/runtime/router.ts
1206
+ class Router {
1207
+ routes = [];
1208
+ executor;
1209
+ logger;
1210
+ store;
1211
+ constructor(baseContext, store) {
1212
+ this.executor = new ActionExecutor(baseContext);
1213
+ this.logger = baseContext.logger;
1214
+ this.store = store;
1215
+ }
1216
+ register(action, trigger) {
1217
+ const { pattern, paramNames } = this.pathToRegex(trigger.path);
1218
+ this.routes.push({
1219
+ method: trigger.method,
1220
+ pattern,
1221
+ paramNames,
1222
+ action,
1223
+ trigger
1224
+ });
1225
+ }
1226
+ async handle(req) {
1227
+ const tracer = new Tracer(`${req.method} ${req.path}`, this.logger, this.store);
1228
+ tracer.bar();
1229
+ const route = this.match(req.method, req.path);
1230
+ if (!route) {
1231
+ tracer.error("Route", `not found`);
1232
+ tracer.fail("404 Not Found", `Route not found: ${req.method} ${req.path}`);
1233
+ throw new Error(`Route not found: ${req.method} ${req.path}`);
1234
+ }
1235
+ tracer.info("Action", route.action.config.name);
1236
+ const params = {};
1237
+ const match = req.path.match(route.pattern);
1238
+ if (match) {
1239
+ route.paramNames.forEach((name, i) => {
1240
+ params[name] = match[i + 1];
1241
+ });
1242
+ }
1243
+ const fullReq = { ...req, params };
1244
+ return await this.executor.execute(route.action, route.trigger.name, fullReq, tracer);
1245
+ }
1246
+ match(method, path) {
1247
+ return this.routes.find((route) => route.method === method && route.pattern.test(path));
1248
+ }
1249
+ pathToRegex(path) {
1250
+ const paramNames = [];
1251
+ const pattern = path.replace(/:([^/]+)/g, (_, paramName) => {
1252
+ paramNames.push(paramName);
1253
+ return "([^/]+)";
1254
+ });
1255
+ return {
1256
+ pattern: new RegExp(`^${pattern}$`),
1257
+ paramNames
1258
+ };
1259
+ }
1260
+ getRoutes() {
1261
+ return this.routes;
1262
+ }
1263
+ }
1264
+
1265
+ // src/runtime/scheduler.ts
1266
+ import Baker from "cronbake";
1267
+ class Scheduler {
1268
+ baker = Baker.create();
1269
+ executor;
1270
+ logger;
1271
+ store;
1272
+ constructor(baseContext, store) {
1273
+ this.executor = new ActionExecutor(baseContext);
1274
+ this.logger = baseContext.logger;
1275
+ this.store = store;
1276
+ }
1277
+ schedule(action, trigger) {
1278
+ const triggerName = trigger.name || "cron";
1279
+ this.store?.jobs.upsert({
1280
+ action_name: action.config.name,
1281
+ trigger_name: triggerName,
1282
+ schedule: trigger.schedule
1283
+ });
1284
+ this.baker.add({
1285
+ name: `${action.config.name}:${triggerName}`,
1286
+ cron: trigger.schedule,
1287
+ callback: async () => {
1288
+ const tracer = new Tracer(`Cron: ${action.config.name}`, this.logger, this.store);
1289
+ tracer.bar();
1290
+ tracer.info("Schedule", trigger.schedule);
1291
+ try {
1292
+ await this.executor.execute(action, triggerName, {}, tracer);
1293
+ this.store?.jobs.recordRun(action.config.name, triggerName);
1294
+ } catch (error) {
1295
+ this.store?.jobs.recordRun(action.config.name, triggerName, error.message);
1296
+ }
1297
+ }
1298
+ });
1299
+ }
1300
+ start() {
1301
+ this.baker.bakeAll();
1302
+ this.logger.info("Scheduler started");
1303
+ }
1304
+ stop() {
1305
+ this.baker.stopAll();
1306
+ this.logger.info("Stopped all cron jobs");
1307
+ }
1308
+ }
1309
+
1310
+ // src/runtime/server.ts
1311
+ class GravityServer {
1312
+ config;
1313
+ router;
1314
+ eventBus;
1315
+ scheduler;
1316
+ mcpServer;
1317
+ httpServer;
1318
+ logger;
1319
+ store;
1320
+ actions = [];
1321
+ constructor(config) {
1322
+ this.config = config;
1323
+ this.logger = new Logger(config.logger ?? {});
1324
+ if (config.store !== null) {
1325
+ this.store = new GravityStore(config.store ?? {});
1326
+ this.logger.setStore(this.store);
1327
+ }
1328
+ const baseContext = this.buildBaseContext();
1329
+ this.router = new Router(baseContext, this.store);
1330
+ this.eventBus = new EventBus(baseContext, this.store);
1331
+ this.scheduler = new Scheduler(baseContext, this.store);
1332
+ this.httpServer = new HttpServer(this.config, this.router, this.logger);
1333
+ if (this.config.mcp?.enabled) {
1334
+ this.mcpServer = new McpServer(baseContext, this.logger);
1335
+ }
1336
+ }
1337
+ buildBaseContext() {
1338
+ return {
1339
+ logger: this.logger,
1340
+ db: undefined,
1341
+ storage: undefined,
1342
+ enqueue: async (eventType, data) => {
1343
+ const event = {
1344
+ id: `evt-${Date.now()}-${Math.random().toString(36).slice(2)}`,
1345
+ type: eventType,
1346
+ data,
1347
+ timestamp: new Date
1348
+ };
1349
+ await this.eventBus.publish(event);
1350
+ },
1351
+ schedule: async (when, action, input) => {
1352
+ await this.scheduleAction(when, action, input);
1353
+ }
1354
+ };
1355
+ }
1356
+ async scheduleAction(when, action, input) {
1357
+ const actionName = action.config.name;
1358
+ if (typeof when === "number") {
1359
+ const runAt = new Date(Date.now() + when * 1000);
1360
+ this.logger.info("Scheduling action", { actionName, runAt, input });
1361
+ } else if (when instanceof Date) {
1362
+ this.logger.info("Scheduling action", { actionName, runAt: when, input });
1363
+ } else if (typeof when === "string") {
1364
+ this.logger.info("Scheduling recurring action", {
1365
+ actionName,
1366
+ cron: when,
1367
+ input
1368
+ });
1369
+ }
1370
+ }
1371
+ async initialize() {
1372
+ this.actions = await loadActions(this.config.actionsDir || "./src/actions", this.logger);
1373
+ for (const action of this.actions) {
1374
+ if (!action.config.triggers)
1375
+ continue;
1376
+ for (const trigger of action.config.triggers) {
1377
+ switch (trigger.kind) {
1378
+ case "api": {
1379
+ const apiTrigger = trigger;
1380
+ this.router.register(action, apiTrigger);
1381
+ this.logger.info("Registered API route", {
1382
+ method: apiTrigger.method,
1383
+ path: apiTrigger.path,
1384
+ action: action.config.name
1385
+ });
1386
+ break;
1387
+ }
1388
+ case "cron": {
1389
+ const cronTrigger = trigger;
1390
+ this.scheduler.schedule(action, cronTrigger);
1391
+ this.logger.info("Registered cron job", {
1392
+ schedule: cronTrigger.schedule,
1393
+ action: action.config.name
1394
+ });
1395
+ break;
1396
+ }
1397
+ case "event": {
1398
+ const eventTrigger = trigger;
1399
+ this.eventBus.subscribe(eventTrigger.event, action, eventTrigger.name);
1400
+ this.logger.info("Registered event handler", {
1401
+ event: eventTrigger.event,
1402
+ action: action.config.name
1403
+ });
1404
+ break;
1405
+ }
1406
+ case "agent": {
1407
+ const agentTrigger = trigger;
1408
+ this.mcpServer?.register(action, agentTrigger);
1409
+ this.logger.info("Registered MCP tool", {
1410
+ tool: agentTrigger.tool,
1411
+ action: action.config.name
1412
+ });
1413
+ break;
1414
+ }
1415
+ }
1416
+ }
1417
+ }
1418
+ this.logger.info("Gravity initialized", {
1419
+ actions: this.actions.length,
1420
+ routes: this.router.getRoutes().length
1421
+ });
1422
+ }
1423
+ async start() {
1424
+ await this.httpServer.start();
1425
+ this.scheduler.start();
1426
+ if (this.mcpServer) {
1427
+ await this.mcpServer.start();
1428
+ }
1429
+ }
1430
+ getRouter() {
1431
+ return this.router;
1432
+ }
1433
+ getEventBus() {
1434
+ return this.eventBus;
1435
+ }
1436
+ getActions() {
1437
+ return this.actions;
1438
+ }
1439
+ getStore() {
1440
+ return this.store;
1441
+ }
1442
+ getHttpUrl() {
1443
+ return this.httpServer.getUrl();
1444
+ }
1445
+ async stop() {
1446
+ await this.httpServer.stop();
1447
+ this.scheduler.stop();
1448
+ await this.mcpServer?.stop();
1449
+ this.store?.close();
1450
+ }
1451
+ }
1452
+
1453
+ // src/runtime/dev.ts
1454
+ async function createDevServer(config) {
1455
+ const logger = new Logger(config.logger ?? {});
1456
+ const server = new GravityServer(config);
1457
+ await server.initialize();
1458
+ if (config.dev?.watch !== false) {
1459
+ const actionsDir = config.actionsDir || "./src/actions";
1460
+ const watcher = watch(actionsDir, { recursive: true }, async (eventType, filename) => {
1461
+ if (!filename)
1462
+ return;
1463
+ if (!/\.(ts|js)$/.test(filename))
1464
+ return;
1465
+ logger.info("Action file changed, regenerating...", {
1466
+ filename,
1467
+ eventType
1468
+ });
1469
+ try {
1470
+ const actions = await loadActions(actionsDir, logger);
1471
+ await generateActionTypes(actions);
1472
+ logger.info("Types regenerated");
1473
+ } catch (error) {
1474
+ logger.error("Failed to regenerate types", {
1475
+ error: error.message
1476
+ });
1477
+ }
1478
+ });
1479
+ process.on("SIGINT", () => {
1480
+ watcher.close();
1481
+ server.stop();
1482
+ process.exit(0);
1483
+ });
1484
+ }
1485
+ return server;
1486
+ }
1487
+
1488
+ // src/cli/commands/dev.ts
1489
+ var devCommand = new Command3("dev").description("Start the development server").option("-p, --port <number>", "Port to listen on", parseInt).action(async (options) => {
1490
+ console.log(`\uD83C\uDF0B Starting Basalt dev server...
1491
+ `);
1492
+ const config = await loadConfig();
1493
+ if (options.port) {
1494
+ config.server = { ...config.server, port: options.port };
1495
+ }
1496
+ const actions = await loadActions(config.actionsDir || "./src/actions");
1497
+ console.log("✓ Loaded actions");
1498
+ const server = await createDevServer(config);
1499
+ await server.start();
1500
+ const port = config.server?.port || 3000;
1501
+ const host = config.server?.host || "localhost";
1502
+ console.log(`
1503
+ ✓ Server ready
1504
+
1505
+ Local: http://${host}:${port}
1506
+ Actions: ${actions.length}
1507
+ Watch: ${config.dev?.watch !== false ? "enabled" : "disabled"}
1508
+
1509
+ Press Ctrl+C to stop
1510
+ `);
1511
+ });
1512
+
1513
+ // src/cli/commands/start.ts
1514
+ import { Command as Command4 } from "commander";
1515
+ var startCommand = new Command4("start").description("Start the production server").action(async () => {
1516
+ console.log("Starting production server...");
1517
+ const config = await loadConfig();
1518
+ const server = new GravityServer(config);
1519
+ await server.initialize();
1520
+ await server.start();
1521
+ });
1522
+
1523
+ // src/cli/index.ts
1524
+ var program = new Command5;
1525
+ program.name("basalt").description("Basalt CLI").version("0.1.0");
1526
+ program.addCommand(devCommand);
1527
+ program.addCommand(buildCommand);
1528
+ program.addCommand(startCommand);
1529
+ program.addCommand(createActionCommand);
1530
+ program.parse(process.argv);