gravity-run 0.0.2

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,845 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BadRequestError,
4
+ ForbiddenError,
5
+ UnauthorizedError,
6
+ globalLogger,
7
+ loadConfig
8
+ } from "../shared/chunk-b636e30q.js";
9
+
10
+ // src/cli/index.ts
11
+ import { Command as Command5 } from "commander";
12
+
13
+ // src/cli/commands/build.ts
14
+ import { Command } from "commander";
15
+ var buildCommand = new Command("build").description("Build the project for production").action(async () => {
16
+ console.log("Building project...");
17
+ const proc = Bun.spawn([
18
+ "bun",
19
+ "build",
20
+ "./src/index.ts",
21
+ "--outdir",
22
+ "./dist",
23
+ "--target",
24
+ "bun"
25
+ ], {
26
+ stdout: "inherit",
27
+ stderr: "inherit"
28
+ });
29
+ await proc.exited;
30
+ console.log("Build complete.");
31
+ });
32
+
33
+ // src/cli/commands/create-action.ts
34
+ import { mkdir, writeFile } from "node:fs/promises";
35
+ import { join } from "node:path";
36
+ import { Command as Command2 } from "commander";
37
+ import inquirer from "inquirer";
38
+ var createActionCommand = new Command2("create:action").description("Create a new action").action(async () => {
39
+ const answers = await inquirer.prompt([
40
+ {
41
+ type: "input",
42
+ name: "name",
43
+ message: "Action name (e.g. user/create):",
44
+ validate: (input) => input.length > 0
45
+ },
46
+ {
47
+ type: "checkbox",
48
+ name: "triggers",
49
+ message: "Select triggers:",
50
+ choices: ["api", "cron", "event", "agent"]
51
+ }
52
+ ]);
53
+ const name = answers.name;
54
+ const parts = name.split("/");
55
+ const fileName = parts.pop() || "index";
56
+ const dir = parts.join("/");
57
+ const baseDir = "./src/actions";
58
+ const targetDir = join(baseDir, dir);
59
+ const targetFile = join(targetDir, `${fileName}.ts`);
60
+ await mkdir(targetDir, { recursive: true });
61
+ const content = `import { action } from 'basalt'
62
+ import { Type } from '@sinclair/typebox'
63
+
64
+ export default action({
65
+ name: '${name}',
66
+ description: 'Auto-generated action',
67
+ input: Type.Object({
68
+ // Define input schema
69
+ name: Type.String(),
70
+ }),
71
+ output: Type.Object({
72
+ result: Type.String(),
73
+ }),
74
+ triggers: [
75
+ ${answers.triggers.map((t) => {
76
+ if (t === "api")
77
+ return ` { kind: 'api', method: 'POST', path: '/${name}' },`;
78
+ if (t === "cron")
79
+ return ` { kind: 'cron', schedule: '0 * * * *' },`;
80
+ if (t === "event")
81
+ return ` { kind: 'event', event: '${name.replace("/", ".")}' },`;
82
+ if (t === "agent")
83
+ return ` { kind: 'agent', tool: '${name.replace("/", "_")}', description: 'Execute ${name}' },`;
84
+ return "";
85
+ }).join(`
86
+ `)}
87
+ ],
88
+ handler: async (input, ctx) => {
89
+ ctx.log.info('Executing ${name}', { input })
90
+ return {
91
+ result: \`Hello \${input.name}\`
92
+ }
93
+ }
94
+ })
95
+ `;
96
+ await writeFile(targetFile, content);
97
+ console.log(`✓ Created action at ${targetFile}`);
98
+ });
99
+
100
+ // src/cli/commands/dev.ts
101
+ import { Command as Command3 } from "commander";
102
+
103
+ // src/runtime/dev.ts
104
+ import { watch } from "node:fs";
105
+
106
+ // src/codegen/types.ts
107
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
108
+ import { join as join2 } from "node:path";
109
+ async function generateActionTypes(actions, outputDir = ".basalt") {
110
+ const typeMap = {};
111
+ for (const action of actions) {
112
+ const inputType = schemaToTypeString(action.config.input);
113
+ typeMap[action.config.name] = inputType;
114
+ }
115
+ const dts = `
116
+ // Auto-generated by Basalt - DO NOT EDIT
117
+ // Generated at ${new Date().toISOString()}
118
+
119
+ declare module 'basalt/types' {
120
+ export interface ActionRegistry {
121
+ ${Object.entries(typeMap).map(([name, type]) => ` '${name}': ${type}`).join(`
122
+ `)}
123
+ }
124
+ }
125
+
126
+ export {}
127
+ `.trim();
128
+ await mkdir2(outputDir, { recursive: true });
129
+ await writeFile2(join2(outputDir, "types.d.ts"), dts, "utf-8");
130
+ }
131
+ function schemaToTypeString(schema) {
132
+ if (!schema)
133
+ return "any";
134
+ const kind = schema.type;
135
+ if (kind === "object") {
136
+ const props = schema.properties || {};
137
+ const entries = Object.entries(props).map(([key, val]) => {
138
+ return `${key}: ${schemaToTypeString(val)}`;
139
+ });
140
+ return `{ ${entries.join("; ")} }`;
141
+ }
142
+ if (kind === "string")
143
+ return "string";
144
+ if (kind === "number")
145
+ return "number";
146
+ if (kind === "boolean")
147
+ return "boolean";
148
+ if (kind === "array") {
149
+ const items = schema.items;
150
+ return `Array<${schemaToTypeString(items)}>`;
151
+ }
152
+ return "any";
153
+ }
154
+
155
+ // src/runtime/loader.ts
156
+ import { pathToFileURL } from "node:url";
157
+ async function loadActions(dir) {
158
+ const glob = new Bun.Glob(`${dir}/**/*.ts`);
159
+ const files = glob.scanSync();
160
+ const actions = [];
161
+ for (const file of files) {
162
+ const module = await import(pathToFileURL(file).href);
163
+ for (const exported of Object.values(module)) {
164
+ if (isAction(exported)) {
165
+ actions.push(exported);
166
+ }
167
+ }
168
+ }
169
+ return actions;
170
+ }
171
+ function isAction(obj) {
172
+ return typeof obj === "function" && "config" in obj && "handler" in obj && typeof obj.config === "object" && "name" in obj.config;
173
+ }
174
+
175
+ // src/runtime/executor.ts
176
+ import { Value } from "typebox/value";
177
+ class ActionExecutor {
178
+ baseContext;
179
+ constructor(baseContext) {
180
+ this.baseContext = baseContext;
181
+ }
182
+ async execute(action, triggerName, sourceData) {
183
+ const config = action.config;
184
+ const trigger = config.triggers?.find((t) => t.name === triggerName);
185
+ if (!trigger) {
186
+ throw new BadRequestError(`Trigger '${triggerName}' not found for action '${config.name}'`);
187
+ }
188
+ const actor = await this.resolveActor(trigger, sourceData);
189
+ const ctx = {
190
+ requestId: this.baseContext.requestId || `req-${Date.now()}-${Math.random().toString(36).slice(2)}`,
191
+ triggerName,
192
+ triggerType: trigger.kind,
193
+ actor,
194
+ logger: this.baseContext.logger || globalLogger,
195
+ db: this.baseContext.db,
196
+ storage: this.baseContext.storage,
197
+ enqueue: this.baseContext.enqueue || this.createEnqueueFn(),
198
+ schedule: this.baseContext.schedule || this.createScheduleFn(),
199
+ org: this.baseContext.org,
200
+ ...this.baseContext
201
+ };
202
+ const logger = ctx.logger.child({
203
+ action: config.name,
204
+ requestId: ctx.requestId,
205
+ trigger: triggerName
206
+ });
207
+ logger.info("Action execution started", {
208
+ actor: actor.type,
209
+ triggerType: trigger.kind
210
+ });
211
+ try {
212
+ let input = sourceData;
213
+ if (trigger.map) {
214
+ input = await trigger.map(sourceData);
215
+ }
216
+ if (config.input && !Value.Check(config.input, input)) {
217
+ const errors = [...Value.Errors(config.input, input)];
218
+ throw new BadRequestError(`Validation Error: ${JSON.stringify(errors)}`);
219
+ }
220
+ await this.checkRateLimit(ctx, config);
221
+ if (config.access) {
222
+ await this.checkAccess(ctx, config.access);
223
+ }
224
+ await this.checkGuard(ctx, input, config);
225
+ const result = await action(input, ctx);
226
+ logger.info("Action execution completed");
227
+ return result;
228
+ } catch (error) {
229
+ logger.error("Action execution failed", {
230
+ error: error.message,
231
+ stack: error.stack
232
+ });
233
+ throw error;
234
+ }
235
+ }
236
+ async resolveActor(trigger, sourceData) {
237
+ if (typeof trigger.actor === "function") {
238
+ return await trigger.actor(sourceData);
239
+ }
240
+ if (trigger.actor) {
241
+ return trigger.actor;
242
+ }
243
+ return { type: "public", id: "guest", roles: [] };
244
+ }
245
+ async checkRateLimit(ctx, config) {
246
+ if (!config.rateLimit)
247
+ return;
248
+ const rules = this.resolveRateLimitRules(config.rateLimit, ctx.triggerType);
249
+ if (rules.length === 0)
250
+ return;
251
+ for (const rule of rules) {}
252
+ }
253
+ resolveRateLimitRules(config, triggerType) {
254
+ if (!config)
255
+ return [];
256
+ let rules;
257
+ if ("limit" in config || Array.isArray(config)) {
258
+ rules = config;
259
+ } else {
260
+ const structured = config;
261
+ const specific = structured[triggerType];
262
+ const common = structured.common;
263
+ if (specific && common) {
264
+ const sRules = Array.isArray(specific) ? specific : [specific];
265
+ const cRules = Array.isArray(common) ? common : [common];
266
+ return [...cRules, ...sRules];
267
+ }
268
+ rules = specific || common;
269
+ }
270
+ if (!rules)
271
+ return [];
272
+ return Array.isArray(rules) ? rules : [rules];
273
+ }
274
+ async checkAccess(ctx, access) {
275
+ if (ctx.actor.type === "system")
276
+ return;
277
+ let config = { ...access.common };
278
+ if (ctx.triggerType === "api" && access.api) {
279
+ config = { ...config, ...access.api };
280
+ } else if (ctx.triggerType === "agent" && access.agent) {
281
+ config = { ...config, ...access.agent };
282
+ }
283
+ if (Object.keys(config).length === 0 && (access.authenticated !== undefined || access.roles)) {
284
+ config = {
285
+ authenticated: access.authenticated,
286
+ roles: access.roles
287
+ };
288
+ }
289
+ if (config.authenticated) {
290
+ if (ctx.actor.type === "public") {
291
+ throw new UnauthorizedError("Authentication required");
292
+ }
293
+ }
294
+ if (config.roles && config.roles.length > 0) {
295
+ if (ctx.actor.type === "public") {
296
+ throw new UnauthorizedError("Authentication required");
297
+ }
298
+ const userRoles = ctx.actor.roles || [];
299
+ const hasRole = config.roles.some((role) => userRoles.includes(role));
300
+ if (!hasRole) {
301
+ throw new ForbiddenError(`User lacks required role(s): ${config.roles.join(", ")}`);
302
+ }
303
+ }
304
+ }
305
+ async checkGuard(ctx, input, config) {
306
+ if (!config.guard)
307
+ return;
308
+ const guardFn = config.guard[ctx.triggerName] || config.guard[ctx.triggerType];
309
+ if (guardFn) {
310
+ const allowed = await guardFn(input, ctx);
311
+ if (!allowed) {
312
+ throw new ForbiddenError("Guard check failed");
313
+ }
314
+ }
315
+ }
316
+ createEnqueueFn() {
317
+ return async (eventType, data) => {
318
+ globalLogger.warn("enqueue called but no event bus configured", {
319
+ eventType,
320
+ data
321
+ });
322
+ };
323
+ }
324
+ createScheduleFn() {
325
+ return async (actionName, input, runAt) => {
326
+ globalLogger.warn("schedule called but no scheduler configured", {
327
+ actionName,
328
+ runAt,
329
+ input
330
+ });
331
+ };
332
+ }
333
+ }
334
+
335
+ // src/runtime/event-bus.ts
336
+ class EventBus {
337
+ subscriptions = [];
338
+ executor;
339
+ constructor(baseContext) {
340
+ this.executor = new ActionExecutor(baseContext);
341
+ }
342
+ subscribe(pattern, action, triggerName) {
343
+ this.subscriptions.push({ pattern, action, triggerName });
344
+ }
345
+ async publish(event) {
346
+ const matching = this.subscriptions.filter((sub) => this.matchPattern(sub.pattern, event.type));
347
+ if (matching.length === 0) {
348
+ globalLogger.debug("No subscribers for event", { eventType: event.type });
349
+ return;
350
+ }
351
+ const results = await Promise.allSettled(matching.map(async (sub) => {
352
+ try {
353
+ return await this.executor.execute(sub.action, sub.triggerName, event);
354
+ } catch (error) {
355
+ globalLogger.error("Event handler failed", {
356
+ event: event.type,
357
+ action: sub.action.config.name,
358
+ error: error.message
359
+ });
360
+ throw error;
361
+ }
362
+ }));
363
+ const failed = results.filter((r) => r.status === "rejected").length;
364
+ const succeeded = results.filter((r) => r.status === "fulfilled").length;
365
+ globalLogger.info("Event published", {
366
+ event: event.type,
367
+ handlers: matching.length,
368
+ succeeded,
369
+ failed
370
+ });
371
+ }
372
+ matchPattern(pattern, eventType) {
373
+ const regex = new RegExp(`^${pattern.replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
374
+ return regex.test(eventType);
375
+ }
376
+ }
377
+
378
+ // src/runtime/http.ts
379
+ class HttpServer {
380
+ config;
381
+ server;
382
+ router;
383
+ constructor(config, router) {
384
+ this.config = config;
385
+ this.router = router;
386
+ }
387
+ async start() {
388
+ const port = this.config.server?.port || 3000;
389
+ const host = this.config.server?.host || "localhost";
390
+ const corsConfig = this.config.server?.cors;
391
+ this.server = Bun.serve({
392
+ port,
393
+ hostname: host,
394
+ fetch: async (req) => {
395
+ const url = new URL(req.url);
396
+ if (req.method === "OPTIONS") {
397
+ return new Response(null, {
398
+ status: 204,
399
+ headers: getCorsHeaders(corsConfig)
400
+ });
401
+ }
402
+ try {
403
+ const path = url.pathname;
404
+ const method = req.method;
405
+ let body;
406
+ if (["POST", "PUT", "PATCH"].includes(method)) {
407
+ const contentType = req.headers.get("content-type") || "";
408
+ if (contentType.includes("application/json")) {
409
+ body = await req.json();
410
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
411
+ const formData = await req.formData();
412
+ body = Object.fromEntries(formData);
413
+ } else if (contentType.includes("multipart/form-data")) {
414
+ const formData = await req.formData();
415
+ body = Object.fromEntries(formData);
416
+ } else {
417
+ body = await req.text();
418
+ }
419
+ }
420
+ const query = {};
421
+ url.searchParams.forEach((value, key) => {
422
+ query[key] = value;
423
+ });
424
+ const headers = {};
425
+ req.headers.forEach((value, key) => {
426
+ headers[key] = value;
427
+ });
428
+ const apiReq = {
429
+ method,
430
+ path,
431
+ params: {},
432
+ query,
433
+ body,
434
+ headers,
435
+ raw: req
436
+ };
437
+ const result = await this.router.handle(apiReq);
438
+ return new Response(JSON.stringify(result), {
439
+ status: 200,
440
+ headers: {
441
+ "Content-Type": "application/json",
442
+ ...getCorsHeaders(corsConfig)
443
+ }
444
+ });
445
+ } catch (error) {
446
+ globalLogger.error("HTTP request failed", {
447
+ error: error.message,
448
+ stack: error.stack
449
+ });
450
+ let status = 500;
451
+ let message = "Internal Server Error";
452
+ if (error.name === "BadRequestError") {
453
+ status = 400;
454
+ message = error.message;
455
+ } else if (error.name === "UnauthorizedError") {
456
+ status = 401;
457
+ message = error.message;
458
+ } else if (error.name === "ForbiddenError") {
459
+ status = 403;
460
+ message = error.message;
461
+ } else if (error.message.includes("Route not found")) {
462
+ status = 404;
463
+ message = error.message;
464
+ }
465
+ return new Response(JSON.stringify({ error: message }), {
466
+ status,
467
+ headers: {
468
+ "Content-Type": "application/json",
469
+ ...getCorsHeaders(corsConfig)
470
+ }
471
+ });
472
+ }
473
+ },
474
+ error(error) {
475
+ globalLogger.error("HTTP server error", {
476
+ error: error.message,
477
+ stack: error.stack
478
+ });
479
+ return new Response(JSON.stringify({ error: "Internal Server Error" }), {
480
+ status: 500,
481
+ headers: {
482
+ "Content-Type": "application/json"
483
+ }
484
+ });
485
+ }
486
+ });
487
+ globalLogger.info("HTTP server started", {
488
+ url: `http://${host}:${port}`
489
+ });
490
+ }
491
+ async stop() {
492
+ if (this.server) {
493
+ this.server.stop();
494
+ globalLogger.info("HTTP server stopped");
495
+ }
496
+ }
497
+ getUrl() {
498
+ if (!this.server)
499
+ return null;
500
+ return `http://${this.server.hostname}:${this.server.port}`;
501
+ }
502
+ }
503
+ function getCorsHeaders(corsConfig) {
504
+ if (!corsConfig)
505
+ return {};
506
+ const headers = {};
507
+ if (corsConfig.origin) {
508
+ const origin = Array.isArray(corsConfig.origin) ? corsConfig.origin.join(", ") : corsConfig.origin;
509
+ headers["Access-Control-Allow-Origin"] = origin;
510
+ }
511
+ if (corsConfig.credentials) {
512
+ headers["Access-Control-Allow-Credentials"] = "true";
513
+ }
514
+ headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS";
515
+ headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization";
516
+ return headers;
517
+ }
518
+
519
+ // src/runtime/mcp-server.ts
520
+ class McpServer {
521
+ context;
522
+ constructor(context) {
523
+ this.context = context;
524
+ }
525
+ register(action, trigger) {
526
+ globalLogger.info("Registering MCP tool", {
527
+ tool: trigger.tool,
528
+ action: action.config.name,
529
+ context: this.context
530
+ });
531
+ }
532
+ async start() {
533
+ globalLogger.info("MCP Server started");
534
+ }
535
+ async stop() {
536
+ globalLogger.info("MCP Server stopped");
537
+ }
538
+ }
539
+
540
+ // src/runtime/router.ts
541
+ class Router {
542
+ routes = [];
543
+ executor;
544
+ constructor(baseContext) {
545
+ this.executor = new ActionExecutor(baseContext);
546
+ }
547
+ register(action, trigger) {
548
+ const { pattern, paramNames } = this.pathToRegex(trigger.path);
549
+ this.routes.push({
550
+ method: trigger.method,
551
+ pattern,
552
+ paramNames,
553
+ action,
554
+ trigger
555
+ });
556
+ }
557
+ async handle(req) {
558
+ const route = this.match(req.method, req.path);
559
+ if (!route) {
560
+ throw new Error(`Route not found: ${req.method} ${req.path}`);
561
+ }
562
+ const match = req.path.match(route.pattern);
563
+ const params = {};
564
+ if (match) {
565
+ route.paramNames.forEach((name, i) => {
566
+ params[name] = match[i + 1];
567
+ });
568
+ }
569
+ const fullReq = {
570
+ ...req,
571
+ params
572
+ };
573
+ return await this.executor.execute(route.action, route.trigger.name, fullReq);
574
+ }
575
+ match(method, path) {
576
+ return this.routes.find((route) => route.method === method && route.pattern.test(path));
577
+ }
578
+ pathToRegex(path) {
579
+ const paramNames = [];
580
+ const pattern = path.replace(/:([^/]+)/g, (_, paramName) => {
581
+ paramNames.push(paramName);
582
+ return "([^/]+)";
583
+ });
584
+ return {
585
+ pattern: new RegExp(`^${pattern}$`),
586
+ paramNames
587
+ };
588
+ }
589
+ getRoutes() {
590
+ return this.routes.map((r) => ({
591
+ method: r.method,
592
+ path: r.trigger.path,
593
+ action: r.action.config.name
594
+ }));
595
+ }
596
+ }
597
+
598
+ // src/runtime/scheduler.ts
599
+ import Baker from "cronbake";
600
+ class Scheduler {
601
+ baker = Baker.create();
602
+ executor;
603
+ constructor(baseContext) {
604
+ this.executor = new ActionExecutor(baseContext);
605
+ }
606
+ schedule(action, trigger) {
607
+ this.baker.add({
608
+ name: `${action.config.name}:${trigger.name || "cron"}`,
609
+ cron: trigger.schedule,
610
+ callback: async () => {
611
+ try {
612
+ globalLogger.info("Running scheduled action", {
613
+ action: action.config.name,
614
+ schedule: trigger.schedule
615
+ });
616
+ await this.executor.execute(action, trigger.name, {});
617
+ } catch (error) {
618
+ globalLogger.error("Scheduled action failed", {
619
+ action: action.config.name,
620
+ error: error.message,
621
+ stack: error.stack
622
+ });
623
+ }
624
+ }
625
+ });
626
+ }
627
+ start() {
628
+ this.baker.bakeAll();
629
+ globalLogger.info("Scheduler started");
630
+ }
631
+ stop() {
632
+ this.baker.stopAll();
633
+ globalLogger.info("Stopped all cron jobs");
634
+ }
635
+ }
636
+
637
+ // src/runtime/server.ts
638
+ class BasaltServer {
639
+ config;
640
+ router;
641
+ eventBus;
642
+ scheduler;
643
+ mcpServer;
644
+ httpServer;
645
+ actions = [];
646
+ constructor(config) {
647
+ this.config = config;
648
+ const baseContext = this.buildBaseContext();
649
+ this.router = new Router(baseContext);
650
+ this.eventBus = new EventBus(baseContext);
651
+ this.scheduler = new Scheduler(baseContext);
652
+ this.httpServer = new HttpServer(this.config, this.router);
653
+ if (this.config.mcp?.enabled) {
654
+ this.mcpServer = new McpServer(baseContext);
655
+ }
656
+ }
657
+ buildBaseContext() {
658
+ return {
659
+ logger: globalLogger,
660
+ db: undefined,
661
+ storage: undefined,
662
+ enqueue: async (eventType, data) => {
663
+ const event = {
664
+ id: `evt-${Date.now()}-${Math.random().toString(36).slice(2)}`,
665
+ type: eventType,
666
+ data,
667
+ timestamp: new Date
668
+ };
669
+ await this.eventBus.publish(event);
670
+ },
671
+ schedule: async (when, action, input) => {
672
+ await this.scheduleAction(when, action, input);
673
+ }
674
+ };
675
+ }
676
+ async scheduleAction(when, action, input) {
677
+ const actionName = action.config.name;
678
+ if (typeof when === "number") {
679
+ const runAt = new Date(Date.now() + when * 1000);
680
+ globalLogger.info("Scheduling action", { actionName, runAt, input });
681
+ } else if (when instanceof Date) {
682
+ globalLogger.info("Scheduling action", { actionName, runAt: when, input });
683
+ } else if (typeof when === "string") {
684
+ globalLogger.info("Scheduling recurring action", {
685
+ actionName,
686
+ cron: when,
687
+ input
688
+ });
689
+ }
690
+ }
691
+ async initialize() {
692
+ this.actions = await loadActions(this.config.actionsDir || "./src/actions");
693
+ for (const action of this.actions) {
694
+ if (!action.config.triggers)
695
+ continue;
696
+ for (const trigger of action.config.triggers) {
697
+ switch (trigger.kind) {
698
+ case "api": {
699
+ const apiTrigger = trigger;
700
+ this.router.register(action, apiTrigger);
701
+ globalLogger.info("Registered API route", {
702
+ method: apiTrigger.method,
703
+ path: apiTrigger.path,
704
+ action: action.config.name
705
+ });
706
+ break;
707
+ }
708
+ case "cron": {
709
+ const cronTrigger = trigger;
710
+ this.scheduler.schedule(action, cronTrigger);
711
+ globalLogger.info("Registered cron job", {
712
+ schedule: cronTrigger.schedule,
713
+ action: action.config.name
714
+ });
715
+ break;
716
+ }
717
+ case "event": {
718
+ const eventTrigger = trigger;
719
+ this.eventBus.subscribe(eventTrigger.event, action, eventTrigger.name);
720
+ globalLogger.info("Registered event handler", {
721
+ event: eventTrigger.event,
722
+ action: action.config.name
723
+ });
724
+ break;
725
+ }
726
+ case "agent": {
727
+ const agentTrigger = trigger;
728
+ this.mcpServer?.register(action, agentTrigger);
729
+ globalLogger.info("Registered MCP tool", {
730
+ tool: agentTrigger.tool,
731
+ action: action.config.name
732
+ });
733
+ break;
734
+ }
735
+ }
736
+ }
737
+ }
738
+ globalLogger.info("Basalt initialized", {
739
+ actions: this.actions.length,
740
+ routes: this.router.getRoutes().length
741
+ });
742
+ }
743
+ async start() {
744
+ await this.httpServer.start();
745
+ this.scheduler.start();
746
+ if (this.mcpServer) {
747
+ await this.mcpServer.start();
748
+ }
749
+ }
750
+ getRouter() {
751
+ return this.router;
752
+ }
753
+ getEventBus() {
754
+ return this.eventBus;
755
+ }
756
+ getActions() {
757
+ return this.actions;
758
+ }
759
+ getHttpUrl() {
760
+ return this.httpServer.getUrl();
761
+ }
762
+ async stop() {
763
+ await this.httpServer.stop();
764
+ this.scheduler.stop();
765
+ await this.mcpServer?.stop();
766
+ }
767
+ }
768
+
769
+ // src/runtime/dev.ts
770
+ async function createDevServer(config) {
771
+ const server = new BasaltServer(config);
772
+ await server.initialize();
773
+ if (config.dev?.watch !== false) {
774
+ const actionsDir = config.actionsDir || "./src/actions";
775
+ const watcher = watch(actionsDir, { recursive: true }, async (eventType, filename) => {
776
+ if (!filename)
777
+ return;
778
+ if (!/\.(ts|js)$/.test(filename))
779
+ return;
780
+ globalLogger.info("Action file changed, regenerating...", {
781
+ filename,
782
+ eventType
783
+ });
784
+ try {
785
+ const actions = await loadActions(actionsDir);
786
+ await generateActionTypes(actions);
787
+ globalLogger.info("Types regenerated");
788
+ } catch (error) {
789
+ globalLogger.error("Failed to regenerate types", {
790
+ error: error.message
791
+ });
792
+ }
793
+ });
794
+ process.on("SIGINT", () => {
795
+ watcher.close();
796
+ server.stop();
797
+ process.exit(0);
798
+ });
799
+ }
800
+ return server;
801
+ }
802
+
803
+ // src/cli/commands/dev.ts
804
+ var devCommand = new Command3("dev").description("Start the development server").option("-p, --port <number>", "Port to listen on", parseInt).action(async (options) => {
805
+ console.log(`\uD83C\uDF0B Starting Basalt dev server...
806
+ `);
807
+ const config = await loadConfig();
808
+ if (options.port) {
809
+ config.server = { ...config.server, port: options.port };
810
+ }
811
+ const actions = await loadActions(config.actionsDir || "./src/actions");
812
+ console.log("✓ Loaded actions");
813
+ const server = await createDevServer(config);
814
+ await server.start();
815
+ const port = config.server?.port || 3000;
816
+ const host = config.server?.host || "localhost";
817
+ console.log(`
818
+ ✓ Server ready
819
+
820
+ Local: http://${host}:${port}
821
+ Actions: ${actions.length}
822
+ Watch: ${config.dev?.watch !== false ? "enabled" : "disabled"}
823
+
824
+ Press Ctrl+C to stop
825
+ `);
826
+ });
827
+
828
+ // src/cli/commands/start.ts
829
+ import { Command as Command4 } from "commander";
830
+ var startCommand = new Command4("start").description("Start the production server").action(async () => {
831
+ console.log("Starting production server...");
832
+ const config = await loadConfig();
833
+ const server = new BasaltServer(config);
834
+ await server.initialize();
835
+ await server.start();
836
+ });
837
+
838
+ // src/cli/index.ts
839
+ var program = new Command5;
840
+ program.name("basalt").description("Basalt CLI").version("0.1.0");
841
+ program.addCommand(devCommand);
842
+ program.addCommand(buildCommand);
843
+ program.addCommand(startCommand);
844
+ program.addCommand(createActionCommand);
845
+ program.parse(process.argv);